From 00a5e2e44eccc0d85de4f5b0e3600faf10cfb056 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 14:11:40 +0100 Subject: [PATCH 01/34] rename dispatcher --- includes/{class-activity-dispatcher.php => class-dispatcher.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename includes/{class-activity-dispatcher.php => class-dispatcher.php} (100%) diff --git a/includes/class-activity-dispatcher.php b/includes/class-dispatcher.php similarity index 100% rename from includes/class-activity-dispatcher.php rename to includes/class-dispatcher.php From f224701c11ce6dd89b521878a6f1a7aaad3300a9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 14:48:31 +0100 Subject: [PATCH 02/34] Simple dispatcher based on the Outbox-Collection This is a simple rewrite of the current dispatcher system, to use the Outbox instead of the Scheduler. This is a first draft and will be improved over time, to better handle: * Re-tries * Errors * Logging * Batch processing --- activitypub.php | 2 +- includes/class-dispatcher.php | 225 +++++++--------------------------- 2 files changed, 42 insertions(+), 185 deletions(-) diff --git a/activitypub.php b/activitypub.php index cb52d0c16..4446452bf 100644 --- a/activitypub.php +++ b/activitypub.php @@ -65,7 +65,7 @@ function rest_init() { function plugin_init() { \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 670a3a2f2..422ae8195 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -1,37 +1,29 @@ get_wp_user_id() ) && - ! is_user_disabled( Actors::BLOG_USER_ID ) - ) { - $transformer->change_wp_user_id( Actors::BLOG_USER_ID ); - } - - if ( null !== $user_id ) { - $transformer->change_wp_user_id( $user_id ); - } - - $user_id = $transformer->get_wp_user_id(); - - if ( is_user_disabled( $user_id ) ) { - return; - } - - $activity = $transformer->to_activity( $type ); - - self::send_activity_to_followers( $activity, $user_id, $wp_object ); - } - - /** - * Send Announces to followers and mentioned users. + * Process the outbox. * - * @param mixed $wp_object The ActivityPub Post. - * @param string $type The Activity-Type. + * @param int $id The outbox ID. */ - public static function send_announce( $wp_object, $type ) { - if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) { - return; - } + public static function process_outbox( $id ) { + $activity = \get_post( $id ); - if ( is_user_disabled( Actors::BLOG_USER_ID ) ) { + // If the activity is not a post, return. + if ( ! $activity ) { return; } - $transformer = Factory::get_transformer( $wp_object ); + $actor_type = \get_post_meta( $activity->ID, '_activitypub_activity_actor', true ); - if ( \is_wp_error( $transformer ) ) { - return; + switch ( $actor_type ) { + case 'blog': + $actor_id = Actors::BLOG_USER_ID; + break; + case 'application': + $actor_id = Actors::APPLICATION_USER_ID; + break; + case 'user': + default: + $actor_id = $activity->post_author; + break; } - $user_id = Actors::BLOG_USER_ID; - $activity = $transformer->to_activity( $type ); - $user = Actors::get_by_id( Actors::BLOG_USER_ID ); - - $announce = new Activity(); - $announce->set_type( 'Announce' ); - $announce->set_object( $activity ); - $announce->set_actor( $user->get_id() ); + $type = \get_post_meta( $activity->ID, '_activitypub_activity_type', true ); + $transformer = Factory::get_transformer( $activity->post_content ); + $activity = $transformer->to_activity( $type ); - self::send_activity_to_followers( $announce, $user_id, $wp_object ); - } - - /** - * Send a "Update" Activity when a user updates their profile. - * - * @param int $user_id The user ID to send an update for. - */ - public static function send_profile_update( $user_id ) { - $user = Actors::get_by_various( $user_id ); - - // Bail if that's not a good user. - if ( is_wp_error( $user ) ) { - return; - } - - // Build the update. - $activity = new Activity(); - $activity->set_type( 'Update' ); - $activity->set_actor( $user->get_id() ); - $activity->set_object( $user->get_id() ); - $activity->set_to( array( 'https://www.w3.org/ns/activitystreams#Public' ) ); - - // Send the update. - self::send_activity_to_followers( $activity, $user_id, $user ); + self::send_activity_to_followers( $activity, $actor_id ); } /** * Send an Activity to all followers and mentioned users. * * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. */ - private static function send_activity_to_followers( $activity, $user_id, $wp_object ) { + private static function send_activity_to_followers( $activity, $actor_id, $wp_object = null ) { /** * Filters whether to send an Activity to followers. * * @param bool $send_activity_to_followers Whether to send the Activity to followers. * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) { + if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $wp_object ) ) { return; } @@ -175,10 +90,10 @@ private static function send_activity_to_followers( $activity, $user_id, $wp_obj * Filters the list of inboxes to send the Activity to. * * @param array $inboxes The list of inboxes to send to. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param Activity $activity The ActivityPub Activity. */ - $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $user_id, $activity ); + $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $actor_id, $activity ); $inboxes = array_unique( $inboxes ); if ( empty( $inboxes ) ) { @@ -188,80 +103,22 @@ private static function send_activity_to_followers( $activity, $user_id, $wp_obj $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + safe_remote_post( $inbox, $json, $actor_id ); } set_wp_object_state( $wp_object, 'federated' ); } - /** - * Send a "Create" or "Update" Activity for a WordPress Post. - * - * @param int $id The WordPress Post ID. - * @param string $type The Activity-Type. - */ - public static function send_post( $id, $type ) { - $post = get_post( $id ); - - if ( ! $post || is_post_disabled( $post ) ) { - return; - } - - /** - * Fires when an Activity is being sent for any object type. - * - * @param WP_Post $post The WordPress Post. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $post, $type ); - - /** - * Fires when a specific type of Activity is being sent. - * - * @param WP_Post $post The WordPress Post. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $post ); - } - - /** - * Send a "Create" or "Update" Activity for a WordPress Comment. - * - * @param int $id The WordPress Comment ID. - * @param string $type The Activity-Type. - */ - public static function send_comment( $id, $type ) { - $comment = get_comment( $id ); - - if ( ! $comment ) { - return; - } - - /** - * Fires when an Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $comment, $type ); - - /** - * Fires when a specific type of Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $comment ); - } - /** * Default filter to add Inboxes of Followers. * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. * * @return array The filtered Inboxes */ - public static function add_inboxes_of_follower( $inboxes, $user_id ) { - $follower_inboxes = Followers::get_inboxes( $user_id ); + public static function add_inboxes_of_follower( $inboxes, $actor_id ) { + $follower_inboxes = Followers::get_inboxes( $actor_id ); return array_merge( $inboxes, $follower_inboxes ); } @@ -270,12 +127,12 @@ public static function add_inboxes_of_follower( $inboxes, $user_id ) { * Default filter to add Inboxes of Mentioned Actors * * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param int $actor_id The WordPress Actor-ID. * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes. */ - public static function add_inboxes_by_mentioned_actors( $inboxes, $user_id, $activity ) { + public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) { $cc = $activity->get_cc() ?? array(); $to = $activity->get_to() ?? array(); @@ -302,12 +159,12 @@ function ( $actor ) { * Default filter to add Inboxes of Posts that are set as `in-reply-to` * * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param int $actor_id The WordPress Actor-ID. * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ - public static function add_inboxes_of_replied_urls( $inboxes, $user_id, $activity ) { + public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) { $in_reply_to = $activity->get_in_reply_to(); if ( ! $in_reply_to ) { From 318a2fd37965c2e6a39258927d652beb9be81eb4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 15:07:39 +0100 Subject: [PATCH 03/34] update changelog --- CHANGELOG.md | 9 ++++++++- readme.txt | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56e2fbc3..c7e8f3941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Support for WPML post locale +### Added + +* Outbox queue + +### Changed + +* Rewrite the current dispatcher system, to use the Outbox instead of the Scheduler. + ### Removed * Built-in support for nodeinfo2. Use the [NodeInfo plugin](https://wordpress.org/plugins/nodeinfo/) instead. @@ -42,7 +50,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Outbox queue * Comment counts get updated when the plugin is activated/deactivated/deleted * Added a filter to make custom comment types manageable in WP.com Calypso diff --git a/readme.txt b/readme.txt index af290bc64..a47cf3af6 100644 --- a/readme.txt +++ b/readme.txt @@ -134,8 +134,8 @@ For reasons of data protection, it is not possible to see the followers of other = Unreleased = - * Added: Outbox queue +* Changed: Rewrite the current dispatcher system, to use the Outbox instead of a Scheduler. * Changed: Improved content negotiation and AUTHORIZED_FETCH support for third-party plugins = 4.7.3 = From dcc68d33f9a1731cda9bf1e6e857d51ea95a21b0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:27:26 +0100 Subject: [PATCH 04/34] mark post as `publish` after federation id done --- includes/class-dispatcher.php | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 422ae8195..bf77e1651 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -37,14 +37,14 @@ public static function init() { * @param int $id The outbox ID. */ public static function process_outbox( $id ) { - $activity = \get_post( $id ); + $outbox_item = \get_post( $id ); // If the activity is not a post, return. - if ( ! $activity ) { + if ( ! $outbox_item ) { return; } - $actor_type = \get_post_meta( $activity->ID, '_activitypub_activity_actor', true ); + $actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true ); switch ( $actor_type ) { case 'blog': @@ -59,30 +59,30 @@ public static function process_outbox( $id ) { break; } - $type = \get_post_meta( $activity->ID, '_activitypub_activity_type', true ); - $transformer = Factory::get_transformer( $activity->post_content ); + $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + $transformer = Factory::get_transformer( $outbox_item->post_content ); $activity = $transformer->to_activity( $type ); - self::send_activity_to_followers( $activity, $actor_id ); + self::send_activity_to_followers( $activity, $actor_id, $outbox_item ); } /** * Send an Activity to all followers and mentioned users. * - * @param Activity $activity The ActivityPub Activity. - * @param int $actor_id The actor ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. */ - private static function send_activity_to_followers( $activity, $actor_id, $wp_object = null ) { + private static function send_activity_to_followers( $activity, $actor_id, $outbox_item = null ) { /** * Filters whether to send an Activity to followers. * - * @param bool $send_activity_to_followers Whether to send the Activity to followers. - * @param Activity $activity The ActivityPub Activity. - * @param int $actor_id The actor ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. + * @param bool $send_activity_to_followers Whether to send the Activity to followers. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $wp_object ) ) { + if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $outbox_item ) ) { return; } @@ -106,7 +106,8 @@ private static function send_activity_to_followers( $activity, $actor_id, $wp_ob safe_remote_post( $inbox, $json, $actor_id ); } - set_wp_object_state( $wp_object, 'federated' ); + $outbox_item->post_status = 'publish'; + \wp_update_post( $outbox_item ); } /** From f04c6825aa1e23ba5833a7158874f8ac146c5570 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:33:57 +0100 Subject: [PATCH 05/34] show only published activities --- includes/rest/class-outbox-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 9adbddbea..20fb156b0 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,7 +120,7 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'publish', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( From 3f4672bdb7481dcc122da888adcd79bfb1bbc72e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:35:56 +0100 Subject: [PATCH 06/34] fix missing rename --- includes/class-dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index bf77e1651..c7a1cc5e4 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -55,7 +55,7 @@ public static function process_outbox( $id ) { break; case 'user': default: - $actor_id = $activity->post_author; + $actor_id = $outbox_item->post_author; break; } From 5c6446d901c58d682d906ab2bf37d2ea2832145c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 00:18:09 +0100 Subject: [PATCH 07/34] use pending instead of draft --- includes/collection/class-outbox.php | 2 +- tests/includes/collection/class-test-outbox.php | 2 +- tests/includes/rest/class-test-outbox-controller.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index d7cd69683..f68a4fd60 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -44,7 +44,7 @@ public static function add( $activity_object, $activity_type, $user_id, $content 'post_content' => $activity_object->to_json(), // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), - 'post_status' => 'draft', + 'post_status' => 'pending', 'meta_input' => array( '_activitypub_activity_type' => $activity_type, '_activitypub_activity_actor' => $actor, diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index 653300589..57724696e 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -33,7 +33,7 @@ public function test_add( $data, $type, $user_id, $json ) { $post = get_post( $id ); $this->assertInstanceOf( 'WP_Post', $post ); - $this->assertEquals( 'draft', $post->post_status ); + $this->assertEquals( 'pending', $post->post_status ); $this->assertEquals( $json, $post->post_content ); $this->assertEquals( $type, get_post_meta( $id, '_activitypub_activity_type', true ) ); diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 047ed6237..2e76ae12e 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -162,7 +162,7 @@ public function test_get_items_specific_user() { array( 'post_author' => $user_id, 'post_type' => 'ap_outbox', - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', 'post_content' => wp_json_encode( array( @@ -333,7 +333,7 @@ public function test_get_items_activity_type( $type, $activity, $allowed ) { array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => "https://example.org/activity/{$type}", 'post_content' => \wp_json_encode( array( @@ -442,7 +442,7 @@ public function test_get_items_content_visibility( $visibility, $public_visible, array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', 'post_content' => \wp_json_encode( array( From 7f28a615c3d3e580893aaa77214d4db358115faf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 20:08:47 +0100 Subject: [PATCH 08/34] do not check for post_status --- includes/rest/class-outbox-controller.php | 4 +--- tests/includes/rest/class-test-outbox-controller.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 20fb156b0..fededf618 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,8 +120,6 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'publish', - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( @@ -171,7 +169,7 @@ public function get_items( $request ) { 'orderedItems' => array(), ); - update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); + \update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); foreach ( $query_result as $outbox_item ) { $response['orderedItems'][] = $this->prepare_item_for_response( $outbox_item, $request ); } diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 2e76ae12e..f287f3c9a 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -162,8 +162,8 @@ public function test_get_items_specific_user() { array( 'post_author' => $user_id, 'post_type' => 'ap_outbox', - 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', + 'post_status' => 'pending', 'post_content' => wp_json_encode( array( '@context' => array( 'https://www.w3.org/ns/activitystreams' ), From a1ce4e8abc65c4e911e5f8c85caf58aafc671db4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 20:23:11 +0100 Subject: [PATCH 09/34] fix tests props @obenland --- includes/rest/class-outbox-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index fededf618..95ab97a5b 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,6 +120,7 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'any', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( From 98bbfa15bde35ab16140450593172477f3304f17 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Tue, 21 Jan 2025 16:18:34 -0600 Subject: [PATCH 10/34] Send `Update`s to Blog Actor in dual mode --- includes/class-dispatcher.php | 31 ++++++++++++ tests/includes/class-test-dispatcher.php | 64 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/includes/class-test-dispatcher.php diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index c7a1cc5e4..781b26c74 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -29,6 +29,7 @@ public static function init() { \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 ); + \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'maybe_add_inboxes_of_blog_user' ), 10, 3 ); } /** @@ -203,4 +204,34 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi return $inboxes; } + + /** + * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param array $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes + */ + public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return $inboxes; + } + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return $inboxes; + } + // Only if this is an Update. + if ( 'Update' !== $activity->get_type() ) { + return $inboxes; + } + + $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); + $inboxes = array_merge( $inboxes, $blog_inboxes ); + $inboxes = array_unique( $inboxes ); + + return $inboxes; + } } diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php new file mode 100644 index 000000000..8dc91e90e --- /dev/null +++ b/tests/includes/class-test-dispatcher.php @@ -0,0 +1,64 @@ +createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when actor is blog user + */ + public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, Actors::BLOG_USER_ID, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when activity type is not Update + */ + public function test_maybe_add_inboxes_of_blog_user_not_update() { + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class ); + + // Mock the static method using reflection. + $activity->expects( $this->once() ) + ->method( '__call' ) + ->with( 'get_type' ) + ->willReturn( 'Create' ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $this->assertEquals( $inboxes, $result ); + } +} From d83147b72991f07ef686a565f086104ad7720b06 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 07:37:13 +0100 Subject: [PATCH 11/34] Update includes/class-dispatcher.php Co-authored-by: Konstantin Obenland --- includes/class-dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 781b26c74..deafd0bae 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -23,7 +23,7 @@ class Dispatcher { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ), 10, 1 ); + \add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) ); // Default filters to add Inboxes to sent to. \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 ); From bc9e6af6d607f73eadc3ece10335e4070fefa5c8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 07:37:21 +0100 Subject: [PATCH 12/34] Update includes/class-dispatcher.php Co-authored-by: Konstantin Obenland --- includes/class-dispatcher.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index deafd0bae..5e37e80b7 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -107,8 +107,7 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo safe_remote_post( $inbox, $json, $actor_id ); } - $outbox_item->post_status = 'publish'; - \wp_update_post( $outbox_item ); + \wp_publish_post( $outbox_item ); } /** From 21d8e46a75be74546984a4eaa66aa2680405f9db Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 07:37:39 +0100 Subject: [PATCH 13/34] Update includes/rest/class-outbox-controller.php Co-authored-by: Konstantin Obenland --- includes/rest/class-outbox-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 95ab97a5b..3ab15b63c 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -121,6 +121,7 @@ public function get_items( $request ) { 'paged' => $page, 'post_type' => Outbox::POST_TYPE, 'post_status' => 'any', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( From 06edf3bb83474014e275cd0de4cae492f3bde6fd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:13:52 +0100 Subject: [PATCH 14/34] Check if Activity should be sent to followers --- includes/class-dispatcher.php | 46 +++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 5e37e80b7..712d95266 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -26,7 +26,7 @@ public static function init() { \add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) ); // Default filters to add Inboxes to sent to. - \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 ); + \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'maybe_add_inboxes_of_blog_user' ), 10, 3 ); @@ -118,7 +118,11 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo * * @return array The filtered Inboxes */ - public static function add_inboxes_of_follower( $inboxes, $actor_id ) { + public static function add_inboxes_of_follower( $inboxes, $actor_id, $activity ) { + if ( ! self::should_send_to_followers( $activity, $actor_id ) ) { + return $inboxes; + } + $follower_inboxes = Followers::get_inboxes( $actor_id ); return array_merge( $inboxes, $follower_inboxes ); @@ -214,6 +218,10 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi * @return array The filtered Inboxes */ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { + if ( ! self::should_send_to_followers( $activity, $actor_id ) ) { + return $inboxes; + } + // Only if we're in both Blog and User modes. if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { return $inboxes; @@ -233,4 +241,38 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act return $inboxes; } + + /** + * Check if passed Activity is public. + * + * @param array $activity The Activity object as array. + * @param int $actor_id The Actor-ID. + * + * @return boolean True if public, false if not. + */ + protected static function should_send_to_followers( $activity, $actor_id ) { + // Check if follower endpoint is set. + $actor = Actors::get_by_id( $actor_id ); + + if ( ! $actor ) { + return $inboxes; + } + + // Check if follower endpoint is set. + $cc = $activity->get_cc() ?? array(); + $to = $activity->get_to() ?? array(); + + $audience = array_merge( $cc, $to ); + + if ( + // Check if activity is public. + in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) || + // ...or check if follower endpoint is set. + in_array( $actor->get_followers(), $audience, true ) + ) { + return true; + } + + return false; + } } From 7957a271d2c05676d2c478cdf0d07232b89185c3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:16:12 +0100 Subject: [PATCH 15/34] the unique check will be done `send_activity_to_followers` --- includes/class-dispatcher.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 712d95266..198f848a3 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -236,10 +236,8 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act } $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); - $inboxes = array_merge( $inboxes, $blog_inboxes ); - $inboxes = array_unique( $inboxes ); - return $inboxes; + return array_merge( $inboxes, $blog_inboxes ); } /** From 212c02f7aadc2fc32f7e6ad2e1fc50c6fddbab3c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:47:38 +0100 Subject: [PATCH 16/34] fix tests --- includes/class-dispatcher.php | 4 ++-- tests/includes/class-test-dispatcher.php | 26 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 198f848a3..0d8cb0a1a 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -252,8 +252,8 @@ protected static function should_send_to_followers( $activity, $actor_id ) { // Check if follower endpoint is set. $actor = Actors::get_by_id( $actor_id ); - if ( ! $actor ) { - return $inboxes; + if ( ! $actor || is_wp_error( $actor ) ) { + return false; } // Check if follower endpoint is set. diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index 8dc91e90e..2ecbabe28 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -17,6 +17,7 @@ * @covers Activitypub\Dispatcher */ class Test_Dispatcher extends WP_UnitTestCase { + /** * Test maybe_add_inboxes_of_blog_user when actor mode is not ACTIVITYPUB_ACTOR_AND_BLOG_MODE */ @@ -26,7 +27,7 @@ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class ); - $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); $this->assertEquals( $inboxes, $result ); } @@ -50,15 +51,28 @@ public function test_maybe_add_inboxes_of_blog_user_not_update() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); - $activity = $this->createMock( Activity::class ); + $activity = $this->createMock( Activity::class, array( '__call' ) ); // Mock the static method using reflection. - $activity->expects( $this->once() ) + $activity->expects( $this->any() ) ->method( '__call' ) - ->with( 'get_type' ) - ->willReturn( 'Create' ); + ->willReturnCallback( function( $name, $args ) { + if ( 'get_to' === $name ) { + return array( 'https://www.w3.org/ns/activitystreams#Public' ); + } + + if ( 'get_cc' === $name ) { + return array(); + } + + if ( 'get_type' === $name ) { + return 'Create'; + } + + return null; + } ); - $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); $this->assertEquals( $inboxes, $result ); } } From 361d891495617a51c62753be585e27269e6ef0cc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:51:04 +0100 Subject: [PATCH 17/34] fix PHPCS --- includes/class-dispatcher.php | 1 + tests/includes/class-test-dispatcher.php | 26 +++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 0d8cb0a1a..738d19532 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -115,6 +115,7 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo * * @param array $inboxes The list of Inboxes. * @param int $actor_id The WordPress Actor-ID. + * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index 2ecbabe28..dbcf54168 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -56,21 +56,23 @@ public function test_maybe_add_inboxes_of_blog_user_not_update() { // Mock the static method using reflection. $activity->expects( $this->any() ) ->method( '__call' ) - ->willReturnCallback( function( $name, $args ) { - if ( 'get_to' === $name ) { - return array( 'https://www.w3.org/ns/activitystreams#Public' ); - } + ->willReturnCallback( + function ( $name ) { + if ( 'get_to' === $name ) { + return array( 'https://www.w3.org/ns/activitystreams#Public' ); + } - if ( 'get_cc' === $name ) { - return array(); - } + if ( 'get_cc' === $name ) { + return array(); + } - if ( 'get_type' === $name ) { - return 'Create'; - } + if ( 'get_type' === $name ) { + return 'Create'; + } - return null; - } ); + return null; + } + ); $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); $this->assertEquals( $inboxes, $result ); From 978e5b5fe8cdc186186b78e4f04f2f7f06fb4fca Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 11:07:11 +0100 Subject: [PATCH 18/34] move scheduler behind action --- includes/class-scheduler.php | 20 ++++++++++++++++++++ includes/functions.php | 26 +++++++++----------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 36ad1b809..c174b7245 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -28,6 +28,8 @@ public static function init() { // Follower Cleanups. \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_item_for_federation' ) ); } /** @@ -138,4 +140,22 @@ public static function cleanup_followers() { } } } + + /** + * Schedule the outbox item for federation. + * + * @param int $id The ID of the outbox item. + */ + public static function schedule_outbox_item_for_federation( $id ) { + $hook = 'activitypub_process_outbox'; + $args = array( $id ); + + if ( false === wp_next_scheduled( $hook, $args ) ) { + \wp_schedule_single_event( + \time() + 10, + $hook, + $args + ); + } + } } diff --git a/includes/functions.php b/includes/functions.php index 380777e27..ec1da7a24 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1424,6 +1424,7 @@ function get_content_visibility( $post_id ) { $_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; $options = array( ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, + ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, ); @@ -1554,43 +1555,34 @@ function is_self_ping( $id ) { * Add an object to the outbox. * * @param mixed $data The object to add to the outbox. - * @param string $type The type of the Activity. + * @param string $activity_type The type of the Activity. * @param integer $user_id The User-ID. * @param string $content_visibility The visibility of the content. * * @return boolean|int The ID of the outbox item or false on failure. */ -function add_to_outbox( $data, $type = 'Create', $user_id = 0, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { +function add_to_outbox( $data, $activity_type = 'Create', $user_id = 0, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { $transformer = Transformer_Factory::get_transformer( $data ); if ( ! $transformer || is_wp_error( $transformer ) ) { return false; } - $activity = $transformer->to_object(); + $activity_object = $transformer->to_object(); - if ( ! $activity || is_wp_error( $activity ) ) { + if ( ! $activity_object || \is_wp_error( $activity_object ) ) { return false; } set_wp_object_state( $data, 'federate' ); - $id = Outbox::add( $activity, $type, $user_id, $content_visibility ); + $outbox_activity_id = Outbox::add( $activity_object, $activity_type, $user_id, $content_visibility ); - if ( ! $id ) { + if ( ! $outbox_activity_id ) { return false; } - $hook = 'activitypub_process_outbox'; - $args = array( $id ); + \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity_object, $user_id, $content_visibility ); - if ( false === wp_next_scheduled( $hook, $args ) ) { - \wp_schedule_single_event( - \time() + 10, - $hook, - $args - ); - } - - return $id; + return $outbox_activity_id; } From 72d2fd605f6dd6d2f085a146fb390c28cc4058e8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 11:07:30 +0100 Subject: [PATCH 19/34] Add `private` visibility --- includes/class-activitypub.php | 2 +- includes/constants.php | 1 + tests/includes/rest/class-test-outbox-controller.php | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index f0cb83166..c62a300d9 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -602,7 +602,7 @@ private static function register_post_types() { 'sanitize_callback' => function ( $value ) { $schema = array( 'type' => 'string', - 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ); diff --git a/includes/constants.php b/includes/constants.php index e12450205..98da5d4a4 100644 --- a/includes/constants.php +++ b/includes/constants.php @@ -71,4 +71,5 @@ // Post visibility constants. \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC', '' ); \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC', 'quiet_public' ); +\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE', 'private' ); \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL', 'local' ); diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index f287f3c9a..86555d44e 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -409,6 +409,11 @@ public function data_content_visibility() { 'public_visible' => false, 'private_visible' => true, ), + 'private' => array( + 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'public_visible' => false, + 'private_visible' => true, + ), 'local' => array( 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, 'public_visible' => false, From 99c13891f7d9a4c7fc2bf4b041d4ef8744b3a361 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 12:12:49 +0100 Subject: [PATCH 20/34] Add Announce activity --- includes/class-dispatcher.php | 4 +-- includes/class-scheduler.php | 4 +-- includes/scheduler/class-post.php | 43 ++++++++++++++++++++++++++ includes/transformer/class-factory.php | 2 +- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 738d19532..fe0d35052 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -9,7 +9,7 @@ use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; -use Activitypub\Transformer\Factory; +use Activitypub\Transformer\Factory as Transformer_Factory; /** * ActivityPub Dispatcher Class. @@ -61,7 +61,7 @@ public static function process_outbox( $id ) { } $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); - $transformer = Factory::get_transformer( $outbox_item->post_content ); + $transformer = Transformer_Factory::get_transformer( $outbox_item->post_content ); $activity = $transformer->to_activity( $type ); self::send_activity_to_followers( $activity, $actor_id, $outbox_item ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index c174b7245..cbcf5f96c 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -29,7 +29,7 @@ public static function init() { \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_item_for_federation' ) ); + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) ); } /** @@ -146,7 +146,7 @@ public static function cleanup_followers() { * * @param int $id The ID of the outbox item. */ - public static function schedule_outbox_item_for_federation( $id ) { + public static function schedule_outbox_activity_for_federation( $id ) { $hook = 'activitypub_process_outbox'; $args = array( $id ); diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 14e1ac325..26a8806a9 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -7,6 +7,10 @@ namespace Activitypub\Scheduler; +use Activitypub\Collection\Outbox; +use Activitypub\Collection\Actors; +use Activitypub\Transformer\Factory; + use function Activitypub\add_to_outbox; use function Activitypub\is_post_disabled; use function Activitypub\get_wp_object_state; @@ -45,6 +49,8 @@ public static function transition_attachment_status( $post_id ) { self::schedule_post_activity( 'trash', '', $post_id ); break; } + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_announces' ), 10, 4 ); } /** @@ -110,4 +116,41 @@ public static function schedule_post_activity( $new_status, $old_status, $post ) // Add the post to the outbox. add_to_outbox( $post, $type, $post->post_author, $content_visibility ); } + + /** + * Send announces. + * + * @param int $outbox_activity_id The outbox activity ID. + * @param array $activity_object The activity object. + * @param int $actor_id The actor ID. + * @param int $content_visibility The content visibility. + */ + public static function send_announces( $outbox_activity_id, $activity_object, $actor_id, $content_visibility ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return; + } + + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return; + } + + // Only if the content is public or quiet public. + if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) { + return; + } + + $activity_type = \get_post_meta( $outbox_activity_id, '_activitypub_activity_type', true ); + + // Only if the activity is a Create, Update or Delete. + if ( ! in_array( $activity_type, array( 'Create', 'Update', 'Delete' ), true ) ) { + return; + } + + $transformer = Factory::get_transformer( $activity_object ); + $activity = $transformer->to_activity( $activity_type ); + + Outbox::add( $activity, 'Announce', Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + } } diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index a4662eca4..08eb70c4b 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -94,7 +94,7 @@ public static function get_transformer( $data ) { return new User( $data ); } break; - case 'Base_Object': + case 'Activitypub\Activity\Base_Object': return new Activity_Object( $data ); case 'json': return new Json( $data ); From 6707f8749635410460ba2779adeb8b1ca35bddb1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 12:36:50 +0100 Subject: [PATCH 21/34] Announce the full object! --- includes/transformer/class-base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 94537c9b9..3c9582ec9 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -146,7 +146,7 @@ public function to_activity( $type ) { $activity->set_object( $object ); // Use simple Object (only ID-URI) for Like and Announce. - if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) { + if ( in_array( $type, array( 'Like' ), true ) ) { $activity->set_object( $object->get_id() ); } From 243184b39fc075bbde15480084784af7a3180f9b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 12:39:07 +0100 Subject: [PATCH 22/34] fix indent --- tests/includes/class-test-dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index dbcf54168..aa759922c 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -59,7 +59,7 @@ public function test_maybe_add_inboxes_of_blog_user_not_update() { ->willReturnCallback( function ( $name ) { if ( 'get_to' === $name ) { - return array( 'https://www.w3.org/ns/activitystreams#Public' ); + return array( 'https://www.w3.org/ns/activitystreams#Public' ); } if ( 'get_cc' === $name ) { From 17d3b37faf688a34d65aa4fe69f73453c47ce7a8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 15:54:05 +0100 Subject: [PATCH 23/34] Update includes/transformer/class-base.php Co-authored-by: Konstantin Obenland --- includes/transformer/class-base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 3c9582ec9..c4ae115d8 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -146,7 +146,7 @@ public function to_activity( $type ) { $activity->set_object( $object ); // Use simple Object (only ID-URI) for Like and Announce. - if ( in_array( $type, array( 'Like' ), true ) ) { + if ( 'Like' === $type ) { $activity->set_object( $object->get_id() ); } From 0722b126d0407d889147a512c95cabdae208a18f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 15:57:06 +0100 Subject: [PATCH 24/34] add doc-block --- includes/functions.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/includes/functions.php b/includes/functions.php index ec1da7a24..054750216 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1582,6 +1582,14 @@ function add_to_outbox( $data, $activity_type = 'Create', $user_id = 0, $content return false; } + /** + * Action triggered after an object has been added to the outbox. + * + * @param int $outbox_activity_id The ID of the outbox item. + * @param \Activitypub\Activity\Base_Object $activity_object The activity object. + * @param int $user_id The User-ID. + * @param string $content_visibility The visibility of the content. + */ \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity_object, $user_id, $content_visibility ); return $outbox_activity_id; From 14a372d52ede47a8e4850eaa97a13fb51c698277 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 17:43:56 +0100 Subject: [PATCH 25/34] only boost content not profile updates --- includes/scheduler/class-post.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 26a8806a9..60c777b75 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -148,6 +148,11 @@ public static function send_announces( $outbox_activity_id, $activity_object, $a return; } + // Check if the object is an article, image, audio, video, event or document and ignore profile updates and other activities. + if ( ! in_array( $activity_object->get_type(), array( 'Note', 'Article', 'Image', 'Audio', 'Video', 'Event', 'Document' ), true ) ) { + return; + } + $transformer = Factory::get_transformer( $activity_object ); $activity = $transformer->to_activity( $activity_type ); From 70266cfa58ccae5e36bd978068fe8b8ae183e924 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Wed, 22 Jan 2025 12:55:13 -0600 Subject: [PATCH 26/34] Also handle `Delete` when bundling Blog Actor inboxes --- includes/class-dispatcher.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index fe0d35052..e7fb5e864 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -231,13 +231,13 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act if ( Actors::BLOG_USER_ID === $actor_id ) { return $inboxes; } - // Only if this is an Update. - if ( 'Update' !== $activity->get_type() ) { + // Only if this is an Update or Delete. Create handles its own Announce in dual user mode. + if ( ! in_array( $activity->get_type(), array( 'Update', 'Delete' ), true ) ) { return $inboxes; } $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); - + // array_unique is done in `send_activity_to_followers()`, no need here. return array_merge( $inboxes, $blog_inboxes ); } From 5b82940f37abd6581487ddee994b90ebcb7fcb7b Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:17:09 -0600 Subject: [PATCH 27/34] Update docs --- includes/class-dispatcher.php | 23 ++++++++++++----------- tests/includes/class-test-dispatcher.php | 9 +++++++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index e7fb5e864..9ee12df0f 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -7,6 +7,7 @@ namespace Activitypub; +use Activitypub\Activity\Activity; use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; use Activitypub\Transformer\Factory as Transformer_Factory; @@ -113,9 +114,9 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo /** * Default filter to add Inboxes of Followers. * - * @param array $inboxes The list of Inboxes. - * @param int $actor_id The WordPress Actor-ID. - * @param array $activity The ActivityPub Activity. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ @@ -132,9 +133,9 @@ public static function add_inboxes_of_follower( $inboxes, $actor_id, $activity ) /** * Default filter to add Inboxes of Mentioned Actors * - * @param array $inboxes The list of Inboxes. - * @param int $actor_id The WordPress Actor-ID. - * @param array $activity The ActivityPub Activity. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. * * @return array The filtered Inboxes. */ @@ -212,9 +213,9 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi /** * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. * - * @param array $inboxes The list of Inboxes. - * @param int $actor_id The WordPress Actor-ID. - * @param array $activity The ActivityPub Activity. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ @@ -244,8 +245,8 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act /** * Check if passed Activity is public. * - * @param array $activity The Activity object as array. - * @param int $actor_id The Actor-ID. + * @param Activity $activity The Activity object. + * @param int $actor_id The Actor-ID. * * @return boolean True if public, false if not. */ diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index aa759922c..e63192091 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -10,16 +10,17 @@ use Activitypub\Collection\Followers; use Activitypub\Dispatcher; - /** * Test class for Activitypub Dispatcher. * - * @covers Activitypub\Dispatcher + * @coversDefaultClass Activitypub\Dispatcher */ class Test_Dispatcher extends WP_UnitTestCase { /** * Test maybe_add_inboxes_of_blog_user when actor mode is not ACTIVITYPUB_ACTOR_AND_BLOG_MODE + * + * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); @@ -33,6 +34,8 @@ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { /** * Test maybe_add_inboxes_of_blog_user when actor is blog user + * + * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); @@ -46,6 +49,8 @@ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { /** * Test maybe_add_inboxes_of_blog_user when activity type is not Update + * + * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_not_update() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); From 6f87326820896eecb059e3a5fb1034d55e2a2ee6 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:18:52 -0600 Subject: [PATCH 28/34] Avoid activitypub_actor_mode bleeding into other tests --- tests/includes/class-test-dispatcher.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index e63192091..389cf89b3 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -16,6 +16,14 @@ * @coversDefaultClass Activitypub\Dispatcher */ class Test_Dispatcher extends WP_UnitTestCase { + /** + * Tear down the test case. + */ + public function tear_down() { + \delete_option( 'activitypub_actor_mode' ); + + parent::tear_down(); + } /** * Test maybe_add_inboxes_of_blog_user when actor mode is not ACTIVITYPUB_ACTOR_AND_BLOG_MODE @@ -23,7 +31,7 @@ class Test_Dispatcher extends WP_UnitTestCase { * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { - update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class ); @@ -38,7 +46,7 @@ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { - update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class ); @@ -53,7 +61,7 @@ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_not_update() { - update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class, array( '__call' ) ); From 8c778451c4a91fc8439d14ba9ad3d0c4fcd533d5 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:23:13 -0600 Subject: [PATCH 29/34] Fix comments tests --- tests/includes/scheduler/class-test-comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/includes/scheduler/class-test-comment.php b/tests/includes/scheduler/class-test-comment.php index 62584ea87..f8eb440f9 100644 --- a/tests/includes/scheduler/class-test-comment.php +++ b/tests/includes/scheduler/class-test-comment.php @@ -166,7 +166,7 @@ private function get_latest_outbox_item( $title = '' ) { array( 'post_type' => Outbox::POST_TYPE, 'posts_per_page' => 1, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => $title, 'orderby' => 'date', 'order' => 'DESC', From 8332017a202012c4926d4ecb5670b095ab676873 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:24:20 -0600 Subject: [PATCH 30/34] Account for inheritance in Activity objects --- includes/transformer/class-factory.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index 08eb70c4b..946390200 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -94,12 +94,14 @@ public static function get_transformer( $data ) { return new User( $data ); } break; - case 'Activitypub\Activity\Base_Object': - return new Activity_Object( $data ); case 'json': return new Json( $data ); } + if ( $data instanceof \Activitypub\Activity\Base_Object ) { + return new Activity_Object( $data ); + } + return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) ); } } From 3382d7ff13d08230d407328e7ce052cd097464ed Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 15:15:25 -0600 Subject: [PATCH 31/34] Move hook to the right place --- includes/scheduler/class-post.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 60c777b75..e02bdbda0 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -30,6 +30,8 @@ public static function init() { \add_action( 'add_attachment', array( self::class, 'transition_attachment_status' ) ); \add_action( 'edit_attachment', array( self::class, 'transition_attachment_status' ) ); \add_action( 'delete_attachment', array( self::class, 'transition_attachment_status' ) ); + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_announces' ), 10, 4 ); } /** @@ -49,8 +51,6 @@ public static function transition_attachment_status( $post_id ) { self::schedule_post_activity( 'trash', '', $post_id ); break; } - - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_announces' ), 10, 4 ); } /** From eb9bef6417a2a9f6b62d9a1297b8b85986a836f9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 23 Jan 2025 11:47:40 +0100 Subject: [PATCH 32/34] fix typo! --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e8f3941..58f59ed15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Untitled] +## [Unreleased] ### Changed From 80160d4553654c117a97df4f929651486cc3dd4c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 23 Jan 2025 11:55:13 +0100 Subject: [PATCH 33/34] trigger scheduler --- includes/scheduler/class-post.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index e02bdbda0..0760bd6ad 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -7,6 +7,7 @@ namespace Activitypub\Scheduler; +use Activitypub\Scheduler; use Activitypub\Collection\Outbox; use Activitypub\Collection\Actors; use Activitypub\Transformer\Factory; @@ -156,6 +157,13 @@ public static function send_announces( $outbox_activity_id, $activity_object, $a $transformer = Factory::get_transformer( $activity_object ); $activity = $transformer->to_activity( $activity_type ); - Outbox::add( $activity, 'Announce', Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + $outbox_activity_id = Outbox::add( $activity, 'Announce', Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + + if ( ! $outbox_activity_id ) { + return false; + } + + // Schedule the outbox item for federation. + Scheduler::schedule_outbox_activity_for_federation( $outbox_activity_id ); } } From 0ab2883eaa1123d4ec7bbcdd30f2f9394b4cf34b Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 23 Jan 2025 08:31:20 -0600 Subject: [PATCH 34/34] Fix tests --- tests/includes/scheduler/class-test-actor.php | 2 +- tests/includes/scheduler/class-test-post.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/includes/scheduler/class-test-actor.php b/tests/includes/scheduler/class-test-actor.php index 0f9236a77..48501d0d5 100644 --- a/tests/includes/scheduler/class-test-actor.php +++ b/tests/includes/scheduler/class-test-actor.php @@ -229,7 +229,7 @@ private function get_latest_outbox_item( $title = '' ) { array( 'post_type' => Outbox::POST_TYPE, 'posts_per_page' => 1, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => $title, 'orderby' => 'date', 'order' => 'DESC', diff --git a/tests/includes/scheduler/class-test-post.php b/tests/includes/scheduler/class-test-post.php index a5fa05eab..764621d25 100644 --- a/tests/includes/scheduler/class-test-post.php +++ b/tests/includes/scheduler/class-test-post.php @@ -135,7 +135,7 @@ private function get_latest_outbox_item( $title = '' ) { array( 'post_type' => Outbox::POST_TYPE, 'posts_per_page' => 1, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => $title, 'orderby' => 'date', 'order' => 'DESC',