From 87aae44c72da138f0f1032f0bfcff6fac39c7973 Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Tue, 23 Oct 2018 18:52:36 -0400 Subject: [PATCH] [WIP] Implement WP->Mastodon comment syncing --- includes/client/comments.php | 96 +++++++++++++++++++++++++++++++++--- includes/init.php | 6 +++ includes/schema.php | 1 + includes/server/actors.php | 68 +++++++++++++++++++++++-- includes/server/objects.php | 9 ++++ 5 files changed, 169 insertions(+), 11 deletions(-) diff --git a/includes/client/comments.php b/includes/client/comments.php index 88eabfa..f014948 100644 --- a/includes/client/comments.php +++ b/includes/client/comments.php @@ -4,13 +4,31 @@ namespace pterotype\comments; require_once plugin_dir_path( __FILE__ ) . '../server/activities/create.php'; require_once plugin_dir_path( __FILE__ ) . '../server/activities/update.php'; require_once plugin_dir_path( __FILE__ ) . '../server/activities/delete.php'; +require_once plugin_dir_path( __FILE__ ) . '../server/objects.php'; -function handle_transition_comment_status( $old_status, $new_status, $comment ) { +function handle_comment_post( $comment_id, $comment_approved ) { + xdebug_break(); + if ( $comment_approved ) { + $comment = \get_comment( $comment_id ); + handle_transition_comment_status( 'approve', 'nonexistent', $comment ); + } +} + +function handle_edit_comment( $comment_id ) { + $comment = \get_comment( $comment_id ); + if ( $comment->comment_approved ) { + handle_transition_comment_status( 'approve', 'approve', $comment ); + } +} + +function handle_transition_comment_status( $new_status, $old_status, $comment ) { + xdebug_break(); + // This creates a new commenter actor if necessary $actor_slug = get_comment_actor_slug( $comment ); $actor_outbox = get_rest_url( null, sprintf( 'pterotype/v1/actor/%s/outbox', $actor_slug ) ); - $comment_object = comment_to_object( $comment ); + $comment_object = comment_to_object( $comment, $actor_slug ); $activity = null; if ( $new_status == 'approve' && $old_status != 'approve' ) { // Create @@ -24,7 +42,7 @@ function handle_transition_comment_status( $old_status, $new_status, $comment ) } if ( $activity && ! is_wp_error( $activity ) ) { $followers = \pterotype\followers\get_followers_collection( $actor_slug ); - $activity['to'] = get_comment_to( $comment, $followers['id'] ); + $activity['to'] = get_comment_to( $comment_object, $followers['id'] ); $server = rest_get_server(); $request = \WP_REST_Request::from_url( $actor_outbox ); $request->set_method( 'POST' ); @@ -43,15 +61,50 @@ function get_comment_actor_slug( $comment ) { } function get_comment_user_actor_slug( $user_id ) { - + if ( \user_can( $user_id, 'publish_posts' ) ) { + return PTEROTYPE_BLOG_ACTOR_SLUG; + } else { + $user = \get_userdata( $user_id ); + return $user->user_nicename; + } } function get_comment_email_actor_slug( $email_address ) { - + $slug = \pterotype\actors\upsert_commenter_actor( $email_address ); + return $slug; } -function comment_to_object( $comment ) { - +function comment_to_object( $comment, $actor_slug ) { + $post = \get_post( $comment->comment_post_ID ); + \setup_postdata( $post ); + $post_permalink = \get_permalink( $post ); + $post_object = \pterotype\objects\get_object_by( 'url', $post_permalink ); + $inReplyTo = $post_object['id']; + if ( $comment->comment_parent !== '0' ) { + $parent_comment = \get_comment( $comment->comment_parent ); + $parent_object = \pterotype\objects\get_object_by( + 'url', \get_comment_link( $parent_comment ) + ); + if ( $parent_object ) { + $inReplyTo = $parent_object['id']; + } + } + $link = \get_comment_link( $comment ); + $object = array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'type' => 'Note', + 'content' => $comment->comment_content, + 'attributedTo' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s', $actor_slug ) + ), + 'url' => $link, + 'inReplyTo' => $inReplyTo, + ); + $existing = \pterotype\objects\get_object_by( 'url', $link ); + if ( $existing ) { + $object['id'] = $existing['id']; + } + return $object; } function get_comment_to( $comment, $followers_id ) { @@ -59,7 +112,34 @@ function get_comment_to( $comment, $followers_id ) { 'https://www.w3.org/ns/activitystreams#Public', $followers_id, ); - // TODO traverse comment reply chain to retrieve others to address to + $to = array_unique( array_merge( $to, traverse_reply_chain( $comment ) ) ); return $to; } + +function traverse_reply_chain( $comment ) { + return traverse_reply_chain_helper( $comment, 0, array() ); +} + +function traverse_reply_chain_helper( $object, $depth, $acc ) { + if ( $depth === 50 ) { + return $acc; + } + if ( array_key_exists( 'inReplyTo', $object ) ) { + return $acc; + } + $parent = \pterotype\util\dereference_object( $object['inReplyTo'] ); + $recipients = array(); + foreach( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $field ) { + if ( array_key_exists( $field, $parent ) ) { + $new_recipients = $parent[$field]; + if ( ! is_array( $new_recipients ) ) { + $new_recipients = array( $new_recipients ); + } + $recipients = array_unique( array_merge( $recipients, $new_recipients ) ); + } + } + return traverse_reply_chain_helper( + $parent, $depth + 1, array_unique( array_merge( $acc, $recipients ) ) + ); +} ?> diff --git a/includes/init.php b/includes/init.php index cb71a9d..539fcd8 100644 --- a/includes/init.php +++ b/includes/init.php @@ -7,6 +7,7 @@ require_once plugin_dir_path( __FILE__ ) . 'server/actors.php'; require_once plugin_dir_path( __FILE__ ) . 'schema.php'; require_once plugin_dir_path( __FILE__ ) . 'server/webfinger.php'; require_once plugin_dir_path( __FILE__ ) . 'client/posts.php'; +require_once plugin_dir_path( __FILE__ ) . 'client/comments.php'; require_once plugin_dir_path( __FILE__ ) . 'server/async.php'; add_action( 'rest_api_init', function() { @@ -36,5 +37,10 @@ add_action( 'parse_request', '\pterotype\webfinger\parse_request', 111 ); add_filter( 'query_vars', '\pterotype\webfinger\query_vars' ); add_action( 'well_known_webfinger', '\pterotype\webfinger\handle' ); add_action( 'transition_post_status', '\pterotype\posts\handle_post_status_change', 10, 3 ); +add_action( + 'transition_comment_status', '\pterotype\comments\handle_transition_comment_status', 10, 3 +); +add_action( 'comment_post', '\pterotype\comments\handle_comment_post', 10, 2 ); +add_action( 'edit_comment', '\pterotype\comments\handle_edit_comment', 10, 1 ); add_action( 'template_redirect', '\pterotype\api\handle_non_api_requests' ); ?> diff --git a/includes/schema.php b/includes/schema.php index 346aa3c..4365df5 100644 --- a/includes/schema.php +++ b/includes/schema.php @@ -180,6 +180,7 @@ function migration_0_0_1() { } function migration_1_1_0() { + global $wpdb; $wpdb->query( " CREATE TABLE {$wpdb->prefix}pterotype_comments ( diff --git a/includes/server/actors.php b/includes/server/actors.php index 42a8349..277e7e1 100644 --- a/includes/server/actors.php +++ b/includes/server/actors.php @@ -40,12 +40,54 @@ function get_actor_from_row( $row ) { $user = get_user_by( 'slug', $row->slug ); return get_user_actor( $user ); case 'commenter': - return new \WP_Error( - 'not_implemented', __( 'Commenter actors not yet implemented', 'pterotype' ) - ); + return get_commenter_actor( $row->slug ); } } +function get_commenter_actor( $slug ) { + $actor_id = get_actor_id( $slug ); + $email_address = str_replace( '[AT]', '@', $slug ); + $actor = array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'type' => 'Person', + 'id' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s', $slug ) + ), + 'following' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s/following', $slug ) + ), + 'followers' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s/followers', $slug ) + ), + 'liked' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s/liked', $slug ) + ), + 'inbox' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s/inbox', $slug ) + ), + 'outbox' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s/outbox', $slug ) + ), + 'name' => $email_address, + // TODO in the future, make this configurable, both here and in the Webfinger handler + 'preferredUsername' => $email_address, + 'publicKey' => array( + 'id' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s#publicKey', $slug ) + ), + 'owner' => get_rest_url( + null, sprintf( '/pterotype/v1/actor/%s', $slug ) + ), + 'publicKeyPem' => \pterotype\pgp\get_public_key( $actor_id ), + ), + ); + // TODO retrieve commenter name, url, and icon + return $actor; +} + function get_blog_actor() { $actor_id = get_actor_id( PTEROTYPE_BLOG_ACTOR_SLUG ); $actor = array( @@ -175,4 +217,24 @@ function create_actor( $slug, $type ) { } return $res->object; } + +function upsert_commenter_actor( $email_address ) { + global $wpdb; + $slug = str_replace( '@', '[AT]', $email_address ); + $existing = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}pterotype_actors WHERE slug = %s", + $slug + ) ); + if ( $existing !== null ) { + return $slug; + } + create_actor( $slug, 'commenter' ); + $actor_id = get_actor_id( $slug ); + $keys_created = \pterotype\pgp\get_public_key( $actor_id ); + if ( ! $keys_created ) { + $keys = \pterotype\pgp\gen_key( $slug ); + \pterotype\pgp\persist_key( $actor_id, $keys['publickey'], $keys['privatekey'] ); + } + return $slug; +} ?> diff --git a/includes/server/objects.php b/includes/server/objects.php index 39f5b57..57ec5b4 100644 --- a/includes/server/objects.php +++ b/includes/server/objects.php @@ -228,6 +228,15 @@ function get_objects_by( $field, $value ) { ); } +function get_object_by( $field, $value ) { + $objects = get_objects_by( $field, $value ); + if ( count( $objects ) === 0 ) { + return null; + } else { + return $objects[0]; + } +} + function delete_object( $object ) { global $wpdb; $object = \pterotype\util\dereference_object( $object );