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'];
+ }
+
+ ?>
+
+
+
'; 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