From 6a906e5fe2f1441bdf05782314d2ee3eadda252d Mon Sep 17 00:00:00 2001 From: Matthew Exon Date: Tue, 3 Jan 2023 21:42:43 +0100 Subject: [PATCH] refactor support for threaded comments from ActivityPub --- includes/functions.php | 37 +++++++++++++++ includes/rest/class-inbox.php | 86 +++++++++++++++++++---------------- 2 files changed, 84 insertions(+), 39 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 1abb9d1..3e79e5e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -311,3 +311,40 @@ 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] ); + \error_log( "@@@ found result for " . $field_name . ": " . $result ); + if ( $result > 0 ) { + return $result; + } +} diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 8891853..38d1d50 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -425,12 +425,13 @@ 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 ) { + private static function convert_object_to_comment_data( $object ) { // check if Activity is public or not if ( ! self::is_activity_public( $object ) ) { // @todo maybe send email @@ -439,55 +440,62 @@ class Inbox { $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); - $comment_post_id = 0; - // TODO: search for an existing comment with the source url and abandon if it's already there + // Objects must have IDs + if ( ! isset( $object['object']['id'] ) ) { + \error_log( "Comment provided without ID" ); + return; + } + $id = $object['object']['id']; - if ( isset( $object['object']['context'] ) ) { - $comment_post_id = \url_to_postid( $object['object']['context'] ); + // 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; } - if ( ! $comment_post_id && isset( $object['object']['inReplyTo'] ) ) { - $comment_post_id = \url_to_postid( $object['object']['inReplyTo'] ); - } + $parent_comment = \Activitypub\object_id_to_comment( $in_reply_to ); - $comment_parent_id = 0; - $comment_meta = array( - 'source_id' => \esc_url_raw( $object['object']['id'] ), - 'source_url' => \esc_url_raw( $object['object']['url'] ), - 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ), - 'protocol' => 'activitypub', - ); - - if ( isset( $object['object']['inReplyTo'] ) ) { - $comment_meta['parent_url'] = $object['object']['inReplyTo']; - $comment_query = new \WP_Comment_Query( array( 'meta_key' => 'source_id', 'meta_value' => $object['object']['inReplyTo'] ) ); - if ( $comment_query->comments ) { - foreach ( $comment_query->comments as $comment ) { - if ( ! $comment_parent_id ) { - $comment_parent_id = $comment->comment_ID; - } - if ( ! $comment_post_id ) { - $comment_post_id = $comment->comment_post_ID; - } - } - } - } - - // save only replys and reactions + // save only replies and reactions + $comment_post_id = \Activitypub\object_to_post_id_by_field_name( $object, 'context' ) ?? + \Activitypub\object_to_post_id_by_field_name( $object, 'inReplyTo' ) ?? + ( $parent_comment ? $parent_comment->comment_post_ID : 0 ); if ( ! $comment_post_id ) { - return false; + return; } - $commentdata = array( + 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' => $comment_parent_id, - 'comment_meta' => $comment_meta, + '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 );