From 56308cdef9830632c3ee87b9e94bc2652d1329f9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 19 Dec 2024 19:58:00 +0100 Subject: [PATCH] Include @mentions in the JSON representation of the reply (#1086) * Include @mentions in the JSON representation of the reply See #844 * add tests * added changelog * PHPCS fixes * PHPCS fixes * remove unused WP_HTML_Tag_Processor class props @obenland * update tests * add escaping * Use factories and make callback removable --------- Co-authored-by: Konstantin Obenland --- CHANGELOG.md | 4 + includes/transformer/class-comment.php | 94 ++++++++--- readme.txt | 1 + .../transformer/class-test-comment.php | 157 ++++++++++++++++++ 4 files changed, 237 insertions(+), 19 deletions(-) create mode 100644 tests/includes/transformer/class-test-comment.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b0be9d06..d09b6765f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* `@mentions` in the JSON representation of the reply + ### Improved * Direct Messages: Improve HTML to e-mail text conversion diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index 930b5d31e..f689b8cf2 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -53,31 +53,30 @@ public function change_wp_user_id( $user_id ) { * @return \Activitypub\Activity\Base_Object The ActivityPub Object. */ public function to_object() { - $comment = $this->wp_object; - $object = parent::to_object(); - - $object->set_url( $this->get_id() ); - $object->set_type( 'Note' ); + $object = parent::to_object(); + + $content = $this->get_content(); + $at_replies = ''; + $reply_context = $this->extract_reply_context( array() ); + + foreach ( $reply_context as $acct => $url ) { + $at_replies .= sprintf( + '%s ', + esc_url( $url ), + esc_html( $acct ) + ); + } - $published = \strtotime( $comment->comment_date_gmt ); - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); + $at_replies = trim( $at_replies ); - $updated = \get_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', true ); - if ( $updated > $published ) { - $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); + if ( $at_replies ) { + $content = sprintf( '

%s

%s', $at_replies, $content ); } + $object->set_content( $content ); $object->set_content_map( array( - $this->get_locale() => $this->get_content(), - ) - ); - $path = sprintf( 'actors/%d/followers', intval( $comment->comment_author ) ); - - $object->set_to( - array( - 'https://www.w3.org/ns/activitystreams#Public', - get_rest_url_by_path( $path ), + $this->get_locale() => $content, ) ); @@ -308,4 +307,61 @@ public function get_locale() { */ return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->wp_object ); } + + /** + * Returns the updated date of the comment. + * + * @return string|null The updated date of the comment. + */ + public function get_updated() { + $updated = \get_comment_meta( $this->wp_object->comment_ID, 'activitypub_comment_modified', true ); + $published = \get_comment_meta( $this->wp_object->comment_ID, 'activitypub_comment_published', true ); + + if ( $updated > $published ) { + return \gmdate( 'Y-m-d\TH:i:s\Z', $updated ); + } + + return null; + } + + /** + * Returns the published date of the comment. + * + * @return string The published date of the comment. + */ + public function get_published() { + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $this->wp_object->comment_date_gmt ) ); + } + + /** + * Returns the URL of the comment. + * + * @return string The URL of the comment. + */ + public function get_url() { + return $this->get_id(); + } + + /** + * Returns the type of the comment. + * + * @return string The type of the comment. + */ + public function get_type() { + return 'Note'; + } + + /** + * Returns the to of the comment. + * + * @return array The to of the comment. + */ + public function get_to() { + $path = sprintf( 'actors/%d/followers', intval( $this->wp_object->comment_author ) ); + + return array( + 'https://www.w3.org/ns/activitystreams#Public', + get_rest_url_by_path( $path ), + ); + } } diff --git a/readme.txt b/readme.txt index b79774944..afce22033 100644 --- a/readme.txt +++ b/readme.txt @@ -138,6 +138,7 @@ For reasons of data protection, it is not possible to see the followers of other = 4.5.1 = +* Added: `@mentions` in the JSON representation of the reply * Improved: Reactions block: Remove the `wp-block-editor` dependency for frontend views * Fixed: Direct Messages: Don't send notification for received public activities diff --git a/tests/includes/transformer/class-test-comment.php b/tests/includes/transformer/class-test-comment.php new file mode 100644 index 000000000..a4a3efd73 --- /dev/null +++ b/tests/includes/transformer/class-test-comment.php @@ -0,0 +1,157 @@ +post->create(); + + // Mock the WebFinger wp_safe_remote_get. + add_filter( 'pre_http_request', array( self::class, 'pre_http_request' ), 10, 3 ); + } + + /** + * Clean up after tests. + */ + public static function wpTearDownAfterClass() { + wp_delete_post( self::$post_id, true ); + remove_filter( 'pre_http_request', array( self::class, 'pre_http_request' ) ); + } + + /** + * Test content generation with reply context. + * + * @covers ::to_object + */ + public function test_content_with_reply_context() { + // Create a parent ActivityPub comment. + $parent_comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_author_url' => 'https://remote.example/@author', + 'comment_meta' => array( + 'protocol' => 'activitypub', + ), + ) + ); + + // Create a reply comment. + $reply_comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_parent' => $parent_comment_id, + 'comment_author_url' => 'https://example.net/@remote', + 'comment_meta' => array( + 'protocol' => 'activitypub', + ), + ) + ); + + // Create a reply comment. + $test_comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_parent' => $reply_comment_id, + 'comment_author_url' => 'https://example.com/@test', + ) + ); + + // Transform comment to ActivityPub object. + $comment = get_comment( $test_comment_id ); + $transformer = new Comment( $comment ); + $object = $transformer->to_object(); + + // Get the content. + $content = $object->get_content(); + + // Test that reply context is added. + $this->assertEquals( '

@remote@example.net @author@remote.example

This is a comment

', $content ); + + // Clean up. + wp_delete_comment( $reply_comment_id, true ); + wp_delete_comment( $parent_comment_id, true ); + wp_delete_comment( $test_comment_id, true ); + } + + /** + * Test content generation with reply context. + * + * @param mixed $data The response data. + * @param array $parsed_args The request arguments. + * @param string $url The request URL. + * @return mixed The response data. + */ + public static function pre_http_request( $data, $parsed_args, $url ) { + if ( str_starts_with( $url, 'https://remote.example' ) ) { + return self::dummy_response( + wp_json_encode( + array( + 'subject' => 'acct:author@remote.example', + 'links' => array( + 'self' => array( 'href' => 'https://remote.example/@author' ), + ), + ) + ) + ); + } + + if ( str_starts_with( $url, 'https://example.net/' ) ) { + return self::dummy_response( + wp_json_encode( + array( + 'subject' => 'https://example.net/@remote', + 'aliases' => array( + 'acct:remote@example.net', + ), + 'links' => array( + 'self' => array( 'href' => 'https://example.net/@remote' ), + ), + ) + ) + ); + } + + return $data; + } + + /** + * Create a dummy response. + * + * @param string $body The body of the response. + * + * @return array The dummy response. + */ + private static function dummy_response( $body ) { + return array( + 'headers' => array(), + 'body' => $body, + 'response' => array( 'code' => 200 ), + 'cookies' => array(), + 'filename' => null, + ); + } +}