From a3ba5d2a4873b35c0f4c04f38e1e379274d98724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 09:24:45 +0200 Subject: [PATCH 01/15] typo in phpdoc --- includes/collection/class-followers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index b51224fd0..fdd80daa7 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -111,7 +111,7 @@ public static function get_follower( $user_id, $actor ) { } /** - * Get a Follower by Actor indepenent from the User. + * Get a Follower by Actor independent from the User. * * @param string $actor The Actor URL. * From a2360660cb3d56ef4d2aebbf2679c72d37d3f7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 11:04:33 +0200 Subject: [PATCH 02/15] add first draft for adding replies collections to posts and comments --- includes/activity/class-activity.php | 15 ++++ includes/collection/class-replies.php | 114 ++++++++++++++++++++++++++ includes/rest/class-collection.php | 82 ++++++++++++++++++ includes/transformer/class-base.php | 13 ++- includes/transformer/class-post.php | 2 +- 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 includes/collection/class-replies.php diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php index 44dc3c3a2..e6e3ecbb4 100644 --- a/includes/activity/class-activity.php +++ b/includes/activity/class-activity.php @@ -90,6 +90,21 @@ class Activity extends Base_Object { */ protected $result; + /** + * Identifies a Collection containing objects considered to be responses + * to this object. + * WordPress has a strong core system of approving replies. We only include + * approved replies here. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies + * + * @var array + * | ObjectType + * | Link + * | null + */ + protected $replies; + /** * An indirect object of the activity from which the * activity is directed. diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php new file mode 100644 index 000000000..6373909de --- /dev/null +++ b/includes/collection/class-replies.php @@ -0,0 +1,114 @@ +ID ) ); + } elseif ( $wp_object instanceof WP_Comment ) { + return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) ); + } else { + return null; + } + } + + /** + * Get the replies Collection. + * + * @param WP_Post|WP_Comment $wp_object + * @param int $page + */ + public static function get_replies( $wp_object ) { + $id = self::get_replies_id( $wp_object ); + + if ( ! $id ) { + return null; + } + + $replies = array( + 'id' => $id , + 'type' => 'Collection', + ); + + $replies['first'] = self::get_collection_page( $wp_object, 0, $replies['id'] ); + + return $replies; + } + + public static function get_collection_page( $wp_object, $page, $part_of = null ) { + $per_page = 10; + $offset = intval( $page ) * $per_page; + $args = array( + 'status' => 'approve', + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + ); + + if ( $wp_object instanceof WP_Post ) { // && $wp_object->comment_count ) { + $args['parent'] = 0; + $args['post_id'] = $wp_object->ID; + } elseif ( $wp_object instanceof WP_Comment ) { + $args = array( + 'parent' => $wp_object->comment_ID, + ); + } else { + return new WP_Error(); + } + + $number_of_replies = get_comments(array_merge($args, array('count' => true ) ) ); + + $args['number'] = $per_page; + $args['offset'] = $offset; + + $comments = get_comments( $args ); + + if ( ! isset( $part_of ) ) { + $part_of = self::get_replies_id( $wp_object ); + } + + $id = add_query_arg( 'page', $page, $part_of ); + + $comment_ids = array(); + // Only add external comments from the fedi verse. + // Maybe use the Comment class more and the function is_local_comment etc. + foreach ( $comments as $comment ) { + if ( is_local_comment( $comment ) ) { + continue; + } + $comment_meta = \get_comment_meta( $comment->comment_ID ); + if ( ! empty( $comment_meta['source_url'][0] ) ) { + $comment_ids[] = $comment_meta['source_url'][0]; + } elseif ( ! empty( $comment_meta['source_id'][0] ) ) { + $comment_ids[] = $comment_meta['source_id'][0]; + } + } + + $collection_page = array( + 'id' => $id, + 'type' => 'CollectionPage', + 'partOf' => $part_of, + 'items' => $comment_ids, + ); + + if ( $number_of_replies / $per_page > $page + 1 ) { + $collection_page['next'] = add_query_arg( 'page', $page + 1 , $part_of ); + } + + return $collection_page; + } +} \ No newline at end of file diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index 296789fb2..acbafadbb 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -6,7 +6,10 @@ use Activitypub\Activity\Actor; use Activitypub\Activity\Base_Object; use Activitypub\Collection\Users as User_Collection; +use Activitypub\Collection\Replies as Replies_Collection; + use Activitypub\Transformer\Factory; +use WP_Error; use function Activitypub\esc_hashtag; use function Activitypub\is_single_user; @@ -69,6 +72,64 @@ public static function register_routes() { ), ) ); + + \register_rest_route( + ACTIVITYPUB_REST_NAMESPACE, + '/(?P[\w\-\.]+)s/(?P[\w\-\.]+)/replies', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( self::class, 'replies_get' ), + 'args' => self::request_parameters_for_replies(), + 'permission_callback' => '__return_true', + ), + ) + ); + } + + /** + * The endpoint for replies collections + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response The response object. + */ + public static function replies_get( $request ) { + $type = $request->get_param( 'type' ); + + if ( 'post' === $type ) { + $wp_object = get_post( $request->get_param( 'id' ) ) ; + } elseif ( 'comment' === $type ) { + $wp_object = get_comment( $request->get_param( 'id' ) ); + } + + + if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) { + return new WP_Error( + 'activitypub_replies_collection_does_not_exist', + \sprintf( + // translators: %s: The type (post, comment, etc.) for which no replies collection exists. + \__( + 'No reply collection exists for the type %s.' + ), + $type + ) + ); + } + + $page = $request->get_param( 'page' ); + if ( isset( $page ) ) { + $response = Replies_Collection::get_collection_page( $wp_object, $page ); + } else { + $response = Replies_Collection::get_replies( $wp_object ); + + } + + if ( is_wp_error( $response ) ) { + return $response; + } + + return new WP_REST_Response( $response, 200 ); } /** @@ -225,4 +286,25 @@ public static function request_parameters() { return $params; } + + /** + * The supported parameters + * + * @return array list of parameters + */ + public static function request_parameters_for_replies() { + $params = array(); + + $params['type'] = array( + 'required' => true, + 'type' => 'string', + ); + + $params['id'] = array( + 'required' => true, + 'type' => 'string', + ); + + return $params; + } } diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 5041fa963..2092a7ec0 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -1,12 +1,13 @@ wp_object, $this->get_id() ); + return $replies; + } + /** * Returns the ID of the WordPress Object. * diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 7a2c7c140..bdd548a71 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -88,7 +88,7 @@ public function to_object() { * * @return string The Posts ID. */ - public function get_id() { + protected function get_id() { return $this->get_url(); } From d7a80349faf98e703b1e39b4dc564c1e4d9085c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 12:31:09 +0200 Subject: [PATCH 03/15] refactoring --- includes/collection/class-replies.php | 139 ++++++++++++++++++-------- includes/rest/class-collection.php | 17 +++- includes/transformer/class-base.php | 4 +- 3 files changed, 112 insertions(+), 48 deletions(-) diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index 6373909de..acc5f09d6 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -10,11 +10,50 @@ class Replies { + /** + * Build base arguments for fetching the comments of either a WordPress post or comment. + * + * @param WP_Post|WP_Comment $wp_object + */ + private static function build_args( $wp_object ) { + $args = array( + 'status' => 'approve', + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + ); + + if ( $wp_object instanceof WP_Post ) { + $args['parent'] = 0; // TODO: maybe this is unnecessary. + $args['post_id'] = $wp_object->ID; + } elseif ( $wp_object instanceof WP_Comment ) { + $args['parent'] = $wp_object->comment_ID; + } else { + return new WP_Error(); + } + + return $args; + } + + /** + * Adds pagination args comments query. + * + * @param array $args Query args built by self::build_args. + * @param int $page The current pagination page. + * @param int $comments_per_page The number of comments per page. + */ + private static function add_pagination_args( $args, $page, $comments_per_page ) { + $args['number'] = $comments_per_page; + $args['offset'] = intval( $page ) * $comments_per_page;; + + return $args; + } + + /** * Get the replies collections ID. * * @param WP_Post|WP_Comment $wp_object - * + * * @return string The rest URL of the replies collection. */ private static function get_replies_id( $wp_object ) { @@ -23,17 +62,19 @@ private static function get_replies_id( $wp_object ) { } elseif ( $wp_object instanceof WP_Comment ) { return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) ); } else { - return null; + return new WP_Error(); } } /** - * Get the replies Collection. + * Get the replies collection. * * @param WP_Post|WP_Comment $wp_object * @param int $page + * + * @return array An associative array containing the replies collection without JSON-LD context. */ - public static function get_replies( $wp_object ) { + public static function get_collection( $wp_object ) { $id = self::get_replies_id( $wp_object ); if ( ! $id ) { @@ -50,41 +91,19 @@ public static function get_replies( $wp_object ) { return $replies; } - public static function get_collection_page( $wp_object, $page, $part_of = null ) { - $per_page = 10; - $offset = intval( $page ) * $per_page; - $args = array( - 'status' => 'approve', - 'orderby' => 'comment_date_gmt', - 'order' => 'ASC', - ); - - if ( $wp_object instanceof WP_Post ) { // && $wp_object->comment_count ) { - $args['parent'] = 0; - $args['post_id'] = $wp_object->ID; - } elseif ( $wp_object instanceof WP_Comment ) { - $args = array( - 'parent' => $wp_object->comment_ID, - ); - } else { - return new WP_Error(); - } - - $number_of_replies = get_comments(array_merge($args, array('count' => true ) ) ); - - $args['number'] = $per_page; - $args['offset'] = $offset; - - $comments = get_comments( $args ); - - if ( ! isset( $part_of ) ) { - $part_of = self::get_replies_id( $wp_object ); - } - - $id = add_query_arg( 'page', $page, $part_of ); - + /** + * Get the ActivityPub ID's from a list of comments. + * + * It takes only federated/non-local comments into account, others also do not have an + * ActivityPub ID available. + * + * @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from. + * + * @return string[] A list of the ActivityPub ID's. + */ + private static function get_activitypub_comment_ids( $comments ) { $comment_ids = array(); - // Only add external comments from the fedi verse. + // Only add external comments from the fediverse. // Maybe use the Comment class more and the function is_local_comment etc. foreach ( $comments as $comment ) { if ( is_local_comment( $comment ) ) { @@ -97,16 +116,54 @@ public static function get_collection_page( $wp_object, $page, $part_of = null ) $comment_ids[] = $comment_meta['source_id'][0]; } } + return $comment_ids; + } + + /** + * Returns a replies collection page as an associative array. + * + * @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage + * + * @param WP_Post|WP_Comment $wp_object The post of comment the replies are for. + * @param int $page The current pagination page. + * @param string $part_of The collection id/url the returned CollectionPage belongs to. + * + * @return array A CollectionPage as an associative array. + */ + public static function get_collection_page( $wp_object, $page, $part_of = null ) { + // Build initial arguments for fetching approved comments. + $args = self::build_args($wp_object); + + // Retrieve the partOf if not already given. + $part_of = $part_of ?? self::get_replies_id( $wp_object ); + + // If the collection page does not exist. + if ( is_wp_error($args) || is_wp_error( $part_of ) ) { + return null; + } + + // Get to total replies count. + $total_replies = get_comments( array_merge( $args, array( 'count' => true ) ) ); + + // Modify query args to retrieve paginated results. + $comments_per_page = get_option( 'comments_per_page'); + + // Fetch internal and external comments for current page. + $comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) ); + + // Get the ActivityPub ID's of the comments, without out local-only comments. + $comment_ids = self::get_activitypub_comment_ids( $comments ); + // Build the associative CollectionPage array. $collection_page = array( - 'id' => $id, + 'id' => \add_query_arg( 'page', $page, $part_of ), 'type' => 'CollectionPage', 'partOf' => $part_of, 'items' => $comment_ids, ); - if ( $number_of_replies / $per_page > $page + 1 ) { - $collection_page['next'] = add_query_arg( 'page', $page + 1 , $part_of ); + if ( $total_replies / $comments_per_page > $page + 1 ) { + $collection_page['next'] = \add_query_arg( 'page', $page + 1 , $part_of ); } return $collection_page; diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index acbafadbb..22b4f503b 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -6,7 +6,7 @@ use Activitypub\Activity\Actor; use Activitypub\Activity\Base_Object; use Activitypub\Collection\Users as User_Collection; -use Activitypub\Collection\Replies as Replies_Collection; +use Activitypub\Collection\Replies; use Activitypub\Transformer\Factory; use WP_Error; @@ -117,18 +117,25 @@ public static function replies_get( $request ) { ); } - $page = $request->get_param( 'page' ); + $page = intval( $request->get_param( 'page' ) ); + + // If the request paremeter page is present get the CollectionPage otherwiese the replies collection. if ( isset( $page ) ) { - $response = Replies_Collection::get_collection_page( $wp_object, $page ); + $response = Replies::get_collection_page( $wp_object, $page ); } else { - $response = Replies_Collection::get_replies( $wp_object ); - + $response = Replies::get_collection( $wp_object ); } if ( is_wp_error( $response ) ) { return $response; } + // Add ActivityPub Context. + $response = array_merge( + array( '@context' => Base_Object::JSON_LD_CONTEXT ), + $response + ); + return new WP_REST_Response( $response, 200 ); } diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 2092a7ec0..f33e56428 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -6,7 +6,7 @@ use Activitypub\Activity\Activity; use Activitypub\Activity\Base_Object; -use Activitypub\Collection\Replies as Replies_Collection; +use Activitypub\Collection\Replies; /** @@ -115,7 +115,7 @@ abstract protected function get_id(); * Get the replies Collection. */ public function get_replies() { - $replies = Replies_Collection::get_replies( $this->wp_object, $this->get_id() ); + $replies = Replies::get_collection( $this->wp_object, $this->get_id() ); return $replies; } From d9fc81afce8960f50befa137c4697a11c83c980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 12:47:06 +0200 Subject: [PATCH 04/15] Fix php CodeSniffer violations --- includes/collection/class-replies.php | 36 +++++++++++++++------------ includes/rest/class-collection.php | 8 +++--- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index acc5f09d6..015994478 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -1,14 +1,16 @@ $id , + 'id' => $id, 'type' => 'Collection', ); @@ -132,13 +136,13 @@ private static function get_activitypub_comment_ids( $comments ) { */ public static function get_collection_page( $wp_object, $page, $part_of = null ) { // Build initial arguments for fetching approved comments. - $args = self::build_args($wp_object); + $args = self::build_args( $wp_object ); // Retrieve the partOf if not already given. $part_of = $part_of ?? self::get_replies_id( $wp_object ); // If the collection page does not exist. - if ( is_wp_error($args) || is_wp_error( $part_of ) ) { + if ( is_wp_error( $args ) || is_wp_error( $part_of ) ) { return null; } @@ -146,7 +150,7 @@ public static function get_collection_page( $wp_object, $page, $part_of = null ) $total_replies = get_comments( array_merge( $args, array( 'count' => true ) ) ); // Modify query args to retrieve paginated results. - $comments_per_page = get_option( 'comments_per_page'); + $comments_per_page = get_option( 'comments_per_page' ); // Fetch internal and external comments for current page. $comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) ); @@ -163,9 +167,9 @@ public static function get_collection_page( $wp_object, $page, $part_of = null ) ); if ( $total_replies / $comments_per_page > $page + 1 ) { - $collection_page['next'] = \add_query_arg( 'page', $page + 1 , $part_of ); + $collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of ); } return $collection_page; } -} \ No newline at end of file +} diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index 22b4f503b..84922bf6b 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -97,13 +97,13 @@ public static function register_routes() { public static function replies_get( $request ) { $type = $request->get_param( 'type' ); + // Get the WordPress object of that "owns" the requested replies. if ( 'post' === $type ) { - $wp_object = get_post( $request->get_param( 'id' ) ) ; + $wp_object = get_post( $request->get_param( 'id' ) ); } elseif ( 'comment' === $type ) { - $wp_object = get_comment( $request->get_param( 'id' ) ); + $wp_object = get_comment( $request->get_param( 'id' ) ); } - if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) { return new WP_Error( 'activitypub_replies_collection_does_not_exist', @@ -119,7 +119,7 @@ public static function replies_get( $request ) { $page = intval( $request->get_param( 'page' ) ); - // If the request paremeter page is present get the CollectionPage otherwiese the replies collection. + // If the request parameter page is present get the CollectionPage otherwise the replies collection. if ( isset( $page ) ) { $response = Replies::get_collection_page( $wp_object, $page ); } else { From a6835a48f90655e57195299ad563de72a14c0ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 12:48:36 +0200 Subject: [PATCH 05/15] fix typo in php comment --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index b8d58cc2b..34c48f299 100644 --- a/activitypub.php +++ b/activitypub.php @@ -37,7 +37,7 @@ \defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false ); \defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false ); \defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false ); -// Disable reactions like `Like` and `Accounce` by default +// Disable reactions like `Like` and `Announce` by default \defined( 'ACTIVITYPUB_DISABLE_REACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_REACTIONS', true ); \defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false ); \defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false ); From a9a22a9d356261c2f30d388ca9e8f59786b23ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 13:09:10 +0200 Subject: [PATCH 06/15] add draft for testing replies --- tests/test-class-activitypub-replies.php | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/test-class-activitypub-replies.php diff --git a/tests/test-class-activitypub-replies.php b/tests/test-class-activitypub-replies.php new file mode 100644 index 000000000..5e0a8631c --- /dev/null +++ b/tests/test-class-activitypub-replies.php @@ -0,0 +1,35 @@ + 1, + 'post_content' => 'test', + ) + ); + + $comment = array( + 'comment_type' => 'comment', + 'comment_content' => 'This is a comment.', + 'comment_author_url' => 'https://example.com', + 'comment_author_email' => '', + 'comment_meta' => array( + 'protocol' => 'activitypub', + ), + 'comment_post_ID' => $post->ID, + ); + + $comment_id = wp_insert_comment( $comment ); + + $replies = Activitypub\Collection\Replies::get_collection( $post ); + + $this->assertEquals( $replies['id'], sprintf( 'https://example.com/wp-json/activitypub/1.0/posts/%d/replies', $post->ID ) ); + $this->assertCount( 0, $replies['first']['items'] ); + + wp_set_comment_status( $comment_id, 'approve' ); + + $replies = Activitypub\Collection\Replies::get_collection( $post ); + $this->assertCount( 1, $replies['first']['items'] ); + $this->assertEquals( $replies['first']['items'][0], sprintf( 'https://example.com/?c=%d', $comment_id ) ); + } +} From b1413cbb5aa601f140e976ee54b8fa776355df9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 13:17:47 +0200 Subject: [PATCH 07/15] replies: test with own comment --- tests/test-class-activitypub-replies.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-class-activitypub-replies.php b/tests/test-class-activitypub-replies.php index 5e0a8631c..5b143714b 100644 --- a/tests/test-class-activitypub-replies.php +++ b/tests/test-class-activitypub-replies.php @@ -9,12 +9,13 @@ public function test_replies_collection_of_post_with_federated_comments() { ); $comment = array( + 'user_id' => 1, 'comment_type' => 'comment', 'comment_content' => 'This is a comment.', 'comment_author_url' => 'https://example.com', 'comment_author_email' => '', 'comment_meta' => array( - 'protocol' => 'activitypub', + 'activitypub_status' => 'federated', ), 'comment_post_ID' => $post->ID, ); From ee3b19bec520ed506f399c13bfcf9105af4701b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 1 Sep 2024 16:33:34 +0200 Subject: [PATCH 08/15] fix basic test for replies collection --- tests/test-class-activitypub-replies.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/test-class-activitypub-replies.php b/tests/test-class-activitypub-replies.php index 5b143714b..de8ffca45 100644 --- a/tests/test-class-activitypub-replies.php +++ b/tests/test-class-activitypub-replies.php @@ -1,13 +1,16 @@ 1, 'post_content' => 'test', ) ); + $source_id = 'https://example.instance/notes/123'; + $comment = array( 'user_id' => 1, 'comment_type' => 'comment', @@ -15,22 +18,22 @@ public function test_replies_collection_of_post_with_federated_comments() { 'comment_author_url' => 'https://example.com', 'comment_author_email' => '', 'comment_meta' => array( - 'activitypub_status' => 'federated', + 'protocol' => 'activitypub', + 'source_id' => $source_id, ), - 'comment_post_ID' => $post->ID, + 'comment_post_ID' => $post_id, ); $comment_id = wp_insert_comment( $comment ); - $replies = Activitypub\Collection\Replies::get_collection( $post ); - - $this->assertEquals( $replies['id'], sprintf( 'https://example.com/wp-json/activitypub/1.0/posts/%d/replies', $post->ID ) ); + wp_set_comment_status( $comment_id, 'hold' ); + $replies = Activitypub\Collection\Replies::get_collection( get_post( $post_id ) ); + $this->assertEquals( $replies['id'], sprintf( 'http://example.org/index.php?rest_route=/activitypub/1.0/posts/%d/replies', $post_id ) ); $this->assertCount( 0, $replies['first']['items'] ); wp_set_comment_status( $comment_id, 'approve' ); - - $replies = Activitypub\Collection\Replies::get_collection( $post ); + $replies = Activitypub\Collection\Replies::get_collection( get_post( $post_id ) ); $this->assertCount( 1, $replies['first']['items'] ); - $this->assertEquals( $replies['first']['items'][0], sprintf( 'https://example.com/?c=%d', $comment_id ) ); + $this->assertEquals( $replies['first']['items'][0], $source_id ); } } From 60ffb6511cf07eccbc6c5e689c63d06e2c8658ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sat, 14 Sep 2024 12:12:35 +0200 Subject: [PATCH 09/15] Restrict 'type' parameter for replies to 'post' or 'comment' in REST API --- includes/rest/class-collection.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index 84922bf6b..3cace57e5 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -305,6 +305,7 @@ public static function request_parameters_for_replies() { $params['type'] = array( 'required' => true, 'type' => 'string', + 'enum' => array( 'post', 'comment' ), ); $params['id'] = array( From 2037579494cb8988edf5b809ae30c7a0af9a7c76 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 14 Sep 2024 14:18:09 +0200 Subject: [PATCH 10/15] some cleanups --- includes/collection/class-replies.php | 4 ++-- includes/rest/class-collection.php | 26 ++++++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index 015994478..b50f3fadb 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -147,10 +147,10 @@ public static function get_collection_page( $wp_object, $page, $part_of = null ) } // Get to total replies count. - $total_replies = get_comments( array_merge( $args, array( 'count' => true ) ) ); + $total_replies = \get_comments( array_merge( $args, array( 'count' => true ) ) ); // Modify query args to retrieve paginated results. - $comments_per_page = get_option( 'comments_per_page' ); + $comments_per_page = \get_option( 'comments_per_page' ); // Fetch internal and external comments for current page. $comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) ); diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index 3cace57e5..aa7a3bbf6 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -98,10 +98,14 @@ public static function replies_get( $request ) { $type = $request->get_param( 'type' ); // Get the WordPress object of that "owns" the requested replies. - if ( 'post' === $type ) { - $wp_object = get_post( $request->get_param( 'id' ) ); - } elseif ( 'comment' === $type ) { - $wp_object = get_comment( $request->get_param( 'id' ) ); + switch ( $type ) { + case 'comment': + $wp_object = \get_comment( $request->get_param( 'id' ) ); + break; + case 'post': + default: + $wp_object = \get_post( $request->get_param( 'id' ) ); + break; } if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) { @@ -109,9 +113,7 @@ public static function replies_get( $request ) { 'activitypub_replies_collection_does_not_exist', \sprintf( // translators: %s: The type (post, comment, etc.) for which no replies collection exists. - \__( - 'No reply collection exists for the type %s.' - ), + \__( 'No reply collection exists for the type %s.', 'activitypub' ), $type ) ); @@ -132,7 +134,7 @@ public static function replies_get( $request ) { // Add ActivityPub Context. $response = array_merge( - array( '@context' => Base_Object::JSON_LD_CONTEXT ), + array( '@context' => Base_Object::JSON_LD_CONTEXT ), $response ); @@ -288,7 +290,7 @@ public static function request_parameters() { $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); return $params; @@ -304,13 +306,13 @@ public static function request_parameters_for_replies() { $params['type'] = array( 'required' => true, - 'type' => 'string', - 'enum' => array( 'post', 'comment' ), + 'type' => 'string', + 'enum' => array( 'post', 'comment' ), ); $params['id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); return $params; From 3b5accc575976f3078da63b58822687bc74250cf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 16 Sep 2024 17:40:14 +0200 Subject: [PATCH 11/15] prefer ID over URL --- includes/collection/class-replies.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index b50f3fadb..2b9fbe074 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -113,11 +113,12 @@ private static function get_activitypub_comment_ids( $comments ) { if ( is_local_comment( $comment ) ) { continue; } + $comment_meta = \get_comment_meta( $comment->comment_ID ); - if ( ! empty( $comment_meta['source_url'][0] ) ) { - $comment_ids[] = $comment_meta['source_url'][0]; - } elseif ( ! empty( $comment_meta['source_id'][0] ) ) { + if ( ! empty( $comment_meta['source_id'][0] ) ) { $comment_ids[] = $comment_meta['source_id'][0]; + } elseif ( ! empty( $comment_meta['source_url'][0] ) ) { + $comment_ids[] = $comment_meta['source_url'][0]; } } return $comment_ids; From 2e9e740c88dd7510db4388ce9a533b7a958455f9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 16 Sep 2024 17:41:10 +0200 Subject: [PATCH 12/15] rename to `reply_id` to make clear that it is not the WordPress comment_id --- includes/collection/class-replies.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index 2b9fbe074..5f35ec213 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -105,7 +105,7 @@ public static function get_collection( $wp_object ) { * * @return string[] A list of the ActivityPub ID's. */ - private static function get_activitypub_comment_ids( $comments ) { + private static function get_reply_ids( $comments ) { $comment_ids = array(); // Only add external comments from the fediverse. // Maybe use the Comment class more and the function is_local_comment etc. @@ -157,7 +157,7 @@ public static function get_collection_page( $wp_object, $page, $part_of = null ) $comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) ); // Get the ActivityPub ID's of the comments, without out local-only comments. - $comment_ids = self::get_activitypub_comment_ids( $comments ); + $comment_ids = self::get_reply_ids( $comments ); // Build the associative CollectionPage array. $collection_page = array( From 769e434f02b7acdbe641ca60c5488b5b7c90e64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 23 Sep 2024 14:06:45 +0200 Subject: [PATCH 13/15] modularize retrieving of comment link via comment meta --- includes/class-comment.php | 44 ++++++++++++++++++-------- includes/collection/class-replies.php | 10 +++--- includes/rest/class-comment.php | 8 ++--- includes/transformer/class-comment.php | 9 ++---- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index 24b0a4542..efc128872 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -340,6 +340,31 @@ public static function comment_class( $classes, $css_class, $comment_id ) { return $classes; } + /** + * Gets the public comment link (id/url) via the WordPress comments meta. + * + * If $prefer_id is true it prefers the 'source_id' meta-key over 'source_url'. + * If it is false, it's the other way round. + * + * @param int $wp_comment_id The internal WordPress comment ID. + * @param bool $prefer_id Whether to prefer the source_id comment meta field over the source_url one. + * @return string|null The ActivityPub id/url of the comment. + */ + public static function get_comment_link_from_meta( $wp_comment_id, $prefer_id = true ) { + $preferred_source = $prefer_id ? 'source_id' : 'source_url'; + $fallback_source = $prefer_id ? 'source_url' : 'source_id'; + + $comment_meta = \get_comment_meta( $wp_comment_id ); + + if ( ! empty( $comment_meta[ $preferred_source ][0] ) ) { + return $comment_meta[ $preferred_source ][0]; + } elseif ( ! empty( $comment_meta[ $fallback_source ][0] ) ) { + return $comment_meta[ $fallback_source ][0]; + } + + return null; + } + /** * Link remote comments to source url. * @@ -353,15 +378,9 @@ public static function remote_comment_link( $comment_link, $comment ) { return $comment_link; } - $comment_meta = \get_comment_meta( $comment->comment_ID ); + $public_comment_link = self::get_comment_link_from_meta( $comment->comment_ID, false); - if ( ! empty( $comment_meta['source_url'][0] ) ) { - return $comment_meta['source_url'][0]; - } elseif ( ! empty( $comment_meta['source_id'][0] ) ) { - return $comment_meta['source_id'][0]; - } - - return $comment_link; + return $public_comment_link ?? $comment_link; } @@ -374,13 +393,12 @@ public static function remote_comment_link( $comment_link, $comment ) { */ public static function generate_id( $comment ) { $comment = \get_comment( $comment ); - $comment_meta = \get_comment_meta( $comment->comment_ID ); // show external comment ID if it exists - if ( ! empty( $comment_meta['source_id'][0] ) ) { - return $comment_meta['source_id'][0]; - } elseif ( ! empty( $comment_meta['source_url'][0] ) ) { - return $comment_meta['source_url'][0]; + $public_comment_link = self::get_comment_link_from_meta( $comment->comment_ID ); + + if ( $public_comment_link ) { + return $public_comment_link; } // generate URI based on comment ID diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index 5f35ec213..9e3fbd0f7 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -5,6 +5,8 @@ use WP_Comment; use WP_Error; +use Activitypub\Comment; + use function Activitypub\is_local_comment; use function Activitypub\get_rest_url_by_path; @@ -114,11 +116,9 @@ private static function get_reply_ids( $comments ) { continue; } - $comment_meta = \get_comment_meta( $comment->comment_ID ); - if ( ! empty( $comment_meta['source_id'][0] ) ) { - $comment_ids[] = $comment_meta['source_id'][0]; - } elseif ( ! empty( $comment_meta['source_url'][0] ) ) { - $comment_ids[] = $comment_meta['source_url'][0]; + $public_comment_id = Comment::get_comment_link_from_meta( $comment->comment_ID ); + if ( $public_comment_id ) { + $comment_ids[] = $public_comment_id; } } return $comment_ids; diff --git a/includes/rest/class-comment.php b/includes/rest/class-comment.php index a31b5978e..2dfc767e2 100644 --- a/includes/rest/class-comment.php +++ b/includes/rest/class-comment.php @@ -75,13 +75,9 @@ public static function remote_reply_get( WP_REST_Request $request ) { return $template; } - $comment_meta = \get_comment_meta( $comment_id ); + $resource = Comment_Utils::get_comment_link_from_meta( $comment_id ); - if ( ! empty( $comment_meta['source_id'][0] ) ) { - $resource = $comment_meta['source_id'][0]; - } elseif ( ! empty( $comment_meta['source_url'][0] ) ) { - $resource = $comment_meta['source_url'][0]; - } else { + if ( ! $resource ) { $resource = Comment_Utils::generate_id( $comment ); } diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index 72cf11f60..52764047e 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -134,13 +134,8 @@ protected function get_in_reply_to() { } if ( $parent_comment ) { - $comment_meta = \get_comment_meta( $parent_comment->comment_ID ); - - if ( ! empty( $comment_meta['source_id'][0] ) ) { - $in_reply_to = $comment_meta['source_id'][0]; - } elseif ( ! empty( $comment_meta['source_url'][0] ) ) { - $in_reply_to = $comment_meta['source_url'][0]; - } elseif ( ! empty( $parent_comment->user_id ) ) { + $in_reply_to = Comment_Utils::get_comment_link_from_meta( $parent_comment->comment_ID ); + if ( ! $in_reply_to && ! empty( $parent_comment->user_id ) ) { $in_reply_to = Comment_Utils::generate_id( $parent_comment ); } } else { From 7cc237aacd6ce4cc984020dd53233f9fc5603854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 23 Sep 2024 14:11:26 +0200 Subject: [PATCH 14/15] fix phpcs --- includes/class-comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index efc128872..9ad7597ea 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -378,7 +378,7 @@ public static function remote_comment_link( $comment_link, $comment ) { return $comment_link; } - $public_comment_link = self::get_comment_link_from_meta( $comment->comment_ID, false); + $public_comment_link = self::get_comment_link_from_meta( $comment->comment_ID, false ); return $public_comment_link ?? $comment_link; } From e22eb49e91b3338b57d834c11386f612171f6055 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 25 Sep 2024 11:42:05 +0200 Subject: [PATCH 15/15] I think we should be more precise with this and maybe there are other fallbacks coming --- includes/class-comment.php | 47 ++++++++++++------- includes/collection/class-replies.php | 8 ++-- includes/rest/class-comment.php | 2 +- includes/transformer/class-comment.php | 2 +- tests/test-class-activitypub-comment.php | 57 ++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 22 deletions(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index 9ad7597ea..5f546b711 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -341,25 +341,40 @@ public static function comment_class( $classes, $css_class, $comment_id ) { } /** - * Gets the public comment link (id/url) via the WordPress comments meta. + * Gets the public comment id via the WordPress comments meta. * - * If $prefer_id is true it prefers the 'source_id' meta-key over 'source_url'. - * If it is false, it's the other way round. + * @param int $wp_comment_id The internal WordPress comment ID. + * @param bool $fallback Whether the code should fall back to `source_url` if `source_id` is not set. * - * @param int $wp_comment_id The internal WordPress comment ID. - * @param bool $prefer_id Whether to prefer the source_id comment meta field over the source_url one. - * @return string|null The ActivityPub id/url of the comment. + * @return string|null The ActivityPub id/url of the comment. */ - public static function get_comment_link_from_meta( $wp_comment_id, $prefer_id = true ) { - $preferred_source = $prefer_id ? 'source_id' : 'source_url'; - $fallback_source = $prefer_id ? 'source_url' : 'source_id'; + public static function get_source_id( $wp_comment_id, $fallback = true ) { + $comment_meta = \get_comment_meta( $wp_comment_id ); + + if ( ! empty( $comment_meta['source_id'][0] ) ) { + return $comment_meta['source_id'][0]; + } elseif ( ! empty( $comment_meta['source_url'][0] && $fallback ) ) { + return $comment_meta['source_url'][0]; + } + + return null; + } + /** + * Gets the public comment url via the WordPress comments meta. + * + * @param int $wp_comment_id The internal WordPress comment ID. + * @param bool $fallback Whether the code should fall back to `source_id` if `source_url` is not set. + * + * @return string|null The ActivityPub id/url of the comment. + */ + public static function get_source_url( $wp_comment_id, $fallback = true ) { $comment_meta = \get_comment_meta( $wp_comment_id ); - if ( ! empty( $comment_meta[ $preferred_source ][0] ) ) { - return $comment_meta[ $preferred_source ][0]; - } elseif ( ! empty( $comment_meta[ $fallback_source ][0] ) ) { - return $comment_meta[ $fallback_source ][0]; + if ( ! empty( $comment_meta['source_url'][0] ) ) { + return $comment_meta['source_url'][0]; + } elseif ( ! empty( $comment_meta['source_id'][0] && $fallback ) ) { + return $comment_meta['source_id'][0]; } return null; @@ -378,7 +393,7 @@ public static function remote_comment_link( $comment_link, $comment ) { return $comment_link; } - $public_comment_link = self::get_comment_link_from_meta( $comment->comment_ID, false ); + $public_comment_link = self::get_source_url( $comment->comment_ID ); return $public_comment_link ?? $comment_link; } @@ -392,10 +407,10 @@ public static function remote_comment_link( $comment_link, $comment ) { * @return string ActivityPub URI for comment */ public static function generate_id( $comment ) { - $comment = \get_comment( $comment ); + $comment = \get_comment( $comment ); // show external comment ID if it exists - $public_comment_link = self::get_comment_link_from_meta( $comment->comment_ID ); + $public_comment_link = self::get_source_id( $comment->comment_ID ); if ( $public_comment_link ) { return $public_comment_link; diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index 9e3fbd0f7..ade818362 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -62,7 +62,7 @@ private static function add_pagination_args( $args, $page, $comments_per_page ) * * @return string The rest URL of the replies collection. */ - private static function get_replies_id( $wp_object ) { + private static function get_id( $wp_object ) { if ( $wp_object instanceof WP_Post ) { return get_rest_url_by_path( sprintf( 'posts/%d/replies', $wp_object->ID ) ); } elseif ( $wp_object instanceof WP_Comment ) { @@ -81,7 +81,7 @@ private static function get_replies_id( $wp_object ) { * @return array An associative array containing the replies collection without JSON-LD context. */ public static function get_collection( $wp_object ) { - $id = self::get_replies_id( $wp_object ); + $id = self::get_id( $wp_object ); if ( ! $id ) { return null; @@ -116,7 +116,7 @@ private static function get_reply_ids( $comments ) { continue; } - $public_comment_id = Comment::get_comment_link_from_meta( $comment->comment_ID ); + $public_comment_id = Comment::get_source_id( $comment->comment_ID ); if ( $public_comment_id ) { $comment_ids[] = $public_comment_id; } @@ -140,7 +140,7 @@ public static function get_collection_page( $wp_object, $page, $part_of = null ) $args = self::build_args( $wp_object ); // Retrieve the partOf if not already given. - $part_of = $part_of ?? self::get_replies_id( $wp_object ); + $part_of = $part_of ?? self::get_id( $wp_object ); // If the collection page does not exist. if ( is_wp_error( $args ) || is_wp_error( $part_of ) ) { diff --git a/includes/rest/class-comment.php b/includes/rest/class-comment.php index 2dfc767e2..c9f911b46 100644 --- a/includes/rest/class-comment.php +++ b/includes/rest/class-comment.php @@ -75,7 +75,7 @@ public static function remote_reply_get( WP_REST_Request $request ) { return $template; } - $resource = Comment_Utils::get_comment_link_from_meta( $comment_id ); + $resource = Comment_Utils::get_source_id( $comment_id ); if ( ! $resource ) { $resource = Comment_Utils::generate_id( $comment ); diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index 52764047e..5b53c5e5d 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -134,7 +134,7 @@ protected function get_in_reply_to() { } if ( $parent_comment ) { - $in_reply_to = Comment_Utils::get_comment_link_from_meta( $parent_comment->comment_ID ); + $in_reply_to = Comment_Utils::get_source_id( $parent_comment->comment_ID ); if ( ! $in_reply_to && ! empty( $parent_comment->user_id ) ) { $in_reply_to = Comment_Utils::generate_id( $parent_comment ); } diff --git a/tests/test-class-activitypub-comment.php b/tests/test-class-activitypub-comment.php index d1447c3f1..514fb67f0 100644 --- a/tests/test-class-activitypub-comment.php +++ b/tests/test-class-activitypub-comment.php @@ -1,5 +1,62 @@ 'comment id', + 'comment_content' => 'This is a comment id test', + 'comment_author_url' => 'https://example.com', + 'comment_author_email' => '', + 'comment_meta' => array( + 'protocol' => 'activitypub', + 'source_id' => 'https://example.com/id', + ), + ) + ); + + $this->assertEquals( 'https://example.com/id', \Activitypub\Comment::get_source_url( $comment_id ) ); + $this->assertEquals( 'https://example.com/id', \Activitypub\Comment::get_source_id( $comment_id ) ); + $this->assertEquals( 'https://example.com/id', \Activitypub\Comment::get_source_id( $comment_id, false ) ); + $this->assertEquals( null, \Activitypub\Comment::get_source_url( $comment_id, false ) ); + + $comment_id = wp_insert_comment( + array( + 'comment_type' => 'comment url', + 'comment_content' => 'This is a comment url test', + 'comment_author_url' => 'https://example.com', + 'comment_author_email' => '', + 'comment_meta' => array( + 'protocol' => 'activitypub', + 'source_url' => 'https://example.com/url', + ), + ) + ); + + $this->assertEquals( 'https://example.com/url', \Activitypub\Comment::get_source_id( $comment_id ) ); + $this->assertEquals( 'https://example.com/url', \Activitypub\Comment::get_source_url( $comment_id ) ); + $this->assertEquals( 'https://example.com/url', \Activitypub\Comment::get_source_url( $comment_id, false ) ); + $this->assertEquals( null, \Activitypub\Comment::get_source_id( $comment_id, false ) ); + + $comment_id = wp_insert_comment( + array( + 'comment_type' => 'comment url and id', + 'comment_content' => 'This is a comment url and id test', + 'comment_author_url' => 'https://example.com', + 'comment_author_email' => '', + 'comment_meta' => array( + 'protocol' => 'activitypub', + 'source_url' => 'https://example.com/url', + 'source_id' => 'https://example.com/id', + ), + ) + ); + + $this->assertEquals( 'https://example.com/id', \Activitypub\Comment::get_source_id( $comment_id ) ); + $this->assertEquals( 'https://example.com/id', \Activitypub\Comment::get_source_id( $comment_id, false ) ); + $this->assertEquals( 'https://example.com/url', \Activitypub\Comment::get_source_url( $comment_id ) ); + $this->assertEquals( 'https://example.com/url', \Activitypub\Comment::get_source_url( $comment_id, false ) ); + } + /** * @dataProvider ability_to_federate_comment */