diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index bf5dc2c..14d9661 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -2,10 +2,12 @@ namespace Activitypub; use WP_Post; +use WP_Comment; use Activitypub\Activity\Activity; use Activitypub\Collection\Users; use Activitypub\Collection\Followers; use Activitypub\Transformer\Post; +use Activitypub\Transformer\Comment; use function Activitypub\is_single_user; use function Activitypub\is_user_disabled; @@ -25,6 +27,7 @@ class Activity_Dispatcher { public static function init() { \add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 ); \add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 ); + \add_action( 'activitypub_send_comment_activity', array( self::class, 'send_comment_activity' ), 10, 2 ); } /** @@ -124,200 +127,29 @@ class Activity_Dispatcher { } /** - * Send "delete" activities. + * Send Activities to followers and mentioned users. * - * @param str $activitypub_url - * @param int $user_id + * @param WP_Comment $wp_comment The ActivityPub Comment. + * @param string $type The Activity-Type. */ - public static function send_delete_url_activity( $activitypub_url, $user_id ) { - // get latest version of post - $actor = \get_author_posts_url( $user_id ); - $deleted = \current_time( 'Y-m-d\TH:i:s\Z', true ); + public static function send_comment_activity( WP_Comment $wp_comment, $type ) { - $activitypub_activity = new \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_SIMPLE ); - $activitypub_activity->set_id( $activitypub_url . '#delete' ); - $activitypub_activity->set_actor( $actor ); - $activitypub_activity->set_object( - array( - 'id' => $activitypub_url, - 'type' => 'Tombstone', - ) - ); - $activitypub_activity->set_deleted( $deleted ); + $object = Comment::transform( $wp_comment )->to_object(); - foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { - $activitypub_activity->set_to( $to ); - $activity = $activitypub_activity->to_json(); // phpcs:ignore - \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } - } + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); - /** - * Send "create" activities for comments - * - * @param \Activitypub\Model\Comment $activitypub_comment - */ - public static function send_comment_activity( $activitypub_comment_id ) { - //ONLY FOR LOCAL USERS ? - $activitypub_comment = \get_comment( $activitypub_comment_id ); - $user_id = $activitypub_comment->user_id; - $activitypub_comment = new \Activitypub\Model\Comment( $activitypub_comment ); - $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); - $activitypub_activity->from_comment( $activitypub_comment->to_array() ); + $follower_inboxes = Followers::get_inboxes( $wp_comment->user_id ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); - $inboxes = \Activitypub\get_follower_inboxes( $user_id ); + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); - $followers_url = \get_rest_url( null, '/activitypub/1.0/users/' . intval( $user_id ) . '/followers' ); - foreach ( $activitypub_activity->get_cc() as $cc ) { - if ( $cc === $followers_url ) { - continue; - } - $inbox = \Activitypub\get_inbox_by_actor( $cc ); - if ( ! $inbox || \is_wp_error( $inbox ) ) { - continue; - } - // init array if empty - if ( ! isset( $inboxes[ $inbox ] ) ) { - $inboxes[ $inbox ] = array(); - } - $inboxes[ $inbox ][] = $cc; - } + $json = $activity->to_json(); - foreach ( $inboxes as $inbox => $to ) { - $to = array_values( array_unique( $to ) ); - $activitypub_activity->set_to( $to ); - $activity = $activitypub_activity->to_json(); - \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } - } - - /** - * Forward replies to followers - * - * @param \Activitypub\Model\Comment $activitypub_comment - */ - public static function inbox_forward_activity( $activitypub_comment_id ) { - $activitypub_comment = \get_comment( $activitypub_comment_id ); - - //original author should NOT recieve a copy of their own post - $replyto[] = $activitypub_comment->comment_author_url; - $activitypub_activity = \unserialize( get_comment_meta( $activitypub_comment->comment_ID, 'ap_object', true ) ); - - //will be forwarded to the parent_comment->author or post_author followers collection - $parent_comment = \get_comment( $activitypub_comment->comment_parent ); - if ( ! is_null( $parent_comment ) ) { - $user_id = $parent_comment->user_id; - } else { - $original_post = \get_post( $activitypub_comment->comment_post_ID ); - $user_id = $original_post->post_author; - } - - unset( $activitypub_activity['user_id'] ); // remove user_id from $activitypub_comment - - foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $cc ) { - //Forward reply to followers, skip sender - if ( in_array( $cc, $replyto ) ) { - continue; - } - - $activitypub_activity['object']['cc'] = $cc; - $activitypub_activity['cc'] = $cc; - - $activity = \wp_json_encode( $activitypub_activity, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT ); - \Activitypub\forward_remote_post( $inbox, $activity, $user_id ); - } - } - - /** - * Send "update" activities. - * - * @param \Activitypub\Model\Comment $activitypub_comment - */ - public static function send_update_comment_activity( $activitypub_comment_id ) { - $activitypub_comment = \get_comment( $activitypub_comment_id ); - $updated = \get_comment_meta( $activitypub_comment_id, 'ap_last_modified', true ); - - $user_id = $activitypub_comment->user_id; - if ( ! $user_id ) { // Prevent sending received/anonymous comments. - return; - } - $activitypub_comment = new \Activitypub\Model\Comment( $activitypub_comment ); - $activitypub_comment->set_update( $updated ); - $activitypub_activity = new \Activitypub\Model\Activity( 'Update', \Activitypub\Model\Activity::TYPE_FULL ); - $activitypub_activity->from_comment( $activitypub_comment->to_array() ); - $activitypub_activity->set_update( $updated ); - - $inboxes = \Activitypub\get_follower_inboxes( $user_id ); - $followers_url = \get_rest_url( null, '/activitypub/1.0/users/' . intval( $user_id ) . '/followers' ); - - foreach ( $activitypub_activity->get_cc() as $cc ) { - if ( $cc === $followers_url ) { - continue; - } - $inbox = \Activitypub\get_inbox_by_actor( $cc ); - if ( ! $inbox || \is_wp_error( $inbox ) ) { - continue; - } - // init array if empty - if ( ! isset( $inboxes[ $inbox ] ) ) { - $inboxes[ $inbox ] = array(); - } - $inboxes[ $inbox ][] = $cc; - } - - foreach ( $inboxes as $inbox => $to ) { - $to = array_values( array_unique( $to ) ); - $activitypub_activity->set_to( $to ); - $activity = $activitypub_activity->to_json(); - \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } - } - - /** - * Send "delete" activities. - * - * @param \Activitypub\Model\Comment $activitypub_comment - */ - public static function send_delete_comment_activity( $activitypub_comment_id ) { - // get comment - $activitypub_comment = \get_comment( $activitypub_comment_id ); - $user_id = $activitypub_comment->user_id; - // Prevent sending received/anonymous comments - if ( ! $user_id ) { - return; - } - - $deleted = \wp_date( 'Y-m-d\TH:i:s\Z', \strtotime( $activitypub_comment->comment_date_gmt ) ); - - $activitypub_comment = new \Activitypub\Model\Comment( $activitypub_comment ); - $activitypub_comment->set_deleted( $deleted ); - $activitypub_activity = new \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_FULL ); - $activitypub_activity->from_comment( $activitypub_comment->to_array() ); - $activitypub_activity->set_deleted( $deleted ); - - $inboxes = \Activitypub\get_follower_inboxes( $user_id ); - $followers_url = \get_rest_url( null, '/activitypub/1.0/users/' . intval( $user_id ) . '/followers' ); - - foreach ( $activitypub_activity->get_cc() as $cc ) { - if ( $cc === $followers_url ) { - continue; - } - $inbox = \Activitypub\get_inbox_by_actor( $cc ); - if ( ! $inbox || \is_wp_error( $inbox ) ) { - continue; - } - // init array if empty - if ( ! isset( $inboxes[ $inbox ] ) ) { - $inboxes[ $inbox ] = array(); - } - $inboxes[ $inbox ][] = $cc; - } - - foreach ( $inboxes as $inbox => $to ) { - $to = array_values( array_unique( $to ) ); - $activitypub_activity->set_to( $to ); - $activity = $activitypub_activity->to_json(); - \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $json, $wp_comment->user_id ); } } } diff --git a/includes/model/class-comment.php b/includes/model/class-comment.php deleted file mode 100644 index b75a591..0000000 --- a/includes/model/class-comment.php +++ /dev/null @@ -1,305 +0,0 @@ -comment = $comment; - $this->id = $this->generate_comment_id(); - $this->comment_author_url = \get_author_posts_url( $this->comment->user_id ); - $this->in_reply_to = $this->generate_parent_url(); - $this->content_warning = $this->generate_content_warning(); - $this->permalink = $this->generate_permalink(); - $this->context = $this->generate_context(); - $this->to_recipients = $this->generate_mention_recipients(); - $this->tags = $this->generate_tags(); - $this->update = $this->generate_update(); - $this->deleted = $this->generate_trash(); - $this->replies = $this->generate_replies(); - } - - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - return $this->$var; - } - - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - $this->$var = $params[0]; - } - } - - public function to_array() { - $comment = $this->comment; - - $array = array( - 'id' => $this->id, - 'type' => 'Note', - 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $comment->comment_date_gmt ) ), - 'attributedTo' => $this->comment_author_url, - 'summary' => $this->content_warning, - 'inReplyTo' => $this->in_reply_to, - 'content' => $this->get_content(), - 'contentMap' => array( - \strstr( \get_locale(), '_', true ) => $this->get_content(), - ), - 'context' => $this->context, - // 'source' => \get_comment_link( $comment ), //non-conforming, see https://www.w3.org/TR/activitypub/#source-property - 'url' => \get_comment_link( $comment ), //link for mastodon - 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'tag' => $this->tags, - ); - if ( $this->replies ) { - $array['replies'] = $this->replies; - } - if ( $this->update ) { - $array['updated'] = $this->update; - } - if ( $this->deleted ) { - $array['deleted'] = $this->deleted; - } - - return \apply_filters( 'activitypub_comment', $array ); - } - - public function to_json() { - return \wp_json_encode( $this->to_array(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT ); - } - - public function generate_comment_author_link() { - return \get_author_posts_url( $this->comment->comment_author ); - } - - public function generate_comment_id() { - return \Activitypub\set_ap_comment_id( $this->comment->comment_ID ); - } - - public function generate_permalink() { - return \get_comment_link( $this->comment ); - } - - /** - * What is status being replied to - * Comment ID or Post ID - */ - public function generate_parent_url() { - $comment = $this->comment; - $parent_comment = \get_comment( $comment->comment_parent ); - if ( $comment->comment_parent ) { - //is parent remote? - $in_reply_to = \get_comment_meta( $comment->comment_parent, 'source_url', true ); - if ( ! $in_reply_to ) { - $in_reply_to = add_query_arg( - array( - 'p' => $comment->comment_post_ID, - 'replytocom' => $comment->comment_parent, - ), - trailingslashit( site_url() ) - ); - } - } else { //parent is_post - // Backwards compatibility - $pretty_permalink = \get_post_meta( $comment->comment_post_ID, 'activitypub_canonical_url', true ); - if ( $pretty_permalink ) { - $in_reply_to = $pretty_permalink; - } else { - $in_reply_to = add_query_arg( - array( - 'p' => $comment->comment_post_ID, - ), - trailingslashit( site_url() ) - ); - } - } - return $in_reply_to; - } - - public function generate_context() { - $comment = $this->comment; - // support pretty_permalinks - $pretty_permalink = \get_post_meta( $comment->comment_post_ID, 'activitypub_canonical_url', true ); - if ( $pretty_permalink ) { - $context = $pretty_permalink; - } else { - $context = add_query_arg( - array( - 'p' => $comment->comment_post_ID, - ), - trailingslashit( site_url() ) - ); - } - return $context; - } - - /** - * Generate courtesy Content Warning - * If parent status used CW let's just copy it - * TODO: Move to preprocess_comment / row_actions - * Add option for wrapping CW in Details/Summary markup - * Figure out some CW syntax: [shortcode-style], {brackets-style}? - * So it can be inserted into reply textbox, and removed or modified at will - */ - public function generate_content_warning() { - $comment = $this->comment; - $content_warning = null; - - // Temporarily generate Summary from parent - $parent_comment = \get_comment( $comment->comment_parent ); - if ( $parent_comment ) { - //get (received) comment - $ap_object = \unserialize( \get_comment_meta( $comment->comment_parent, 'ap_object', true ) ); - if ( isset( $ap_object['object']['summary'] ) ) { - $content_warning = $ap_object['object']['summary']; - } - } - // TODO Replace auto generate with Summary shortcode - /*summary = \get_comment_meta( $this->comment->comment_ID, 'summary', true ) ; - if ( !empty( $summary ) ) { - $content_warning = \Activitypub\add_summary( $summary ); - } */ - return $content_warning; - } - - /** - * Who is being replied to - */ - public function get_content() { - $comment = $this->comment; - - if ( isset( $this->content ) ) { - return $this->content; - } - - $comment_content = $comment->comment_content; - - $filtered_content = \apply_filters( 'the_content', $comment_content, $comment ); - $decoded_content = \html_entity_decode( $filtered_content, \ENT_QUOTES, 'UTF-8' ); - - $content = \trim( \preg_replace( '/[\n\r\t]/', '', $decoded_content ) ); - - $this->content = $content; - - return $content; - } - - /** - * Mention user being replied to - */ - public function generate_tags() { - if ( $this->tags ) { - return $this->tags; - } - - $tags = array(); - - $mentions = apply_filters( 'activitypub_extract_mentions', array(), $this->comment->comment_content, $this ); - if ( $mentions ) { - foreach ( $mentions as $mention => $url ) { - $tag = array( - 'type' => 'Mention', - 'href' => $url, - 'name' => $mention, - ); - $tags[] = $tag; - } - } - - $this->tags = $tags; - - return $tags; - } - - /** - * Generate updated datetime - */ - public function generate_update() { - $comment = $this->comment; - $updated = null; - if ( \get_comment_meta( $comment->comment_ID, 'ap_last_modified', true ) ) { - $updated = \wp_date( 'Y-m-d\TH:i:s\Z', \get_comment_meta( $comment->comment_ID, 'ap_last_modified', true ) ); - } - return $updated; - } - - /** - * Generate deleted datetime - */ - public function generate_trash() { - $comment = $this->comment; - $deleted = null; - if ( 'trash' === $comment->status ) { - $deleted = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $comment->comment_date_gmt ) ); - } - return $deleted; - } - - /** - * Generate replies collections - */ - public function generate_replies() { - $comment = $this->comment; - $replies = array(); - $args = array( - 'post_id' => $comment->comment_post_ID, - 'parent' => $comment->comment_ID, - 'status' => 'approve', - 'hierarchical' => false, - ); - $comments_list = \get_comments( $args ); - - if ( $comments_list ) { - $items = array(); - foreach ( $comments_list as $comment ) { - // remote replies - $source_url = \get_comment_meta( $comment->comment_ID, 'source_url', true ); - if ( ! empty( $source_url ) ) { - $items[] = $source_url; - } else { - // local replies - $comment_url = \add_query_arg( // - array( - 'p' => $comment->comment_post_ID, - 'replytocom' => $comment->comment_ID, - ), - trailingslashit( site_url() ) - ); - $items[] = $comment_url; - } - } - - $replies = (object) array( - 'type' => 'Collection', - 'id' => \add_query_arg( array( 'replies' => '' ), $this->id ), - 'first' => (object) array( - 'type' => 'CollectionPage', - 'partOf' => \add_query_arg( array( 'replies' => '' ), $this->id ), - 'items' => $items, - ), - ); - } - return $replies; - } -} diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php new file mode 100644 index 0000000..224b177 --- /dev/null +++ b/includes/transformer/class-comment.php @@ -0,0 +1,247 @@ +wp_comment = $wp_comment; + } + + /** + * Transforms the WP_Comment object to an ActivityPub Object + * + * @see \Activitypub\Activity\Base_Object + * + * @return \Activitypub\Activity\Base_Object The ActivityPub Object + */ + public function to_object() { + $wp_comment = $this->wp_comment; + $object = new Base_Object(); + + $object->set_id( set_ap_comment_id( $wp_comment ) ); + $object->set_url( \get_comment_link( $wp_comment->ID ) ); + $object->set_context( \get_permalink( $wp_comment->comment_post_ID ) ); + $object->set_type( 'Note' ); + + $published = \strtotime( $wp_comment->comment_date_gmt ); + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); + + $updated = \get_comment_meta( $wp_comment->comment_ID, 'ap_last_modified', true ); + if ( $updated > $published ) { + $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); + } + + $object->set_attributed_to( $this->get_attributed_to() ); + $object->set_in_reply_to( $this->get_in_reply_to() ); + $object->set_content( $this->get_content() ); + $object->set_content_map( + array( + \strstr( \get_locale(), '_', true ) => $this->get_content(), + ) + ); + $path = sprintf( 'users/%d/followers', intval( $wp_comment->comment_author ) ); + + $object->set_to( + array( + 'https://www.w3.org/ns/activitystreams#Public', + get_rest_url_by_path( $path ), + ) + ); + $object->set_cc( $this->get_cc() ); + $object->set_tag( $this->get_tags() ); + return $object; + } + + /** + * Returns the User-URL of the Author of the Post. + * + * If `single_user` mode is enabled, the URL of the Blog-User is returned. + * + * @return string The User-URL. + */ + protected function get_attributed_to() { + if ( is_single_user() ) { + $user = new Blog_User(); + return $user->get_url(); + } + + return Users::get_by_id( $this->wp_comment->user_id )->get_url(); + } + + /** + * Returns a list of Mentions, used in the Comment. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#Mention + * + * @return array The list of Mentions. + */ + protected function get_cc() { + $cc = array(); + + $mentions = $this->get_mentions(); + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $cc[] = $url; + } + } + + return $cc; + } + + /** + * Returns a list of Tags, used in the Comment. + * + * This includes Hash-Tags and Mentions. + * + * @return array The list of Tags. + */ + protected function get_tags() { + // TODO Delete Or Modify + $tags = array(); + + $comment_tags = self::get_hashtags(); + if ( $comment_tags ) { + foreach ( $comment_tags as $comment_tag ) { + $tag_link = \get_tag_link( $comment_tag ); + if ( ! $tag_link ) { + continue; + } + $tag = array( + 'type' => 'Hashtag', + 'href' => \esc_url( $tag_link ), + 'name' => esc_hashtag( $comment_tag ), + ); + $tags[] = $tag; + } + } + + $mentions = $this->get_mentions(); + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $tag = array( + 'type' => 'Mention', + 'href' => \esc_url( $url ), + 'name' => \esc_html( $mention ), + ); + $tags[] = $tag; + } + } + + return $tags; + } + + /** + * Returns the content for the ActivityPub Item. + * + * The content will be generated based on the user settings. + * + * @return string The content. + */ + protected function get_content() { + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_comment = $this->wp_comment; + $content = $wp_comment->comment_content; + + $content = \wpautop( $content ); + $content = \preg_replace( '/[\n\r\t]/', '', $content ); + $content = \trim( $content ); + + $content = \apply_filters( 'the_content', $content, $wp_comment ); + $content = \html_entity_decode( $content, \ENT_QUOTES, 'UTF-8' ); + return $content; + } + + /** + * Helper function to get the @-Mentions from the comment content. + * + * @return array The list of @-Mentions. + */ + protected function get_mentions() { + return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_comment->comment_content, $this->wp_comment ); + } + + /** + * Helper function to get the #HashTags from the comment content. + * + * @return array The list of @-Mentions. + */ + protected function get_hashtags() { + $wp_comment = $this->wp_comment; + $content = $this->get_content(); + + $tags = []; + //TODO fix hashtag + if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $content, $match ) ) { + $tags = \implode( ', ', $match[1] ); + } + \error_log( "get_hashtags: tags: " . \print_r( $tags, true ) ); + $hashtags = []; + preg_match_all("/(#\w+)/u", $content, $matches); + if ($matches) { + $hashtagsArray = array_count_values($matches[0]); + $hashtags = array_keys($hashtagsArray); + } + \error_log( "get_hashtags: hashtags: " . \print_r( $hashtags, true ) ); + return $hashtags; + + } + + /** + * Helper function to get the InReplyTo parent Comment URI. + * + * @return array The in_reply_to URI. + */ + protected function get_in_reply_to() { + $wp_comment = $this->wp_comment; + $in_reply_to = get_in_reply_to( $wp_comment ); + error_log( 'get_in_reply_to: ' . print_r( $in_reply_to, true ) ); + return $in_reply_to; + } +}