[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
This commit is contained in:
		| @@ -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. | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -42,15 +42,16 @@ class Activitypub_create | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @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 <brunoccast@fc.up.pt> | ||||
|      */ | ||||
|     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']); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										91
									
								
								plugins/ActivityPub/lib/models/Activitypub_message.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								plugins/ActivityPub/lib/models/Activitypub_message.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| <?php | ||||
| // This file is part of GNU social - https://www.gnu.org/software/social | ||||
| // | ||||
| // GNU social is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // GNU social is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with GNU social.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| /** | ||||
|  * ActivityPub implementation for GNU social | ||||
|  * | ||||
|  * @package   GNUsocial | ||||
|  * @author    Diogo Cordeiro <diogo@fc.up.pt> | ||||
|  * @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 <brunoccast@fc.up.pt> | ||||
|  * @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 <brunoccast@fc.up.pt> | ||||
|      */ | ||||
|     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 <brunoccast@fc.up.pt> | ||||
|      * @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); | ||||
|     } | ||||
| } | ||||
| @@ -118,10 +118,11 @@ class Activitypub_notice | ||||
|      * @author Diogo Cordeiro <diogo@fc.up.pt> | ||||
|      * @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']); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 <diogo@fc.up.pt> | ||||
|      */ | ||||
|     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 <brunoccast@fc.up.pt> | ||||
|      */ | ||||
|     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 <diogo@fc.up.pt> | ||||
|      * @return array To Inbox URLs | ||||
|      */ | ||||
|     private function to_inbox() | ||||
|     private function to_inbox(): array | ||||
|     { | ||||
|         $to_inboxes = []; | ||||
|         foreach ($this->to as $to_profile) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user