From 6ab14f1b0782dd4090249ff7072bf6cc6186e1d5 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 3 Jun 2020 00:35:40 -0400 Subject: [PATCH 1/7] Adds a per user inbox for ActivityPub messages --- activitypub.php | 2 + includes/class-admin.php | 14 +++ includes/peer/class-messages.php | 38 ++++++ includes/rest/class-inbox.php | 51 +++++++- includes/table/messages-list.php | 192 +++++++++++++++++++++++++++++++ templates/messages-list.php | 20 ++++ 6 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 includes/peer/class-messages.php create mode 100644 includes/table/messages-list.php create mode 100644 templates/messages-list.php diff --git a/activitypub.php b/activitypub.php index fe834cdb..6561bea4 100644 --- a/activitypub.php +++ b/activitypub.php @@ -22,8 +22,10 @@ function init() { \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|^)#(\w*[A-Za-z_]+\w*)' ); require_once \dirname( __FILE__ ) . '/includes/table/followers-list.php'; + require_once \dirname( __FILE__ ) . '/includes/table/messages-list.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; + require_once \dirname( __FILE__ ) . '/includes/peer/class-messages.php'; require_once \dirname( __FILE__ ) . '/includes/functions.php'; require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; diff --git a/includes/class-admin.php b/includes/class-admin.php index b9b7d59a..bfff8e81 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -33,6 +33,10 @@ public static function admin_menu() { $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( '\Activitypub\Admin', 'followers_list_page' ) ); \add_action( 'load-' . $followers_list_page, array( '\Activitypub\Admin', 'add_followers_list_help_tab' ) ); + + $messages_list_page = \add_menu_page( \__( 'Messages', 'activitypub' ), \__( 'Messages', 'activitypub' ), 'read', 'activitypub-messages-list', array( '\Activitypub\Admin', 'messages_list_page' ), 'dashicons-format-chat', 26 ); + + \add_action( 'load-' . $messages_list_page, array( '\Activitypub\Admin', 'add_messages_list_help_tab' ) ); } /** @@ -49,6 +53,13 @@ public static function followers_list_page() { \load_template( \dirname( __FILE__ ) . '/../templates/followers-list.php' ); } + /** + * Load user messages page + */ + public static function messages_list_page() { + \load_template( \dirname( __FILE__ ) . '/../templates/messages-list.php' ); + } + /** * Register PubSubHubbub settings */ @@ -139,6 +150,9 @@ public static function add_settings_help_tab() { public static function add_followers_list_help_tab() { // todo } + public static function add_messages_list_help_tab() { + // todo + } public static function add_fediverse_profile( $user ) { ?> diff --git a/includes/peer/class-messages.php b/includes/peer/class-messages.php new file mode 100644 index 00000000..b6580312 --- /dev/null +++ b/includes/peer/class-messages.php @@ -0,0 +1,38 @@ + array('activitypub', 'activitypub_dm', 'activitypub_fo', 'activitypub_ul'), + ); + $messages = get_comments( $args ); + + if ( ! $messages ) { + return array(); + } + + $current_user_id = get_current_user_id(); + $personal_messages = array(); + + foreach ( $messages as $message ) : + $target_user = get_comment_meta( $message->comment_ID, 'target_user', true ); + if ( $current_user_id == $target_user ) { + $personal_messages[] = $message; + } + endforeach; + return $personal_messages; + } + + public static function count_messages( ) { + $messages = self::get_messages( ); + return \count( $messages ); + } + +} diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 7aa0effb..90e143d1 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -84,7 +84,8 @@ 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' ); + //error_log( print_r( $request, true ) ); $data = $request->get_params(); $type = $request->get_param( 'type' ); @@ -244,27 +245,69 @@ 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'] ); + ///$to = get_user_by( 'id', $object['user_id'] ); + $from = parse_url($object['actor']); + + error_log( '$object'); + error_log( print_r($object, true) ); $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_type' => 'activitypub',//activitypub_private_message + 'comment_author_email' => $meta["name"] . '@' . $from["host"], + 'comment_parent' => 0,//are threaded comments doable? 'comment_meta' => array( + 'target_user' => $object['user_id'], 'source_url' => \esc_url_raw( $object['object']['url'] ), 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ), 'protocol' => 'activitypub', ), ); +// TODO Parse audience : set comment_type, comment_meta +// Public +// [to] ('https://www.w3.org/ns/activitystreams#Public') +// [cc] ( [0]$self/.../followers, [1]tagged actors) +// unlisted +// [to] ($self/.../followers) +// [cc] ( [0]/activitystreams#Public, [1]tagged actors) +// follows_only +// [to] ($object['attributedTo']/followers) +// [cc] ([0]tagged actors) +// DM/private +// [to] ([0]tagged actors) +// +// TODO if $object['attachment']... append to content +// + //DM + if ( empty( $object['cc'] ) + && ( !array_search( 'https://www.w3.org/ns/activitystreams#Public', $object['to'] ) + || !array_search( 'https://www.w3.org/ns/activitystreams#Public', $object['cc'] ) ) ) { + $commentdata['comment_type'] = 'activitypub_dm'; + error_log( 'AP: Direct Message' ); + } + //Followers Only + if ( $object['to'][0] === $object['object']['attributedTo'] . '/followers' ) { + $commentdata['comment_type'] = 'activitypub_fo'; + error_log( 'AP: Followers Only' ); + } + + //error_log( print_r($commentdata, true) ); + // disable flood control \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); $state = \wp_new_comment( $commentdata, true ); + // if ($commentdata['comment_type'] === 'activitypub') { + // //site setting or user setting + // error_log('Auto Approve'); + // // \wp_set_comment_status( $state, 'approve' ); + // } + // re-add flood control \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); } 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/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(); + ?> +
+
From 5559f642bafd582b4e187e499c4c01a3e6d9354c Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 25 Sep 2020 20:42:13 -0400 Subject: [PATCH 2/7] Comment threading (start of mentions) --- activitypub.php | 20 +- includes/class-activity-dispatcher.php | 172 +++++++++- includes/class-activitypub.php | 162 +++++++-- includes/class-admin.php | 14 + includes/class-c2s.php | 312 +++++++++++++++++ includes/class-mentions.php | 133 ++++++++ includes/class-signature.php | 4 +- includes/functions.php | 158 ++++++++- includes/model/class-activity.php | 16 +- includes/model/class-comment.php | 95 ++++++ includes/model/class-post.php | 101 +++--- includes/peer/class-followers.php | 11 +- includes/peer/class-mentions.php | 175 ++++++++++ includes/rest/class-followers.php | 1 + includes/rest/class-following.php | 1 + includes/rest/class-inbox.php | 456 +++++++++++++++++++++++-- includes/rest/class-nodeinfo.php | 3 + includes/rest/class-outbox.php | 282 ++++++++++++++- includes/rest/class-server.php | 21 +- includes/rest/class-webfinger.php | 2 + includes/table/followers-list.php | 115 ++++++- includes/table/mentions-list.php | 262 ++++++++++++++ templates/audience.php | 4 + templates/followers-list.php | 2 +- templates/mentions-list.php | 23 ++ 25 files changed, 2388 insertions(+), 157 deletions(-) create mode 100644 includes/class-c2s.php create mode 100644 includes/class-mentions.php create mode 100644 includes/model/class-comment.php create mode 100644 includes/peer/class-mentions.php create mode 100644 includes/table/mentions-list.php create mode 100644 templates/audience.php create mode 100644 templates/mentions-list.php diff --git a/activitypub.php b/activitypub.php index fe834cdb..918cc7fe 100644 --- a/activitypub.php +++ b/activitypub.php @@ -22,8 +22,10 @@ function init() { \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|^)#(\w*[A-Za-z_]+\w*)' ); require_once \dirname( __FILE__ ) . '/includes/table/followers-list.php'; + require_once \dirname( __FILE__ ) . '/includes/table/mentions-list.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; + require_once \dirname( __FILE__ ) . '/includes/peer/class-mentions.php'; require_once \dirname( __FILE__ ) . '/includes/functions.php'; require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; @@ -33,6 +35,14 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; \Activitypub\Model\Post::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(); @@ -72,7 +82,7 @@ function init() { return '\Activitypub\Rest\Server'; } ); } -\add_action( 'plugins_loaded', '\Activitypub\init' ); +add_action( 'plugins_loaded', '\Activitypub\init' ); /** * Add rewrite rules @@ -98,3 +108,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/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index a4ed0c7e..605404c6 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -10,43 +10,47 @@ */ 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' ) ); } /** - * Send "create" activities. + * Send "create" activities * - * @param \Activitypub\Model\Post $activitypub_post + * @param int $post_id */ - public static function send_post_activity( $activitypub_post ) { - // get latest version of post - $user_id = $activitypub_post->get_post_author(); + public static function send_post_activity( $post_id ) { + $post = \get_post( $post_id ); + $user_id = $post->post_author; + $activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity->from_post( $activitypub_post->to_array() ); foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { $activitypub_activity->set_to( $to ); $activity = $activitypub_activity->to_json(); // phpcs:ignore - +\error_log( 'send_post_activity: ' . print_r( $activitypub_activity, true )); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } } /** - * Send "update" activities. + * Send "update" activities * - * @param \Activitypub\Model\Post $activitypub_post + * @param int $post_id */ - public static function send_update_activity( $activitypub_post ) { - // get latest version of post - $user_id = $activitypub_post->get_post_author(); + public static function send_update_activity( $post_id ) { + $post = \get_post( $post_id ); + $user_id = $post->post_author; + $activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_activity = new \Activitypub\Model\Activity( 'Update', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity->from_post( $activitypub_post->to_array() ); @@ -59,22 +63,156 @@ public static function send_update_activity( $activitypub_post ) { } /** - * Send "delete" activities. + * Send "delete" activities * - * @param \Activitypub\Model\Post $activitypub_post + * @param int $post_id */ - public static function send_delete_activity( $activitypub_post ) { - // get latest version of post - $user_id = $activitypub_post->get_post_author(); + public static function send_delete_activity( $post_id ) { + $post = \get_post( $post_id ); + $user_id = $post->post_author; + $activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_activity = new \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity->from_post( $activitypub_post->to_array() ); foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { + $too[] = 'https://www.w3.org/ns/activitystreams#Public'; + $too[] = $to; $activitypub_activity->set_to( $to ); $activity = $activitypub_activity->to_json(); // phpcs:ignore \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } } + + /** + * Send "create" activities for comments + * + * @param int $comment_id + */ + public static function send_comment_activity( $comment_id ) { + //ONLY FOR LOCAL USERS ? + $comment = \get_comment( $comment_id ); + //$user = get_user_by( 'login', \get_comment_author( $comment_id ) );// + $user_id = $comment->user_id; + + //error_log( 'dispatcher:send_comment:$user_id: ' . $user_id ); + + // $ap_object = json_encode( get_comment_meta( $comment_id, 'ap_object', true ) ); + // error_log( 'dispatcher:send_comment:ap_object' ); + \error_log( print_r( $comment, true ) ); + $replyto = get_comment_meta( $comment->comment_parent, 'comment_author_url', true );// + //error_log( 'dispatcher:send_comment:replyto' ); + //\error_log( 'Activity_Dispatcher::send_comment_activity' ); + + $activitypub_comment = new \Activitypub\Model\Comment( $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:payload' ); + error_log(print_r($activitypub_activity, true)); + \error_log( '$user_id: '. $user_id ); + foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { + \error_log( '$inbox: '. $inbox ); + \error_log( '$to: '. print_r( $to[0], true ) ); + $activitypub_activity->set_to( $to[0] ); + //error_log( 'Activity_Dispatcher::send_comment_activity:set_to(): ' . print_r( $to, true )); + $activity = $activitypub_activity->to_json(); // phpcs:ignore + error_log(print_r($activity, true)); + // Send reply to followers, skip if replying to followers (avoid duplicate replies) + // if( in_array( $to, $replyto ) || ( $replyto == $to ) ) { + // continue; + // } + \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + } + // 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 int $comment_id + */ + public static function inbox_forward_activity( $comment_id ) { + \error_log( 'Activity_Dispatcher::inbox_forward_activity' ); + $comment = \get_comment( $comment_id ); + //why do I need the parent here? to get inbox ID? + $parent_comment = \get_comment( $comment->comment_parent ); + + //$user = get_user_by( 'login', \get_comment_author( $parent_comment->comment_ID ) ); + //\error_log( print_r( \get_comment_author( $parent_comment['comment_ID'] ), true ) ); + //\error_log( '$parent_comment->user_id' . print_r( $parent_comment ), true ); + $replyto[] = $comment->comment_author_url; + $activitypub_activity = unserialize( get_comment_meta( $comment_id, 'ap_object', true ) ); + $user_id = $activitypub_activity['user_id']; + if ( $user_id === 0 ) { + $parent_comment = \get_comment( $comment->comment_parent ); + $user_id = $parent_comment->user_id; + } + //remove user_id from $activitypub_activity + unset($activitypub_activity['user_id']); + \error_log( '$activitypub_activity: ' ); + \error_log( print_r( $activitypub_activity, true ) ); + + //if is foreign user + /* + When Activities are received in the inbox, the server needs to forward these to recipients that the origin was unable to deliver them to. + To do this, the server MUST target and deliver to the values of to, cc, and/or audience if and only if all of the following are true: + +This is the first time the server has seen this Activity. +The values of to, cc, and/or audience contain a Collection owned by the server. (user followers collection) +The values of inReplyTo, object, target and/or tag are objects owned by the server. +The server SHOULD recurse through these values to look for linked objects owned by the server, +and SHOULD set a maximum limit for recursion (ie. the point at which the thread is so deep the recipients +followers may not mind if they are no longer getting updates that don't directly involve the recipient). +The server MUST only target the values of to, cc, and/or audience on the original object being forwarded, and not pick up +any new addressees whilst recursing through the linked objects (in case these addressees were purposefully amended by or via the client). +*/ + //$replyto = get_comment_meta( $comment_id, 'replyto', true ); + //error_log( print_r( $replyto, true) ); + + // $activitypub_comment = new \Activitypub\Model\Comment( $comment ); + // $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); + // $activitypub_activity->from_comment( $activitypub_comment->to_array() ); +//error_log(print_r($activitypub_activity, true)); + + foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { + \error_log( '$user_id: ' . $user_id ); + \error_log( '$inbox: '. $inbox ); + \error_log( '$to: '. print_r($to, true ) ); + array_push( $activitypub_activity['object']['to'], $to[0] ); + array_push( $activitypub_activity['to'], $to[0] ); + //Forward reply to followers, skip + if( in_array( $to, $replyto ) || ( $replyto == $to ) ) { + continue; + } + error_log(print_r($activitypub_activity, true)); + //$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:' ); + error_log( print_r( $activity, true ) ); + error_log( 'dispatch:forward:inbox: '. $inbox ); + \Activitypub\forward_remote_post( $inbox, $activity, $user_id ); + array_pop( $activitypub_activity['object']['to'] ); + array_pop( $activitypub_activity['to'] ); + } + } } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 6c7c0c89..4b801737 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -8,30 +8,32 @@ */ 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_action( 'wp_head', array( '\Activitypub\Activitypub', 'author_atom_uri' ), 2 ); + \add_filter( 'status_edit_pre', array( '\Activitypub\Activitypub', 'set_post_type_status_private'), 10, 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(); - foreach ( $post_types as $post_type ) { \add_post_type_support( $post_type, 'activitypub' ); } \add_action( 'transition_post_status', array( '\Activitypub\Activitypub', 'schedule_post_activity' ), 10, 3 ); + \add_action( 'transition_comment_status', array( '\Activitypub\Activitypub', 'schedule_comment_activity' ), 10, 3 ); + //\add_action( 'transition_comment_status', array( '\Activitypub\Activitypub', 'schedule_inbox_forward_activity' ), 10, 3 ); } /** - * 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 +65,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 +81,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 +98,134 @@ public static function add_rewrite_endpoint() { } /** - * Schedule Activities. + * 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 ) { + \error_log( '$status: ' . $status ); + $audience = \get_post_meta( $post_id, '_ap_audience_meta_key' ); + if ( in_array( 'private_message', $audience ) || in_array( 'followers_only', $audience ) ) { + $status = 'private'; + } + return $status; + } + + /** + * Schedule Activities * - * @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. + error_log('schedule_post_activity ($new_status):' . $new_status . ': ' . $post->ID ); + // check if post-type supports ActivityPub $post_types = \get_post_types_by_support( 'activitypub' ); if ( ! \in_array( $post->post_type, $post_types, true ) ) { return; } - $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 ) ); + $audience = \get_post_meta( $post->ID, '_ap_audience_meta_key' ); + + if ( 'publish' === $new_status && $new_status !== $old_status ) { + if ( in_array( 'private_message', $audience ) || in_array( 'followers_only', $audience ) ) { + $new_status = 'private_message'; + error_log('schedule_post_activity ($new_status):' . $new_status . ': ' . $post->ID ); + \wp_schedule_single_event( \time(), 'activitypub_send_private_activity', array( $post->ID ) ); + } else { + \wp_schedule_single_event( \time(), 'activitypub_send_post_activity', array( $post->ID ) ); + } + } elseif ( 'publish' === $new_status ) { + \wp_schedule_single_event( \time(), 'activitypub_send_private_activity', array( $post->ID ) ); } elseif ( 'publish' === $new_status ) { - \wp_schedule_single_event( \time(), 'activitypub_send_update_activity', array( $activitypub_post ) ); + \wp_schedule_single_event( \time(), 'activitypub_send_update_activity', array( $post->ID ) ); } 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( $post ) ) ); } } /** - * Replaces the default avatar. + * Schedule Activities * - * @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 int $comment + */ + public static function schedule_comment_activity( $new_status, $old_status, $comment ) { + \error_log( 'schedule_comment_activity'); + + // error_log( 'schedule_comment_activity: $new_status: ' . $new_status); + // error_log( 'schedule_comment_activity: $old_status: ' . $old_status ); + + \error_log( print_r( $comment, true ) ); + + //reply to federated, + + //$ap_object = get_comment_meta( $comment->comment_ID, 'ap_object', true ); + + if ( 'approved' === $new_status && 'approved' !== $old_status ) { + $ap_object = get_comment_meta( $comment->comment_ID, 'ap_object', true ); + if ( $ap_object ) { + \error_log( 'schedule_inbox_forward_activity: ID: ' . $comment->comment_ID ); + \error_log( print_r( $comment, true ) ); + \wp_schedule_single_event( \time(), 'activitypub_inbox_forward_activity', array( $comment->comment_ID ) ); + } else { + $replyto = comment_author_url( $comment->comment_parent ); + \error_log( 'schedule_comment_activity: ID: ' . $comment->comment_ID ); + error_log( 'schedule_comment_activity: replyto: ' . $replyto ); + \wp_schedule_single_event( \time(), 'send_comment_activity', array( $comment->comment_ID ) ); + } + + + // } elseif ( 'approved' === $new_status && ( $ap_object ) ) { + // error_log( 'approved: $replyto: ' . $replyto ); + // \wp_schedule_single_event( \time(), 'activitypub_send_comment_activity', array( $comment->comment_ID ) ); + } else { + error_log( 'not_approved: other comment action: ' . print_r( $comment, true ) ); + } + } + + /** + * Schedule Activities + * + * @param int $post_id + */ + public static function schedule_inbox_forward_activity( $new_status, $old_status, $comment ) { + error_log( 'schedule_inbox_forward_activity: ID: ' . $comment->comment_ID ); + $ap_object = get_comment_meta( $comment->comment_ID, 'ap_object', true ); + error_log( print_r( unserialize( $ap_object ), true ) ); + if ( !$ap_object ) { + return; + } + // error_log( 'schedule_inbox_forward_activity: $new_status: ' . $new_status); + // error_log( 'schedule_inbox_forward_activity: $old_status: ' . $old_status ); + + //foreign comment has just been approved: forward should happen here, no? + //error_log( print_r( $comment, true ) ); + + //$ap_object = get_comment_meta( $comment->comment_ID, 'ap_object', true ); + $replyto = $comment->comment_author_url; + if ( ( 'approved' === $new_status && 'approved' !== $old_status ) && ( $ap_object ) ) { + //error_log( 'schedule_inbox_forward_activity>replyto: ' . $replyto ); + //error_log( print_r( , true ) ); + \wp_schedule_single_event( \time(), 'activitypub_inbox_forward_activity', array( $comment->comment_ID ) ); + // } elseif ( 'approved' === $new_status && ( $ap_object ) ) { + // error_log( 'approved: $replyto: ' . $replyto ); + // \wp_schedule_single_event( \time(), 'activitypub_send_comment_activity', array( $comment->comment_ID ) ); + } else { + error_log( 'schedule_inbox_forward_activity: not_approved: $from: ' . $replyto ); + } + } + + /** + * 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 * * @return array $args */ @@ -141,15 +238,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 +263,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 +276,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 ); + ?> diff --git a/includes/class-c2s.php b/includes/class-c2s.php new file mode 100644 index 00000000..a04c5811 --- /dev/null +++ b/includes/class-c2s.php @@ -0,0 +1,312 @@ + $default_dashboard['dashboard_compose_widget'], + 'dashboard_inbox_widget' => $default_dashboard['dashboard_inbox_widget'], + ); + unset( $default_dashboard['dashboard_compose_widget'] ); + unset( $default_dashboard['dashboard_inbox_widget'] ); + + // Merge the two arrays together so our widget is at the beginning. + $sorted_dashboard = array_merge( $ap_widget_backup, $default_dashboard ); + $wp_meta_boxes['dashboard']['normal']['core'] = $sorted_dashboard; + } + + /** + * Compose Dashboard widget + */ + public static function dashboard_compose_widget_render( $post, $callback_args ) { + //esc_html_e( "Hello World, this is my Compose Widget!", "activitypub" ); + global $post; + if ( ! current_user_can( 'edit_posts' ) ) { + return; + } + $post = get_default_post_to_edit( 'post', true ); + $post_ID = (int) $post->ID; + ?> +
+ +

+ + + + + 'save-post' ) ); ?> +
+

+
+ DEBUG
'; print_r($mentions); echo '
'; + } + + /** + * 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' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array(); + $ap_post_types[] = 'activitypub_mentions'; + //$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 + */ + public static function save_post_audience($post_id) { + //wp_verify_nonce('ap_audience_meta'); + if (array_key_exists('_ap_audience', $_POST)) { + \error_log('array_key_exists-_ap_audience: ' . $_POST['_ap_audience']); + update_post_meta( + $post_id, + '_ap_audience', + $_POST['_ap_audience'] + ); + } + if (array_key_exists('_ap_mentions', $_POST)) { + \error_log('array_key_exists-_ap_mentions: ' . $_POST['_ap_mentions']); + update_post_meta( + $post_id, + '_ap_mentions', + $_POST['_ap_mentions'] + ); + } + // if (array_key_exists('_ap_replyto', $_POST)) { + // \error_log('array_key_exists-_ap_replyto: ' . $_POST['_ap_replyto']); + // update_post_meta( + // $post_id, + // '_ap_replyto', + // $_POST['_ap_replyto'] + // ); + // } + // if (array_key_exists('parent_id', $_POST)) { + // \error_log('array_key_exists-_ap_replyto: ' . $_POST['parent_id']); + // remove_action( 'save_post_activitypub', 'save_post_audience' ); + + // // update the post, which calls save_post again + // wp_update_post( + // array( 'ID' => $post_id, + // 'post_parent' => $_POST['parent_id'] + // ) + // ); + + // // re-hook this function + // add_action( 'save_post_activitypub', 'save_post_audience' ); + // } + } + + /** + * Saves post as child of parent for reply graph + */ + public static function save_post_parent($data, $postarr){ + if ( isset( $postarr["post_parent"] ) ) { + $data["post_parent"] = $postarr["post_parent"]; + } + // if ( $postarr["post_status"] === 'publish' && $postarr["_ap_audience"] === 'private_message' ) { + // $data["post_status"] = $postarr["_ap_audience"]; + // } + // error_log( 'C2S:save_post_parent:data: ' . print_r($data,true)); + // error_log( 'C2S:save_post_parent:postarr: ' . print_r($postarr,true)); + return $data; + } + + /** + * 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, '_ap_audience', true); + $mentions = get_post_meta($post->ID, '_ap_mentions', true); + //$replyto = get_post_meta($post->ID, '_ap_replyto', true); + if ( isset( $post->post_parent ) ){ + $replyto = $post_parent = $post->post_parent; + } + } + if (array_key_exists('_ap_audience', $_REQUEST)) { + $audience = $_REQUEST['_ap_audience']; + } + if (array_key_exists('_ap_mentions', $_REQUEST)) { + \error_log('array_key_exists-_ap_mentions: ' . $_REQUEST['_ap_mentions']); + $mentions = $_REQUEST['_ap_mentions']; + } + // if (array_key_exists('_ap_replyto', $_REQUEST)) { + // \error_log('array_key_exists-_ap_replyto: ' . $_REQUEST['_ap_replyto']); + // $replyto = $_REQUEST['_ap_replyto']; + // $replyto = get_post_meta( $post_parent, '_source_url', true); + // } + if (array_key_exists('post_parent', $_REQUEST)) { + \error_log('array_key_exists-parent_id: ' . $_REQUEST['post_parent']); + $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..9412d3f7 --- /dev/null +++ b/includes/class-mentions.php @@ -0,0 +1,133 @@ + _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' => 'Private and Direct Messages from the fediverse', + 'public' => false, + 'show_ui' => true,//TODO true for dev + 'show_in_admin_bar' => true,//TODO true for dev + //'show_in_rest' => true,? + 'menu_icon' => 'dashicons-format-chat',//TODO change to ActivityPub logo + // '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', + //'page-attributes', + array( + 'post_status' => 'private', + ) +// 'comments',//for public coments? or no that complicates things? + ), + 'hierarchical' => true,//allows thread like comments + 'has_archive' => false, + 'rewrite' => false, + //'query_var' => false, + 'delete_with_user' => true, + ); + \register_post_type( 'activitypub_mentions', $post_type_args ); + + $private_message_args = array( + 'label' => _x( 'Private Message', 'post' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Private (%s)', 'Private (%s)' ), + 'post_type' => array( 'activitypub_mentions' ), + 'show_in_metabox_dropdown' => true, + 'show_in_inline_dropdown' => true, + 'dashicon' => 'dashicons-businessman', + ); + \register_post_status( 'private_message', $private_message_args ); + + $followers_only_args = array( + 'label' => _x( 'Followers only', 'post' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Followers only (%s)', 'Followers only (%s)' ), + ); + \register_post_status( 'followers_only', $followers_only_args ); + + $unlisted_message_args = array( + 'label' => _x( 'Unlisted', 'post' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Unlisted post (%s)', 'Unlisted post (%s)' ), + ); + \register_post_status( 'unlisted', $unlisted_message_args ); + + $public_message_args = array( + 'label' => _x( 'Public', 'post' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + 'label_count' => _n_noop( 'Public post (%s)', 'Public post (%s)' ), + ); + \register_post_status( 'public', $public_message_args ); + } + + 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 90dec8a7..52f5e58c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -40,7 +40,7 @@ function safe_remote_post( $url, $body, $user_id ) { ), 'body' => $body, ); - +//\error_log( 'signature:' . print_r( $args, true ) ); $response = \wp_safe_remote_post( $url, $args ); \do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id ); @@ -48,6 +48,33 @@ function safe_remote_post( $url, $body, $user_id ) { return $response; } +function forward_remote_post( $url, $body, $user_id ) { + $date = \gmdate( 'D, d M Y H:i:s T' ); + //$signature = \Activitypub\Signature::generate_signature( $user_id, $url, $date ); + + $wp_version = \get_bloginfo( 'version' ); + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $args = array( + 'timeout' => 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 ); + + \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 +116,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 +184,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 +215,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 +229,6 @@ function get_follower_inboxes( $user_id ) { } $inboxes[ $inbox ][] = $follower; } - return $inboxes; } @@ -253,13 +280,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 +306,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; } @@ -288,6 +315,89 @@ 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 ) { + $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; + } +} + +function add_recipients( $recipient, $self ) { + $cc = array( 'https://www.w3.org/ns/activitystreams#Public' ); + $cc[] = $recipient; + $cc[] = $self . 'followers'; + return $cc; +} + +function tag_user( $recipient ) { + $tagged_user = array( + 'type' => 'Mention', + 'href' => $recipient, + 'name' => \Activitypub\url_to_webfinger( $recipient ), + ); + $tag[] = $tagged_user; + return $tag; +} + +function url_to_webfinger( $user_url ) { + $user_url = \untrailingslashit( $user_url ); + $user_url_array = explode( '/', $user_url ); + $user_name = end( $user_url_array ); + $url_host = parse_url( $user_url , PHP_URL_HOST ); + $webfinger = '@' . $user_name . '@' . $url_host; + return $webfinger; +} + +/** + * 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 + */ +function normalize_comment_url( $comment ) { + $comment_id = explode( '#comment-', \get_comment_link( $comment ) ); + $comment_id = $comment_id[0] . '?' . $comment_id[1]; + return $comment_id; +} + /** * Get the blacklist from the WordPress options table * @@ -296,8 +406,8 @@ function url_to_authorid( $url ) { * @uses apply_filters() Calls 'activitypub_blacklist' filter */ function get_blacklist() { - $blacklist = \get_option( 'activitypub_blacklist', 'gab.com' ); - $blacklist_hosts = \explode( \PHP_EOL, $blacklist ); + $blacklist = \get_option( 'activitypub_blacklist' ); + $blacklist_hosts = \explode( PHP_EOL, $blacklist ); // if no values have been set, revert to the defaults if ( ! $blacklist || ! $blacklist_hosts || ! \is_array( $blacklist_hosts ) ) { @@ -325,10 +435,38 @@ function get_blacklist() { */ function is_blacklisted( $url ) { foreach ( \ActivityPub\get_blacklist() as $blacklisted_host ) { - if ( \stripos( $url, $blacklisted_host ) !== false ) { + if ( \strpos( $url, $blacklisted_host ) !== false ) { return true; } } return false; } + +/** + * in_audience + * return true if wp_user is in an audience array + */ +// function in_audience( $needles, $haystack ) { +// foreach ($needles as $needle) { +// if ( \strpos( $haystack, $needle ) !== false ) { +// return true; +// } +// } +// } + +// function in_audience( $needles, $haystack ) { +// foreach ($needles as $needle) { +// if ( \strpos( $haystack, $needle ) !== false ) { +// return true; +// } +// } +// } + +/* polyfill against php 8 */ +// https://php.watch/versions/8.0/str_contains +if ( !function_exists( 'str_contains' ) ) { + function str_contains( $haystack, $needle ) { + return '' === $needle || false !== \strpos( $haystack, $needle ); + } +} diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index 4aa41077..dae3740a 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -41,7 +41,9 @@ public function __call( $method, $params ) { } if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { + //\error_log('set_params: ' . print_r( $params, true ) ); $this->$var = $params[0]; + //$this->$var = [ 'https://www.w3.org/ns/activitystreams#Public', $params[0][0]]; } } @@ -50,10 +52,17 @@ public function from_post( $object ) { $this->published = $object['published']; $this->actor = $object['attributedTo']; $this->id = $object['id']; + $this->cc = array( 'https://www.w3.org/ns/activitystreams#Public', $object['attributedTo'] . 'followers' ); } public function from_comment( $object ) { - + \error_log( 'from_comment' ); + $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_array() { @@ -69,10 +78,11 @@ public function to_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 to_simple_array() { + \error_log( 'to_simple_array' ); $activity = array( '@context' => $this->context, 'type' => $this->type, @@ -90,6 +100,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..a5b3f7a9 --- /dev/null +++ b/includes/model/class-comment.php @@ -0,0 +1,95 @@ +comment = \get_comment( $comment ); + } + + public function get_comment() { + return $this->comment; + } + + public function get_comment_author() { + return $this->comment->comment_author; + } + + public function to_array() { + $comment = $this->comment; + error_log( 'to_array()' ); + // error_log( print_r($comment, true) ); + $ap_object = \get_comment_meta( $comment->comment_ID, 'ap_object', true ); + // if($ap_object){ + // \error_log('is_ap_object:'); + // \error_log( print_r( $ap_object, true ) ); + // } + $parent_ap_object = \get_comment_meta( $comment->comment_parent, 'ap_object', true ); + if($parent_ap_object){ + // \error_log('parent_ap_object:'); + // \error_log( print_r( \unserialize($parent_ap_object), true ) ); + $parent_ap_object = \unserialize($parent_ap_object); + + } + $cc_recipients = $mentions = null; + $self = $comment->comment_author_url; + $parent_comment = \get_comment( $comment->comment_parent ); + if ( $parent_comment ) { + $recipient = $parent_comment->comment_author_url; + $cc_recipients = \Activitypub\add_recipients( $recipient, $self ); + $mentions = \Activitypub\tag_user( $recipient ); + $inReplyTo = \get_comment_meta( $comment->comment_parent, 'source_url', true ); + } else { + $inReplyTo = $comment->comment_parent; + } + + //ID must be unique https://www.w3.org/TR/activitypub/#obj-id + $comment_id = \Activitypub\normalize_comment_url( $comment ); + + // error_log( '$cc_recipients: ' . print_r( $cc_recipients, true ) ); + // error_log( '$mentions: ' . print_r( $mentions, true ) ); + //comment_id $source_url = get_comment_meta( $comment->comment_ID, 'source_url', true ); + + //error_log('$inReplyTo: ' . $inReplyTo ); + //error_log( print_r($inReplyTo, true) ); + if( empty( $ap_object ) ) { + $array = array( + 'id' => $comment_id, //\get_comment_link( $comment ), + 'type' => 'Note', + 'published' => \date( 'Y-m-d\TH:i:s\Z', \strtotime( $comment->comment_date ) ), + 'attributedTo' => \esc_url_raw( $comment->comment_author_url ), + 'summary' => '',//$this->get_the_title(), + 'inReplyTo' => \esc_url_raw( $inReplyTo ), + 'content' => $comment->comment_content, + 'contentMap' => array( + \strstr( \get_locale(), '_', true ) => $comment->comment_content, + ), + 'source' => \get_comment_link( $comment ), + 'url' => \get_comment_link( $comment ), + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + //'to' => array( 'https://www.w3.org/ns/activitystreams#Public', $self ), + 'cc' => $cc_recipients, + 'tag' => $mentions, + ); + } else { + $array = unserialize( $ap_object ); + } + + //error_log( 'to_array' ); + 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 ); + } + +} \ No newline at end of file diff --git a/includes/model/class-post.php b/includes/model/class-post.php index ed7e9c0a..0d427665 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -8,13 +8,6 @@ */ class Post { private $post; - private $post_author; - private $permalink; - private $summary; - private $content; - private $attachments; - private $tags; - private $object_type; /** * Initialize the class, registering WordPress hooks @@ -26,64 +19,54 @@ public static function init() { public function __construct( $post = null ) { $this->post = \get_post( $post ); - - $this->post_author = $this->post->post_author; - $this->permalink = $this->generate_permalink(); - $this->summary = $this->generate_the_title(); - $this->content = $this->generate_the_content(); - $this->attachments = $this->generate_attachments(); - $this->tags = $this->generate_tags(); - $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() { $post = $this->post; $array = array( - 'id' => $this->permalink, - 'type' => $this->object_type, + 'id' => \get_permalink( $post ), + 'type' => $this->get_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( //$this->get_the_comments(); 'activitypub/1.0/post/$post->ID/replies' + '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 ); - } - - public function generate_permalink() { - $post = $this->post; - $permalink = \get_permalink( $post ); - - // replace 'trashed' for delete activity - return \str_replace( '__trashed', '', $permalink ); + return \wp_json_encode( $this->to_array(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT ); } - public function generate_attachments() { + public function get_attachments() { $max_images = \apply_filters( 'activitypub_max_images', 3 ); $images = array(); @@ -131,7 +114,7 @@ public function generate_attachments() { $image = array( 'type' => 'Image', 'url' => $thumbnail[0], - 'mediaType' => $mimetype, + 'mediaType' => $mimetype ); if ( $alt ) { $image['name'] = $alt; @@ -143,7 +126,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 ); @@ -169,7 +152,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' ) ); } @@ -226,23 +209,23 @@ public function generate_object_type() { return $object_type; } - public function generate_the_content() { + public function get_the_content() { if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) { - return $this->generate_the_post_summary(); + return $this->get_the_post_summary(); } if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) { - return $this->generate_the_title(); + return $this->get_the_title(); } - return $this->generate_the_post_content(); + return $this->get_the_post_content(); } - public function generate_the_title() { - if ( 'Article' === $this->generate_object_type() ) { - $title = \generate_the_title( $this->post ); + public function get_the_title() { + if ( 'Article' === $this->get_object_type() ) { + $title = \get_the_title( $this->post ); - return \html_entity_decode( $title, \ENT_QUOTES, 'UTF-8' ); + return \html_entity_decode( $title, ENT_QUOTES, 'UTF-8' ); } return null; @@ -255,7 +238,7 @@ public function generate_the_title() { * * @return string The excerpt. */ - public function generate_the_post_excerpt( $excerpt_length = 400 ) { + public function get_the_post_excerpt( $excerpt_length = 400 ) { $post = $this->post; $excerpt = \get_post_field( 'post_excerpt', $post ); @@ -290,7 +273,7 @@ public function generate_the_post_excerpt( $excerpt_length = 400 ) { * * @return string The content. */ - public function generate_the_post_content() { + public function get_the_post_content() { $post = $this->post; $content = \get_post_field( 'post_content', $post ); @@ -298,9 +281,9 @@ public function generate_the_post_content() { $filtered_content = \apply_filters( 'the_content', $content ); $filtered_content = \apply_filters( 'activitypub_the_content', $filtered_content, $this->post ); - $decoded_content = \html_entity_decode( $filtered_content, \ENT_QUOTES, 'UTF-8' ); + $decoded_content = \html_entity_decode( $filtered_content, ENT_QUOTES, 'UTF-8' ); - $allowed_html = \apply_filters( 'activitypub_allowed_html', '