diff --git a/activitypub.php b/activitypub.php index eb8285b8..e8e72004 100644 --- a/activitypub.php +++ b/activitypub.php @@ -34,6 +34,14 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; \Activitypub\Activity_Dispatcher::init(); + require_once \dirname( __FILE__ ) . '/includes/model/class-comment.php'; + + require_once \dirname( __FILE__ ) . '/includes/class-mentions.php'; + \Activitypub\Mentions::init(); + + require_once \dirname( __FILE__ ) . '/includes/class-c2s.php'; + \Activitypub\C2S::init(); + require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php'; \Activitypub\Activitypub::init(); @@ -102,3 +110,11 @@ function flush_rewrite_rules() { } \register_activation_hook( __FILE__, '\Activitypub\flush_rewrite_rules' ); \register_deactivation_hook( __FILE__, '\flush_rewrite_rules' ); + +/** + * rewrite api + */ +// function rest_url_prefix( ) { +// return 'api'; +// } +// \add_filter( 'rest_url_prefix', '\Activitypub\rest_url_prefix' ); \ No newline at end of file diff --git a/includes/activitypub-client.js b/includes/activitypub-client.js new file mode 100644 index 00000000..6dbc200c --- /dev/null +++ b/includes/activitypub-client.js @@ -0,0 +1,22 @@ +(function($) { + /** + * Reply Comment-edit screen + */ + //Insert Mentions into comment content on reply + $('.comment-inline.button-link').on('click', function( event){ + // Summary/ContentWarning Syntax {}? []CW[] + var summary = $(this).attr('data-summary') ? '{' + $(this).attr('data-summary') + '} ' : ''; + var recipients = $(this).attr('data-recipients') ? $(this).attr('data-recipients') + ' ' : ''; + setTimeout(function() { + if ( summary || recipients ){ + $('#replycontent').val( summary + recipients ) + } + + }, 100); + }) + //Clear Mentions from content on cancel + $('.cancel.button').on('click', function(){ + $('#replycontent').val(''); + }); + +})( jQuery ); \ No newline at end of file diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index a4ed0c7e..69f8fef3 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -10,12 +10,15 @@ */ class Activity_Dispatcher { /** - * Initialize the class, registering WordPress hooks. + * Initialize the class, registering WordPress hooks */ public static function init() { \add_action( 'activitypub_send_post_activity', array( '\Activitypub\Activity_Dispatcher', 'send_post_activity' ) ); \add_action( 'activitypub_send_update_activity', array( '\Activitypub\Activity_Dispatcher', 'send_update_activity' ) ); \add_action( 'activitypub_send_delete_activity', array( '\Activitypub\Activity_Dispatcher', 'send_delete_activity' ) ); + \add_action( 'activitypub_send_comment_activity', array( '\Activitypub\Activity_Dispatcher', 'send_comment_activity' ) ); + \add_action( 'activitypub_inbox_forward_activity', array( '\Activitypub\Activity_Dispatcher', 'inbox_forward_activity' ) ); + \add_action( 'activitypub_send_delete_comment_activity', array( '\Activitypub\Activity_Dispatcher', 'send_delete_comment_activity' ) ); } /** @@ -33,7 +36,6 @@ public static function send_post_activity( $activitypub_post ) { 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 ); } } @@ -45,7 +47,7 @@ public static function send_post_activity( $activitypub_post ) { */ public static function send_update_activity( $activitypub_post ) { // get latest version of post - $user_id = $activitypub_post->get_post_author(); + $user_id = $activitypub_post->post_author; $activitypub_activity = new \Activitypub\Model\Activity( 'Update', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity->from_post( $activitypub_post->to_array() ); @@ -65,7 +67,7 @@ public static function send_update_activity( $activitypub_post ) { */ public static function send_delete_activity( $activitypub_post ) { // get latest version of post - $user_id = $activitypub_post->get_post_author(); + $user_id = $activitypub_post->post_author; $activitypub_activity = new \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity->from_post( $activitypub_post->to_array() ); @@ -77,4 +79,127 @@ public static function send_delete_activity( $activitypub_post ) { \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } } + + /** + * 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; + $replyto = get_comment_meta( $activitypub_comment->comment_parent, 'comment_author_url', true );// + + //error_log( 'dispatcher:send_comment:$activitypub_comment: ' . print_r( $activitypub_comment, true ) ); + + $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() ); + + \error_log( 'Activity_Dispatcher::send_comment_activity: ' . print_r($activitypub_activity, true)); + foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { + \error_log( '$user_id: ' . $user_id . ', $inbox: '. $inbox . ', $to: '. print_r($to, true ) ); + $activitypub_activity->set_to( $to[0] ); + $activity = $activitypub_activity->to_json(); // phpcs:ignore + + // Send reply to followers, skip if replying to followers (avoid duplicate replies) + // if( in_array( $to, $replyto ) || ( $replyto == $to ) ) { + // break; + // } + \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + } + // TODO: Reply (to followers and non-followers) + // if( is_array( $replyto ) && count( $replyto ) > 1 ) { + // foreach ( $replyto as $to ) { + // $inbox = \Activitypub\get_inbox_by_actor( $to ); + // $activitypub_activity->set_to( $to ); + // $activity = $activitypub_activity->to_json(); // phpcs:ignore + // error_log( 'dispatches->replyto: ' . $to ); + // \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + // } + // } elseif ( !is_array( $replyto ) ) { + // $inbox = \Activitypub\get_inbox_by_actor( $to ); + // $activitypub_activity->set_to( $replyto ); + // $activity = $activitypub_activity->to_json(); // phpcs:ignore + // error_log( 'dispatch->replyto: ' . $replyto ); + // \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 ) { + //\error_log( 'Activity_Dispatcher::inbox_forward_activity' . print_r( $activitypub_comment, true ) ); + $activitypub_comment = \get_comment( $activitypub_comment_id ); + + //original author should NOT recieve a copy of ther 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 + //TODO verify that ... what? + $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; + } + + //remove user_id from $activitypub_comment + unset($activitypub_activity['user_id']); + + foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { + \error_log( '$user_id: ' . $user_id . ', $inbox: '. $inbox . ', $to: '. print_r($to, true ) ); + + //Forward reply to followers, skip sender + if( in_array( $to, $replyto ) || ( $replyto == $to ) ) { + error_log( 'dispatch:forward: nope:' . print_r( $to, true ) ); + break; + } + + $activitypub_activity['object']['to'] = $to; + $activitypub_activity['to'] = $to; + + //$activitypub_activity + //$activitypub_activity->set_to( $to ); + //$activity = $activitypub_activity->to_json(); // phpcs:ignore + + $activity = \wp_json_encode( $activitypub_activity, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT ); + error_log( 'dispatch:forward:activity:' . print_r( $activity, true ) ); + \Activitypub\forward_remote_post( $inbox, $activity, $user_id ); + + //reset //unnecessary + //array_pop( $activitypub_activity->object->to[] ); + //array_pop( $activitypub_activity->to[] ); + } + } + + /** + * Send "delete" activities. + * + * @param \Activitypub\Model\Comment $activitypub_comment + */ + public static function send_delete_comment_activity( $activitypub_comment_id ) { + // get latest version of post + $activitypub_comment = \get_comment( $activitypub_comment_id ); + $user_id = $activitypub_comment->post_author; + error_log( 'dispatch:send_delete_comment_activity: $user_id :' . print_r( $user_id , true ) ); + + $activitypub_comment = new \Activitypub\Model\Comment( $activitypub_comment ); + $activitypub_activity = new \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_FULL ); + $activitypub_activity->from_comment( $activitypub_comment->to_array() ); + + 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 ); + } + } } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 6c7c0c89..dae1fabc 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -8,30 +8,37 @@ */ class Activitypub { /** - * Initialize the class, registering WordPress hooks. + * Initialize the class, registering WordPress hooks */ public static function init() { \add_filter( 'template_include', array( '\Activitypub\Activitypub', 'render_json_template' ), 99 ); \add_filter( 'query_vars', array( '\Activitypub\Activitypub', 'add_query_vars' ) ); \add_action( 'init', array( '\Activitypub\Activitypub', 'add_rewrite_endpoint' ) ); - \add_filter( 'pre_get_avatar_data', array( '\Activitypub\Activitypub', 'pre_get_avatar_data' ), 11, 2 ); // Add support for ActivityPub to custom post types - $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array(); - + $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page', 'mentions' ) ); foreach ( $post_types as $post_type ) { \add_post_type_support( $post_type, 'activitypub' ); } - + + \add_action( 'pre_get_posts', array( '\Activitypub\Activitypub', 'private_inbox' ), 10, 2 ); + \add_filter( 'status_edit_pre', array( '\Activitypub\Activitypub', 'set_post_type_status_private' ), 10, 2 ); + \add_action( 'transition_post_status', array( '\Activitypub\Activitypub', 'preprocess_post' ), 1, 3 ); \add_action( 'transition_post_status', array( '\Activitypub\Activitypub', 'schedule_post_activity' ), 10, 3 ); + \add_filter( 'preprocess_comment' , array( '\Activitypub\Activitypub', 'preprocess_comment' ) ); + \add_filter( 'comment_post' , array( '\Activitypub\Activitypub', 'postprocess_comment' ), 10, 3 ); + \add_action( 'transition_comment_status', array( '\Activitypub\Activitypub', 'schedule_comment_activity' ), 20, 3 ); + + \add_filter( 'pre_get_avatar_data', array( '\Activitypub\Activitypub', 'pre_get_avatar_data' ), 11, 2 ); + \add_action( 'wp_head', array( '\Activitypub\Activitypub', 'author_atom_uri' ), 2 );// TODO test if needed } /** - * Return a AS2 JSON version of an author, post or page. + * Return a AS2 JSON version of an author, post or page * - * @param string $template The path to the template object. + * @param string $template the path to the template object * - * @return string The new path to the JSON template. + * @return string the new path to the JSON template */ public static function render_json_template( $template ) { if ( ! \is_author() && ! \is_singular() ) { @@ -63,7 +70,7 @@ public static function render_json_template( $template ) { return $json_template; } - // Accept header as an array. + // accept header as an array $accept = \explode( ',', \trim( $accept_header ) ); if ( @@ -79,7 +86,8 @@ public static function render_json_template( $template ) { } /** - * Add the 'activitypub' query variable so WordPress won't mangle it. + * Add the 'photos' query variable so WordPress + * won't mangle it. */ public static function add_query_vars( $vars ) { $vars[] = 'activitypub'; @@ -95,40 +103,247 @@ public static function add_rewrite_endpoint() { } /** - * Schedule Activities. + * Private Inbox + * + * pre_get_posts + */ + public static function private_inbox( $query ) { + //TODO can the wp_post_count be updated? https://wordpress.stackexchange.com/a/151876/87622 + if( is_admin() && $query->is_main_query() ) { + if ( $query->query['post_type'] === 'activitypub' ) { + + // Only show posts to their author + $query->set( 'author', \get_current_user_id() ); + + // Hide reported posts from all_posts list + if ( empty( $query->query['post_status'] ) ) { + $query->set( 'post_status', array( 'publish', 'pending', 'draft', 'auto-draft', 'private', 'future', 'inbox' ) ); + } + + // Allow moderators access to reported posts + // TODO create moderate_mentions capability (align with delete_posts or moderate_comments) + if ( $query->query['post_status'] === 'moderation' && current_user_can( 'moderate_comments' ) ) { + $query->set( 'author', '' ); + } + + // TODO orderby date desc + // Orderby + $orderby = $query->get( 'orderby'); + + // if( 'author' == $orderby ) { + // $query->set('meta_key','author'); + // $query->set('orderby','meta_value'); + // } + // if( 'type' == $orderby ) { + // $query->set('meta_key','type'); + // $query->set('orderby','meta_value'); + // } + } + } + + if( ! is_admin() ) { + return; + } + } + + /** + * ActivityPub Audience sets Post status to private (but also immediately publishes post) + * + * TODO: Unexpected outcome of set audience+save_draft (Private Publish) + * + * @param int $post_id + * @param string $status + */ + public static function set_post_type_status_private( $status, $post_id ) { + $audience = \get_post_meta( $post_id, '_audience' ); + if ( in_array( 'private', $audience ) || in_array( 'followers_only', $audience ) ) { + $status = 'private'; + } + return $status; + } + + /** + * Schedule Activities + * transition_post_status + * https://developer.wordpress.org/reference/hooks/transition_post_status/ + */ + public static function preprocess_post( $new_status, $old_status, $post ) { + if ( isset( $_POST['_audience'] ) ) { + update_post_meta( $post->ID, '_audience', $_POST['_audience'] ); + } + if ( isset( $_POST['_mentions'] ) ) { + update_post_meta( $post->ID, '_mentions', $_POST['_mentions'] ); + } + if ( isset( $_POST['post_content'] ) || isset( $_POST['post_parent'] ) ) { + + $update_post['ID'] = $post->ID; + + // Tag users + $tagged_content = \Activitypub\transform_tags( $post->post_content ); + if ( ! empty ( $tagged_content['mentions'] ) ) { + + // TODO : How to not replace previously saved mentions? + // Only hook on publish? + \update_post_meta( $post->ID, '_mentions', $tagged_content['mentions'] ); + $update_post['post_content'] = $tagged_content['content']; + + } + // Set parent_post + if ( isset( $_POST['post_parent'] ) ) { + $update_post['post_parent'] = $_POST['post_parent']; + } + + \wp_update_post( $update_post, true ); + } + } + + /** + * Schedule Post Activities + * https://developer.wordpress.org/reference/hooks/transition_post_status/ * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param WP_Post $post Post object. + * @param int $post_id */ public static function schedule_post_activity( $new_status, $old_status, $post ) { - // Do not send activities if post is password protected. + // do not send activities if post is password protected if ( \post_password_required( $post ) ) { return; } - // Check if post-type supports ActivityPub. + // check if post-type supports ActivityPub $post_types = \get_post_types_by_support( 'activitypub' ); if ( ! \in_array( $post->post_type, $post_types, true ) ) { return; } + // do not send inbox or moderation activities + if ( $new_status === 'inbox' || $new_status === 'moderation' ) { + return; + } + $audience = \get_post_meta( $post->ID, '_audience' ); $activitypub_post = new \Activitypub\Model\Post( $post ); - - if ( 'publish' === $new_status && 'publish' !== $old_status ) { - \wp_schedule_single_event( \time(), 'activitypub_send_post_activity', array( $activitypub_post ) ); + + if ( 'publish' === $new_status && $new_status !== $old_status ) { + if ( in_array( 'private', $audience ) || in_array( 'followers_only', $audience ) ) { + //\wp_schedule_single_event( \time(), 'activitypub_send_private_activity', array( $activitypub_post ) ); + } else { + \wp_schedule_single_event( \time(), 'activitypub_send_post_activity', array( $activitypub_post ) ); + } + } elseif ( 'private' === $new_status ) { + //\wp_schedule_single_event( \time(), 'activitypub_send_private_activity', array( $activitypub_post ) ); } elseif ( 'publish' === $new_status ) { \wp_schedule_single_event( \time(), 'activitypub_send_update_activity', array( $activitypub_post ) ); } elseif ( 'trash' === $new_status ) { - \wp_schedule_single_event( \time(), 'activitypub_send_delete_activity', array( $activitypub_post ) ); + \wp_schedule_single_event( \time(), 'activitypub_send_delete_activity', array( get_permalink( $activitypub_post ) ) ); + } + } + + /** + * preprocess local comments for federated replies + */ + public static function preprocess_comment( $commentdata ) { + + //must only process replies from local actors + if ( !empty( $commentdata['user_id'] ) ) { + //\error_log( 'is_local user' );//TODO Test + //TODO TEST + $post_type = \get_object_subtype( 'post', $commentdata['comment_post_ID'] ); + $ap_post_types = \get_option( 'activitypub_support_post_types' ); + if ( !\is_null( $ap_post_types ) ) { + if ( in_array( $post_type, $ap_post_types ) ) { + $commentdata['comment_type'] = 'activitypub'; + // transform webfinger mentions to links and add @mentions to cc + $tagged_content = \Activitypub\transform_tags( $commentdata['comment_content'] ); + $commentdata['comment_content'] = $tagged_content['content']; + $commentdata['comment_meta']['mentions'] = $tagged_content['mentions']; + } + } + } + return $commentdata; + } + + /** + * postprocess_comment for federating replies and inbox-forwarding + */ + public static function postprocess_comment( $comment_id, $comment_approved, $commentdata ) { + //Admin users comments bypass transition_comment_status (auto approved) + + //\error_log( 'postprocess_comment_handler: comment_status: ' . $comment_approved ); + if ( $commentdata['comment_type'] === 'activitypub' ) { + if ( + ( $comment_approved === 1 ) && + ! empty( $commentdata['user_id'] ) && + ( $user = get_userdata( $commentdata['user_id'] ) ) && // get the user data + in_array( 'administrator', $user->roles ) // check the roles + ) { + // Only for Admins? + $mentions = \get_comment_meta( $comment_id, 'mentions', true ); + //\ActivityPub\Activity_Dispatcher::send_comment_activity( $comment_id ); // performance > followers collection + \wp_schedule_single_event( \time(), 'activitypub_send_comment_activity', array( $comment_id ) ); + + } else { + // TODO check that this is unused + // TODO comment test as anon + // TODO comment test as registered + // TODO comment test as anyother site settings + + + // $replyto = get_comment_meta( $comment_id, 'replyto', true ); + + //inbox forward prep + // if ( !empty( $ap_object ) ) { + // //if is remote user (has ap_object) + // //error_log( print_r( $ap_object, true ) ); + // // TODO verify that deduplication check happens at object create. + + // //if to/cc/audience contains local followers collection + // //$local_user = \get_comment_author_url( $comment_id ); + // //$is_local_user = \Activitypub\url_to_authorid( $commentdata['comment_author_url'] ); + + // } + } + } + } + + /** + * Schedule Activities + * + * @param int $comment + */ + public static function schedule_comment_activity( $new_status, $old_status, $activitypub_comment ) { + + // TODO format $activitypub_comment = new \Activitypub\Model\Comment( $comment ); + if ( 'approved' === $new_status && 'approved' !== $old_status ) { + //should only federate replies from local actors + //should only federate replies to federated actors + + $ap_object = unserialize( \get_comment_meta( $activitypub_comment->comment_ID, 'ap_object', true ) ); + if ( empty( $ap_object ) ) { + \wp_schedule_single_event( \time(), 'activitypub_send_comment_activity', array( $activitypub_comment->comment_ID ) ); + } else { + $local_user = \get_author_posts_url( $ap_object['user_id'] ); + if ( !is_null( $local_user ) ) { + if ( in_array( $local_user, $ap_object['to'] ) + || in_array( $local_user, $ap_object['cc'] ) + || in_array( $local_user, $ap_object['audience'] ) + || in_array( $local_user, $ap_object['tag'] ) + ) { + //if inReplyTo, object, target and/or tag are (local-wp) objects + //\ActivityPub\Activity_Dispatcher::inbox_forward_activity( $activitypub_comment ); + \wp_schedule_single_event( \time(), 'activitypub_inbox_forward_activity', array( $activitypub_comment->comment_ID ) ); + } + } + } + } elseif ( 'trash' === $new_status ) { + \wp_schedule_single_event( \time(), 'activitypub_send_delete_comment_activity', array( $activitypub_comment ) ); + } else { } } /** - * Replaces the default avatar. + * Replaces the default avatar * - * @param array $args Arguments passed to get_avatar_data(), after processing. - * @param int|string|object $id_or_email A user ID, email address, or comment object. + * @param array $args Arguments passed to get_avatar_data(), after processing. + * @param int|string|object $id_or_email A user ID, email address, or comment object * * @return array $args */ @@ -141,15 +356,15 @@ public static function pre_get_avatar_data( $args, $id_or_email ) { return $args; } - $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) ); + $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment', 'activitypub' ) ); if ( ! empty( $id_or_email->comment_type ) && ! \in_array( $id_or_email->comment_type, (array) $allowed_comment_types, true ) ) { $args['url'] = false; /** This filter is documented in wp-includes/link-template.php */ return \apply_filters( 'get_avatar_data', $args, $id_or_email ); } - // Check if comment has an avatar. - $avatar = self::get_avatar_url( $id_or_email->comment_ID ); + // check if comment has an avatar + $avatar = self::get_avatar_url( $id_or_email->comment_ID, ['default' => 'default'] ); if ( $avatar ) { if ( ! isset( $args['class'] ) || ! \is_array( $args['class'] ) ) { @@ -166,7 +381,8 @@ public static function pre_get_avatar_data( $args, $id_or_email ) { } /** - * Function to retrieve Avatar URL if stored in meta. + * Function to retrieve Avatar URL if stored in meta + * * * @param int|WP_Comment $comment * @@ -178,4 +394,12 @@ public static function get_avatar_url( $comment ) { } return \get_comment_meta( $comment->comment_ID, 'avatar_url', true ); } + + public static function author_atom_uri(){ + if ( is_author() ) { + $obj_id = get_queried_object_id(); + $current_url = get_author_posts_url( $obj_id ); + ?>post_status === 'inbox') { + $num = 1; + } + return $num; + } + + public static function activitypub_posts_custom_columns( $column, $post_id ) { + switch ( $column ) { + case 'actor': + $user = \wp_get_current_user(); + $status = \get_post_field( 'post_status', $post_id, 'display' ); + $author_meta = \get_post_meta( $post_id ); + if ( $status === 'inbox' || $status === 'moderation' ) { + $author = $author_meta['_author'][0]; + $author_url = $author_meta['_author_url'][0]; + $avatar_url = $author_meta['_avatar_url'][0]; + $webfinger = \Activitypub\url_to_webfinger( $author_url ); + echo "$author
$author_url"; + } else { + $author_url = \get_author_posts_url( $user->ID ); + echo "" . get_avatar( $user->ID, 32 ) . $user->display_name . "
$author_url"; + } + break; + + case 'type': + $audience = \get_post_meta( $post_id, '_audience', true ); + if ( !empty( $audience ) ) { + echo $audience; + } else { + _e( '', 'activitypub' ); + } + break; + + case 'content': + echo \get_post_field( 'post_content', $post_id, 'display' ); + break; + } + } + + public static function activitypub_posts_sortable_columns( $columns ) { + $custom_col_order = array( + $columns['type'] => __( 'Type', 'activitypub' ), + $columns['author'] => __( 'Author', 'activitypub' ), + ); + return $custom_col_order; + } + + public static function activitypub_posts_columns( $columns ) { + //\error_log( 'columns: ' . print_r( $columns, true ) ); + $custom_col_order = array( + 'cb' => $columns['cb'], + 'actor' => __( 'Actor', 'activitypub' ), + 'type' => __( 'Type', 'activitypub' ), + 'title' => $columns['title'], + 'content' => __( 'Content', 'activitypub' ), + 'date' => $columns['date'], + ); + return $custom_col_order; + } + + /** + * page_row_actions + */ + public static function activitypub_post_row_actions( $actions, $post ) { + if ( $post->post_type == "mention" ) { + + if ( $post->post_status == "inbox" ) { + + $trash = $actions['trash']; + + // Get post attributes for Reply & Quick reply + $mentions = \Activitypub\get_recipients( $post->post_ID, true ); + $summary = \Activitypub\get_summary( $post->post_ID ); + + // Reply to this post + $reply_url = admin_url( 'post-new.php?post_type=mention&post_parent=' . $post->ID . '&_mentions=' . $mentions .'&title=' . $summary ); + $reply_link = add_query_arg( array( 'action' => 'reply' ), $reply_url ); + $actions = array( + 'reply' => sprintf( '%2$s', + esc_url( $reply_link ), + esc_html( __( 'Reply', 'activitypub' ) ) ) + ); + + // Quick Reply to this mention + /*$reply_format = ''; + $actions['inline hide-if-no-js'] = sprintf( + $reply_format, + $post->post_ID, + 'replyto', + 'postinline', + esc_attr__( 'Reply to this mention' ), + $mentions, + $summary, + __( 'Quick reply', 'activitypub' ) + );*/ + + // Block user + /*$block_url = admin_url( 'edit.php?post_type=activitypub&post=' . $post->ID ); + $block_link = wp_nonce_url( add_query_arg( array( 'action' => 'block' ), $block_url ) ); + $actions = array_merge( $actions, array( + 'block' => sprintf( '%3$s', + esc_url( $block_link ), + __( 'Block this user', 'activitypub' ), + __( 'Block', 'activitypub' ) + ) + ) + );*/ + + // Report post to moderation. + $report_url = admin_url( 'edit.php?post_type=mention&post=' . $post->ID ); + $report_link = wp_nonce_url( add_query_arg( array( 'action' => 'report' ), $report_url ) ); + $actions = array_merge( $actions, array( + 'report' => sprintf( '%3$s', + esc_url( $report_link ), + __( 'Report this mention to moderation', 'activitypub' ), + __( 'Report', 'activitypub' ) + ) + ) + ); + + $actions['trash'] = $trash; + } + } + + return $actions; + } + + /** + * Row Actions + * load-edit.php + */ + public static function activitypub_post_actions() { + + if( isset( $_GET['post_type'] ) && $_GET['post_type'] === 'mention') { + + if( isset( $_GET['action'] ) ) { + // Report post for moderation (Change status) + if( $_GET['action'] === 'report') { + $post_id = $_GET['post']; + $update_post = array( + 'post_type' => 'mention', + 'ID' => $post_id, + 'edit_date' => false, + 'post_status' => 'moderation', + ); + $u_post_id = wp_update_post($update_post); + if( is_wp_error( $u_post_id ) ) { + error_log( $u_post_id ); + //wp_send_json_success( array( 'post_id' => $u_post_id ), 200 ); + } + } + // Block actor + // if( $_GET['action'] === 'block' ) { + // $post_id = $_GET['post']; + // } + } + + } + } + + public static function reply_comments_actions( $actions, $comment ) { + //unset( $actions['reply'] ); + $recipients = \Activitypub\get_recipients( $comment->comment_ID ); + $summary = \Activitypub\get_summary( $comment->comment_ID ); + + //TODO revise for non-js reply action + // Public Reply + $reply_button = ''; + $actions['reply'] = sprintf( + $reply_button, + $comment->comment_ID, + $comment->comment_post_ID, + 'replyto', + 'vim-r comment-inline', + esc_attr__( 'Reply to this comment' ), + $recipients, + $summary, + __( 'Reply', 'activitypub' ) + ); + + // Private + // $actions['private_reply'] = sprintf( + // $format, + // $comment->comment_ID, + // $comment->comment_post_ID, + // 'private_replyto', + // 'vim-r comment-inline', + // esc_attr__( 'Reply in private to this comment' ), + // $recipients, + // $summary, + // __( 'Private reply', 'activitypub' ) + // ); + + return $actions; + } + + public static function scripts_reply_comments( $hook ) { + if ('edit-comments.php' !== $hook) { + return; + } + wp_enqueue_script( 'activitypub_client', + plugin_dir_url(__FILE__) . '/activitypub-client.js', + array('jquery'), + filemtime( plugin_dir_path(__FILE__) . '/activitypub-client.js' ), + true + ); + } + + /** + * Adds ActivityPub Metabox to supported post types + */ + public static function add_audience_metabox() { + $ap_post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page', 'mention' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array(); + $ap_post_types[] = 'mention'; + //$ap_post_types[] = 'comment';//TODO + foreach ($ap_post_types as $post_types) { + add_meta_box( + 'activitypub_post_audience',// Unique ID + __( 'Audience', 'activitypub' ), // Box title + [self::class, 'post_audience_html'],// Content callback, must be of type callable + $post_types,// Post types, Comments + 'side', + 'high', + ); + } + } + + /** + * Save Audience and Mentions Meta Fields + * save_post_activitypub + */ + public static function save_post_audience( $post_id ) { + //wp_verify_nonce('ap_audience_meta'); + // update_post_meta( + // $post_id, + // ); + // } + // } + } + + /** + * Audience fields + * + */ + public static function post_audience_html($post) + { + wp_nonce_field( 'ap_audience_meta', 'ap_audience_meta_nonce' ); + $audience = $mentions = null; + if ( isset ( $post->ID ) ) { + $audience = get_post_meta($post->ID, '_audience', true); + $mentions = get_post_meta($post->ID, '_mentions', true); +// $replyto = get_post_meta( $post->ID, '_inreplyto', true); + if ( isset( $post->post_parent ) ){ + $replyto = $post_parent = $post->post_parent; + } + } + if (array_key_exists('_audience', $_REQUEST)) { + $audience = $_REQUEST['_audience']; + } + if (array_key_exists('_mentions', $_REQUEST)) { + $mentions = $_REQUEST['_mentions']; + } + // $replyto = get_post_meta( $post_parent, '_source_url', true); + // } + if (array_key_exists('post_parent', $_REQUEST)) { + $post_parent = $replyto = $_REQUEST['post_parent']; + } + + ?> + + +
+ + +
+ +
+ + +
+ + ID ); // Save post_ID. + // } + ?> +
+ + +
+ +
+ + +
+ *

'; + } + add_action( 'comment_form_logged_in_after', 'add_review_phone_field_on_comment_form' ); + add_action( 'comment_form_after_fields', 'add_review_phone_field_on_comment_form' ); + + + // Save phone number + add_action( 'comment_post', 'save_comment_review_phone_field' ); + function save_comment_review_phone_field( $comment_id ){ + if( isset( $_POST['phone'] ) ) + update_comment_meta( $comment_id, 'phone', esc_attr( $_POST['phone'] ) ); + } + + function print_review_phone( $id ) { + $val = get_comment_meta( $id, "phone", true ); + $title = $val ? '' . $val . '' : ''; + return $title; + } + */ \ No newline at end of file diff --git a/includes/class-mentions.php b/includes/class-mentions.php new file mode 100644 index 00000000..24bfd145 --- /dev/null +++ b/includes/class-mentions.php @@ -0,0 +1,130 @@ + _x( 'Mentions', 'Post type general name', 'activitypub' ), + 'singular_name' => _x( 'Mention', 'Post type singular name', 'activitypub' ), + 'menu_name' => _x( 'Mentions', 'Admin Menu text', 'activitypub' ), + 'name_admin_bar' => _x( 'Mention', 'Add New on Toolbar', 'activitypub' ), + 'add_new' => __( 'Add New', 'activitypub' ), + 'add_new_item' => __( 'Send Mentions', 'activitypub' ), + 'new_item' => __( 'New Mentions', 'activitypub' ), + 'edit_item' => __( 'Edit Mentions', 'activitypub' ), + 'view_item' => __( 'View Mentions', 'activitypub' ), + 'all_items' => __( 'All Mentions', 'activitypub' ), + 'search_items' => __( 'Search Mentions', 'activitypub' ), + 'parent_item_colon' => __( 'Parent Mentions:', 'activitypub' ), + 'not_found' => __( 'No Mentions found.', 'activitypub' ), + 'not_found_in_trash' => __( 'No Mentions found in Trash.', 'activitypub' ), + 'featured_image' => _x( 'Mentions Cover Image', 'Overrides the “Featured Image” phrase for this post type. Added in 4.3', 'activitypub' ), + 'set_featured_image' => _x( 'Set cover image', 'Overrides the “Set featured image” phrase for this post type. Added in 4.3', 'activitypub' ), + 'remove_featured_image' => _x( 'Remove cover image', 'Overrides the “Remove featured image” phrase for this post type. Added in 4.3', 'activitypub' ), + 'use_featured_image' => _x( 'Use as cover image', 'Overrides the “Use as featured image” phrase for this post type. Added in 4.3', 'activitypub' ), + 'archives' => _x( 'Mention archives', 'The post type archive label used in nav menus. Default “Post Archives”. Added in 4.4', 'activitypub' ), + 'insert_into_item' => _x( 'Insert into Mention', 'Overrides the “Insert into post”/”Insert into page” phrase (used when inserting media into a post). Added in 4.4', 'activitypub' ), + 'uploaded_to_this_item' => _x( 'Uploaded to this Mention', 'Overrides the “Uploaded to this post”/”Uploaded to this page” phrase (used when viewing media attached to a post). Added in 4.4', 'activitypub' ), + 'filter_items_list' => _x( 'Filter Mentions list', 'Screen reader text for the filter links heading on the post type listing screen. Default “Filter posts list”/”Filter pages list”. Added in 4.4', 'activitypub' ), + 'items_list_navigation' => _x( 'Mentions list navigation', 'Screen reader text for the pagination heading on the post type listing screen. Default “Posts list navigation”/”Pages list navigation”. Added in 4.4', 'activitypub' ), + 'items_list' => _x( 'Mentions list', 'Screen reader text for the items list heading on the post type listing screen. Default “Posts list”/”Pages list”. Added in 4.4', 'activitypub' ), + ); + + $post_type_args = array( + 'label' => 'Mentions',//Change menu name to ActivityPub + 'labels' => $labels, + 'description' => 'Mentions to and from the fediverse', + 'public' => true, + 'show_ui' => true, + 'show_in_admin_bar' => true, + //'show_in_rest' => true, //eventually use tagging via https://developer.wordpress.org/block-editor/components/autocomplete/ + 'has_archive' => true, + 'menu_icon' => 'dashicons-format-chat', + // 'capability_type' => 'activitypub', + // 'capabilities' => array( + // 'publish_posts' => 'publish_ap_posts', + // 'edit_posts' => 'edit_ap_posts', + // 'edit_others_posts' => 'edit_others_ap_posts', + // 'read_private_posts' => 'read_private_ap_posts', + // 'edit_post' => 'edit_ap_posts', + // 'delete_post' => 'delete_ap_posts', + // 'read_post' => 'read_ap_posts', + // ), + 'supports' => array( + 'title', + 'editor', + //'thumbnail', + //'comments', + //'trackbacks', + array( + 'post_status' => 'inbox', + ) + ), + 'hierarchical' => true,//allows thread like comments + 'has_archive' => false, + 'rewrite' => false, + //'query_var' => false, + 'delete_with_user' => true,//delete all personal posts + ); + \register_post_type( 'mention', $post_type_args ); + + $inbox_message_args = array( + 'label' => _x( 'Inbox', 'post' ), + 'label_count' => _n_noop( 'Inbox (%s)', 'Inbox (%s)' ), + 'public' => false, + 'protected' => true, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'post_type' => array( 'mention' ), + ); + \register_post_status( 'inbox', $inbox_message_args ); + + $moderation_args = array( + 'label' => _x( 'Moderation', 'post' ), + 'label_count' => _n_noop( 'Moderation (%s)', 'Moderation (%s)' ), + 'public' => false, + 'protected' => true, + 'exclude_from_search' => false,//true? + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'post_type' => array( 'mention' ), + ); + \register_post_status( 'moderation', $moderation_args ); + + + } + + /** + * Rename Title label + * https://developer.wordpress.org/reference/hooks/enter_title_here/ + */ + public static function mentions_title ( $input ) { + if( 'mention' === get_post_type() ) { + return __( 'Add a Summary / Content Warning', 'activitypub' ); + } else { + return $input; + } + } + + public static function post_type_dump () { + global $wp_post_types; + echo '
'; print_r( $wp_post_types ); echo '
'; + } + +} diff --git a/includes/class-signature.php b/includes/class-signature.php index 3dffb109..ffd5003d 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -53,7 +53,7 @@ public static function generate_key_pair( $user_id ) { $config = array( 'digest_alg' => 'sha512', 'private_key_bits' => 2048, - 'private_key_type' => \OPENSSL_KEYTYPE_RSA, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, ); $key = \openssl_pkey_new( $config ); @@ -91,7 +91,7 @@ public static function generate_signature( $user_id, $url, $date ) { $signed_string = "(request-target): post $path\nhost: $host\ndate: $date"; $signature = null; - \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); + \openssl_sign( $signed_string, $signature, $key, OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore $key_id = \get_author_posts_url( $user_id ) . '#main-key'; diff --git a/includes/functions.php b/includes/functions.php index 43cc40e0..2fc86637 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,6 +1,8 @@ 100, + 'limit_response_size' => 1048576, + 'redirection' => 3, + 'user-agent' => "$user_agent; ActivityPub", + 'headers' => array( + 'Accept' => 'application/activity+json', + 'Content-Type' => 'application/activity+json', + 'Signature' => $signature, + 'Date' => $date, + ), + 'body' => $body, + ); + + $response = \wp_safe_remote_post( $url, $args ); + \error_log( 'forward_remote_post: wp_safe_remote_post: ' . print_r( $response, true ) ); + \do_action( 'activitypub_forward_remote_post_response', $response, $url, $body, $user_id ); + + return $response; +} + function safe_remote_get( $url, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); $signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date ); @@ -89,7 +118,7 @@ function get_webfinger_resource( $user_id ) { $user = \get_user_by( 'id', $user_id ); - return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + return $user->user_login . '@' . \wp_parse_url( \home_url(), PHP_URL_HOST ); } /** @@ -157,7 +186,7 @@ function get_inbox_by_actor( $actor ) { return $metadata['inbox']; } - return new \WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata ); + return new \WP_Error( 'activitypub_no_inbox', __( 'No "Inbox" found', 'activitypub' ), $metadata ); } /** @@ -188,6 +217,7 @@ function get_publickey_by_actor( $actor, $key_id ) { function get_follower_inboxes( $user_id ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); + $inboxes = array(); foreach ( $followers as $follower ) { @@ -201,7 +231,6 @@ function get_follower_inboxes( $user_id ) { } $inboxes[ $inbox ][] = $follower; } - return $inboxes; } @@ -253,13 +282,13 @@ function url_to_authorid( $url ) { global $wp_rewrite; // check if url hase the same host - if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) { + if ( wp_parse_url( site_url(), PHP_URL_HOST ) !== wp_parse_url( $url, PHP_URL_HOST ) ) { return 0; } // first, check to see if there is a 'author=N' to match against if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) { - $id = \absint( $values[1] ); + $id = absint( $values[1] ); if ( $id ) { return $id; } @@ -279,7 +308,7 @@ function url_to_authorid( $url ) { // match the rewrite rule with the passed url if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) { - $user = \get_user_by( 'slug', $match[2] ); + $user = get_user_by( 'slug', $match[2] ); if ( $user ) { return $user->ID; } @@ -287,3 +316,241 @@ function url_to_authorid( $url ) { return 0; } + +/** + * Verify if url is a local comment, + * Or if it is a previously received remote comment + * + * return int comment_id + */ +function url_to_commentid( $comment_url ) { + if ( empty( $comment_url ) ) { + return null; + } + $post_url = \url_to_postid( $comment_url ); + + if ( $post_url ) { + //for local comment parent + $comment_id = explode( '#comment-', $comment_url ); + if ( isset( $comment_id[1] ) ){ + return $comment_id[1]; + } else { + return null; + } + + } else { + //remote comment parent, assuming the parent was also recieved + //Compare inReplyTo with source_url from meta, to determine if local comment_id exists for peer replied object + $comment_args = array( + 'type' => 'activitypub', + 'meta_query' => array( + array( + 'key' => 'source_url', + 'value' => $comment_url, + ) + ) + ); + $comments_query = new \WP_Comment_Query; + $comments = $comments_query->query( $comment_args ); + $found_comment_ids = array(); + if ( $comments ) { + foreach ( $comments as $comment ) { + $found_comment_ids[] = $comment->comment_ID; + } + return $found_comment_ids[0]; + } + return null; + } +} + +//add recipients to CC +function add_recipients( $recipient ) { + $cc = array( AS_PUBLIC ); + $cc[] = $recipient; + return $cc; +} + +/** + * Get tagged users from received AP object meta + * @param string $object_id a comment_id to search + * @param boolean $post defaults to searching a comment_id + * + * @return array of tagged users + */ +function get_recipients( $object_id, $post = null ) { + $tagged_users_name = null; + if ( $post ) { + //post + $ap_object = \unserialize( \get_post_meta( $object_id, '_ap_object' ) ); + } else { + //comment + $ap_object = \unserialize( \get_comment_meta( $object_id, 'ap_object', true ) ); + } + + if ( !empty( $ap_object ) ) { + $tagged_users_name[] = \Activitypub\url_to_webfinger( $ap_object['actor'] ); + if ( !empty( $ap_object['object']['tag'] ) ) { + $author_post_url = \get_author_posts_url( $ap_object['user_id'] ); + foreach ( $ap_object['object']['tag'] as $tag ) { + if ( $author_post_url == $tag['href'] ) { + continue; + } + if ( in_array( 'Mention', $tag ) ) { + $tagged_users_name[] = $tag['name']; + } + } + } + return implode( ' ', $tagged_users_name ); + } +} + +/** + * Add summary to reply + */ +function get_summary( $comment_id ) { + $ap_object = \unserialize( \get_comment_meta( $comment_id, 'ap_object', true ) ); + if ( !empty( $ap_object ) ) { + if ( !empty( $ap_object['object']['summary'] ) ) { + \error_log( 'summary: ' . $ap_object['object']['summary'] ); + return \esc_attr( $ap_object['object']['summary'] ); + } + } +} + +/** + * parse content for tags to transform + */ +function transform_tags( $content ) { + //#tags + + //@Mentions + $mentions = null; + $webfinger_tags = \Activitypub\webfinger_extract( $content, true ); + if ( !empty( $webfinger_tags) ) { + foreach ( $webfinger_tags[0] as $webfinger_tag ) { + $ap_profile = \Activitypub\Rest\Webfinger::webfinger_lookup( $webfinger_tag ); + if ( ! empty( $ap_profile ) ) { + $short_tag = \Activitypub\webfinger_short_tag( $webfinger_tag ); + $webfinger_link = "{$short_tag}"; + //$webfinger_link = "{$short_tag}";//trips at title attribute + $content = str_replace( $webfinger_tag, $webfinger_link, $content ); + $mentions[] = $ap_profile; + } + } + } + // Return mentions separately to attach to comment/post meta + $content_mentions['mentions'] = $mentions; + $content_mentions['content'] = $content; + return $content_mentions; +} + +function tag_user( $recipient ) { + $tagged_user = array( + 'type' => 'Mention', + 'href' => $recipient, + 'name' => \Activitypub\url_to_webfinger( $recipient ), + ); + $tag[] = $tagged_user; + return $tag; +} + +function webfinger_extract( $string ) { + preg_match_all("/@[\._a-zA-Z0-9-]+@[\._a-zA-Z0-9-]+/i", $string, $matches); + //preg_match_all("/(?:(? $value ) { + if ( empty( $value ) ) { + unset( $blacklist_hosts[ $key ] ); + } else { + $blacklist_hosts[ $key ] = \trim( $blacklist_hosts[ $key ] ); + } + } + + return \apply_filters( 'activitypub_blacklist', $blacklist_hosts ); +} + +/** + * Check if an URL is blacklisted + * + * @param string $url an URL to check + * + * @return boolean + */ +function is_blacklisted( $url ) { + foreach ( \ActivityPub\get_blacklist() as $blacklisted_host ) { + if ( \strpos( $url, $blacklisted_host ) !== false ) { + return true; + } + } + + return false; +} diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index 1b5931e9..f14cd57a 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -61,7 +61,12 @@ public function from_post( $object ) { } public function from_comment( $object ) { - + $this->object = $object; + $this->published = $object['published']; + $this->actor = $object['attributedTo']; + $this->id = $object['id'] . '-activity'; + $this->cc = $object['cc']; + $this->tag = $object['tag']; } public function to_comment() { @@ -90,10 +95,11 @@ public function to_array() { * @return void */ public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); + return \wp_json_encode( $this->to_array(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT ); } public function to_simple_array() { + \error_log( 'to_simple_array' ); $activity = array( '@context' => $this->context, 'type' => $this->type, @@ -111,6 +117,6 @@ public function to_simple_array() { } public function to_simple_json() { - return \wp_json_encode( $this->to_simple_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); + return \wp_json_encode( $this->to_simple_array(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT ); } } diff --git a/includes/model/class-comment.php b/includes/model/class-comment.php new file mode 100644 index 00000000..dd1d322a --- /dev/null +++ b/includes/model/class-comment.php @@ -0,0 +1,167 @@ +comment = $comment; + + $this->comment_author_url = \get_author_posts_url( $this->comment->user_id ); + $this->safe_comment_id = $this->generate_comment_id(); + $this->inReplyTo = $this->generate_parent_url(); + $this->contentWarning = $this->generate_content_warning(); + $this->permalink = $this->generate_permalink(); + $this->cc_recipients = $this->generate_recipients(); + $this->tags = $this->generate_tags(); + } + + 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' => \Activitypub\Model\Comment::normalize_comment_id( $comment ), + 'type' => 'Note', + 'published' => \date( 'Y-m-d\TH:i:s\Z', \strtotime( $comment->comment_date_gmt ) ), + 'attributedTo' => $this->comment_author_url, + 'summary' => $this->contentWarning, + 'inReplyTo' => $this->inReplyTo, + 'content' => $comment->comment_content, + 'contentMap' => array( + \strstr( \get_locale(), '_', true ) => $comment->comment_content, + ), + 'source' => \get_comment_link( $comment ), + 'url' => \get_comment_link( $comment ),//link for mastodon + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),//audience logic + 'cc' => $this->cc_recipients, + 'tag' => $this->tags, + ); + + 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_permalink() { + $comment = $this->comment; + $permalink = \get_comment_link( $comment ); + + // replace 'trashed' for delete activity + return \str_replace( '__trashed', '', $permalink ); + } + + /** + * What is status is being replied to + * Comment ID or Post ID + */ + public function generate_parent_url() { + $comment = $this->comment; + $parent_comment = \get_comment( $comment->comment_parent ); + if ( $parent_comment ) { + //reply to local (received) comment + $inReplyTo = \get_comment_meta( $comment->comment_parent, 'source_url', true ); + } else { + //reply to local post + $inReplyTo = \get_permalink( $comment->comment_post_ID ); + } + return $inReplyTo; + } + + /** + * Generate Content Warning from peer + * If peer 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; + $contentWarning = null; + $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'] ) ) { + $contentWarning = $ap_object['object']['summary']; + } + } + /*$summary = \get_comment_meta( $this->comment->comment_ID, 'summary', true ) ; + if ( !empty( $summary ) ) { + $contentWarning = \Activitypub\add_summary( $summary ); + } */ + return $contentWarning; + } + + /** + * Who is being replied to + */ + public function generate_recipients() { + //TODO Add audience logic get parent audience + $cc_recipients = array( AS_PUBLIC ); + $mentions = \get_comment_meta( $this->comment->comment_ID, 'mentions', true ) ; + if ( !empty( $mentions ) ) { + foreach ($mentions as $mention) { + $cc_recipients[] = $mention['href']; + } + } + return $cc_recipients; + } + + /** + * Mention user being replied to + */ + public function generate_tags() { + $mentions = \get_comment_meta( $this->comment->comment_ID, 'mentions', true ) ; + if ( !empty( $mentions ) ) { + foreach ($mentions as $mention) { + $mention_tags[] = array( + 'type' => 'Mention', + 'href' => $mention['href'], + 'name' => '@' . $mention['name'], + ); + } + return $mention_tags; + } + } + + /** + * Transform comment url, replace #fragment with ?query + * + * AP Object ID must be unique + * + * https://www.w3.org/TR/activitypub/#obj-id + * https://github.com/tootsuite/mastodon/issues/13879 + */ + public function normalize_comment_id( $comment ) { + $comment_id = explode( '#comment-', \get_comment_link( $comment ) ); + $comment_id = $comment_id[0] . '?comment-' . $comment_id[1]; + return $comment_id; + } +} \ No newline at end of file diff --git a/includes/model/class-post.php b/includes/model/class-post.php index efacd6ff..c8c0887c 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -28,16 +28,12 @@ public function __construct( $post = null ) { $this->object_type = $this->generate_object_type(); } - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - return $this->$var; - } + public function get_post() { + return $this->post; + } - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - $this->$var = $params[0]; - } + public function get_post_author() { + return $this->post->post_author; } public function to_array() { @@ -48,23 +44,33 @@ public function to_array() { 'type' => $this->object_type, 'published' => \date( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date ) ), 'attributedTo' => \get_author_posts_url( $post->post_author ), - 'summary' => $this->summary, + 'summary' => $this->get_the_title(), 'inReplyTo' => null, - 'content' => $this->content, + 'content' => $this->get_the_content(), 'contentMap' => array( - \strstr( \get_locale(), '_', true ) => $this->content, + \strstr( \get_locale(), '_', true ) => $this->get_the_content(), ), 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'attachment' => $this->attachments, - 'tag' => $this->tags, + 'attachment' => $this->get_attachments(), + 'tag' => $this->get_tags(), + 'replies' => array( + 'id' => \get_rest_url(null, 'activitypub/1.0/post/') . $post->ID . '/replies', + 'type' => 'Collection', + 'first' => array( + 'type' => 'CollectionPage', + 'next' => \get_rest_url(null, 'activitypub/1.0/post/') . $post->ID . '/replies', + 'partOf' => \get_rest_url(null, 'activitypub/1.0/post/') . $post->ID . '/replies', + 'items' => [], + ), + ), ); return \apply_filters( 'activitypub_post', $array ); } public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); + return \wp_json_encode( $this->to_array(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT ); } public function generate_id() { @@ -123,7 +129,7 @@ public function generate_attachments() { $image = array( 'type' => 'Image', 'url' => $thumbnail[0], - 'mediaType' => $mimetype, + 'mediaType' => $mimetype ); if ( $alt ) { $image['name'] = $alt; @@ -135,7 +141,7 @@ public function generate_attachments() { return $images; } - public function generate_tags() { + public function get_tags() { $tags = array(); $post_tags = \get_the_tags( $this->post->ID ); @@ -150,6 +156,20 @@ public function generate_tags() { } } + $mention_tags = \get_post_meta( $this->post->ID, '_mentions' ); + if ( !empty( $mention_tags ) ) { + foreach ($mention_tags as $mention) { + if ( !empty( $mention ) ) { + $mention_tag = array( + 'type' => 'Mention', + 'href' => $mention['href'], + 'name' => '@' . $mention['name'], + ); + $tags[] = $mention_tag; + } + } + } + return $tags; } @@ -161,7 +181,7 @@ public function generate_tags() { * * @return string the object-type */ - public function generate_object_type() { + public function get_object_type() { if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) { return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) ); } diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index c1846a48..bc5af7b2 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -10,7 +10,6 @@ class Followers { public static function get_followers( $author_id ) { $followers = \get_user_option( 'activitypub_followers', $author_id ); - if ( ! $followers ) { return array(); } @@ -21,7 +20,7 @@ public static function get_followers( $author_id ) { isset( $follower['type'] ) && 'Person' === $follower['type'] && isset( $follower['id'] ) && - false !== \filter_var( $follower['id'], \FILTER_VALIDATE_URL ) + false !== \filter_var( $follower['id'], FILTER_VALIDATE_URL ) ) { $followers[ $key ] = $follower['id']; } @@ -45,7 +44,7 @@ public static function add_follower( $actor, $author_id ) { isset( $actor['type'] ) && 'Person' === $actor['type'] && isset( $actor['id'] ) && - false !== \filter_var( $actor['id'], \FILTER_VALIDATE_URL ) + false !== \filter_var( $actor['id'], FILTER_VALIDATE_URL ) ) { $actor = $actor['id']; } @@ -68,8 +67,12 @@ public static function add_follower( $actor, $author_id ) { public static function remove_follower( $actor, $author_id ) { $followers = \get_user_option( 'activitypub_followers', $author_id ); - + \error_log('author_id: ' . print_r( $author_id, true ) ); + \error_log('actor: ' . print_r( $actor, true ) ); + \error_log('followers_array: ' . print_r( $followers, true ) ); foreach ( $followers as $key => $value ) { + \error_log('value: ' . print_r( $value, true ) ); + \error_log('key: ' . print_r( $key, true ) ); if ( $value === $actor ) { unset( $followers[ $key ] ); } diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index ad033e78..271fd43e 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -17,6 +17,7 @@ public static function init() { \add_filter( 'rest_pre_serve_request', array( '\Activitypub\Rest\Inbox', 'serve_request' ), 11, 4 ); \add_action( 'activitypub_inbox_follow', array( '\Activitypub\Rest\Inbox', 'handle_follow' ), 10, 2 ); \add_action( 'activitypub_inbox_unfollow', array( '\Activitypub\Rest\Inbox', 'handle_unfollow' ), 10, 2 ); + \add_action( 'activitypub_inbox_undo', array( '\Activitypub\Rest\Inbox', 'handle_unfollow' ), 10, 2 ); //\add_action( 'activitypub_inbox_like', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 ); //\add_action( 'activitypub_inbox_announce', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 ); \add_action( 'activitypub_inbox_create', array( '\Activitypub\Rest\Inbox', 'handle_create' ), 10, 2 ); @@ -47,6 +48,17 @@ public static function register_routes() { ), ) ); + + // \register_rest_route( + // 'activitypub/1.0', '/users/(?P\d+)/inbox', array( + // array( + // 'methods' => \WP_REST_Server::READABLE, + // 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_get' ), + // 'args' => self::get_parameters(), + // 'permission_callback' => '__return_true' + // ), + // ) + // ); } /** @@ -88,6 +100,7 @@ public static function serve_request( $served, $result, $request, $server ) { public static function user_inbox( $request ) { $user_id = $request->get_param( 'user_id' ); +// error_log( 'user_inbox $request: ' . print_r( $request, true ) ); $data = $request->get_params(); $type = $request->get_param( 'type' ); @@ -97,6 +110,29 @@ public static function user_inbox( $request ) { return new \WP_REST_Response( array(), 202 ); } + /** + * Renders the user-inbox + * + * @param WP_REST_Request $request + * + * @return WP_REST_Response + */ + public static function user_inbox_get( $request ) { + $user_id = $request->get_param( 'user_id' ); + + $json = new \stdClass(); + + $json->{'@context'} = \Activitypub\get_context(); + $json->id = \get_rest_url( null, "/activitypub/1.0/users/$user_id/inbox" ); // phpcs:ignore + $json->type = 'OrderedCollection'; + + $response = new \WP_REST_Response( $json, 200 ); + + $response->header( 'Content-Type', 'application/activity+json' ); + + return $response; + } + /** * The shared inbox * @@ -235,6 +271,55 @@ public static function shared_inbox_request_parameters() { return $params; } + /** + * The supported parameters + * + * @return array list of parameters + */ + public static function get_parameters() { + $params = array(); + + $params['page'] = array( + 'type' => 'integer', + ); + + $params['user_id'] = array( + 'required' => true, + 'type' => 'integer', + ); + + $params['id'] = array( +// 'required' => true, + 'type' => 'string', + 'validate_callback' => function( $param, $request, $key ) { + if ( ! \is_string( $param ) ) { + $param = $param['id']; + } + return ! \Activitypub\is_blacklisted( $param ); + }, + 'sanitize_callback' => 'esc_url_raw', + ); + + $params['actor'] = array( +// 'required' => true, + //'type' => array( 'object', 'string' ), + 'validate_callback' => function( $param, $request, $key ) { + if ( ! \is_string( $param ) ) { + $param = $param['id']; + } + return ! \Activitypub\is_blacklisted( $param ); + }, + 'sanitize_callback' => function( $param, $request, $key ) { + if ( ! \is_string( $param ) ) { + $param = $param['id']; + } + return \esc_url_raw( $param ); + }, + ); + + return $params; + } + /** * Handles "Follow" requests * @@ -256,7 +341,6 @@ public static function handle_follow( $object, $user_id ) { $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); $activity = $activity->to_simple_json(); - $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } @@ -288,7 +372,7 @@ public static function handle_reaction( $object, $user_id ) { 'comment_type' => \esc_attr( \strtolower( $object['type'] ) ), 'comment_parent' => 0, 'comment_meta' => array( - 'source_url' => \esc_url_raw( $object['id'] ), + 'source_url' => \esc_url_raw( $object['attributedTo'] ), 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ), 'protocol' => 'activitypub', ), @@ -314,31 +398,122 @@ public static function handle_reaction( $object, $user_id ) { */ public static function handle_create( $object, $user_id ) { $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + $avatar_url = null; + $audience = \Activitypub\get_audience( $object ); + + // Security TODO: + // static function: enforce host check ($object['id'] must match $object['object']['url'] && $object['actor'] domain ) + // move to before handle_create + + //Determine parent post and/or parent comment + $comment_post_ID = $object_parent = $object_parent_ID = 0; + if ( isset( $object['object']['inReplyTo'] ) ) { + $comment_post_ID = \url_to_postid( $object['object']['inReplyTo'] ); + //if not a direct reply to a post, remote post parent + if ( $comment_post_ID === 0 ) { + //verify if reply to a local or remote received comment + $object_parent_ID = \Activitypub\url_to_commentid( \esc_url_raw( $object['object']['inReplyTo'] ) ); + if ( !is_null( $object_parent_ID ) ) { + //replied to a local comment (which has a post_ID) + $object_parent = get_comment( $object_parent_ID ); + $comment_post_ID = $object_parent->comment_post_ID; + } + } + } + + //not all implementaions use url + if ( isset( $object['object']['url'] ) ) { + $source_url = \esc_url_raw( $object['object']['url'] ); + } else { + //could also try $object['object']['source']? + $source_url = \esc_url_raw( $object['object']['id'] ); + } - $commentdata = array( - 'comment_post_ID' => \url_to_postid( $object['object']['inReplyTo'] ), - '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_meta' => array( - 'source_url' => \esc_url_raw( $object['object']['url'] ), - 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ), - 'protocol' => 'activitypub', - ), - ); + // if no name is set use peer username + if ( !empty( $meta['name'] ) ) { + $name = \esc_attr( $meta['name'] ); + } else { + $name = \esc_attr( $meta['preferredUsername'] ); + } + // if avatar is set + if ( !empty( $meta['icon']['url'] ) ) { + $avatar_url = \esc_attr( $meta['icon']['url'] ); + } - // disable flood control - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); - // do not require email for AP entries - add_filter( 'pre_option_require_name_email', '__return_false' ); - $state = \wp_new_comment( $commentdata, true ); - remove_filter( 'pre_option_require_name_email', '__return_false' ); + // Check if has Parent(make WP_Comment) or Not(make WP_Post) + if ( !empty( $comment_post_ID ) ) { - // re-add flood control - \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); + } + + //Only create WP_Comment for public replies to posts + if ( ( in_array( 'https://www.w3.org/ns/activitystreams#Public', $object['to'] ) + || in_array( 'https://www.w3.org/ns/activitystreams#Public', $object['cc'] ) ) + && ( !empty( $comment_post_ID ) + || !empty ( $object_parent ) + ) ) { + + $commentdata = array( + 'comment_post_ID' => $comment_post_ID, + 'comment_author' => $name, + 'comment_author_url' => \esc_url_raw( $object['actor'] ), + 'comment_content' => \wp_filter_kses( $object['object']['content'] ), + 'comment_type' => 'activitypub', + 'comment_author_email' => '', + 'comment_parent' => $object_parent_ID, + 'comment_meta' => array( + 'inReplyTo' => \esc_url_raw( $object['object']['inReplyTo'] ),//needed? (if replying to someone else on thread, but not received)non-wp status - comment_post_ID, object_parent + 'source_url' => $source_url, + 'protocol' => 'activitypub', + ), + ); + + // disable flood control + \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); + + // do not require email for AP entries + \add_filter( 'pre_option_require_name_email', '__return_false' ); + $state = \wp_new_comment( $commentdata, true ); + \remove_filter( 'pre_option_require_name_email', '__return_false' ); + + // re-add flood control + \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); + + } else { + //Not a public reply to a public post + $title = $summary = null; + if ( isset( $object['object']['summary'] ) ) { + $title = \wp_trim_words( $object['object']['summary'], 10 ); + $summary = \wp_strip_all_tags( $object['object']['summary'] ); + } + + $postdata = array( + 'post_author' => $object['user_id'], + 'post_content' => \wp_filter_kses( $object['object']['content'] ), + 'post_title' => $title, + 'post_excerpt' => $summary, + 'post_status' => 'inbox',//private + 'post_type' => 'mention', + 'post_parent' => \esc_url_raw( $object['object']['inReplyTo'] ), + 'meta_input' => array( + 'audience' => $audience, + 'ap_object' => \serialize( $object ), + 'inreplyto' => \esc_url_raw( $object['object']['inReplyTo'] ), + 'author' => $name, + 'author_url' => \esc_url_raw( $object['actor'] ), + 'source_url' => $source_url, + 'avatar_url' => $avatar_url, + 'protocol' => 'activitypub', + ), + ); + $post_id = \wp_insert_post( $postdata ); + if( !is_wp_error( $post_id ) ) { + error_log( $post_id ); + wp_send_json_success( array( 'post_id' => $post_id ), 200 ); + } else { + error_log( $post_id->get_error_message() ); + wp_send_json_error( $post_id->get_error_message() ); + } + } } } diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 5aa18f75..fdfcb7e3 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -23,9 +23,20 @@ public static function register_routes() { \register_rest_route( 'activitypub/1.0', '/users/(?P\d+)/outbox', array( array( - 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Outbox', 'user_outbox' ), - 'args' => self::request_parameters(), + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( '\Activitypub\Rest\Outbox', 'user_outbox' ), + 'args' => self::request_parameters(), + 'permission_callback' => '__return_true', + ), + ) + ); + + \register_rest_route( + 'activitypub/1.0', '/post/(?P\d+)/replies', array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( '\Activitypub\Rest\Outbox', 'post_replies' ), + 'args' => self::request_parameters(), 'permission_callback' => '__return_true', ), ) @@ -41,9 +52,9 @@ public static function register_routes() { public static function user_outbox( $request ) { $user_id = $request->get_param( 'id' ); $author = \get_user_by( 'ID', $user_id ); - +// /with_replies include comments if ( ! $author ) { - return new \WP_Error( 'rest_invalid_param', \__( 'User not found', 'activitypub' ), array( + return new \WP_Error( 'rest_invalid_param', __( 'User not found', 'activitypub' ), array( 'status' => 404, 'params' => array( 'user_id' => \__( 'User not found', 'activitypub' ), @@ -52,7 +63,8 @@ public static function user_outbox( $request ) { } $page = $request->get_param( 'page', 0 ); - + //$page = $request->get_param( 'page', true ); + error_log('outbox'); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ @@ -67,14 +79,23 @@ public static function user_outbox( $request ) { $json->type = 'OrderedCollectionPage'; $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/outbox" ); // phpcs:ignore - $count_posts = \wp_count_posts(); - $json->totalItems = \intval( $count_posts->publish ); // phpcs:ignore + $count_posts = \count_user_posts( $user_id );// get allowed post types, public_only = true + $json->totalItems = \intval( $count_posts ); // phpcs:ignore $posts = \get_posts( array( 'posts_per_page' => 10, 'author' => $user_id, 'offset' => $page * 10, ) ); + $comments = \get_comments( + array( + 'number' => 10, + 'paged' => true, + 'user_id' => $user_id, + 'status' => 'approved', + 'offset' => $page * 10, + ) + ); $json->first = \add_query_arg( 'page', 0, $json->partOf ); // phpcs:ignore $json->last = \add_query_arg( 'page', ( \ceil ( $json->totalItems / 10 ) ) - 1, $json->partOf ); // phpcs:ignore @@ -89,6 +110,12 @@ public static function user_outbox( $request ) { $activitypub_activity->from_post( $activitypub_post->to_array() ); $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore } + foreach ( $comments as $comment ) { + $activitypub_comment = new \Activitypub\Model\Comment( $comment ); + $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_NONE ); + $activitypub_activity->from_post( $activitypub_comment->to_array() ); + $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore + } // filter output $json = \apply_filters( 'activitypub_outbox_array', $json ); @@ -105,6 +132,75 @@ public static function user_outbox( $request ) { return $response; } + /** + * Renders the replies collection for a post + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public static function post_replies( $request ) { + $post_id = $request->get_param( 'id' ); + $comments = \get_comments( array('post_id' => $post_id) ); + + if ( ! $comments ) { + return new \WP_Error( 'rest_invalid_param', __( 'No comments found', 'activitypub' ), array( + 'status' => 404, + 'params' => array( + 'post_id' => \__( 'No comments found', 'activitypub' ), + ), + ) ); + } + + $page = $request->get_param( 'page', 0 ); + error_log('comments'); + /* + * Action triggerd prior to the ActivityPub profile being created and sent to the client + */ + \do_action( 'activitypub_replies_pre' ); + + $json = new \stdClass(); + + $json->{'@context'} = \Activitypub\get_context(); + $json->id = \home_url( \add_query_arg( null, null ) ); + //$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); + //$json->actor = \get_author_posts_url( $user_id ); + $json->type = 'CollectionPage'; + + $json->partOf = \get_rest_url( null, "/activitypub/1.0/post/$post_id/replies" ); // phpcs:ignore + + $count_comments = \get_comments_number( $post_id );// get allowed post types, public_only = true + $json->totalItems = \intval( $count_comments ); // phpcs:ignore + + + $json->first = \add_query_arg( 'page', 0, $json->partOf ); // phpcs:ignore + $json->last = \add_query_arg( 'page', ( \ceil ( $json->totalItems / 10 ) ) - 1, $json->partOf ); // phpcs:ignore + + if ( ( \ceil ( $json->totalItems / 10 ) ) - 1 > $page ) { // phpcs:ignore + $json->next = \add_query_arg( 'page', ++$page, $json->partOf ); // phpcs:ignore + } + + foreach ( $comments as $comment ) { + $activitypub_comment = new \Activitypub\Model\Comment( $comment ); + $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_NONE ); + + $json->orderedItems[] = $activitypub_comment->to_array(); // phpcs:ignore + } + + // filter output + $json = \apply_filters( 'activitypub_comment_array', $json ); + + /* + * Action triggerd after the ActivityPub profile has been created and sent to the client + */ + \do_action( 'activitypub_comment_post' ); + + $response = new \WP_REST_Response( $json, 200 ); + + $response->header( 'Content-Type', 'application/activity+json' ); + + return $response; + } + /** * The supported parameters * @@ -124,4 +220,4 @@ public static function request_parameters() { return $params; } -} +} \ No newline at end of file diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index b5d3e4cb..c97699a4 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -15,6 +15,7 @@ class Webfinger { public static function init() { \add_action( 'rest_api_init', array( '\Activitypub\Rest\Webfinger', 'register_routes' ) ); \add_action( 'webfinger_user_data', array( '\Activitypub\Rest\Webfinger', 'add_webfinger_discovery' ), 10, 3 ); + //\add_action( 'webfinger_lookup', array( '\Activitypub\Rest\Webfinger', 'webfinger_lookup' ), 10, 3 ); } /** @@ -117,4 +118,34 @@ public static function add_webfinger_discovery( $array, $resource, $user ) { return $array; } + + public static function webfinger_lookup( $webfinger ) { + + if ( \substr($webfinger, 0, 1) === '@' ) { + $webfinger = substr( $webfinger, 1 ); + } + $url_host = \explode( '@', $webfinger ); + $webfinger_query = 'https://' . \end( $url_host ) . '/.well-known/webfinger?resource=acct%3A' . \urlencode( $webfinger ); + + $response = \wp_safe_remote_get( $webfinger_query ); + if ( ! is_wp_error( $response ) ) { + $ap_link = json_decode( $response['body'] ); + if ( isset( $ap_link->links ) ) { + foreach ( $ap_link->links as $link ) { + if ( !property_exists( $link, 'type' ) ) { + continue; + } + if ( isset( $link->type ) && $link->type === 'application/activity+json' ) { + $activity_profile['href'] = $link->href; + $activity_profile['name'] = $webfinger; + } + } + } else { + $activity_profile = null; + } + } else { + $activity_profile = null; + } + return $activity_profile; + } } diff --git a/includes/table/followers-list.php b/includes/table/followers-list.php index 81444ee7..f0daf424 100644 --- a/includes/table/followers-list.php +++ b/includes/table/followers-list.php @@ -8,6 +8,7 @@ class Followers_List extends \WP_List_Table { public function get_columns() { return array( + 'cb' => '', 'identifier' => \__( 'Identifier', 'activitypub' ), ); } @@ -16,6 +17,113 @@ public function get_sortable_columns() { return array(); } + /** + * Render the bulk edit checkbox + * + * @param array $item + * + * @return string + */ + function column_cb( $item ) { + return sprintf( + '', $item['identifier'] + ); + } + + /** + * Method for name column + * + * @param array $item an array of DB data + * + * @return string + */ + function column_message($item) { + //$reply_nonce = wp_create_nonce( 'activitypub_delete_follower' ); + $delete_nonce = wp_create_nonce( 'activitypub_delete_follower' ); + + $actions = array( + // 'reply' => sprintf('Reply', + // esc_attr($_REQUEST['page']), + // 'reply', + // $item['ap_message_id'], + // $reply_nonce + // ), + 'delete' => sprintf('Delete', + esc_attr($_REQUEST['page']), + 'delete', + $item['identifier'], + $delete_nonce + ), + ); + return sprintf('%1$s %2$s', $item['identifier'], $this->row_actions($actions) ); + } + + /** + * Delete a message. + * + * @param int $id message ID + */ + public static function delete_follower( $id ) { + $author = wp_get_current_user(); + \Activitypub\Peer\Followers::remove_follower( $id, $author->ID ); + } + + /** + * Returns an associative array containing the bulk action + * + * @return array + */ + public function get_bulk_actions() { + $actions = [ + 'bulk-delete' => 'Bulk delete', + ]; + + return $actions; + } + + public function process_bulk_action() { + + $action = $this->current_action(); + if ( isset( $_REQUEST['_wpnonce'] ) ) { + $nonce = \esc_attr( $_REQUEST['_wpnonce'] ); + } + + switch ($action) { + case 'delete': + if ( wp_verify_nonce( $nonce, 'activitypub_delete_follower' ) ) { + self::delete_follower( absint( $_GET['identifier'] ) ); + } + break; + case 'bulk-delete': + $ids = esc_sql( $_GET['selected'] ); + foreach ( $ids as $id ) { + self::delete_follower( $id ); + } + break; + case 'status': + $ids = esc_sql( $_GET['selected'] ); + foreach ( $ids as $id ) { + $comment = \wp_set_comment_status( $id, 'unread', false ); + error_log( print_r( $comment, true ) ); + //$comment->comment_approved = 'unread'; + } + break; + case 'reply': + if ( ! wp_verify_nonce( $nonce, 'activitypub_reply_message' ) ) { + die( 'Go get a life script kiddies' ); + } else { + $content = ''; + $editor_id = 'mycustomeditor'; + $settings = array( 'media_buttons' => false ); + + wp_editor( $content, $editor_id, $settings ); } + break; + default: + break; + } + + } + public function prepare_items() { $columns = $this->get_columns(); $hidden = array(); @@ -23,10 +131,15 @@ public function prepare_items() { $this->process_action(); $this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() ); + $this->process_bulk_action(); + $this->items = array(); foreach ( \Activitypub\Peer\Followers::get_followers( \get_current_user_id() ) as $follower ) { - $this->items[]['identifier'] = \esc_attr( $follower ); + $this->items[] = array( + 'cb' => 'callback', + 'identifier' => \esc_attr( $follower ), + ); } } diff --git a/includes/table/messages-list.php b/includes/table/messages-list.php new file mode 100644 index 00000000..58df67b8 --- /dev/null +++ b/includes/table/messages-list.php @@ -0,0 +1,192 @@ + __( 'Message', 'activitypub' ), //singular name of the listed records + 'plural' => __( 'Messages', 'activitypub' ), //plural name of the listed records + 'ajax' => false //should this table support ajax? + ] ); + + } + + public function no_items() { + _e( 'You have no messages.', 'activitypub' ); + } + + public function get_columns() { + $columns = [ + 'cb' => '', + 'actor' => \__( 'Actor', 'activitypub' ), + 'message' => \__( 'Message', 'activitypub' ), + 'date' => \__( 'Submitted', 'activitypub' ) + ]; + + return $columns; + } + + /** + * Render a column when no column specific method exists. + * + * @param array $item + * @param string $column_name + * + * @return mixed + */ + public function column_default( $item, $column_name ) { + switch ( $column_name ) { + case 'cb': + case 'actor': + case 'message': + case 'ap_message_id': + case 'date': + return $item[ $column_name ]; + default: + return print_r( $item, true ); //Show the whole array for troubleshooting purposes + } + } + + /** + * Columns to make sortable. + * TODO: implement actual sort + * + * @return array + */ + public function get_sortable_columns() { + $sortable_columns = array( + // 'actor' => array( 'identifier', true ), + // 'date' => array( 'date', true ), + ); + return $sortable_columns; + } + + + /** + * Render the bulk edit checkbox + * + * @param array $item + * + * @return string + */ + function column_cb( $item ) { + return sprintf( + '', $item['ap_message_id'] + ); + } + + /** + * Method for name column + * + * @param array $item an array of DB data + * + * @return string + */ + function column_message($item) { + $reply_nonce = wp_create_nonce( 'activitypub_reply_message' ); + $delete_nonce = wp_create_nonce( 'activitypub_delete_message' ); + + $actions = array( + 'reply' => sprintf('Reply', + esc_attr($_REQUEST['page']), + 'reply', + $item['ap_message_id'], + $reply_nonce + ), + 'delete' => sprintf('Delete', + esc_attr($_REQUEST['page']), + 'delete', + $item['ap_message_id'], + $delete_nonce + ), + ); + return sprintf('%1$s %2$s', $item['message'], $this->row_actions($actions) ); + } + + public function process_bulk_action() { + + $action = $this->current_action(); + if ( isset( $_REQUEST['_wpnonce'] ) ) { + $nonce = \esc_attr( $_REQUEST['_wpnonce'] ); + } + + switch ($action) { + case 'delete': + if ( wp_verify_nonce( $nonce, 'activitypub_delete_message' ) ) { + self::delete_message( absint( $_GET['message'] ) ); + } + break; + case 'bulk-delete': + $ids = esc_sql( $_GET['bulk-selected'] ); + foreach ( $ids as $id ) { + self::delete_message( $id ); + } + break; + // case 'reply': + // if ( wp_verify_nonce( $nonce, 'activitypub_reply_message' ) ) { + // self::delete_message( absint( $_GET['message'] ) ); + // } + // break; + default: + break; + } + + } + + /** + * Returns an associative array containing the bulk action + * + * @return array + */ + public function get_bulk_actions() { + $actions = [ + // 'delete' => 'Delete', + 'bulk-delete' => 'Bulk delete' + ]; + + return $actions; + } + + /** + * Delete a message. + * + * @param int $id message ID + */ + public static function delete_message( $id ) { + wp_delete_comment( $id ); + } + + public function prepare_items() { + $columns = $this->get_columns(); + $hidden = array(); + + $this->process_action(); + $this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() ); + + /** Process bulk action */ + $this->process_bulk_action(); + + $this->items = array(); + + $messages = \Activitypub\Peer\Messages::get_messages(); + + foreach ( $messages as $message ) : + $this->items[] = array( + 'cb' => 'callback', + 'ap_message_id' => $message->comment_ID, + 'actor' => '' . $message->comment_author . '', + 'message' => $message->comment_content, + 'date' => $message->comment_date, + ); + endforeach; + + } + +} diff --git a/templates/audience.php b/templates/audience.php new file mode 100644 index 00000000..2cea3e19 --- /dev/null +++ b/templates/audience.php @@ -0,0 +1,4 @@ +
- + prepare_items(); $token_table->display(); diff --git a/templates/mentions-list.php b/templates/mentions-list.php new file mode 100644 index 00000000..82f0371b --- /dev/null +++ b/templates/mentions-list.php @@ -0,0 +1,23 @@ + +
+

+ +

+ DEBUG
'; print_r($comments); echo '
'; + ?> + + + + + prepare_items(); + $mention_table->display(); + ?> + +
diff --git a/templates/messages-list.php b/templates/messages-list.php new file mode 100644 index 00000000..03c31a89 --- /dev/null +++ b/templates/messages-list.php @@ -0,0 +1,20 @@ + +
+

+ +

+ + + +
+ + prepare_items(); + $message_table->display(); + ?> +
+