diff --git a/.travis.yml b/.travis.yml index 7e65ed9..66854d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ env: - WP_TRAVISCI=travis:phpunit - SVN_REPO: https://plugins.svn.wordpress.org/activitypub/ - GH_REF: https://github.com/pfefferle/wordpress-activitypub.git - - secure: k6uZL/zVYvuSsG4tUMvVC6+DTYKSueOYj6j9csKSa08EzPl0JLPoNN/5MPihi+zZkBPLL+KjBgUUYfBtdcOh/xwPb+lilg5UnvM7TKQEG583RtBbohADvahQHN4mxZb6yBvrmvbB5jNtf+3L4RcOgdONEb2CpGo5n8RRM3MXxF4WSn9s+F+P0fvWCpcZnM9yqgbTJNUaZHw4tRWQ7eYZ5kFxSxKSnZd5+149UAh2YcKjA+ix3rrK0ClOlGaMVZz+SV4/qoNwamxMTMAQ6Be1wIelXz0n92FIonw6euDfBSgNLg+WLiAYoqaM+tEluLV1DRcx5TLUfmAOGli6pEfqL6XZxf8iZheMtn5Ir0nR4vLbOUKEojqEpLwmlUjiTN6RSZbPMquBNz/lOEQd9S/EzPLrtvlBRYAq0EmI62KtXVG+vHta2TTF+LS0caXjVZaHcpF4SYmuG5WyR6d0KpnVw6czvXu7hyq2sNz6lj1hUt15pPZO8tFkJFTs87pOBfEj/GnjIE4Iab2HA/HgdWqFpB+5ZAWH9QDIa9c3+QUQQf2qA7Z1yS0c5SBn/TE+0O3yomyBTD63Zc7gNG2qqw+THql5fzG3iGV5M6db+yTY0INfsJYuRjQXpn9Q35ZTxgXEEvu7naHh162wa14K18zzXoVEjhywOoW3X1Qiz6VFK8s= + - secure: "WaeBLo0MDuK4X7NQvk5Ie5BVnexHtyDfHAV8v7dB8B67d8GCWy9K5I54jTECNRUC+CecMakLq5DOfn+ThtWdkJmoJKnGNFp8ZrWkMsfVJRi3CDED2HkccOrxkmXBj8Z6A8jZjcfVNrEmq/6697xVNRGeaS08l9rokh7pyb8INWY=" matrix: include: - php: 7.2 diff --git a/README.md b/README.md index 5fac62d..65361e1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Donate link:** https://notiz.blog/donate/ **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 -**Tested up to:** 5.0 +**Tested up to:** 5.0.2 **Stable tag:** 0.1.0 **Requires PHP:** 5.6 **License:** MIT @@ -13,9 +13,9 @@ The ActivityPub protocol is a decentralized social networking protocol based upo ## Description ## -This is **BETA** software, see the FAQ to see what works and what still needs to be implemented or is in planning. +This is **BETA** software, see the FAQ to see the current feature set or rather what is still planned. -This plugin enables ActivityPub for your Blog. Your readers will be able to follow your Blogposts on Mastodon and other Federated Plattforms that support ActivityPub. +This plugin implements the ActivityPub for your Blog. Your readers will be able to follow your Blogposts on Mastodon and other Federated Plattforms that support ActivityPub. ## Frequently Asked Questions ## @@ -26,30 +26,34 @@ Implemented: * profile pages (JSON representation) * custom links * functional inbox/outbox -* follow (accept follows) +* follow (accept follows) +* share posts +* receive comments/reactions To implement: -* share posts -* share comments +* signature verification +* better WordPress integration +* better configuration possibilities +* threaded comments support ### Why does the plugin not support ...? ### *ActivityPub* extends WordPress with some fediverse features, but it does not compete with plattforms like Friendi.ca or Mastodon. If you want to have a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU.social](https://gnu.io/social/). -### What are the differences to Pterotype? ### +### What are the differences between this plugin and Pterotype? ### **PHP Version** -*ActivityPub* needs PHP 5.6, *Pterotype* requires 7.2.x +*This plugin* needs PHP 5.6, *Pterotype* requires 7.2.x **Compatibility** -*ActivityPub* is compatible with OStatus and the IndieWeb movement. *Pterotype* implements its own WebFinger endpoint, that is not compatible with the [WebFinger plugin](https://wordpress.org/plugins/webfinger/). +*This plugin* is compatible with OStatus and the IndieWeb movement. *Pterotype* implements for example its own WebFinger endpoint, which is not compatible with the [WebFinger plugin](https://wordpress.org/plugins/webfinger/). **Custom tables** -*Pterotype* creates/uses a bunch of custom tables, *ActivityPub* only uses the native tables and adds as few meta data as possible. +*Pterotype* creates/uses a bunch of custom tables, *this plugin* only uses the native tables and adds as few meta data as possible. ## Changelog ## @@ -59,7 +63,9 @@ Project maintained on github at [pfefferle/wordpress-activitypub](https://github * added basic WebFinger support * added basic NodeInfo support -* fully functional "follow" activity +* fully functional "follow" activity +* send new posts to your followers +* receive comments from your followers ### 0.0.2 ### diff --git a/activitypub.php b/activitypub.php index ff72b36..303ca44 100644 --- a/activitypub.php +++ b/activitypub.php @@ -18,6 +18,7 @@ function activitypub_init() { require_once dirname( __FILE__ ) . '/includes/class-activitypub-signature.php'; require_once dirname( __FILE__ ) . '/includes/class-activitypub-post.php'; + require_once dirname( __FILE__ ) . '/includes/class-activitypub-activity.php'; require_once dirname( __FILE__ ) . '/includes/class-db-activitypub-followers.php'; require_once dirname( __FILE__ ) . '/includes/functions.php'; @@ -29,9 +30,16 @@ function activitypub_init() { // Configure the REST API route require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-outbox.php'; add_action( 'rest_api_init', array( 'Rest_Activitypub_Outbox', 'register_routes' ) ); + add_action( 'activitypub_send_post_activity', array( 'Rest_Activitypub_Outbox', 'send_post_activity' ) ); require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-inbox.php'; add_action( 'rest_api_init', array( 'Rest_Activitypub_Inbox', 'register_routes' ) ); + //add_filter( 'rest_pre_serve_request', array( 'Rest_Activitypub_Inbox', 'serve_request' ), 11, 4 ); + add_action( 'activitypub_inbox_follow', array( 'Rest_Activitypub_Inbox', 'handle_follow' ), 10, 2 ); + add_action( 'activitypub_inbox_unfollow', array( 'Rest_Activitypub_Inbox', 'handle_unfollow' ), 10, 2 ); + add_action( 'activitypub_inbox_like', array( 'Rest_Activitypub_Inbox', 'handle_reaction' ), 10, 2 ); + add_action( 'activitypub_inbox_announce', array( 'Rest_Activitypub_Inbox', 'handle_reaction' ), 10, 2 ); + add_action( 'activitypub_inbox_create', array( 'Rest_Activitypub_Inbox', 'handle_create' ), 10, 2 ); require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-followers.php'; add_action( 'rest_api_init', array( 'Rest_Activitypub_Followers', 'register_routes' ) ); @@ -45,11 +53,17 @@ function activitypub_init() { add_filter( 'nodeinfo_data', array( 'Rest_Activitypub_Nodeinfo', 'add_nodeinfo_discovery' ), 10, 2 ); add_filter( 'nodeinfo2_data', array( 'Rest_Activitypub_Nodeinfo', 'add_nodeinfo2_discovery' ), 10 ); - // Configure activities - require_once dirname( __FILE__ ) . '/includes/class-activitypub-activities.php'; - add_action( 'activitypub_inbox_follow', array( 'Activitypub_Activities', 'accept' ), 10, 2 ); - add_action( 'activitypub_inbox_follow', array( 'Activitypub_Activities', 'follow' ), 10, 2 ); - add_action( 'activitypub_inbox_unfollow', array( 'Activitypub_Activities', 'unfollow' ), 10, 2 ); + add_post_type_support( 'post', 'activitypub' ); + add_post_type_support( 'page', 'activitypub' ); + + $post_types = get_post_types_by_support( 'activitypub' ); + foreach ( $post_types as $post_type ) { + add_action( 'publish_' . $post_type, array( 'Activitypub', 'schedule_post_activity' ) ); + } + + require_once dirname( __FILE__ ) . '/includes/class-activitypub-admin.php'; + add_action( 'admin_menu', array( 'Activitypub_Admin', 'admin_menu' ) ); + add_action( 'admin_init', array( 'Activitypub_Admin', 'register_settings' ) ); } add_action( 'plugins_loaded', 'activitypub_init' ); diff --git a/includes/class-activitypub-activities.php b/includes/class-activitypub-activities.php deleted file mode 100644 index 870a1f2..0000000 --- a/includes/class-activitypub-activities.php +++ /dev/null @@ -1,64 +0,0 @@ - array( 'https://www.w3.org/ns/activitystreams' ), - 'type' => 'Accept', - 'actor' => get_author_posts_url( $author_id ), - 'object' => $data, - 'to' => $data['actor'], - ) - ); - - return activitypub_safe_remote_post( $inbox, $activity, $author_id ); - } - - /** - * [follow description] - * @param [type] $data [description] - * @param [type] $author_id [description] - * @return [type] [description] - */ - public static function follow( $data, $author_id ) { - if ( ! array_key_exists( 'actor', $data ) ) { - return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata ); - } - - Db_Activitypub_Followers::add_follower( $data['actor'], $author_id ); - } - - /** - * [unfollow description] - * @param [type] $data [description] - * @param [type] $author_id [description] - * @return [type] [description] - */ - public static function unfollow( $data, $author_id ) { - - } - - /** - * [create description] - * @param [type] $data [description] - * @param [type] $author_id [description] - * @return [type] [description] - */ - public static function create( $data, $author_id ) { - - } -} diff --git a/includes/class-activitypub-activity.php b/includes/class-activitypub-activity.php new file mode 100644 index 0000000..68e1b92 --- /dev/null +++ b/includes/class-activitypub-activity.php @@ -0,0 +1,84 @@ +context = null; + } elseif ( 'full' === $context ) { + $this->context = get_activitypub_context(); + } + + $this->type = ucfirst( $type ); + $this->published = date( 'Y-m-d\TH:i:s\Z', strtotime( 'now' ) ); + } + + public function __call( $method, $params ) { + $var = strtolower( substr( $method, 4 ) ); + + if ( strncasecmp( $method, 'get', 3 ) === 0 ) { + return $this->$var; + } + + if ( strncasecmp( $method, 'set', 3 ) === 0 ) { + $this->$var = $params[0]; + } + } + + public function from_post( $object ) { + $this->object = $object; + $this->published = $object['published']; + $this->actor = $object['attributedTo']; + $this->id = $object['id']; + } + + public function from_comment( $object ) { + + } + + public function to_array() { + $array = get_object_vars( $this ); + + if ( $this->context ) { + $array = array( '@context' => $this->context ) + $array; + } + + unset( $array['context'] ); + + return $array; + } + + public function to_json() { + return wp_json_encode( $this->to_array() ); + } + + public function to_simple_array() { + return array( + '@context' => $this->context, + 'type' => $this->type, + 'actor' => $this->actor, + 'object' => $this->object, + 'to' => $this->to, + ); + } + + public function to_simple_json() { + return wp_json_encode( $this->to_simple_array() ); + } +} diff --git a/includes/class-activitypub-admin.php b/includes/class-activitypub-admin.php new file mode 100644 index 0000000..bf14d73 --- /dev/null +++ b/includes/class-activitypub-admin.php @@ -0,0 +1,54 @@ +add_help_tab( + array( + 'id' => 'overview', + 'title' => __( 'Overview', 'activitypub' ), + 'content' => + '
' . __( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '
', + ) + ); + + get_current_screen()->set_help_sidebar( + '' . __( 'For more information:', 'activitypub' ) . '
' . + '' . __( 'Test Suite', 'activitypub' ) . '
' . + '' . __( 'W3C Spec', 'activitypub' ) . '
' . + '' . __( 'Give us feedback', 'activitypub' ) . '
' . + '' . __( 'Donate', 'activitypub' ) . '
' + ); + } +} diff --git a/includes/class-activitypub-post.php b/includes/class-activitypub-post.php index 43851ed..09f545a 100644 --- a/includes/class-activitypub-post.php +++ b/includes/class-activitypub-post.php @@ -8,47 +8,129 @@ class Activitypub_Post { private $post; public function __construct( $post = null ) { - if ( ! $post ) { - $post = get_post(); - } - - $this->post = $post; + $this->post = get_post( $post ); } - public function to_json_array( $with_context = false ) { + public function get_post() { + return $this->post; + } + + public function get_post_author() { + return $this->post->post_author; + } + + public function to_array() { $post = $this->post; - $json = new stdClass(); + setup_postdata( $post ); - if ( $with_context ) { - $json->{'@context'} = get_activitypub_context(); - } - - $json->published = date( 'Y-m-d\TH:i:s\Z', strtotime( $post->post_date ) ); - $json->id = $post->guid . '&activitypub'; - $json->type = 'Create'; - $json->actor = get_author_posts_url( $post->post_author ); - $json->to = array( 'https://www.w3.org/ns/activitystreams#Public' ); - $json->cc = array( 'https://www.w3.org/ns/activitystreams#Public' ); - - $json->object = array( - 'id' => $post->guid, + $array = array( + 'id' => get_permalink( $post ), 'type' => $this->get_object_type(), 'published' => date( 'Y-m-d\TH:i:s\Z', strtotime( $post->post_date ) ), + 'attributedTo' => get_author_posts_url( $post->post_author ), + 'summary' => apply_filters( 'the_excerpt', get_post_field( 'post_excerpt', $post->ID ) ), + 'inReplyTo' => null, + 'content' => apply_filters( 'the_content', get_post_field( 'post_content', $post->ID ) ), + 'contentMap' => array( + strstr( get_locale(), '_', true ) => apply_filters( 'the_content', get_post_field( 'post_content', $post->ID ) ), + ), 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'attributedTo' => get_author_posts_url( $post->post_author ), - 'summary' => null, - 'inReplyTo' => null, - 'content' => esc_html( $post->post_content ), - 'contentMap' => array( - strstr( get_locale(), '_', true ) => esc_html( $post->post_content ) , - ), - 'attachment' => array(), - 'tag' => array(), + 'attachment' => $this->get_attachments(), + 'tag' => $this->get_tags(), ); - return apply_filters( 'activitypub_json_post', $json ); + wp_reset_postdata(); + + return apply_filters( 'activitypub_post', $array ); + } + + public function to_json() { + return wp_json_encode( $this->to_array() ); + } + + public function get_attachments() { + $max_images = apply_filters( 'activitypub_max_images', 3 ); + + $images = array(); + + // max images can't be negative or zero + if ( $max_images <= 0 ) { + $max_images = 1; + } + + $id = $this->post->ID; + + $image_ids = array(); + // list post thumbnail first if this post has one + if ( function_exists( 'has_post_thumbnail' ) && has_post_thumbnail( $id ) ) { + $image_ids[] = get_post_thumbnail_id( $id ); + $max_images--; + } + // then list any image attachments + $query = new WP_Query( + array( + 'post_parent' => $id, + 'post_status' => 'inherit', + 'post_type' => 'attachment', + 'post_mime_type' => 'image', + 'order' => 'ASC', + 'orderby' => 'menu_order ID', + 'posts_per_page' => $max_images, + ) + ); + foreach ( $query->get_posts() as $attachment ) { + if ( ! in_array( $attachment->ID, $image_ids ) ) { + $image_ids[] = $attachment->ID; + } + } + // get URLs for each image + foreach ( $image_ids as $id ) { + $thumbnail = wp_get_attachment_image_src( $id, 'full' ); + $mimetype = get_post_mime_type( $id ); + + if ( $thumbnail ) { + $images[] = array( + 'url' => $thumbnail[0], + 'type' => $mimetype + ); + } + } + + $attachments = array(); + + // add attachments + if ( $images ) { + foreach ( $images as $image ) { + $attachment = array( + "type" => "Image", + "url" => $image['url'], + "mediaType" => $image['type'], + ); + $attachments[] = $attachment; + } + } + + return $attachments; + } + + public function get_tags() { + $tags = array(); + + $post_tags = get_the_tags( $this->post->ID ); + if ( $post_tags ) { + foreach( $post_tags as $post_tag ) { + $tag = array( + "type" => "Hashtag", + "href" => get_tag_link( $post_tag->term_id ), + "name" => '#' . $post_tag->name, + ); + $tags[] = $tag; + } + } + + return $tags; } /** diff --git a/includes/class-activitypub-signature.php b/includes/class-activitypub-signature.php index 85cba19..7fc6a79 100644 --- a/includes/class-activitypub-signature.php +++ b/includes/class-activitypub-signature.php @@ -3,37 +3,37 @@ class Activitypub_Signature { /** - * @param int $author_id + * @param int $user_id * * @return mixed */ - public static function get_public_key( $author_id, $force = false ) { - $key = get_user_meta( $author_id, 'magic_sig_public_key' ); + public static function get_public_key( $user_id, $force = false ) { + $key = get_user_meta( $user_id, 'magic_sig_public_key' ); if ( $key && ! $force ) { return $key[0]; } - self::generate_key_pair( $author_id ); - $key = get_user_meta( $author_id, 'magic_sig_public_key' ); + self::generate_key_pair( $user_id ); + $key = get_user_meta( $user_id, 'magic_sig_public_key' ); return $key[0]; } /** - * @param int $author_id + * @param int $user_id * * @return mixed */ - public static function get_private_key( $author_id, $force = false ) { - $key = get_user_meta( $author_id, 'magic_sig_private_key' ); + public static function get_private_key( $user_id, $force = false ) { + $key = get_user_meta( $user_id, 'magic_sig_private_key' ); if ( $key && ! $force ) { return $key[0]; } - self::generate_key_pair( $author_id ); - $key = get_user_meta( $author_id, 'magic_sig_private_key' ); + self::generate_key_pair( $user_id ); + $key = get_user_meta( $user_id, 'magic_sig_private_key' ); return $key[0]; } @@ -41,9 +41,9 @@ class Activitypub_Signature { /** * Generates the pair keys * - * @param int $author_id + * @param int $user_id */ - public static function generate_key_pair( $author_id ) { + public static function generate_key_pair( $user_id ) { $config = array( 'digest_alg' => 'sha512', 'private_key_bits' => 2048, @@ -56,18 +56,18 @@ class Activitypub_Signature { openssl_pkey_export( $key, $priv_key ); // private key - update_user_meta( $author_id, 'magic_sig_private_key', $priv_key ); + update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); $detail = openssl_pkey_get_details( $key ); // public key - update_user_meta( $author_id, 'magic_sig_public_key', $detail['key'] ); + update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); } - public static function generate_signature( $author_id, $inbox, $date ) { - $key = self::get_private_key( $author_id ); + public static function generate_signature( $user_id, $url, $date ) { + $key = self::get_private_key( $user_id ); - $url_parts = wp_parse_url( $inbox ); + $url_parts = wp_parse_url( $url ); $host = $url_parts['host']; $path = '/'; @@ -86,14 +86,14 @@ class Activitypub_Signature { $signature = null; openssl_sign( $signed_string, $signature, $key, OPENSSL_ALGO_SHA256 ); - $signature = base64_encode( $signature ); + $signature = base64_encode( $signature ); // phpcs:ignore - $key_id = get_author_posts_url( $author_id ) . '#main-key'; + $key_id = get_author_posts_url( $user_id ) . '#main-key'; return sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date",signature="%s"', $key_id, $signature ); } - public static function verify_signature() { + public static function verify_signature( $headers, $signature ) { } } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 82fc2e4..ebc42ed 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -70,4 +70,13 @@ class Activitypub { public static function add_rewrite_endpoint() { add_rewrite_endpoint( 'as2', EP_AUTHORS | EP_PERMALINK | EP_PAGES ); } + + /** + * Marks the post as "no webmentions sent yet" + * + * @param int $post_id + */ + public static function schedule_post_activity( $post_id ) { + wp_schedule_single_event( time() + wp_rand( 0, 120 ), 'activitypub_send_post_activity', array( $post_id ) ); + } } diff --git a/includes/class-db-activitypub-followers.php b/includes/class-db-activitypub-followers.php index 70fd6d9..7caba32 100644 --- a/includes/class-db-activitypub-followers.php +++ b/includes/class-db-activitypub-followers.php @@ -1,68 +1,9 @@ 100, - 'limit_response_size' => 1048576, - 'redirection' => 3, - 'user-agent' => "$user_agent; ActivityPub", - 'headers' => array( 'accept' => 'application/activity+json' ), - ); - - $response = wp_safe_remote_get( $actor, $args ); - - if ( is_wp_error( $response ) ) { - return $response; - } - - $metadata = wp_remote_retrieve_body( $response ); - $metadata = json_decode( $metadata, true ); - - if ( ! $metadata ) { - return new WP_Error( 'activitypub_invalid_json', __( 'No valid JSON data', 'activitypub' ), $actor ); - } - - set_transient( 'activitypub_' . $actor, $metadata, WEEK_IN_SECONDS ); - - return $metadata; + public static function get_followers( $author_id ) { + return get_user_option( 'activitypub_followers', $author_id ); } public static function add_follower( $actor, $author_id ) { diff --git a/includes/class-rest-activitypub-followers.php b/includes/class-rest-activitypub-followers.php index 9aa5f7a..7df2714 100644 --- a/includes/class-rest-activitypub-followers.php +++ b/includes/class-rest-activitypub-followers.php @@ -17,10 +17,10 @@ class Rest_Activitypub_Followers { } public static function get( $request ) { - $author_id = $request->get_param( 'id' ); - $author = get_user_by( 'ID', $author_id ); + $user_id = $request->get_param( 'id' ); + $user = get_user_by( 'ID', $user_id ); - if ( ! $author ) { + if ( ! $user ) { return new WP_Error( 'rest_invalid_param', __( 'User not found', 'activitypub' ), array( 'status' => 404, 'params' => array( 'user_id' => __( 'User not found', 'activitypub' ) @@ -39,7 +39,7 @@ class Rest_Activitypub_Followers { $json->{'@context'} = get_activitypub_context(); - $followers = get_user_option( 'activitypub_followers', $author_id ); + $followers = Db_Activitypub_Followers::get_followers( $user_id ); if ( ! is_array( $followers ) ) { $followers = array(); diff --git a/includes/class-rest-activitypub-inbox.php b/includes/class-rest-activitypub-inbox.php index 48e7580..ba5cfaf 100644 --- a/includes/class-rest-activitypub-inbox.php +++ b/includes/class-rest-activitypub-inbox.php @@ -29,6 +29,44 @@ class Rest_Activitypub_Inbox { ); } + /** + * Hooks into the REST API request to verify the signature. + * + * @param bool $served Whether the request has already been served. + * @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response. + * @param WP_REST_Request $request Request used to generate the response. + * @param WP_REST_Server $server Server instance. + * + * @return true + */ + public static function serve_request( $served, $result, $request, $server ) { + if ( '/activitypub' !== substr( $request->get_route(), 0, 12 ) ) { + return $served; + } + + if ( 'POST' !== $request->get_method() ) { + return $served; + } + + $signature = $request->get_header( 'signature' ); + + if ( ! $signature ) { + return $served; + } + + $headers = $request->get_headers(); + + //Activitypub_Signature::verify_signature( $headers, $key ); + + return $served; + } + + /** + * Renders the user-inbox + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ public static function user_inbox( $request ) { $author_id = $request->get_param( 'id' ); $author = get_user_by( 'ID', $author_id ); @@ -40,14 +78,14 @@ class Rest_Activitypub_Inbox { $type = strtolower( $data['type'] ); } - do_action( 'activitypub_inbox', $data, $author_id, $type ); - do_action( "activitypub_inbox_{$type}", $data, $author_id ); - if ( ! is_array( $data ) || ! array_key_exists( 'type', $data ) ) { return new WP_Error( 'rest_invalid_data', __( 'Invalid payload', 'activitypub' ), array( 'status' => 422 ) ); } - return new WP_REST_Response( null, 202 ); + do_action( 'activitypub_inbox', $data, $author_id, $type ); + do_action( "activitypub_inbox_{$type}", $data, $author_id ); + + return new WP_REST_Response( array(), 202 ); } /** @@ -81,4 +119,120 @@ class Rest_Activitypub_Inbox { return $params; } + + /** + * Handles "Follow" requests + * + * @param array $object The activity-object + * @param int $user_id The id of the local blog-user + */ + public static function handle_follow( $object, $user_id ) { + if ( ! array_key_exists( 'actor', $object ) ) { + return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata ); + } + + // save follower + Db_Activitypub_Followers::add_follower( $object['actor'], $user_id ); + + // get inbox + $inbox = activitypub_get_inbox_by_actor( $object['actor'] ); + + // send "Accept" activity + $activity = new Activitypub_Activity( 'Accept', Activitypub_Activity::TYPE_SIMPLE ); + $activity->set_object( $object ); + $activity->set_actor( get_author_posts_url( $user_id ) ); + $activity->set_to( $object['actor'] ); + + $activity = $activity->to_simple_json(); + + $response = activitypub_safe_remote_post( $inbox, $activity, $user_id ); + } + + /** + * Handles "Unfollow" requests + * + * @param array $object The activity-object + * @param int $user_id The id of the local blog-user + */ + public static function handle_unfollow( $object, $user_id ) { + if ( ! array_key_exists( 'actor', $object ) ) { + return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata ); + } + + Db_Activitypub_Followers::remove_follower( $object['actor'], $user_id ); + } + + /** + * Handles "Unfollow" requests + * + * @param array $object The activity-object + * @param int $user_id The id of the local blog-user + */ + public static function handle_reaction( $object, $user_id ) { + if ( ! array_key_exists( 'actor', $object ) ) { + return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata ); + } + + $meta = activitypub_get_remote_metadata_by_actor( $object['actor'] ); + + $commentdata = array( + 'comment_post_ID' => url_to_postid( $object['object'] ), + 'comment_author' => esc_attr( $meta['name'] ), + 'comment_author_email' => '', + 'comment_author_url' => esc_url_raw( $object['actor'] ), + 'comment_content' => esc_url_raw( $object['actor'] ), + 'comment_type' => esc_attr( strtolower( $object['type'] ) ), + 'comment_parent' => 0, + 'comment_meta' => array( + 'source_url' => esc_url_raw( $object['id'] ), + 'avatar_url' => esc_url_raw( $meta['icon']['url'] ), + 'protocol' => 'activitypub', + ), + ); + + // disable flood control + remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); + + $state = wp_new_comment( $commentdata, true ); + + // re-add flood control + add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); + } + + /** + * Handles "Unfollow" requests + * + * @param array $object The activity-object + * @param int $user_id The id of the local blog-user + */ + public static function handle_create( $object, $user_id ) { + if ( ! array_key_exists( 'actor', $object ) ) { + return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata ); + } + + $meta = activitypub_get_remote_metadata_by_actor( $object['actor'] ); + + $commentdata = array( + 'comment_post_ID' => url_to_postid( $object['object']['inReplyTo'] ), + 'comment_author' => esc_attr( $meta['name'] ), + 'comment_author_url' => esc_url_raw( $object['actor'] ), + 'comment_content' => wp_filter_kses( $object['object']['content'] ), + 'comment_type' => '', + 'comment_author_email' => '', + 'comment_parent' => 0, + 'comment_meta' => array( + 'source_url' => esc_url_raw( $object['object']['url'] ), + 'avatar_url' => esc_url_raw( $meta['icon']['url'] ), + 'protocol' => 'activitypub', + ), + ); + + // disable flood control + remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); + + $state = wp_new_comment( $commentdata, true ); + + // re-add flood control + add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); + } } diff --git a/includes/class-rest-activitypub-outbox.php b/includes/class-rest-activitypub-outbox.php index 6e7872d..d10cc5f 100644 --- a/includes/class-rest-activitypub-outbox.php +++ b/includes/class-rest-activitypub-outbox.php @@ -5,6 +5,7 @@ * @author Matthias Pfefferle */ class Rest_Activitypub_Outbox { + /** * Register routes */ @@ -13,16 +14,22 @@ class Rest_Activitypub_Outbox { 'activitypub/1.0', '/users/(?P+ donation?', 'activitypub' ); ?> +
+