From ddeb5c6cca3ba662b6a46d7dbba28e7d4f08d4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sat, 4 Jan 2025 12:17:58 +0100 Subject: [PATCH] Refactoring * Use WP_Post ID of the event source for linking the event source with an cached remote event post * Improve docs * Add tests for retrieving the event_sources list --- .../collection/class-event-sources.php | 42 ++++---- includes/activitypub/handler/class-accept.php | 36 ++++--- includes/activitypub/handler/class-create.php | 10 +- includes/activitypub/handler/class-undo.php | 28 +++--- includes/activitypub/handler/class-update.php | 10 +- .../activitypub/model/class-event-source.php | 15 +-- .../activitypub/transmogrifier/class-base.php | 17 ++-- includes/class-event-sources.php | 44 ++------- .../collection/class-test-event-sources.php | 98 +++++++++++++++++++ 9 files changed, 190 insertions(+), 110 deletions(-) create mode 100644 tests/includes/activitypub/collection/class-test-event-sources.php diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 1d2c5ed..bbdf5a0 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -216,7 +216,6 @@ class Event_Sources { public static function delete_event_source_transients(): void { delete_transient( 'event_bridge_for_activitypub_event_sources' ); delete_transient( 'event_bridge_for_activitypub_event_sources_hosts' ); - delete_transient( 'event_bridge_for_activitypub_event_sources_ids' ); } /** @@ -252,16 +251,16 @@ class Event_Sources { /** * Delete all posts of an event source. * - * @param string $event_source_id The ActivityPub ID of the event source. + * @param int $event_source_post_id The WordPress Post ID of the event source. * @return void */ - public static function delete_events_by_event_source( $event_source_id ) { + public static function delete_events_by_event_source( $event_source_post_id ) { global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( - "SELECT * FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s", + "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s", '_event_bridge_for_activitypub_event_source', - esc_sql( $event_source_id ) + absint( $event_source_post_id ) ) ); @@ -271,13 +270,13 @@ class Event_Sources { } // Loop through the posts and delete them permanently. - foreach ( $results as $post ) { + foreach ( $results as $result ) { // Check if the post has a thumbnail. - $thumbnail_id = get_post_thumbnail_id( $post->post_id ); + $thumbnail_id = get_post_thumbnail_id( $result->post_id ); if ( $thumbnail_id ) { // Remove the thumbnail from the post. - \delete_post_thumbnail( $post->post_id ); + \delete_post_thumbnail( $result->post_id ); // Delete the attachment (and its files) from the media library. if ( self::is_attachment_featured_image( $thumbnail_id ) ) { @@ -285,7 +284,7 @@ class Event_Sources { } } - \wp_delete_post( $post->post_id, true ); + \wp_delete_post( $result->post_id, true ); } // Clean up the query. @@ -295,26 +294,26 @@ class Event_Sources { /** * Remove an Event Source (=Followed ActivityPub actor). * - * @param string $actor The Actor URL. + * @param string $activitypub_id The Events Sources ActivityPub Actor ID/URL. * * @return WP_Post|false|null Post data on success, false or null on failure. */ - public static function remove_event_source( $actor ) { + public static function remove_event_source( $activitypub_id ) { self::delete_event_source_transients(); - $actor = Event_Source::get_by_id( $actor ); + $event_source = Event_Source::get_by_id( $activitypub_id ); - if ( ! $actor ) { + if ( ! $event_source ) { return; } - $post_id = $actor->get__id(); + $post_id = $event_source->get__id(); if ( ! $post_id ) { return; } - self::delete_events_by_event_source( $actor->get_id() ); + self::delete_events_by_event_source( $post_id); $thumbnail_id = get_post_thumbnail_id( $post_id ); @@ -326,7 +325,7 @@ class Event_Sources { // If the deletion was successful delete all transients regarding event sources. if ( $result ) { - self::queue_unfollow_actor( $actor ); + self::queue_unfollow_actor( $activitypub_id ); } return $result; @@ -374,12 +373,11 @@ class Event_Sources { $args = wp_parse_args( $args, $defaults ); $query = new WP_Query( $args ); $total = $query->found_posts; - $actors = array_map( - function ( $post ) { - return Event_Source::init_from_cpt( $post ); - }, - $query->get_posts() - ); + $actors = array(); + + foreach ( $query->get_posts() as $post ) { + $actors[ $post->guid ] = Event_Source::init_from_cpt( $post ); + } $event_sources = compact( 'actors', 'total' ); diff --git a/includes/activitypub/handler/class-accept.php b/includes/activitypub/handler/class-accept.php index b7b2ac5..fc1662b 100644 --- a/includes/activitypub/handler/class-accept.php +++ b/includes/activitypub/handler/class-accept.php @@ -11,7 +11,6 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Collection\Actors; use Activitypub\Model\Blog; use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; -use Event_Bridge_For_ActivityPub\Event_Sources; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; use function Activitypub\object_to_uri; @@ -45,7 +44,8 @@ class Accept { } // Check that we are actually following/or have a pending follow request this actor. - if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) { + $event_source = Event_Source::get_by_id( $activity['actor'] ); + if ( ! $event_source ) { return; } @@ -53,20 +53,28 @@ class Accept { $application = new Blog(); $follow_id = Event_Sources_Collection::compose_follow_id( $application->get_id(), $activity['actor'] ); - if ( object_to_uri( $activity['object'] ) === $follow_id ) { - $post_id = Event_Source::get_by_id( $activity['actor'] )->get__id(); - if ( ! $post_id ) { - return; - } - \update_post_meta( $post_id, '_event_bridge_for_activitypub_accept_of_follow', $activity['id'] ); - \wp_update_post( - array( - 'ID' => $post_id, - 'post_status' => 'publish', - ) - ); + // Check if the object of the `Accept` is indeed the `Follow` request we sent to that actor. + if ( object_to_uri( $activity['object'] ) !== $follow_id ) { + return; } + // Get the WordPress post ID of the Event Source. This should not be able to fail here. + $post_id = $event_source->get__id(); + + if ( ! $post_id ) { + return; + } + + // Save the accept status of the follow request to the event source post. + \update_post_meta( $post_id, '_event_bridge_for_activitypub_accept_of_follow', $activity['id'] ); + \wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + ) + ); + + // Trigger the backfilling of events from this actor. \do_action( 'event_bridge_for_activitypub_backfill_events', $activity['actor'] ); } } diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index 8c977fb..15ebfdc 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -8,6 +8,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Collection\Actors; +use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; use Event_Bridge_For_ActivityPub\Event_Sources; use Event_Bridge_For_ActivityPub\Setup; @@ -51,9 +52,10 @@ class Create { return; } - // Check that we are actually following this actor. - if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) { - return false; + // Check that we are actually following/or have a pending follow request this actor. + $event_source = Event_Source::get_by_id( $activity['actor'] ); + if ( ! $event_source ) { + return; } if ( Event_Sources::is_time_passed( $activity['object']['startTime'] ) ) { @@ -70,6 +72,6 @@ class Create { return; } - $transmogrifier->save( $activity['object'], $activity['actor'] ); + $transmogrifier->save( $activity['object'], $event_source ); } } diff --git a/includes/activitypub/handler/class-undo.php b/includes/activitypub/handler/class-undo.php index b2763a3..1df06c5 100644 --- a/includes/activitypub/handler/class-undo.php +++ b/includes/activitypub/handler/class-undo.php @@ -47,30 +47,24 @@ class Undo { return; } - $id = object_to_uri( $activity['object'] ); + $accept_id = object_to_uri( $activity['object'] ); - // This is what the ID of the follow request would look like. - $args = array( - 'post_type' => Event_Sources_Collection::POST_TYPE, - 'meta_key' => '_event_bridge_for_activitypub_accept_of_follow', - 'meta_query' => array( - array( - 'key' => '_event_bridge_for_activitypub_accept_of_follow', - 'value' => $id, - 'compare' => '=', - ), - ), + global $wpdb; + + $results = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = %s AND meta_value = %s", + '_event_bridge_for_activitypub_accept_of_follow', + esc_sql( $accept_id ) + ) ); - $query = new \WP_Query( $args ); // If no event source with that accept ID is found return. - if ( ! $query->have_posts() ) { + if ( empty( $results ) || ! $results ) { return; } - $post = $query->get_posts()[0]; - - $post_id = is_a( $post, 'WP_Post' ) ? $post->ID : $post; + $post_id = reset( $results )->post_id; \delete_post_meta( $post_id, '_event_bridge_for_activitypub_accept_of_follow' ); } diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index 00bcc37..0d01c82 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -8,6 +8,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Collection\Actors; +use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; use Event_Bridge_For_ActivityPub\Event_Sources; use Event_Bridge_For_ActivityPub\Setup; @@ -51,9 +52,10 @@ class Update { return; } - // Check that we are actually following this actor. - if ( ! Event_Sources::actor_is_event_source( $activity['actor'] ) ) { - return false; + // Check that we are actually following/or have a pending follow request this actor. + $event_source = Event_Source::get_by_id( $activity['actor'] ); + if ( ! $event_source ) { + return; } if ( Event_Sources::is_time_passed( $activity['object']['startTime'] ) ) { @@ -66,6 +68,6 @@ class Update { return; } - $transmogrifier->save( $activity['object'], $activity['actor'] ); + $transmogrifier->save( $activity['object'], $event_source ); } } diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php index ff2a4f5..978aeee 100644 --- a/includes/activitypub/model/class-event-source.php +++ b/includes/activitypub/model/class-event-source.php @@ -122,16 +122,17 @@ class Event_Source extends Actor { /** * Get the Event Source by the ActivityPub ID. * - * @param string $actor_id The ActivityPub actor ID. - * @return Event_Source|WP_Error|false The Event Source Actor, if a WordPress Post representing it is found, false otherwise. + * @param string $activitypub_actor_id The ActivityPub actor ID. + * @return Event_Source|false The Event Source Actor, if a WordPress Post representing it is found, false otherwise. */ - public static function get_by_id( $actor_id ) { - $post = self::get_wp_post_by_activitypub_actor_id( $actor_id ); + public static function get_by_id( $activitypub_actor_id ) { + $event_sources = Event_Sources::get_event_sources(); - if ( $post ) { - $actor = self::init_from_cpt( $post ); + if ( ! array_key_exists( $activitypub_actor_id, $event_sources ) ) { + return false; } - return $actor ?? false; + + return $event_sources[ $activitypub_actor_id ]; } /** diff --git a/includes/activitypub/transmogrifier/class-base.php b/includes/activitypub/transmogrifier/class-base.php index 6307d0b..b0e63ee 100644 --- a/includes/activitypub/transmogrifier/class-base.php +++ b/includes/activitypub/transmogrifier/class-base.php @@ -43,10 +43,10 @@ abstract class Base { /** * Save the ActivityPub event object within WordPress. * - * @param array $activitypub_event The ActivityPub event as associative array. - * @param ?string $actor The ActivityPub ID of the actor which we received the event from. + * @param array $activitypub_event The ActivityPub event as associative array. + * @param Event_Source $event_source The Event Source we received the event from. */ - public function save( $activitypub_event, $actor ) { + public function save( $activitypub_event, $event_source ) { $activitypub_event = Event::init_from_array( $activitypub_event ); if ( is_wp_error( $activitypub_event ) ) { @@ -57,20 +57,23 @@ abstract class Base { $post_id = $this->save_event(); - $event_id = $activitypub_event['id']; + $event_id = $activitypub_event->get_id(); + + $event_source_activitypub_id = $event_source->get_id(); + $event_source_post_id = $event_source->get__id(); if ( $post_id ) { \do_action( 'event_bridge_for_activitypub_write_log', - array( "[ACTIVITYPUB] Processed incoming event {$event_id} from {$actor}" ) + array( "[ACTIVITYPUB] Processed incoming event {$event_id} from {$event_source_activitypub_id}" ) ); update_post_meta( $post_id, '_event_bridge_for_activitypub_is_remote_cached', true ); - update_post_meta( $post_id, '_event_bridge_for_activitypub_event_source', sanitize_url( $actor ) ); + update_post_meta( $post_id, '_event_bridge_for_activitypub_event_source', absint( $event_source_post_id ) ); update_post_meta( $post_id, 'activitypub_content_visibility', constant( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL' ) ?? '' ); } else { \do_action( 'event_bridge_for_activitypub_write_log', - array( "[ACTIVITYPUB] Failed processing incoming event {$event_id} from {$actor}" ) + array( "[ACTIVITYPUB] Failed processing incoming event {$event_id} from {$event_source_activitypub_id}" ) ); } } diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index f6f0f69..af76806 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -119,13 +119,11 @@ class Event_Sources { \register_post_meta( $event_plugin_integration::get_post_type(), - '_event_bridge_for_activitypub_attributed_to', + '_event_bridge_for_activitypub_event_source', array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => function ( $value ) { - return sanitize_url( $value ); - }, + 'type' => 'integer', + 'single' => true, + 'sanitize_callback' => 'absint', ) ); } @@ -283,7 +281,7 @@ class Event_Sources { return $follow_list; } - $event_sources = self::get_event_sources_ids(); + $event_sources = array_keys( Event_Sources_Collection::get_event_sources() ); if ( ! is_array( $event_sources ) ) { return $follow_list; @@ -304,11 +302,11 @@ class Event_Sources { return $hosts; } - $actors = Event_Sources_Collection::get_event_sources(); + $event_sources = Event_Sources_Collection::get_event_sources(); $hosts = array(); - foreach ( $actors as $actor ) { - $url = wp_parse_url( $actor->get_id() ); + foreach ( array_keys( $event_sources ) as $actor ) { + $url = wp_parse_url( $actor ); if ( isset( $url['host'] ) ) { $hosts[] = $url['host']; } @@ -321,30 +319,6 @@ class Event_Sources { return $hosts; } - /** - * Get add Event Sources ActivityPub IDs. - * - * @return array A list with the ActivityPub IDs of all Event Sources (follows). - */ - public static function get_event_sources_ids() { - $ids = get_transient( 'event_bridge_for_activitypub_event_sources_ids' ); - - if ( $ids ) { - return $ids; - } - - $actors = Event_Sources_Collection::get_event_sources(); - - $ids = array(); - foreach ( $actors as $actor ) { - $ids[] = $actor->get_id(); - } - - set_transient( 'event_bridge_for_activitypub_event_sources_ids', $ids ); - - return $ids; - } - /** * Add Event Sources hosts to allowed hosts used by safe redirect. * @@ -511,7 +485,7 @@ class Event_Sources { * @return bool True if the ActivityPub actor ID is followed, false otherwise. */ public static function actor_is_event_source( $actor_id ) { - $event_sources_ids = self::get_event_sources_ids(); + $event_sources_ids = array_keys( Event_Sources_Collection::get_event_sources() ); if ( in_array( $actor_id, $event_sources_ids, true ) ) { return true; } diff --git a/tests/includes/activitypub/collection/class-test-event-sources.php b/tests/includes/activitypub/collection/class-test-event-sources.php new file mode 100644 index 0000000..c60b986 --- /dev/null +++ b/tests/includes/activitypub/collection/class-test-event-sources.php @@ -0,0 +1,98 @@ + 'https://remote.example/@organizer', + 'type' => 'Person', + 'inbox' => 'https://remote.example/@organizer/inbox', + 'outbox' => 'https://remote.example/@organizer/outbox', + 'name' => 'The Organizer', + 'summary' => 'Just a random organizer of events in the Fediverse', + ); + + const FOLLOWED_ACTOR_2 = array( + 'id' => 'https://remote.example/@organizer2', + 'type' => 'Person', + 'inbox' => 'https://remote.example/@organizer2/inbox', + 'outbox' => 'https://remote.example/@organizer2/outbox', + 'name' => 'The Second Organizer', + 'summary' => 'Just a second random organizer of events in the Fediverse', + ); + + /** + * REST Server. + * + * @var WP_REST_Server + */ + protected $server; + + /** + * Set up the test. + */ + public function set_up() { + if ( ! defined( 'GATHERPRESS_CORE_FILE' ) ) { + self::markTestSkipped( 'GatherPress plugin is not active.' ); + } + + \add_option( 'permalink_structure', '/%postname%/' ); + + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + $this->server = $wp_rest_server; + + do_action( 'rest_api_init' ); + + \Activitypub\Rest\Server::add_hooks(); + + // Mock the plugin activation. + \GatherPress\Core\Setup::get_instance()->activate_gatherpress_plugin( false ); + + // Make sure that ActivityPub support is enabled for GatherPress. + $aec = \Event_Bridge_For_ActivityPub\Setup::get_instance(); + $aec->activate_activitypub_support_for_active_event_plugins(); + + // Add event sources (ActivityPub followers). + _delete_all_posts(); + \Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source::init_from_array( self::FOLLOWED_ACTOR_1 )->save(); + \Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source::init_from_array( self::FOLLOWED_ACTOR_2 )->save(); + + \update_option( 'event_bridge_for_activitypub_event_sources_active', true ); + \update_option( + 'event_bridge_for_activitypub_integration_used_for_event_sources_feature', + \Event_Bridge_For_ActivityPub\Integrations\GatherPress::class + ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); + } + + /** + * Testing the fetching of event sources from the database. + * + * @covers \Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources::get_event_sources_with_count + * @covers \Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources::get_event_sources + */ + public function test_get_event_sources_with_count() { + \delete_transient( 'event_bridge_for_activitypub_event_sources' ); + $event_sources = \Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources::get_event_sources(); + + $this->assertCount( 2, $event_sources ); + + $this->assertArrayHasKey( self::FOLLOWED_ACTOR_1['id'], $event_sources ); + $this->assertArrayHasKey( self::FOLLOWED_ACTOR_2['id'], $event_sources ); + } +}