From ebeae261de216da5238fc7c189bf1ec7e1c5792f Mon Sep 17 00:00:00 2001 From: tenma Date: Mon, 19 Aug 2019 23:33:18 +0100 Subject: [PATCH] [AP] Support Private Messaging ActivityPubPlugin: - Subscribe DirectMessage events Activitypub_inbox_handler: - Update handle_create_note to create private messages Activitypub_postman: - Add create_direct_note for sending private messages Activitypub_create: - Update create_to_array to support the 'directMessage' attribute - Add isPrivateNote to verify private activities Activitypub_notice: - Update create_note to support the 'directMessage' attribute - Remove isPrivateNote lib/models: - Add Activitypub_message, the model in charge of private notes --- plugins/ActivityPub/ActivityPubPlugin.php | 73 +++++++++++++-- plugins/ActivityPub/lib/inbox_handler.php | 7 +- .../lib/models/Activitypub_create.php | 22 ++++- .../lib/models/Activitypub_message.php | 91 +++++++++++++++++++ .../lib/models/Activitypub_notice.php | 27 ++---- plugins/ActivityPub/lib/postman.php | 39 +++++++- 6 files changed, 225 insertions(+), 34 deletions(-) create mode 100644 plugins/ActivityPub/lib/models/Activitypub_message.php diff --git a/plugins/ActivityPub/ActivityPubPlugin.php b/plugins/ActivityPub/ActivityPubPlugin.php index 35d5af8faf..c10ba828d4 100644 --- a/plugins/ActivityPub/ActivityPubPlugin.php +++ b/plugins/ActivityPub/ActivityPubPlugin.php @@ -286,6 +286,46 @@ class ActivityPubPlugin extends Plugin return true; } + /** + * Add AP-subscriptions for private messaging + * + * @param User $current current logged user + * @param array &$recipients + * @return void + */ + public function onFillDirectMessageRecipients(User $current, array &$recipients): void { + try { + $subs = Activitypub_profile::getSubscribed($current->getProfile()); + foreach ($subs as $sub) { + if (!$sub->isLocal()) { // AP plugin adds AP users + try { + $value = 'profile:'.$sub->getID(); + $recipients[$value] = substr($sub->getAcctUri(), 5) . " [{$sub->getBestName()}]"; + } catch (ProfileNoAcctUriException $e) { + $recipients[$value] = "[?@?] " . $e->profile->getBestName(); + } + } + } + } catch (NoResultException $e) { + // let it go + } + } + + /** + * Validate AP-recipients for profile page message action addition + * + * @param Profile $recipient + * @return bool hook return value + */ + public function onDirectMessageProfilePageActions(Profile $recipient): bool { + $to = Activitypub_profile::getKV('profile_id', $recipient->getID()); + if ($to instanceof Activitypub_profile) { + return false; // we can validate this profile, signal it + } + + return true; + } + /** * Plugin Nodeinfo information * @@ -815,12 +855,10 @@ class ActivityPubPlugin extends Plugin return true; } - // We handle things locally either because: - // 1. the deleting user has special permissions to do so, - // but still doesn't own the notice - // 2. the notice is an announce, and there's no undo-share - // logic in GS's AP implementation - if (!$notice->isLocal() || $notice->isRepeat()) { + // Handle delete locally either because: + // 1. There's no undo-share logic yet + // 2. The deleting user has previleges to do so (locally) + if ($notice->isRepeat() || ($notice->getProfile()->getID() != $profile->getID())) { return true; } @@ -855,6 +893,29 @@ class ActivityPubPlugin extends Plugin return true; } + /** + * Federate private message + * + * @param Notice $message + * @return void + */ + public function onSendDirectMessage(Notice $message): void { + $from = $message->getProfile(); + if (!$from->isLocal()) { + // nothing to do + return; + } + + $to = Activitypub_profile::from_profile_collection( + $message->getAttentionProfiles() + ); + + if (!empty($to)) { + $postman = new Activitypub_postman($from, $to); + $postman->create_direct_note($message); + } + } + /** * Override the "from ActivityPub" bit in notice lists to link to the * original post and show the domain it came from. diff --git a/plugins/ActivityPub/lib/inbox_handler.php b/plugins/ActivityPub/lib/inbox_handler.php index f1e3a231dc..62f6be567a 100644 --- a/plugins/ActivityPub/lib/inbox_handler.php +++ b/plugins/ActivityPub/lib/inbox_handler.php @@ -206,8 +206,8 @@ class Activitypub_inbox_handler */ private function handle_create_note() { - if (Activitypub_notice::isPrivateNote($this->activity)) { - // Plugin DirectMessage must handle this + if (Activitypub_create::isPrivateNote($this->activity)) { + Activitypub_message::create_message($this->object, $this->actor); } else { Activitypub_notice::create_notice($this->object, $this->actor); } @@ -226,8 +226,7 @@ class Activitypub_inbox_handler $object = $object['id']; } - // some moderator could already have deleted the - // notice, so we test it first + // Already deleted? (By some admin, perhaps?) try { $found = Deleted_notice::getByUri($object); $deleted = ($found instanceof Deleted_notice); diff --git a/plugins/ActivityPub/lib/models/Activitypub_create.php b/plugins/ActivityPub/lib/models/Activitypub_create.php index 6af6253995..6ea760329b 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_create.php +++ b/plugins/ActivityPub/lib/models/Activitypub_create.php @@ -42,15 +42,16 @@ class Activitypub_create * @author Diogo Cordeiro * @param string $actor * @param array $object + * @param bool $directMesssage whether it is a private Create activity or not * @return array pretty array to be used in a response */ - public static function create_to_array(string $actor, array $object): array + public static function create_to_array(string $actor, array $object, bool $directMessage = false): array { $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => $object['id'].'/create', 'type' => 'Create', - 'directMessage' => false, + 'directMessage' => $directMessage, 'to' => $object['to'], 'cc' => $object['cc'], 'actor' => $actor, @@ -89,4 +90,21 @@ class Activitypub_create throw new Exception('This is not a supported Object Type for Create Activity.'); } } + + /** + * Verify if received note is private (direct). + * Note that we're conformant with the (yet) non-standard directMessage attribute: + * https://github.com/w3c/activitypub/issues/196#issuecomment-304958984 + * + * @param array $activity received Create-Note activity + * @return bool true if note is private, false otherwise + * @author Bruno casteleiro + */ + public static function isPrivateNote(array $activity): bool { + if (isset($activity['directMessage'])) { + return $activity['directMessage']; + } + + return empty($activity['cc']) && !in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to']); + } } diff --git a/plugins/ActivityPub/lib/models/Activitypub_message.php b/plugins/ActivityPub/lib/models/Activitypub_message.php new file mode 100644 index 0000000000..65dda4ccb2 --- /dev/null +++ b/plugins/ActivityPub/lib/models/Activitypub_message.php @@ -0,0 +1,91 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ + +defined('GNUSOCIAL') || die(); + +/** + * ActivityPub direct note representation + * + * @author Bruno Casteleiro + * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_message +{ + /** + * Generates a pretty message from a Notice object + * + * @param Notice $message + * @return array array to be used in a response + * @author Bruno Casteleiro + */ + public static function message_to_array(Notice $message): array + { + $from = $message->getProfile(); + + $tags = []; + foreach ($message->getTags() as $tag) { + if ($tag != "") { // Hacky workaround to avoid stupid outputs + $tags[] = Activitypub_tag::tag_to_array($tag); + } + } + + $to = []; + foreach ($message->getAttentionProfiles() as $to_profile) { + $to[] = $href = $to_profile->getUri(); + $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST)); + } + + $item = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => common_local_url('showmessage', ['message' => $message->getID()]), + 'type' => 'Note', + 'published' => str_replace(' ', 'T', $message->created).'Z', + 'attributedTo' => ActivityPubPlugin::actor_uri($from), + 'to' => $to, + 'cc' => [], + 'content' => $message->getRendered(), + 'attachment' => [], + 'tag' => $tags + ]; + + return $item; + } + + /** + * Create a private Notice via ActivityPub Note Object. + * Returns created Notice. + * + * @author Bruno Casteleiro + * @param array $object + * @param Profile $actor_profile + * @return Notice + * @throws Exception + */ + public static function create_message(array $object, Profile $actor_profile = null): Notice + { + return Activitypub_notice::create_notice($object, $actor_profile, true); + } +} diff --git a/plugins/ActivityPub/lib/models/Activitypub_notice.php b/plugins/ActivityPub/lib/models/Activitypub_notice.php index 46893be4e5..307354fe6c 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_notice.php +++ b/plugins/ActivityPub/lib/models/Activitypub_notice.php @@ -118,10 +118,11 @@ class Activitypub_notice * @author Diogo Cordeiro * @param array $object * @param Profile $actor_profile + * @param bool $directMessage * @return Notice * @throws Exception */ - public static function create_notice(array $object, Profile $actor_profile = null) + public static function create_notice(array $object, Profile $actor_profile = null, bool $directMessage = false): Notice { $id = $object['id']; // int $url = isset($object['url']) ? $object['url'] : $id; // string @@ -154,6 +155,10 @@ class Activitypub_notice 'url' => $url, 'is_local' => self::getNotePolicyType($object, $actor_profile)]; + if ($directMessage) { + $options['scope'] = Notice::MESSAGE_SCOPE; + } + // Is this a reply? if (isset($settings['inReplyTo'])) { try { @@ -192,7 +197,9 @@ class Activitypub_notice unset($discovery); foreach ($mentions_profiles as $mp) { - $act->context->attention[ActivityPubPlugin::actor_uri($mp)] = 'http://activitystrea.ms/schema/1.0/person'; + if (!$mp->hasBlocked($actor_profile)) { + $act->context->attention[ActivityPubPlugin::actor_uri($mp)] = 'http://activitystrea.ms/schema/1.0/person'; + } } // Add location if that is set @@ -291,20 +298,4 @@ class Activitypub_notice return Notice::GATEWAY; } } - - /** - * Verify if received note is private (direct). - * Note that we're conformant with the (yet) non-standard directMessage attribute: - * https://github.com/w3c/activitypub/issues/196#issuecomment-304958984 - * - * @param array $activity received Create-Note activity - * @return bool true if note is private, false otherwise - */ - public static function isPrivateNote(array $activity): bool { - if (isset($activity['directMessage'])) { - return $activity['directMessage']; - } - - return empty($activity['cc']) && !in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to']); - } } diff --git a/plugins/ActivityPub/lib/postman.php b/plugins/ActivityPub/lib/postman.php index 485c0f995c..c7a2e19e64 100644 --- a/plugins/ActivityPub/lib/postman.php +++ b/plugins/ActivityPub/lib/postman.php @@ -48,12 +48,12 @@ class Activitypub_postman /** * Create a postman to deliver something to someone * - * @param Profile $from Profile of sender - * @param $to + * @param Profile $from sender Profile + * @param array $to receiver Profiles * @throws Exception * @author Diogo Cordeiro */ - public function __construct($from, $to) + public function __construct(Profile $from, array $to) { $this->actor = $from; $discovery = new Activitypub_explorer(); @@ -302,6 +302,37 @@ class Activitypub_postman } } + /** + * Send a Create direct-notification to remote instances + * + * @param Notice $message + * @author Bruno Casteleiro + */ + public function create_direct_note(Notice $message) + { + $data = Activitypub_create::create_to_array( + $this->actor_uri, + Activitypub_message::message_to_array($message), + true + ); + $data = json_encode($data, JSON_UNESCAPED_SLASHES); + + foreach ($this->to_inbox() as $inbox) { + $res = $this->send($data, $inbox); + + // accummulate errors for later use, if needed + if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) { + $res_body = json_decode($res->getBody(), true); + $errors[] = isset($res_body[0]['error']) ? + $res_body[0]['error'] : "An unknown error occurred."; + } + } + + if (!empty($errors)) { + common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!"); + } + } + /** * Send a Announce notification to remote instances * @@ -368,7 +399,7 @@ class Activitypub_postman * @author Diogo Cordeiro * @return array To Inbox URLs */ - private function to_inbox() + private function to_inbox(): array { $to_inboxes = []; foreach ($this->to as $to_profile) {