diff --git a/includes/functions.php b/includes/functions.php index 8f2e5d9..012709e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -320,3 +320,44 @@ function url_to_authorid( $url ) { return 0; } + +/** + * Examine a comment ID and look up an existing comment it represents. + * + * @param string $id ActivityPub object ID (usually a URL) to check. + * + * @return WP_Comment, or undef if no comment could be found. + */ +function object_id_to_comment( $id ) { + $comment_query = new \WP_Comment_Query( + array( + 'meta_key' => 'source_id', + 'meta_value' => $id, + ) + ); + if ( ! $comment_query->comments ) { + return; + } + if ( count( $comment_query->comments ) > 1 ) { + \error_log( "More than one comment under {$id}" ); + return; + } + return $comment_query->comments[0]; +} + +/** + * Examine an activity object and find the post that the specified URL field refers to. + * + * @param string $field_name The name of the URL field in the object to query. + * + * @return int Post ID, or null on failure. + */ +function object_to_post_id_by_field_name( $object, $field_name ) { + if ( ! isset( $object['object'][ $field_name ] ) ) { + return; + } + $result = \url_to_postid( $object['object'][ $field_name ] ); + if ( $result > 0 ) { + return $result; + } +} diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 5c21f43..5c3cd17 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -425,45 +425,85 @@ class Inbox { } /** - * Handles "Create" requests + * Converts a new ActivityPub object to comment data suitable for creating a comment * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user + * @param array $object The activity-object. + * + * @return array Comment data suitable for creating a comment. */ - public static function handle_create( $object, $user_id ) { - $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); - - if ( ! isset( $object['object']['inReplyTo'] ) ) { - return; + public static function convert_object_to_comment_data( $object ) { + if ( ! isset( $object['object'] ) ) { + return false; } // check if Activity is public or not if ( ! self::is_activity_public( $object ) ) { // @todo maybe send email - return; - } - - $comment_post_id = \url_to_postid( $object['object']['inReplyTo'] ); - - // save only replys and reactions - if ( ! $comment_post_id ) { return false; } - $commentdata = array( + $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + + // Objects must have IDs + if ( ! isset( $object['object']['id'] ) ) { + \error_log( 'Comment provided without ID' ); + return; + } + $id = $object['object']['id']; + + // Only handle replies + if ( ! isset( $object['object']['inReplyTo'] ) ) { + return; + } + $in_reply_to = $object['object']['inReplyTo']; + + // Comment already exists + if ( \Activitypub\object_id_to_comment( $id ) ) { + return; + } + + $parent_comment = \Activitypub\object_id_to_comment( $in_reply_to ); + + // save only replies and reactions + $comment_post_id = \Activitypub\object_to_post_id_by_field_name( $object, 'context' ); + if ( ! $comment_post_id ) { + $comment_post_id = \Activitypub\object_to_post_id_by_field_name( $object, 'inReplyTo' ); + } + if ( ! $comment_post_id ) { + $comment_post_id = $parent_comment->comment_post_ID; + } + if ( ! $comment_post_id ) { + return; + } + + return array( 'comment_post_ID' => $comment_post_id, 'comment_author' => \esc_attr( $meta['name'] ), 'comment_author_url' => \esc_url_raw( $object['actor'] ), 'comment_content' => \wp_filter_kses( $object['object']['content'] ), 'comment_type' => '', 'comment_author_email' => '', - 'comment_parent' => 0, + 'comment_parent' => $parent_comment ? $parent_comment->comment_ID : 0, 'comment_meta' => array( + 'source_id' => \esc_url_raw( $id ), 'source_url' => \esc_url_raw( $object['object']['url'] ), 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ), 'protocol' => 'activitypub', ), ); + } + + /** + * Handles "Create" requests + * + * @param array $object The activity-object + * @param int $user_id The id of the local blog-user + */ + public static function handle_create( $object, $user_id ) { + $commentdata = self::convert_object_to_comment_data( $object ); + if ( ! $commentdata ) { + return false; + } // disable flood control \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); @@ -472,6 +512,7 @@ class Inbox { \add_filter( 'pre_option_require_name_email', '__return_false' ); $state = \wp_new_comment( $commentdata, true ); + // TODO: search for comments with that source url as their parent url and update their parent \remove_filter( 'pre_option_require_name_email', '__return_false' ); diff --git a/tests/test-class-inbox.php b/tests/test-class-inbox.php new file mode 100644 index 0000000..38f978e --- /dev/null +++ b/tests/test-class-inbox.php @@ -0,0 +1,70 @@ +user_url = $authordata->user_url; + + $post = \wp_insert_post( + array( + 'post_author' => 1, + 'post_content' => 'test', + ) + ); + $this->post_permalink = \get_permalink( $post ); + + \add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Inbox', 'get_remote_metadata_by_actor' ), 10, 2); + } + + public static function get_remote_metadata_by_actor( $value, $actor ) { + return array( + "name" => "Example User", + "icon" => array( + "url" => "https://example.com/icon", + ), + ); + } + + public function test_convert_object_to_comment_data_basic() { + $inbox = new \Activitypub\Rest\Inbox(); + $object = array( + "actor" => $this->user_url, + "to" => [ $this->user_url ], + "cc" => [ "https://www.w3.org/ns/activitystreams#Public" ], + "object" => array( + "id" => "123", + "url" => "https://example.com/example", + "inReplyTo" => $this->post_permalink, + "content" => "example", + ), + ); + $converted = $inbox->convert_object_to_comment_data($object); + + $this->assertGreaterThan(1, $converted["comment_post_ID"]); + $this->assertEquals($converted["comment_author"], "Example User"); + $this->assertEquals($converted["comment_author_url"], "http://example.org"); + $this->assertEquals($converted["comment_content"], "example"); + $this->assertEquals($converted["comment_type"], ""); + $this->assertEquals($converted["comment_author_email"], ""); + $this->assertEquals($converted["comment_parent"], 0); + $this->assertArrayHasKey("comment_meta", $converted); + $this->assertEquals($converted["comment_meta"]["source_id"], "http://123"); + $this->assertEquals($converted["comment_meta"]["source_url"], "https://example.com/example"); + $this->assertEquals($converted["comment_meta"]["avatar_url"], "https://example.com/icon"); + $this->assertEquals($converted["comment_meta"]["protocol"], "activitypub"); + } + + public function test_convert_object_to_comment_data_non_public_rejected() { + $inbox = new \Activitypub\Rest\Inbox(); + $object = array( + "to" => ["https://example.com/profile/test"], + "cc" => [], + ); + $converted = $inbox->convert_object_to_comment_data($object); + $this->assertFalse($converted); + } +}