From 5f6cf78da16434b29d34f15f0f09eb691b8de530 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:08:32 -0700 Subject: [PATCH 01/19] Add a parser to the Friends Plugin --- activitypub.php | 8 + includes/class-health-check.php | 63 ++---- includes/functions.php | 10 +- includes/model/class-activity.php | 2 +- includes/rest/class-inbox.php | 5 +- includes/rest/class-webfinger.php | 40 ++++ .../class-friends-feed-parser-activitypub.php | 198 ++++++++++++++++++ templates/author-json.php | 2 +- 8 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 integration/class-friends-feed-parser-activitypub.php diff --git a/activitypub.php b/activitypub.php index d802cd7b8..69dfeb611 100644 --- a/activitypub.php +++ b/activitypub.php @@ -132,3 +132,11 @@ function enable_buddypress_features() { \Activitypub\Integration\Buddypress::init(); } add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); + +add_action( + 'friends_load_parsers', + function( \Friends\Feed $friends_feed ) { + require_once __DIR__ . '/integration/class-friends-feed-parser-activitypub.php'; + $friends_feed->register_parser( Friends_Feed_Parser_ActivityPub::SLUG, new Friends_Feed_Parser_ActivityPub( $friends_feed ) ); + } +); diff --git a/includes/class-health-check.php b/includes/class-health-check.php index 12f8d710c..bdc3b0b6c 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -1,6 +1,8 @@ ID ); - - $url = \wp_parse_url( \home_url(), \PHP_URL_SCHEME ) . '://' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); - - if ( \wp_parse_url( \home_url(), \PHP_URL_PORT ) ) { - $url .= ':' . \wp_parse_url( \home_url(), \PHP_URL_PORT ); - } - - $url = \trailingslashit( $url ) . '.well-known/webfinger'; + $user = \wp_get_current_user(); + $account = \Activitypub\get_webfinger_resource( $user->ID ); - $url = \add_query_arg( 'resource', 'acct:' . $webfinger, $url ); - - // try to access author URL - $response = \wp_remote_get( - $url, - array( - 'headers' => array( 'Accept' => 'application/activity+json' ), - 'redirection' => 0, - ) - ); - - if ( \is_wp_error( $response ) ) { - return new \WP_Error( - 'webfinger_url_not_accessible', - \sprintf( + $url = Webfinger::resolve( $account ); + if ( \is_wp_error( $url ) ) { + $health_messages = array( + 'webfinger_url_not_accessible' => \sprintf( // translators: %s: Author URL \__( '

Your WebFinger endpoint %s is not accessible. Please check your WordPress setup or permalink structure.

', 'activitypub' ), - $url - ) - ); - } - - $response_code = \wp_remote_retrieve_response_code( $response ); - - // check if response is JSON - $body = \wp_remote_retrieve_body( $response ); - - if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) { - return new \WP_Error( - 'webfinger_url_not_accessible', - \sprintf( + $url->get_error_data() + ), + 'webfinger_url_invalid_response' => \sprintf( // translators: %s: Author URL \__( '

Your WebFinger endpoint %s does not return valid JSON for application/jrd+json.

', 'activitypub' ), - $url - ) + $url->get_error_data() + ), + ); + $message = null; + if ( isset( $messages[ $url->get_error_code() ] ) ) { + $message = $health_messages[ $url->get_error_code() ]; + } + return new \WP_Error( + $url->get_error_code(), + $message, + $url->get_error_data() ); } diff --git a/includes/functions.php b/includes/functions.php index 3c33664d8..1c1470f11 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -108,11 +108,19 @@ function get_webfinger_resource( $user_id ) { /** * [get_metadata_by_actor description] * - * @param sting $actor + * @param string $actor * * @return array */ function get_remote_metadata_by_actor( $actor ) { + if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $actor ) ) { + $actor = Rest\Webfinger::resolve( $actor ); + } + + if ( ! $actor ) { + return null; + } + $metadata = \get_transient( 'activitypub_' . $actor ); if ( $metadata ) { diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index eb96d116e..9de1031c2 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -75,7 +75,7 @@ public function from_remote_array( $array ) { } public function to_array() { - $array = \get_object_vars( $this ); + $array = array_filter( \get_object_vars( $this ) ); if ( $this->context ) { $array = array( '@context' => $this->context ) + $array; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 1ffe4514a..3408950ac 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -161,7 +161,6 @@ public static function user_inbox_post( $request ) { public static function shared_inbox_post( $request ) { $data = $request->get_params(); $type = $request->get_param( 'type' ); - $users = self::extract_recipients( $data ); if ( ! $users ) { @@ -407,6 +406,10 @@ public static function handle_reaction( $object, $user_id ) { public static function handle_create( $object, $user_id ) { $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + if ( ! isset( $object['object']['inReplyTo'] ) ) { + return; + } + $comment_post_id = \url_to_postid( $object['object']['inReplyTo'] ); // save only replys and reactions diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 60eb5d2fa..ebf389087 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -120,4 +120,44 @@ public static function add_webfinger_discovery( $array, $resource, $user ) { return $array; } + + public static function resolve( $account ) { + if ( ! preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $account, $m ) ) { + return null; + } + $url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[1] . '/.well-known/webfinger' ); + if ( ! \wp_http_validate_url( $url ) ) { + return new \WP_Error( 'invalid_webfinger_url', null, $url ); + } + + // try to access author URL + $response = \wp_remote_get( + $url, + array( + 'headers' => array( 'Accept' => 'application/activity+json' ), + 'redirection' => 0, + ) + ); + + if ( \is_wp_error( $response ) ) { + return new \WP_Error( 'webfinger_url_not_accessible', null, $url ); + } + + $response_code = \wp_remote_retrieve_response_code( $response ); + + $body = \wp_remote_retrieve_body( $response ); + $body = \json_decode( $body, true ); + + if ( ! isset( $body['links'] ) ) { + return new \WP_Error( 'webfinger_url_invalid_response', null, $url ); + } + + foreach ( $body['links'] as $link ) { + if ( $link['rel'] === 'self' && $link['type'] == 'application/activity+json' ) { + return $link['href']; + } + } + + return new \WP_Error( 'webfinger_url_no_activity_pub', null, $body ); + } } diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php new file mode 100644 index 000000000..164e6be33 --- /dev/null +++ b/integration/class-friends-feed-parser-activitypub.php @@ -0,0 +1,198 @@ +friends_feed = $friends_feed; + + \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); + \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); + \add_filter( 'friends_user_feed_activated', array( $this, 'follow_user' ), 10 ); + \add_filter( 'friends_user_feed_deactivated', array( $this, 'unfollow_user' ), 10 ); + \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); + } + + /** + * Determines if this is a supported feed and to what degree we feel it's supported. + * + * @param string $url The url. + * @param string $mime_type The mime type. + * @param string $title The title. + * @param string|null $content The content, it can't be assumed that it's always available. + * + * @return int Return 0 if unsupported, a positive value representing the confidence for the feed, use 10 if you're reasonably confident. + */ + public function feed_support_confidence( $url, $mime_type, $title, $content = null ) { + if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $url ) ) { + return 10; + } + + return 0; + } + + /** + * Format the feed title and autoselect the posts feed. + * + * @param array $feed_details The feed details. + * + * @return array The (potentially) modified feed details. + */ + public function update_feed_details( $feed_details ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $feed_details['url'] ); + if ( $meta && ! is_wp_error( $meta ) ) { + if ( isset( $meta['preferredUsername'] ) ) { + $feed_details['title'] = $meta['preferredUsername']; + } + $feed_details['url'] = $meta['id']; + } + + return $feed_details; + } + + public function friends_rewrite_incoming_url( $url, $incoming_url ) { + if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $incoming_url ) ) { + $resolved_url = \Activitypub\Rest\Webfinger::resolve( $incoming_url ); + if ( ! is_wp_error( $resolved_url ) ) { + return $resolved_url; + } + } + return $url; + } + + /** + * Discover the feeds available at the URL specified. + * + * @param string $content The content for the URL is already provided here. + * @param string $url The url to search. + * + * @return array A list of supported feeds at the URL. + */ + public function discover_available_feeds( $content, $url ) { + $discovered_feeds = array(); + + $meta = \Activitypub\get_remote_metadata_by_actor( $url ); + if ( $meta && ! is_wp_error( $meta ) ) { + $discovered_feeds[ $meta['id'] ] = array( + 'type' => 'application/activity+json', + 'rel' => 'self', + 'post-format' => 'autodetect', + 'parser' => self::SLUG, + 'autoselect' => true, + ); + } + return $discovered_feeds; + } + + /** + * Fetches a feed and returns the processed items. + * + * @param string $url The url. + * + * @return array An array of feed items. + */ + public function fetch_feed( $url ) { + // There is no feed to fetch, we'll receive items via ActivityPub. + return array(); + } + + /** + * Handles "Create" requests + * + * @param array $object The activity-object + * @param int $user_id The id of the local blog-user + */ + public function handle_received_activity( $object, $user_id ) { + $user_feed = $this->friends_feed->get_user_feed_by_url( $object['actor'] ); + if ( is_wp_error( $user_feed ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + $user_feed = $this->friends_feed->get_user_feed_by_url( $meta['url'] ); + if ( is_wp_error( $user_feed ) ) { + // We're not following this user. + return false; + } + } + switch ( $object['type'] ) { + case 'Accept': + // nothing to do. + break; + case 'Create': + $this->handle_incoming_post( $object['object'], $user_feed ); + + } + + return true; + } + + private function map_type_to_post_format( $type ) { + return 'status'; + } + + private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { + $item = new \Friends\Feed_Item( + array( + 'permalink' => $object['url'], + // 'title' => '', + 'content' => $object['content'], + 'post_format' => $this->map_type_to_post_format( $object['type'] ), + 'date' => $object['published'], + ) + ); + + $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + } + + public function follow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + $to = $meta['id']; + $inbox = \Activitypub\get_inbox_by_actor( $to ); + $user_id = get_current_user_id(); + $actor = \get_author_posts_url( $user_id ); + + $activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE ); + $activity->set_to( null ); + $activity->set_cc( null ); + $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_object( $to ); + $activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); + $activity = $activity->to_json(); + \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + } + + public function unfollow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + $to = $meta['id']; + $inbox = \Activitypub\get_inbox_by_actor( $to ); + $user_id = get_current_user_id(); + $actor = \get_author_posts_url( $user_id ); + + $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); + $activity->set_to( null ); + $activity->set_cc( null ); + $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_object( $to ); + $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); + $activity = $activity->to_json(); + \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + } +} diff --git a/templates/author-json.php b/templates/author-json.php index c5d39a5b2..d5a0b6933 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -16,7 +16,7 @@ $json->url = \get_author_posts_url( $author_id ); $json->icon = array( 'type' => 'Image', - 'url' => \get_avatar_url( $author_id, array( 'size' => 120 ) ), + 'url' => 'https://akirk.blog/wp-content/uploads/2022/11/alex.kirk-small.jpg', ); if ( \has_header_image() ) { From 04db99730d9bf0293a403860893d5a031f8223f1 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:17:59 -0700 Subject: [PATCH 02/19] phpcs --- includes/rest/class-webfinger.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index ebf389087..1b00d8625 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -153,7 +153,7 @@ public static function resolve( $account ) { } foreach ( $body['links'] as $link ) { - if ( $link['rel'] === 'self' && $link['type'] == 'application/activity+json' ) { + if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) { return $link['href']; } } From eff60ed5ddca8be8ee5b5a0f9f3ea31e16460b3e Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Sun, 6 Nov 2022 16:49:53 -0700 Subject: [PATCH 03/19] Fix the signature for HTTP GET requests --- includes/class-signature.php | 6 +++--- includes/functions.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 5caf88465..f78b87dc1 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -70,7 +70,7 @@ public static function generate_key_pair( $user_id ) { \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); } - public static function generate_signature( $user_id, $url, $date, $digest = null ) { + public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { $key = self::get_private_key( $user_id ); $url_parts = \wp_parse_url( $url ); @@ -89,9 +89,9 @@ public static function generate_signature( $user_id, $url, $date, $digest = null } if ( ! empty( $digest ) ) { - $signed_string = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest"; + $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest"; } else { - $signed_string = "(request-target): post $path\nhost: $host\ndate: $date"; + $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } $signature = null; diff --git a/includes/functions.php b/includes/functions.php index 1c1470f11..1b1269c84 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -35,7 +35,7 @@ function get_context() { function safe_remote_post( $url, $body, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); $digest = \Activitypub\Signature::generate_digest( $body ); - $signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date, $digest ); + $signature = \Activitypub\Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); @@ -63,7 +63,7 @@ function safe_remote_post( $url, $body, $user_id ) { function safe_remote_get( $url, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date ); + $signature = \Activitypub\Signature::generate_signature( $user_id, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); From 568b258c771dc73a0814ff5c1a36fd0f9df28989 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:27:05 -0700 Subject: [PATCH 04/19] undo temp change --- templates/author-json.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/author-json.php b/templates/author-json.php index d5a0b6933..c5d39a5b2 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -16,7 +16,7 @@ $json->url = \get_author_posts_url( $author_id ); $json->icon = array( 'type' => 'Image', - 'url' => 'https://akirk.blog/wp-content/uploads/2022/11/alex.kirk-small.jpg', + 'url' => \get_avatar_url( $author_id, array( 'size' => 120 ) ), ); if ( \has_header_image() ) { From 3def5832697fedc8ab72b5bd902767d5b102acf6 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 9 Nov 2022 07:27:50 -0700 Subject: [PATCH 05/19] typo --- includes/class-health-check.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-health-check.php b/includes/class-health-check.php index bdc3b0b6c..8989c2f1f 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -224,7 +224,7 @@ public static function is_webfinger_endpoint_accessible() { ), ); $message = null; - if ( isset( $messages[ $url->get_error_code() ] ) ) { + if ( isset( $health_messages[ $url->get_error_code() ] ) ) { $message = $health_messages[ $url->get_error_code() ]; } return new \WP_Error( From 4300c579aa64f77ed8951787ff2ff06d9205686c Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Mon, 14 Nov 2022 20:04:01 -0500 Subject: [PATCH 06/19] Queue the activitypub request --- .../class-friends-feed-parser-activitypub.php | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 164e6be33..293322fd8 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -20,8 +20,10 @@ public function __construct( \Friends\Feed $friends_feed ) { \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); - \add_filter( 'friends_user_feed_activated', array( $this, 'follow_user' ), 10 ); - \add_filter( 'friends_user_feed_deactivated', array( $this, 'unfollow_user' ), 10 ); + \add_filter( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); + \add_filter( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); + \add_filter( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10 ); + \add_filter( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10 ); \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); } @@ -154,6 +156,18 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); } + public function queue_follow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', array( $user_feed ) ) ) { + return; + } + + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', array( $user_feed ) ); + } + public function follow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { return; @@ -175,6 +189,18 @@ public function follow_user( \Friends\User_Feed $user_feed ) { \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } + public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { + if ( self::SLUG != $user_feed->get_parser() ) { + return; + } + + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ) ) { + return; + } + + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ); + } + public function unfollow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { return; From 8ab20c5de0efdaa4afc2b711ce6b26da87c89eff Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 15 Nov 2022 18:46:40 +0100 Subject: [PATCH 07/19] Don't use full object as cron parameters --- .../class-friends-feed-parser-activitypub.php | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 293322fd8..d2f75b5b5 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -20,10 +20,10 @@ public function __construct( \Friends\Feed $friends_feed ) { \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); - \add_filter( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); - \add_filter( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); - \add_filter( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10 ); - \add_filter( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10 ); + \add_action( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); + \add_action( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); + \add_action( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10, 2 ); + \add_action( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10, 2 ); \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); } @@ -161,14 +161,16 @@ public function queue_follow_user( \Friends\User_Feed $user_feed ) { return; } - if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', array( $user_feed ) ) ) { + $args = array( $user_feed->get_id(), get_current_user_id() ); + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ) ) { return; } - return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', array( $user_feed ) ); + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', $args ); } - public function follow_user( \Friends\User_Feed $user_feed ) { + public function follow_user( $user_feed_id, $user_id ) { + $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); if ( self::SLUG != $user_feed->get_parser() ) { return; } @@ -176,7 +178,6 @@ public function follow_user( \Friends\User_Feed $user_feed ) { $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); - $user_id = get_current_user_id(); $actor = \get_author_posts_url( $user_id ); $activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE ); @@ -194,14 +195,16 @@ public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { return; } - if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ) ) { + $args = array( $user_feed->get_id(), get_current_user_id() ); + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) { return; } - return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', array( $user_feed ) ); + return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', $args ); } - public function unfollow_user( \Friends\User_Feed $user_feed ) { + public function unfollow_user( $user_feed_id, $user_id ) { + $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); if ( self::SLUG != $user_feed->get_parser() ) { return; } @@ -209,7 +212,6 @@ public function unfollow_user( \Friends\User_Feed $user_feed ) { $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); - $user_id = get_current_user_id(); $actor = \get_author_posts_url( $user_id ); $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); From 4cc9cda67a5d309248270982751347dcbfd342c1 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 15 Nov 2022 20:04:01 +0100 Subject: [PATCH 08/19] Remove potentially queued reverse follow/unfollow events --- .../class-friends-feed-parser-activitypub.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index d2f75b5b5..ef5265d78 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -162,6 +162,13 @@ public function queue_follow_user( \Friends\User_Feed $user_feed ) { } $args = array( $user_feed->get_id(), get_current_user_id() ); + + $unfollow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ); + if ( $unfollow_timestamp ) { + // If we just unfollowed, we don't want the event to potentially be executed after our follow event. + wp_unschedule_event( $unfollow_timestamp, $args ); + } + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ) ) { return; } @@ -196,6 +203,13 @@ public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { } $args = array( $user_feed->get_id(), get_current_user_id() ); + + $follow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ); + if ( $follow_timestamp ) { + // If we just followed, we don't want the event to potentially be executed after our unfollow event. + wp_unschedule_event( $follow_timestamp, $args ); + } + if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) { return; } From 8320856e6a74f0ef81620ce705f72b86b9744634 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 18 Nov 2022 21:28:40 +0100 Subject: [PATCH 09/19] Suggest better display name and username --- .../class-friends-feed-parser-activitypub.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index ef5265d78..021f30e09 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -54,11 +54,18 @@ public function feed_support_confidence( $url, $mime_type, $title, $content = nu */ public function update_feed_details( $feed_details ) { $meta = \Activitypub\get_remote_metadata_by_actor( $feed_details['url'] ); - if ( $meta && ! is_wp_error( $meta ) ) { - if ( isset( $meta['preferredUsername'] ) ) { - $feed_details['title'] = $meta['preferredUsername']; - } - $feed_details['url'] = $meta['id']; + if ( ! $meta && is_wp_error( $meta ) ) { + return $meta; + } + + if ( isset( $meta['name'] ) ) { + $feed_details['title'] = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $feed_details['title'] = $meta['preferredUsername']; + } + + if ( isset( $meta['url'] ) ) { + $feed_details['url'] = $meta['url']; } return $feed_details; @@ -90,7 +97,7 @@ public function discover_available_feeds( $content, $url ) { $discovered_feeds[ $meta['id'] ] = array( 'type' => 'application/activity+json', 'rel' => 'self', - 'post-format' => 'autodetect', + 'post-format' => 'status', 'parser' => self::SLUG, 'autoselect' => true, ); From f2b77251cef1ff059fad14f2924426417d47779e Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 18 Nov 2022 21:41:54 +0100 Subject: [PATCH 10/19] Add doc blocks --- .../class-friends-feed-parser-activitypub.php | 82 ++++++++++++++----- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 021f30e09..9832fe826 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -15,6 +15,11 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { const URL = 'https://www.w3.org/TR/activitypub/'; private $friends_feed; + /** + * Constructor. + * + * @param \Friends\Feed $friends_feed The friends feed + */ public function __construct( \Friends\Feed $friends_feed ) { $this->friends_feed = $friends_feed; @@ -71,6 +76,14 @@ public function update_feed_details( $feed_details ) { return $feed_details; } + /** + * Rewrite a Mastodon style URL @username@server to a URL via webfinger. + * + * @param string $url The URL to filter. + * @param string $incoming_url Potentially a mastodon identifier. + * + * @return ( description_of_the_return_value ) + */ public function friends_rewrite_incoming_url( $url, $incoming_url ) { if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $incoming_url ) ) { $resolved_url = \Activitypub\Rest\Webfinger::resolve( $incoming_url ); @@ -145,10 +158,23 @@ public function handle_received_activity( $object, $user_id ) { return true; } + /** + * Map the Activity type to a post fomat. + * + * @param string $type The type. + * + * @return string The determined post format. + */ private function map_type_to_post_format( $type ) { return 'status'; } + /** + * We received a post for a feed, handle it. + * + * @param array $object The object from ActivityPub. + * @param \Friends\User_Feed $user_feed The user feed. + */ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { $item = new \Friends\Feed_Item( array( @@ -163,12 +189,19 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); } + /** + * Prepare to follow the user via a scheduled event. + * + * @param \Friends\User_Feed $user_feed The user feed. + * + * @return bool|WP_Error Whether the event was queued. + */ public function queue_follow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { return; } - $args = array( $user_feed->get_id(), get_current_user_id() ); + $args = array( $user_feed->get_url(), get_current_user_id() ); $unfollow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ); if ( $unfollow_timestamp ) { @@ -183,13 +216,14 @@ public function queue_follow_user( \Friends\User_Feed $user_feed ) { return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', $args ); } - public function follow_user( $user_feed_id, $user_id ) { - $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); - if ( self::SLUG != $user_feed->get_parser() ) { - return; - } - - $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + /** + * Follow a user via ActivityPub at a URL. + * + * @param string $url The url. + * @param int $user_id The current user id. + */ + public function follow_user( $url, $user_id ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $url ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); $actor = \get_author_posts_url( $user_id ); @@ -197,19 +231,26 @@ public function follow_user( $user_feed_id, $user_id ) { $activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE ); $activity->set_to( null ); $activity->set_cc( null ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_actor( $actor ); $activity->set_object( $to ); $activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } + /** + * Prepare to unfollow the user via a scheduled event. + * + * @param \Friends\User_Feed $user_feed The user feed. + * + * @return bool|WP_Error Whether the event was queued. + */ public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { if ( self::SLUG != $user_feed->get_parser() ) { - return; + return false; } - $args = array( $user_feed->get_id(), get_current_user_id() ); + $args = array( $user_feed->get_url(), get_current_user_id() ); $follow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ); if ( $follow_timestamp ) { @@ -218,19 +259,20 @@ public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { } if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) { - return; + return true; } return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', $args ); } - public function unfollow_user( $user_feed_id, $user_id ) { - $user_feed = \Friends\User_Feed::get_by_id( $user_feed_id ); - if ( self::SLUG != $user_feed->get_parser() ) { - return; - } - - $meta = \Activitypub\get_remote_metadata_by_actor( $user_feed->get_url() ); + /** + * Unfllow a user via ActivityPub at a URL. + * + * @param string $url The url. + * @param int $user_id The current user id. + */ + public function unfollow_user( $url, $user_id ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $url ); $to = $meta['id']; $inbox = \Activitypub\get_inbox_by_actor( $to ); $actor = \get_author_posts_url( $user_id ); @@ -238,7 +280,7 @@ public function unfollow_user( $user_feed_id, $user_id ) { $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); $activity->set_to( null ); $activity->set_cc( null ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_actor( $actor ); $activity->set_object( $to ); $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); From c2a19a175c13e648226cc45a34d1e93d3884a324 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 18 Nov 2022 22:04:39 +0100 Subject: [PATCH 11/19] Replace unfollow with undo follow --- .../class-friends-feed-parser-activitypub.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 9832fe826..49a89c8ca 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -69,8 +69,8 @@ public function update_feed_details( $feed_details ) { $feed_details['title'] = $meta['preferredUsername']; } - if ( isset( $meta['url'] ) ) { - $feed_details['url'] = $meta['url']; + if ( isset( $meta['id'] ) ) { + $feed_details['url'] = $meta['id']; } return $feed_details; @@ -277,11 +277,16 @@ public function unfollow_user( $url, $user_id ) { $inbox = \Activitypub\get_inbox_by_actor( $to ); $actor = \get_author_posts_url( $user_id ); - $activity = new \Activitypub\Model\Activity( 'Unfollow', \Activitypub\Model\Activity::TYPE_SIMPLE ); + $activity = new \Activitypub\Model\Activity( 'Undo', \Activitypub\Model\Activity::TYPE_SIMPLE ); $activity->set_to( null ); $activity->set_cc( null ); $activity->set_actor( $actor ); - $activity->set_object( $to ); + $activity->set_object( array( + 'type' => 'Follow', + 'actor' => $actor, + 'object' => $to, + 'id' => $to, + ) ); $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); From 60cf0889d08702aa9c70aa41ee83079eaeb1d4ed Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 25 Nov 2022 11:05:34 +0100 Subject: [PATCH 12/19] lint fixes --- .../class-friends-feed-parser-activitypub.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 49a89c8ca..480800f5d 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -179,7 +179,6 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) $item = new \Friends\Feed_Item( array( 'permalink' => $object['url'], - // 'title' => '', 'content' => $object['content'], 'post_format' => $this->map_type_to_post_format( $object['type'] ), 'date' => $object['published'], @@ -197,7 +196,7 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) * @return bool|WP_Error Whether the event was queued. */ public function queue_follow_user( \Friends\User_Feed $user_feed ) { - if ( self::SLUG != $user_feed->get_parser() ) { + if ( self::SLUG !== $user_feed->get_parser() ) { return; } @@ -246,7 +245,7 @@ public function follow_user( $url, $user_id ) { * @return bool|WP_Error Whether the event was queued. */ public function queue_unfollow_user( \Friends\User_Feed $user_feed ) { - if ( self::SLUG != $user_feed->get_parser() ) { + if ( self::SLUG !== $user_feed->get_parser() ) { return false; } @@ -281,12 +280,14 @@ public function unfollow_user( $url, $user_id ) { $activity->set_to( null ); $activity->set_cc( null ); $activity->set_actor( $actor ); - $activity->set_object( array( - 'type' => 'Follow', - 'actor' => $actor, - 'object' => $to, - 'id' => $to, - ) ); + $activity->set_object( + array( + 'type' => 'Follow', + 'actor' => $actor, + 'object' => $to, + 'id' => $to, + ) + ); $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); From 7036a659917dad33b94a0fada7f96b4e4f4c957f Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 11:30:52 +0100 Subject: [PATCH 13/19] Add support for announce activities --- .../class-friends-feed-parser-activitypub.php | 115 +++++++++++++++--- 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 480800f5d..cc4b2365a 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -23,8 +23,7 @@ class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser { public function __construct( \Friends\Feed $friends_feed ) { $this->friends_feed = $friends_feed; - \add_action( 'activitypub_inbox_create', array( $this, 'handle_received_activity' ), 10, 2 ); - \add_action( 'activitypub_inbox_accept', array( $this, 'handle_received_activity' ), 10, 2 ); + \add_action( 'activitypub_inbox', array( $this, 'handle_received_activity' ), 10, 3 ); \add_action( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 ); \add_action( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 ); \add_action( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10, 2 ); @@ -32,6 +31,16 @@ public function __construct( \Friends\Feed $friends_feed ) { \add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 ); } + /** + * Allow logging a message via an action. + * @param string $message The message to log. + * @param array $objects Optional objects as meta data. + * @return void + */ + private function log( $message, $objects = array() ) { + do_action( 'friends_activitypub_log', $message, $objects ); + } + /** * Determines if this is a supported feed and to what degree we feel it's supported. * @@ -135,23 +144,53 @@ public function fetch_feed( $url ) { * * @param array $object The activity-object * @param int $user_id The id of the local blog-user + * @param string $type The type of the activity. */ - public function handle_received_activity( $object, $user_id ) { - $user_feed = $this->friends_feed->get_user_feed_by_url( $object['actor'] ); - if ( is_wp_error( $user_feed ) ) { - $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); - $user_feed = $this->friends_feed->get_user_feed_by_url( $meta['url'] ); - if ( is_wp_error( $user_feed ) ) { - // We're not following this user. + public function handle_received_activity( $object, $user_id, $type ) { + if ( ! in_array( + $type, + array( + // We don't need to handle 'Accept' types since it's handled by the ActivityPub plugin itself. + 'create', + 'announce', + ) + ) ) { + return false; + } + + $actor_url = $object['actor']; + $user_feed = false; + if ( \wp_http_validate_url( $actor_url ) ) { + // Let's check if we follow this actor. If not it might be a different URL representation. + $user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url ); + } + + if ( is_wp_error( $user_feed ) || ! \wp_http_validate_url( $actor_url ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $actor_url ); + if ( ! $meta || ! isset( $meta['url'] ) ) { + $this->log( 'Received invalid meta for ' . $actor_url ); + return false; + } + + $actor_url = $meta['url']; + if ( ! \wp_http_validate_url( $actor_url ) ) { + $this->log( 'Received invalid meta url for ' . $actor_url ); return false; } } - switch ( $object['type'] ) { - case 'Accept': - // nothing to do. - break; - case 'Create': - $this->handle_incoming_post( $object['object'], $user_feed ); + + $user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url ); + if ( ! $user_feed || is_wp_error( $user_feed ) ) { + $this->log( 'We\'re not following ' . $actor_url ); + // We're not following this user. + return false; + } + + switch ( $type ) { + case 'create': + return $this->handle_incoming_post( $object['object'], $user_feed ); + case 'announce': + return $this->handle_incoming_announce( $object['object'], $user_feed, $user_id ); } @@ -188,6 +227,52 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); } + /** + * We received an announced URL (boost) for a feed, handle it. + * + * @param array $url The announced URL. + * @param \Friends\User_Feed $user_feed The user feed. + */ + private function handle_incoming_announce( $url, \Friends\User_Feed $user_feed, $user_id ) { + $this->log( 'Received announce for ' . $url ); + if ( ! \wp_http_validate_url( $url ) ) { + return false; + } + $response = \Activitypub\safe_remote_get( $url, $user_id ); + if ( \is_wp_error( $response ) ) { + return $response; + } + $json = \wp_remote_retrieve_body( $response ); + $object = \json_decode( $json, true ); + if ( ! $object ) { + $this->log( 'Received invalid json', compact( 'json' ) ); + return false; + } + $this->log( 'Received response', compact( 'url', 'object' ) ); + + $data = array( + 'permalink' => $url, + 'content' => $object['content'], + 'post_format' => $this->map_type_to_post_format( $object['type'] ), + 'date' => $object['published'], + ); + + if ( isset( $object['attributedTo'] ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); + $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); + if ( isset( $meta['name'] ) ) { + $data['author'] = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $data['author'] = $meta['preferredUsername']; + } + } + $this->log( 'Received feed item', compact( 'url', 'data' ) ); + + $item = new \Friends\Feed_Item( $data ); + + $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + } + /** * Prepare to follow the user via a scheduled event. * From a82dea0685363418885bf3141429e32a318dadd2 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 12:46:42 +0100 Subject: [PATCH 14/19] Add unit test --- .gitignore | 2 + includes/functions.php | 4 + .../class-friends-feed-parser-activitypub.php | 54 ++--- tests/bootstrap.php | 4 + ...-class-friends-feed-parser-activitypub.php | 200 ++++++++++++++++++ 5 files changed, 237 insertions(+), 27 deletions(-) create mode 100644 tests/test-class-friends-feed-parser-activitypub.php diff --git a/.gitignore b/.gitignore index 6012ccb8e..aa42b0085 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ composer.lock .DS_Store .idea/ .php_cs.cache +.phpunit.result.cache + diff --git a/includes/functions.php b/includes/functions.php index 1b1269c84..290bb57a3 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -113,6 +113,10 @@ function get_webfinger_resource( $user_id ) { * @return array */ function get_remote_metadata_by_actor( $actor ) { + $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); + if ( $pre ) { + return $pre; + } if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $actor ) ) { $actor = Rest\Webfinger::resolve( $actor ); } diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index cc4b2365a..80eabc795 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -153,7 +153,8 @@ public function handle_received_activity( $object, $user_id, $type ) { // We don't need to handle 'Accept' types since it's handled by the ActivityPub plugin itself. 'create', 'announce', - ) + ), + true ) ) { return false; } @@ -215,16 +216,35 @@ private function map_type_to_post_format( $type ) { * @param \Friends\User_Feed $user_feed The user feed. */ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { - $item = new \Friends\Feed_Item( + $data = array( + 'permalink' => $object['url'], + 'content' => $object['content'], + 'post_format' => $this->map_type_to_post_format( $object['type'] ), + 'date' => $object['published'], + ); + + if ( isset( $object['attributedTo'] ) ) { + $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); + $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); + if ( isset( $meta['name'] ) ) { + $override_author = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $override_author = $meta['preferredUsername']; + } + } + + $this->log( + 'Received feed item', array( - 'permalink' => $object['url'], - 'content' => $object['content'], - 'post_format' => $this->map_type_to_post_format( $object['type'] ), - 'date' => $object['published'], + 'url' => $object['url'], + 'data' => $data, ) ); + $item = new \Friends\Feed_Item( $data ); $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + + return true; } /** @@ -250,27 +270,7 @@ private function handle_incoming_announce( $url, \Friends\User_Feed $user_feed, } $this->log( 'Received response', compact( 'url', 'object' ) ); - $data = array( - 'permalink' => $url, - 'content' => $object['content'], - 'post_format' => $this->map_type_to_post_format( $object['type'] ), - 'date' => $object['published'], - ); - - if ( isset( $object['attributedTo'] ) ) { - $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); - $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); - if ( isset( $meta['name'] ) ) { - $data['author'] = $meta['name']; - } elseif ( isset( $meta['preferredUsername'] ) ) { - $data['author'] = $meta['preferredUsername']; - } - } - $this->log( 'Received feed item', compact( 'url', 'data' ) ); - - $item = new \Friends\Feed_Item( $data ); - - $this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed ); + return $this->handle_incoming_post( $object, $user_feed ); } /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9acf92048..67da18a92 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,6 +19,10 @@ */ function _manually_load_plugin() { require \dirname( \dirname( __FILE__ ) ) . '/activitypub.php'; + $friends_plugin = \dirname( \dirname( \dirname( __FILE__ ) ) ) . '/friends/friends.php'; + if ( file_exists( $friends_plugin ) ) { + require $friends_plugin; + } } \tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php new file mode 100644 index 000000000..a7ac7a8af --- /dev/null +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -0,0 +1,200 @@ +markTestSkipped( 'The Friends plugin is not loaded.' ); + } + parent::set_up(); + + // Manually activate the REST server. + global $wp_rest_server; + $wp_rest_server = new \Spy_REST_Server(); + $this->server = $wp_rest_server; + do_action( 'rest_api_init' ); + + add_filter( + 'rest_url', + function() { + return get_option( 'home' ) . '/wp-json/'; + } + ); + + add_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ), 10, 3 ); + add_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ), 10, 2 ); + add_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ), 10, 2 ); + add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); + + } + + public function tear_down() { + remove_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ) ); + remove_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ) ); + remove_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ) ); + remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) ); + } + + public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { + if ( isset( self::$users[ $actor ] ) ) { + return self::$users[ $actor ]; + } + return $pre; + } + public static function http_request_host_is_external( $in, $host ) { + if ( in_array( $host, array( 'mastodon.local' ), true ) ) { + return true; + } + return $in; + } + public static function http_request_args( $args, $url ) { + if ( in_array( parse_url( $url, PHP_URL_HOST ), array( 'mastodon.local' ), true ) ) { + $args['reject_unsafe_urls'] = false; + } + return $args; + } + public static function pre_http_request( $preempt, $request, $url ) { + $home_url = home_url(); + + // Pretend the url now is the requested one. + update_option( 'home', $p['scheme'] . '://' . $p['host'] ); + $rest_prefix = home_url() . '/wp-json'; + + if ( false === strpos( $url, $rest_prefix ) ) { + // Restore the old home_url. + update_option( 'home', $home_url ); + return $preempt; + } + + $url = substr( $url, strlen( $rest_prefix ) ); + $r = new \WP_REST_Request( $request['method'], $url ); + if ( ! empty( $request['body'] ) ) { + foreach ( $request['body'] as $key => $value ) { + $r->set_param( $key, $value ); + } + } + global $wp_rest_server; + $response = $wp_rest_server->dispatch( $r ); + // Restore the old url. + update_option( 'home', $home_url ); + + return apply_filters( + 'fake_http_response', + array( + 'headers' => array( + 'content-type' => 'text/json', + ), + 'body' => wp_json_encode( $response->data ), + 'response' => array( + 'code' => $response->status, + ), + ), + $p['scheme'] . '://' . $p['host'], + $url, + $request + ); + } + + public function test_incoming_post() { + $now = time() - 10; + $status_id = 123; + + $friend_name = 'Alex'; + $actor = 'https://mastodon.local/users/alex'; + + $friend_id = $this->factory->user->create( + array( + 'user_login' => 'alex-mastodon.local', + 'display_name' => $friend_name, + 'role' => 'friend', + ) + ); + \Friends\User_Feed::save( + new \Friends\User( $friend_id ), + $actor, + array( + 'parser' => 'activitypub', + ) + ); + + self::$users[ $actor ] = array( + 'url' => $actor, + 'name' => $friend_name, + ); + self::$users['https://mastodon.local/@alex'] = self::$users[ $actor ]; + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $friend_id, + ) + ); + + $this->assertEquals( 0, count( $posts ) ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', 'test1' ); + $request->set_param( 'actor', $actor ); + $date = date( \DATE_W3C, $now++ ); + $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => 'test1', + 'attributedTo' => $actor, + 'content' => $content, + 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $friend_id, + ) + ); + + $this->assertEquals( 1, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $friend_id, $posts[0]->post_author ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', 'test1' ); + $request->set_param( 'actor', 'https://mastodon.local/@alex' ); + $date = date( \DATE_W3C, $now++ ); + $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => 'test2', + 'attributedTo' => 'https://mastodon.local/@alex', + 'content' => $content, + 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $friend_id, + ) + ); + + $this->assertEquals( 2, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $friend_id, $posts[0]->post_author ); + } +} From b3a26788eb4e02784d65b3cf729c5df4fd2ce241 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 13:43:09 +0100 Subject: [PATCH 15/19] Add test for announce --- .../class-friends-feed-parser-activitypub.php | 16 +- tests/bootstrap.php | 2 + ...z-blog-2022-11-14-the-at-protocol.response | 1 + ...-class-friends-feed-parser-activitypub.php | 201 ++++++++++++++---- 4 files changed, 169 insertions(+), 51 deletions(-) create mode 100644 tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 80eabc795..79ded636d 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -156,9 +156,8 @@ public function handle_received_activity( $object, $user_id, $type ) { ), true ) ) { - return false; + return false; } - $actor_url = $object['actor']; $user_feed = false; if ( \wp_http_validate_url( $actor_url ) ) { @@ -216,8 +215,13 @@ private function map_type_to_post_format( $type ) { * @param \Friends\User_Feed $user_feed The user feed. */ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) { + $permalink = $object['id']; + if ( isset( $object['url'] ) ) { + $permalink = $object['url']; + } + $data = array( - 'permalink' => $object['url'], + 'permalink' => $permalink, 'content' => $object['content'], 'post_format' => $this->map_type_to_post_format( $object['type'] ), 'date' => $object['published'], @@ -236,7 +240,7 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) $this->log( 'Received feed item', array( - 'url' => $object['url'], + 'url' => $permalink, 'data' => $data, ) ); @@ -254,10 +258,12 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) * @param \Friends\User_Feed $user_feed The user feed. */ private function handle_incoming_announce( $url, \Friends\User_Feed $user_feed, $user_id ) { - $this->log( 'Received announce for ' . $url ); if ( ! \wp_http_validate_url( $url ) ) { + $this->log( 'Received invalid announce', compact( 'url' ) ); return false; } + $this->log( 'Received announce for ' . $url ); + $response = \Activitypub\safe_remote_get( $url, $user_id ); if ( \is_wp_error( $response ) ) { return $response; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 67da18a92..3867ab2f5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,6 +19,8 @@ */ function _manually_load_plugin() { require \dirname( \dirname( __FILE__ ) ) . '/activitypub.php'; + + // Load the Friends plugin if available to test the integrations. $friends_plugin = \dirname( \dirname( \dirname( __FILE__ ) ) ) . '/friends/friends.php'; if ( file_exists( $friends_plugin ) ) { require $friends_plugin; diff --git a/tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response b/tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response new file mode 100644 index 000000000..d6705a15c --- /dev/null +++ b/tests/fixtures/notiz-blog-2022-11-14-the-at-protocol.response @@ -0,0 +1 @@ +a:3:{s:7:"headers";a:15:{s:4:"date";s:29:"Fri, 02 Dec 2022 12:09:11 GMT";s:12:"content-type";s:25:"application/activity+json";s:6:"server";s:5:"nginx";s:15:"x-xrds-location";s:24:"https://notiz.blog/?xrds";s:16:"x-yadis-location";s:24:"https://notiz.blog/?xrds";s:10:"x-pingback";s:29:"https://notiz.blog/xmlrpc.php";s:4:"link";s:541:"; rel="micropub_media", ; rel="micropub", ; rel="friends-base-url", ; rel="webmention", ; rel="http://webmention.org/", ; rel="https://api.w.org/", ; rel="alternate"; type="application/json", ; rel=shortlink";s:13:"cache-control";s:17:"max-age=0, public";s:7:"expires";s:29:"Fri, 02 Dec 2022 12:09:11 GMT";s:16:"x-xss-protection";s:13:"1; mode=block";s:22:"x-content-type-options";s:7:"nosniff";s:25:"strict-transport-security";s:16:"max-age=31536000";s:15:"x-frame-options";s:10:"SAMEORIGIN";s:15:"referrer-policy";s:31:"strict-origin-when-cross-origin";s:17:"x-clacks-overhead";s:19:"GNU Terry Pratchett";}s:4:"body";s:18048:"{"@context":["https:\/\/www.w3.org\/ns\/activitystreams","https:\/\/w3id.org\/security\/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","PropertyValue":"schema:PropertyValue","schema":"http:\/\/schema.org#","pt":"https:\/\/joinpeertube.org\/ns#","toot":"http:\/\/joinmastodon.org\/ns#","value":"schema:value","Hashtag":"as:Hashtag","featured":{"@id":"toot:featured","@type":"@id"},"featuredTags":{"@id":"toot:featuredTags","@type":"@id"}}],"id":"https:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/","type":"Note","published":"2022-11-14T16:49:01Z","attributedTo":"https:\/\/notiz.blog\/author\/matthias-pfefferle\/","summary":null,"inReplyTo":null,"content":"\u003Cp\u003EVor zwei Jahren wollte Twitter in das \u201eDezentrale Netzwerke\u201c-Business einsteigen und gr\u00fcndete eigens daf\u00fcr das \u003Ca href=\u0022https:\/\/notiz.blog\/2019\/12\/13\/twitiverse\/\u0022 data-type=\u0022post\u0022 data-id=\u002218831\u0022\u003EProjekt Bluesky\u003C\/a\u003E. In den folgenden zwei Jahren wurde viel evaluiert und diskutiert, was wohl die beste L\u00f6sung f\u00fcr Twitter sei und wir alle \u003Cem\u003Efieberten\u003C\/em\u003E mit ob es nun \u003Ca href=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022\u003EActivityPub\u003C\/a\u003E oder doch \u003Ca href=\u0022https:\/\/matrix.org\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/matrix.org\/\u0022\u003EMatrix\u003C\/a\u003E werden w\u00fcrde\u2026 \u003C\/p\u003E\u003Cp\u003EAber das Warten hat ein Ende! \u003Ca href=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022\u003EBluesky hat verk\u00fcndet wie es weiter geht\u003C\/a\u003E!\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003ESie entwickeln ein neues Protokoll!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EDas \u003Cem\u003EAT Protocol\u003C\/em\u003E, kurz f\u00fcr \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E!\u003C\/p\u003E\u003Cp\u003EIch hab mir die \u003Ca href=\u0022https:\/\/atproto.com\/guides\/faq\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/atproto.com\/guides\/faq\u0022\u003EFAQ\u003C\/a\u003E mal angeschaut und dort steht warum Bluesky sich gegen ActivityPub entschieden hat:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EAccount portability is the major reason why we chose to build a separate protocol. We consider portability to be crucial because it protects users from sudden bans, server shutdowns, and policy disagreements. Our solution for portability requires both \u003Ca href=\u0022https:\/\/atproto.com\/guides\/data-repos\u0022\u003Esigned data repositories\u003C\/a\u003E and \u003Ca href=\u0022https:\/\/atproto.com\/guides\/identity\u0022\u003EDIDs\u003C\/a\u003E, neither of which are easy to retrofit into ActivityPub. The migration tools for ActivityPub are comparatively limited; they require the original server to provide a redirect and cannot migrate the user\u2019s previous data.\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003EDas erinnert mich ein bisschen an die Subline von meinem \u003Ca href=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022\u003EOpenWeb-Icons Font\u003C\/a\u003E:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EWhy \u003Cem\u003EOpenWeb Icons\u003C\/em\u003E? Because \u003Ca href=\u0022https:\/\/fortawesome.com\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/fortawesome.com\/\u0022\u003EFont Awesome\u003C\/a\u003E had no RSS-icon [\u2026]\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003E\u003Cstrong\u003EWeil ActivityPub keine perfekte L\u00f6sung f\u00fcr \u201eAccount portability\u201c hat, bauen sie ein komplett neues Protokoll?\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EActivityPub ist sicherlich nicht \u201efeature complete\u201c, aber ein guter erster Wurf, was das Fediverse erfolgreich bewiesen hat! Warum arbeitet Twitter also lieber an einem eigen Format anstatt mit dem W3C zusammen an ActivityPub v2?\u003C\/p\u003E\u003Cp\u003EWarum macht sich das W3C \u00fcberhaupt noch die M\u00fche \u201eStandards\u201c zu definieren?\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003EWegen der Interoperabilit\u00e4t!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EW\u00fcrde Twitter mit HTTP(S), HTML oder CSS \u00e4hnlich umgehen, w\u00fcrde der Browser einfach leer bleiben, weil das \u0026$%\u00a7\u0026 Internet nur mit einheitlichen Standards funktioniert!\u003C\/p\u003E\u003Cp\u003EUnd das gleiche gilt auch f\u00fcr dezentralte Netze, zumindest wenn sie erfolgreich sein wollen! Dar\u00fcber hab ich tragischerweise schon vor \u003Cstrong\u003E10 Jahren\u003C\/strong\u003E geschrieben!\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EDiaspora* wurde kaum \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20130630113539\/http:\/\/blog.diasporafoundation.org\/2012\/08\/27\/announcement-diaspora-will-now-be-a-community-project.html\u0022\u003Ef\u00fcr \u201etot\u201c erkl\u00e4rt\u003C\/a\u003E und schon steht das n\u00e4chste Projekt in den Startl\u00f6chern! \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20190603031810\/https:\/\/tent.io\/\u0022\u003ETent.io\u003C\/a\u003E soll ein protocol for distributed social networking and personal data storage werden. Alles neu, alles anders, alles besser als OStatus, DiSo oder Diaspora*. Aber mal ganz ehrlich\u2026 was haben die Diasporas \u0026 Co. bisher geschaffen? Ziel war es Facebooks \u201eWalled Gardens\u201c aufzubrechen und was kam wirklich dabei rum? Eine ganze Reihe an dezentralen \u201eWalled Gardens\u201c. Na danke!\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2012\/11\/15\/dezentrale-walled-gardens\/\u0022\u003EDezentrale \u201eWalled Gardens\u201c\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EDas \u003Ca href=\u0022https:\/\/the-federation.info\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/the-federation.info\/\u0022\u003Efediverse\u003C\/a\u003E hat (wie schon erw\u00e4hnt) bisher einen gro\u00dfartigen Job gemacht und verschiedenste Netzwerke mit den verschiedensten Auspr\u00e4gungen vernetzt! Ich \u003Cs\u003Eglaube\u003C\/s\u003E bin der festen \u00dcberzeugung, dass sich diesmal wirklich das offene Format (\u003Cem\u003EActivityPub\u003C\/em\u003E) durchsetzen wird und Blueskys \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E auch in ein paar Monaten oder Jahren keine Rolle spielen wird! \u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\u0022\u003EBen Werdmuller\u003C\/a\u003E hat eine gesunde Einstellung zu dem Thema:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EI\u2019m so burned out by open source social, but I\u2019m glad to see people throw energy at the problem, even if it\u2019s not how I would have gone about it.\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022\u003ETwitter\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EMehr hab ich dazu eigentlich nicht zu sagen, au\u00dfer dass wir in der \u003Ca href=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022\u003Eaktuellen Folge\u003C\/a\u003E des neunetzcasts sehr ausgiebig \u00fcber genau dieses Problem gesprochen haben!\u003C\/p\u003E\u003Cp\u003E\u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/activitypub\/\u0022\u003E#activitypub\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/bluesky\/\u0022\u003E#bluesky\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/fediverse\/\u0022\u003E#fediverse\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/matrix\/\u0022\u003E#matrix\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/twitter\/\u0022\u003E#twitter\u003C\/a\u003E\u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u0022\u003Ehttps:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u003C\/a\u003E\u003C\/p\u003E","contentMap":{"de":"\u003Cp\u003EVor zwei Jahren wollte Twitter in das \u201eDezentrale Netzwerke\u201c-Business einsteigen und gr\u00fcndete eigens daf\u00fcr das \u003Ca href=\u0022https:\/\/notiz.blog\/2019\/12\/13\/twitiverse\/\u0022 data-type=\u0022post\u0022 data-id=\u002218831\u0022\u003EProjekt Bluesky\u003C\/a\u003E. In den folgenden zwei Jahren wurde viel evaluiert und diskutiert, was wohl die beste L\u00f6sung f\u00fcr Twitter sei und wir alle \u003Cem\u003Efieberten\u003C\/em\u003E mit ob es nun \u003Ca href=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/www.w3.org\/TR\/activitypub\/\u0022\u003EActivityPub\u003C\/a\u003E oder doch \u003Ca href=\u0022https:\/\/matrix.org\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/matrix.org\/\u0022\u003EMatrix\u003C\/a\u003E werden w\u00fcrde\u2026 \u003C\/p\u003E\u003Cp\u003EAber das Warten hat ein Ende! \u003Ca href=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/blueskyweb.xyz\/blog\/10-18-2022-the-at-protocol\u0022\u003EBluesky hat verk\u00fcndet wie es weiter geht\u003C\/a\u003E!\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003ESie entwickeln ein neues Protokoll!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EDas \u003Cem\u003EAT Protocol\u003C\/em\u003E, kurz f\u00fcr \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E!\u003C\/p\u003E\u003Cp\u003EIch hab mir die \u003Ca href=\u0022https:\/\/atproto.com\/guides\/faq\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/atproto.com\/guides\/faq\u0022\u003EFAQ\u003C\/a\u003E mal angeschaut und dort steht warum Bluesky sich gegen ActivityPub entschieden hat:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EAccount portability is the major reason why we chose to build a separate protocol. We consider portability to be crucial because it protects users from sudden bans, server shutdowns, and policy disagreements. Our solution for portability requires both \u003Ca href=\u0022https:\/\/atproto.com\/guides\/data-repos\u0022\u003Esigned data repositories\u003C\/a\u003E and \u003Ca href=\u0022https:\/\/atproto.com\/guides\/identity\u0022\u003EDIDs\u003C\/a\u003E, neither of which are easy to retrofit into ActivityPub. The migration tools for ActivityPub are comparatively limited; they require the original server to provide a redirect and cannot migrate the user\u2019s previous data.\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003EDas erinnert mich ein bisschen an die Subline von meinem \u003Ca href=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/pfefferle.dev\/openwebicons\/\u0022\u003EOpenWeb-Icons Font\u003C\/a\u003E:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EWhy \u003Cem\u003EOpenWeb Icons\u003C\/em\u003E? Because \u003Ca href=\u0022https:\/\/fortawesome.com\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/fortawesome.com\/\u0022\u003EFont Awesome\u003C\/a\u003E had no RSS-icon [\u2026]\u003C\/p\u003E\n\u003C\/blockquote\u003E\u003Cp\u003E\u003Cstrong\u003EWeil ActivityPub keine perfekte L\u00f6sung f\u00fcr \u201eAccount portability\u201c hat, bauen sie ein komplett neues Protokoll?\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EActivityPub ist sicherlich nicht \u201efeature complete\u201c, aber ein guter erster Wurf, was das Fediverse erfolgreich bewiesen hat! Warum arbeitet Twitter also lieber an einem eigen Format anstatt mit dem W3C zusammen an ActivityPub v2?\u003C\/p\u003E\u003Cp\u003EWarum macht sich das W3C \u00fcberhaupt noch die M\u00fche \u201eStandards\u201c zu definieren?\u003C\/p\u003E\u003Cp\u003E\u003Cstrong\u003EWegen der Interoperabilit\u00e4t!\u003C\/strong\u003E\u003C\/p\u003E\u003Cp\u003EW\u00fcrde Twitter mit HTTP(S), HTML oder CSS \u00e4hnlich umgehen, w\u00fcrde der Browser einfach leer bleiben, weil das \u0026$%\u00a7\u0026 Internet nur mit einheitlichen Standards funktioniert!\u003C\/p\u003E\u003Cp\u003EUnd das gleiche gilt auch f\u00fcr dezentralte Netze, zumindest wenn sie erfolgreich sein wollen! Dar\u00fcber hab ich tragischerweise schon vor \u003Cstrong\u003E10 Jahren\u003C\/strong\u003E geschrieben!\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EDiaspora* wurde kaum \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20130630113539\/http:\/\/blog.diasporafoundation.org\/2012\/08\/27\/announcement-diaspora-will-now-be-a-community-project.html\u0022\u003Ef\u00fcr \u201etot\u201c erkl\u00e4rt\u003C\/a\u003E und schon steht das n\u00e4chste Projekt in den Startl\u00f6chern! \u003Ca href=\u0022https:\/\/web.archive.org\/web\/20190603031810\/https:\/\/tent.io\/\u0022\u003ETent.io\u003C\/a\u003E soll ein protocol for distributed social networking and personal data storage werden. Alles neu, alles anders, alles besser als OStatus, DiSo oder Diaspora*. Aber mal ganz ehrlich\u2026 was haben die Diasporas \u0026 Co. bisher geschaffen? Ziel war es Facebooks \u201eWalled Gardens\u201c aufzubrechen und was kam wirklich dabei rum? Eine ganze Reihe an dezentralen \u201eWalled Gardens\u201c. Na danke!\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2012\/11\/15\/dezentrale-walled-gardens\/\u0022\u003EDezentrale \u201eWalled Gardens\u201c\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EDas \u003Ca href=\u0022https:\/\/the-federation.info\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/the-federation.info\/\u0022\u003Efediverse\u003C\/a\u003E hat (wie schon erw\u00e4hnt) bisher einen gro\u00dfartigen Job gemacht und verschiedenste Netzwerke mit den verschiedensten Auspr\u00e4gungen vernetzt! Ich \u003Cs\u003Eglaube\u003C\/s\u003E bin der festen \u00dcberzeugung, dass sich diesmal wirklich das offene Format (\u003Cem\u003EActivityPub\u003C\/em\u003E) durchsetzen wird und Blueskys \u003Cem\u003EAuthenticated Transfer Protocol\u003C\/em\u003E auch in ein paar Monaten oder Jahren keine Rolle spielen wird! \u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\u0022\u003EBen Werdmuller\u003C\/a\u003E hat eine gesunde Einstellung zu dem Thema:\u003C\/p\u003E\u003Cblockquote class=\u0022wp-block-quote\u0022\u003E\n\u003Cp\u003EI\u2019m so burned out by open source social, but I\u2019m glad to see people throw energy at the problem, even if it\u2019s not how I would have gone about it.\u003C\/p\u003E\n\u003Ccite\u003E\u003Ca href=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/twitter.com\/benwerd\/status\/1582554417693270016\u0022\u003ETwitter\u003C\/a\u003E\u003C\/cite\u003E\u003C\/blockquote\u003E\u003Cp\u003EMehr hab ich dazu eigentlich nicht zu sagen, au\u00dfer dass wir in der \u003Ca href=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022 data-type=\u0022URL\u0022 data-id=\u0022https:\/\/neunetz.fm\/neunetzcast-93-was-wir-unter-dezentralitaet-verstehen-und-was-wir-uns-davon-erhoffen\/\u0022\u003Eaktuellen Folge\u003C\/a\u003E des neunetzcasts sehr ausgiebig \u00fcber genau dieses Problem gesprochen haben!\u003C\/p\u003E\u003Cp\u003E\u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/activitypub\/\u0022\u003E#activitypub\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/bluesky\/\u0022\u003E#bluesky\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/fediverse\/\u0022\u003E#fediverse\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/matrix\/\u0022\u003E#matrix\u003C\/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022u-tag u-category\u0022 href=\u0022https:\/\/notiz.blog\/tag\/twitter\/\u0022\u003E#twitter\u003C\/a\u003E\u003C\/p\u003E\u003Cp\u003E\u003Ca href=\u0022https:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u0022\u003Ehttps:\/\/notiz.blog\/2022\/11\/14\/the-at-protocol\/\u003C\/a\u003E\u003C\/p\u003E"},"to":["https:\/\/www.w3.org\/ns\/activitystreams#Public"],"cc":["https:\/\/www.w3.org\/ns\/activitystreams#Public"],"attachment":[{"type":"Image","url":"https:\/\/notiz.blog\/wp-content\/uploads\/2022\/11\/the-at-protocol.png","mediaType":"image\/png","name":"The Logo of the AT Protocol"}],"tag":[{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/activitypub\/","name":"#activitypub"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/bluesky\/","name":"#bluesky"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/fediverse\/","name":"#fediverse"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/matrix\/","name":"#matrix"},{"type":"Hashtag","href":"https:\/\/notiz.blog\/tag\/twitter\/","name":"#twitter"}]}";s:8:"response";a:1:{s:4:"code";i:200;}} \ No newline at end of file diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index a7ac7a8af..6d1568f9e 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -2,6 +2,9 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { public static $users = array(); + private $friend_id; + private $friend_name; + private $actor; public function set_up() { if ( ! class_exists( '\Friends\Friends' ) ) { @@ -23,14 +26,52 @@ function() { ); add_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ), 10, 3 ); + add_filter( 'http_response', array( get_called_class(), 'http_response' ), 10, 3 ); add_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ), 10, 2 ); add_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ), 10, 2 ); add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); + $user_id = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $user_id ); + + $this->friend_name = 'Alex Kirk'; + $this->actor = 'https://mastodon.local/users/akirk'; + + $user_feed = \Friends\User_Feed::get_by_url( $this->actor ); + if ( is_wp_error( $user_feed ) ) { + $this->friend_id = $this->factory->user->create( + array( + 'display_name' => $this->friend_name, + 'role' => 'friend', + ) + ); + \Friends\User_Feed::save( + new \Friends\User( $this->friend_id ), + $this->actor, + array( + 'parser' => 'activitypub', + ) + ); + } else { + $this->friend_id = $user_feed->get_friend_user()->ID; + } + + self::$users[ $this->actor ] = array( + 'url' => $this->actor, + 'name' => $this->friend_name, + ); + self::$users['https://mastodon.local/@akirk'] = self::$users[ $this->actor ]; + + _delete_all_posts(); } public function tear_down() { remove_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ) ); + remove_filter( 'http_response', array( get_called_class(), 'http_response' ) ); remove_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ) ); remove_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ) ); remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) ); @@ -55,6 +96,18 @@ public static function http_request_args( $args, $url ) { return $args; } public static function pre_http_request( $preempt, $request, $url ) { + $p = wp_parse_url( $url ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + if ( file_exists( $cache ) ) { + return apply_filters( + 'fake_http_response', + unserialize( file_get_contents( $cache ) ), + $p['scheme'] . '://' . $p['host'], + $url, + $request + ); + } + $home_url = home_url(); // Pretend the url now is the requested one. @@ -96,57 +149,58 @@ public static function pre_http_request( $preempt, $request, $url ) { ); } + public static function http_response( $response, $args, $url ) { + $p = wp_parse_url( $url ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + if ( ! file_exists( $cache ) ) { + $headers = wp_remote_retrieve_headers( $response ); + file_put_contents( + $cache, + serialize( + array( + 'headers' => $headers->getAll(), + 'body' => wp_remote_retrieve_body( $response ), + 'response' => array( + 'code' => wp_remote_retrieve_response_code( $response ), + ), + ) + ) + ); + } + return $response; + } + public function test_incoming_post() { $now = time() - 10; $status_id = 123; - $friend_name = 'Alex'; - $actor = 'https://mastodon.local/users/alex'; - - $friend_id = $this->factory->user->create( - array( - 'user_login' => 'alex-mastodon.local', - 'display_name' => $friend_name, - 'role' => 'friend', - ) - ); - \Friends\User_Feed::save( - new \Friends\User( $friend_id ), - $actor, - array( - 'parser' => 'activitypub', - ) - ); - - self::$users[ $actor ] = array( - 'url' => $actor, - 'name' => $friend_name, - ); - self::$users['https://mastodon.local/@alex'] = self::$users[ $actor ]; - $posts = get_posts( array( 'post_type' => \Friends\Friends::CPT, - 'author' => $friend_id, + 'author' => $this->friend_id, ) ); - $this->assertEquals( 0, count( $posts ) ); + $post_count = count( $posts ); + + // Let's post a new Note through the REST API. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', 'test1' ); - $request->set_param( 'actor', $actor ); - $date = date( \DATE_W3C, $now++ ); - $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + $request->set_param( 'object', array( 'type' => 'Note', - 'id' => 'test1', - 'attributedTo' => $actor, + 'id' => $id, + 'attributedTo' => $this->actor, 'content' => $content, - 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), 'published' => $date, ) ); @@ -157,28 +211,31 @@ public function test_incoming_post() { $posts = get_posts( array( 'post_type' => \Friends\Friends::CPT, - 'author' => $friend_id, + 'author' => $this->friend_id, ) ); - $this->assertEquals( 1, count( $posts ) ); + $this->assertEquals( $post_count + 1, count( $posts ) ); $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + + // Do another test post, this time with a URL that has an @-id. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', 'test1' ); - $request->set_param( 'actor', 'https://mastodon.local/@alex' ); - $date = date( \DATE_W3C, $now++ ); - $content = 'Test ' . $date . ' ' . rand(); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', 'https://mastodon.local/@akirk' ); $request->set_param( 'object', array( 'type' => 'Note', - 'id' => 'test2', - 'attributedTo' => 'https://mastodon.local/@alex', + 'id' => $id, + 'attributedTo' => 'https://mastodon.local/@akirk', 'content' => $content, - 'url' => 'https://mastodon.local/users/alex/statuses/' . ( $status_id++ ), + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), 'published' => $date, ) ); @@ -189,12 +246,64 @@ public function test_incoming_post() { $posts = get_posts( array( 'post_type' => \Friends\Friends::CPT, - 'author' => $friend_id, + 'author' => $this->friend_id, ) ); - $this->assertEquals( 2, count( $posts ) ); + $this->assertEquals( $post_count + 2, count( $posts ) ); $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + } + + public function test_incoming_announce() { + $now = time() - 10; + $status_id = 123; + + self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array( + 'url' => 'https://notiz.blog/author/matthias-pfefferle/', + 'name' => 'Matthias Pfefferle', + ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $post_count = count( $posts ); + + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + + $object = 'https://notiz.blog/2022/11/14/the-at-protocol/'; + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Announce' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + $request->set_param( 'published', $date ); + $request->set_param( 'object', $object ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $p = wp_parse_url( $object ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + $this->assertFileExists( $cache ); + + $object = json_decode( wp_remote_retrieve_body( unserialize( file_get_contents( $cache ) ) ) ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 1, count( $posts ) ); + $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + } } From db9e69f6e860992bc52a4be985d238056beb081f Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 13:59:00 +0100 Subject: [PATCH 16/19] Fix author name override for announced posts --- integration/class-friends-feed-parser-activitypub.php | 4 ++-- tests/test-class-friends-feed-parser-activitypub.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 79ded636d..69b1c76fe 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -231,9 +231,9 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); if ( isset( $meta['name'] ) ) { - $override_author = $meta['name']; + $data['author'] = $meta['name']; } elseif ( isset( $meta['preferredUsername'] ) ) { - $override_author = $meta['preferredUsername']; + $data['author'] = $meta['preferredUsername']; } } diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index 6d1568f9e..4bbd44a7c 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -253,6 +253,7 @@ public function test_incoming_post() { $this->assertEquals( $post_count + 2, count( $posts ) ); $this->assertEquals( $content, $posts[0]->post_content ); $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) ); } public function test_incoming_announce() { @@ -304,6 +305,7 @@ public function test_incoming_announce() { $this->assertEquals( $post_count + 1, count( $posts ) ); $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); } } From 2542127d720e07b456291fc577a145fa31637427 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 14:00:07 +0100 Subject: [PATCH 17/19] Move tests to front of file --- ...-class-friends-feed-parser-activitypub.php | 279 +++++++++--------- 1 file changed, 140 insertions(+), 139 deletions(-) diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index 4bbd44a7c..397eaabd9 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -6,6 +6,146 @@ class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase { private $friend_name; private $actor; + public function test_incoming_post() { + $now = time() - 10; + $status_id = 123; + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $post_count = count( $posts ); + + // Let's post a new Note through the REST API. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => $id, + 'attributedTo' => $this->actor, + 'content' => $content, + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 1, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + + // Do another test post, this time with a URL that has an @-id. + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + $content = 'Test ' . $date . ' ' . rand(); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Create' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', 'https://mastodon.local/@akirk' ); + $request->set_param( + 'object', + array( + 'type' => 'Note', + 'id' => $id, + 'attributedTo' => 'https://mastodon.local/@akirk', + 'content' => $content, + 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), + 'published' => $date, + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 2, count( $posts ) ); + $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) ); + } + + public function test_incoming_announce() { + $now = time() - 10; + $status_id = 123; + + self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array( + 'url' => 'https://notiz.blog/author/matthias-pfefferle/', + 'name' => 'Matthias Pfefferle', + ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $post_count = count( $posts ); + + $date = gmdate( \DATE_W3C, $now++ ); + $id = 'test' . $status_id; + + $object = 'https://notiz.blog/2022/11/14/the-at-protocol/'; + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); + $request->set_param( 'type', 'Announce' ); + $request->set_param( 'id', $id ); + $request->set_param( 'actor', $this->actor ); + $request->set_param( 'published', $date ); + $request->set_param( 'object', $object ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 202, $response->get_status() ); + + $p = wp_parse_url( $object ); + $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; + $this->assertFileExists( $cache ); + + $object = json_decode( wp_remote_retrieve_body( unserialize( file_get_contents( $cache ) ) ) ); + + $posts = get_posts( + array( + 'post_type' => \Friends\Friends::CPT, + 'author' => $this->friend_id, + ) + ); + + $this->assertEquals( $post_count + 1, count( $posts ) ); + $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); + $this->assertEquals( $this->friend_id, $posts[0]->post_author ); + $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); + + } + + public function set_up() { if ( ! class_exists( '\Friends\Friends' ) ) { return $this->markTestSkipped( 'The Friends plugin is not loaded.' ); @@ -169,143 +309,4 @@ public static function http_response( $response, $args, $url ) { } return $response; } - - public function test_incoming_post() { - $now = time() - 10; - $status_id = 123; - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $post_count = count( $posts ); - - // Let's post a new Note through the REST API. - $date = gmdate( \DATE_W3C, $now++ ); - $id = 'test' . $status_id; - $content = 'Test ' . $date . ' ' . rand(); - - $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); - $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', $id ); - $request->set_param( 'actor', $this->actor ); - - $request->set_param( - 'object', - array( - 'type' => 'Note', - 'id' => $id, - 'attributedTo' => $this->actor, - 'content' => $content, - 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), - 'published' => $date, - ) - ); - - $response = $this->server->dispatch( $request ); - $this->assertEquals( 202, $response->get_status() ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $this->assertEquals( $post_count + 1, count( $posts ) ); - $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $this->friend_id, $posts[0]->post_author ); - - // Do another test post, this time with a URL that has an @-id. - $date = gmdate( \DATE_W3C, $now++ ); - $id = 'test' . $status_id; - $content = 'Test ' . $date . ' ' . rand(); - - $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); - $request->set_param( 'type', 'Create' ); - $request->set_param( 'id', $id ); - $request->set_param( 'actor', 'https://mastodon.local/@akirk' ); - $request->set_param( - 'object', - array( - 'type' => 'Note', - 'id' => $id, - 'attributedTo' => 'https://mastodon.local/@akirk', - 'content' => $content, - 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), - 'published' => $date, - ) - ); - - $response = $this->server->dispatch( $request ); - $this->assertEquals( 202, $response->get_status() ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $this->assertEquals( $post_count + 2, count( $posts ) ); - $this->assertEquals( $content, $posts[0]->post_content ); - $this->assertEquals( $this->friend_id, $posts[0]->post_author ); - $this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) ); - } - - public function test_incoming_announce() { - $now = time() - 10; - $status_id = 123; - - self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array( - 'url' => 'https://notiz.blog/author/matthias-pfefferle/', - 'name' => 'Matthias Pfefferle', - ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $post_count = count( $posts ); - - $date = gmdate( \DATE_W3C, $now++ ); - $id = 'test' . $status_id; - - $object = 'https://notiz.blog/2022/11/14/the-at-protocol/'; - - $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' ); - $request->set_param( 'type', 'Announce' ); - $request->set_param( 'id', $id ); - $request->set_param( 'actor', $this->actor ); - $request->set_param( 'published', $date ); - $request->set_param( 'object', $object ); - - $response = $this->server->dispatch( $request ); - $this->assertEquals( 202, $response->get_status() ); - - $p = wp_parse_url( $object ); - $cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response'; - $this->assertFileExists( $cache ); - - $object = json_decode( wp_remote_retrieve_body( unserialize( file_get_contents( $cache ) ) ) ); - - $posts = get_posts( - array( - 'post_type' => \Friends\Friends::CPT, - 'author' => $this->friend_id, - ) - ); - - $this->assertEquals( $post_count + 1, count( $posts ) ); - $this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content ); - $this->assertEquals( $this->friend_id, $posts[0]->post_author ); - $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); - - } } From 57a95fad0108bfb0abbb4ea03115ca1252d36ae1 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 2 Dec 2022 14:27:00 +0100 Subject: [PATCH 18/19] Add attachment support --- .../class-friends-feed-parser-activitypub.php | 23 +++++++++++++++++++ ...-class-friends-feed-parser-activitypub.php | 20 +++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 69b1c76fe..34f3d77da 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -237,6 +237,29 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) } } + if ( ! empty( $object['attachment'] ) ) { + foreach ( $object['attachment'] as $attachment ) { + if ( ! isset( $attachment['type'] ) || ! isset( $attachment['mediaType'] ) ) { + continue; + } + if ( 'Document' !== $attachment['type'] || strpos( $attachment['mediaType'], 'image/' ) !== 0 ) { + continue; + } + + $data['content'] .= PHP_EOL; + $data['content'] .= ''; + $data['content'] .= '

'; + $data['content'] .= ''; + } + $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); + $this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) ); + if ( isset( $meta['name'] ) ) { + $data['author'] = $meta['name']; + } elseif ( isset( $meta['preferredUsername'] ) ) { + $data['author'] = $meta['preferredUsername']; + } + } + $this->log( 'Received feed item', array( diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index 397eaabd9..f48efb155 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -29,6 +29,9 @@ public function test_incoming_post() { $request->set_param( 'id', $id ); $request->set_param( 'actor', $this->actor ); + $attachment_url = 'https://mastodon.local/files/original/1234.png'; + $attachment_width = 400; + $attachment_height = 600; $request->set_param( 'object', array( @@ -38,6 +41,18 @@ public function test_incoming_post() { 'content' => $content, 'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ), 'published' => $date, + 'attachment' => array( + array( + 'type' => 'Document', + 'mediaType' => 'image/png', + 'url' => $attachment_url, + 'name' => '', + 'blurhash' => '', + 'width' => $attachment_width, + 'height' => $attachment_height, + + ), + ), ) ); @@ -52,7 +67,8 @@ public function test_incoming_post() { ); $this->assertEquals( $post_count + 1, count( $posts ) ); - $this->assertEquals( $content, $posts[0]->post_content ); + $this->assertStringStartsWith( $content, $posts[0]->post_content ); + $this->assertStringContainsString( '', $posts[0]->post_content ); $this->assertEquals( $this->friend_id, $posts[0]->post_author ); // Do another test post, this time with a URL that has an @-id. @@ -144,8 +160,6 @@ public function test_incoming_announce() { $this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) ); } - - public function set_up() { if ( ! class_exists( '\Friends\Friends' ) ) { return $this->markTestSkipped( 'The Friends plugin is not loaded.' ); From f916bca388e3c0e4977c36e4ba732b2ce0f938bb Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Sat, 3 Dec 2022 07:38:02 +0100 Subject: [PATCH 19/19] Add a css class to the image attachments --- integration/class-friends-feed-parser-activitypub.php | 2 +- tests/test-class-friends-feed-parser-activitypub.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/class-friends-feed-parser-activitypub.php b/integration/class-friends-feed-parser-activitypub.php index 34f3d77da..db155c1cd 100644 --- a/integration/class-friends-feed-parser-activitypub.php +++ b/integration/class-friends-feed-parser-activitypub.php @@ -248,7 +248,7 @@ private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) $data['content'] .= PHP_EOL; $data['content'] .= ''; - $data['content'] .= '

'; + $data['content'] .= '

'; $data['content'] .= ''; } $meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] ); diff --git a/tests/test-class-friends-feed-parser-activitypub.php b/tests/test-class-friends-feed-parser-activitypub.php index f48efb155..79f5a74f2 100644 --- a/tests/test-class-friends-feed-parser-activitypub.php +++ b/tests/test-class-friends-feed-parser-activitypub.php @@ -68,7 +68,7 @@ public function test_incoming_post() { $this->assertEquals( $post_count + 1, count( $posts ) ); $this->assertStringStartsWith( $content, $posts[0]->post_content ); - $this->assertStringContainsString( '', $posts[0]->post_content ); + $this->assertStringContainsString( 'post_content ); $this->assertEquals( $this->friend_id, $posts[0]->post_author ); // Do another test post, this time with a URL that has an @-id.