]> git.mxchange.org Git - friendica.git/blobdiff - src/Protocol/ActivityPub/Transmitter.php
Merge pull request #10230 from annando/oauth-login
[friendica.git] / src / Protocol / ActivityPub / Transmitter.php
index 531a23a69bb8ea71c5ce3052dcb3619f6f505731..9773a5a48bbd1aa43ee7b511d2aefe6d263e1fbf 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @copyright Copyright (C) 2020, Friendica
+ * @copyright Copyright (C) 2010-2021, the Friendica project
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -33,6 +33,7 @@ use Friendica\DI;
 use Friendica\Model\APContact;
 use Friendica\Model\Contact;
 use Friendica\Model\Conversation;
+use Friendica\Model\GServer;
 use Friendica\Model\Item;
 use Friendica\Model\ItemURI;
 use Friendica\Model\Profile;
@@ -51,9 +52,6 @@ use Friendica\Util\Map;
 use Friendica\Util\Network;
 use Friendica\Util\XML;
 
-require_once 'include/api.php';
-require_once 'mod/share.php';
-
 /**
  * ActivityPub Transmitter Protocol class
  *
@@ -71,10 +69,10 @@ class Transmitter
        public static function addRelayServerInboxes(array $inboxes = [])
        {
                $contacts = DBA::select('apcontact', ['inbox'],
-                       ["`type` = ? AND `url` IN (SELECT `url` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))",
-                               'Application', 0, Contact::FOLLOWER, Contact::FRIEND]);
+                       ["`type` = ? AND `url` IN (SELECT `url` FROM `contact` WHERE `uid` = ? AND `rel` = ?)",
+                               'Application', 0, Contact::FRIEND]);
                while ($contact = DBA::fetch($contacts)) {
-                       $inboxes[] = $contact['inbox'];
+                       $inboxes[$contact['inbox']] = $contact['inbox'];
                }
                DBA::close($contacts);
 
@@ -89,15 +87,19 @@ class Transmitter
         */
        public static function addRelayServerInboxesForItem(int $item_id, array $inboxes = [])
        {
+               $item = Post::selectFirst(['uid'], ['id' => $item_id]);
+               if (empty($item)) {
+                       return $inboxes;
+               }
+
                $relays = Relay::getList($item_id, [], [Protocol::ACTIVITYPUB]);
                if (empty($relays)) {
                        return $inboxes;
                }
 
                foreach ($relays as $relay) {
-                       if (!in_array($relay['batch'], $inboxes)) {
-                               $inboxes[] = $relay['batch'];
-                       }
+                       $contact = Contact::getByURLForUser($relay['url'], $item['uid'], false, ['id']);
+                       $inboxes[$relay['batch']][] = $contact['id'] ?? 0;
                }
                return $inboxes;
        }
@@ -118,8 +120,7 @@ class Transmitter
                $activity_id = ActivityPub\Transmitter::activityIDFromContact($contact['id']);
                $success = ActivityPub\Transmitter::sendActivity('Follow', $url, 0, $activity_id);
                if ($success) {
-                       $rel = $contact['rel'] == Contact::SHARING ? Contact::FRIEND : Contact::FOLLOWER;
-                       DBA::update('contact', ['rel' => $rel], ['id' => $contact['id']]);
+                       DBA::update('contact', ['rel' => Contact::FRIEND], ['id' => $contact['id']]);
                }
 
                return $success;
@@ -141,8 +142,7 @@ class Transmitter
 
                $success = self::sendContactUndo($url, $contact['id'], 0);
                if ($success || $force) {
-                       $rel = $contact['rel'] == Contact::FRIEND ? Contact::SHARING : Contact::NOTHING;
-                       DBA::update('contact', ['rel' => $rel], ['id' => $contact['id']]);
+                       DBA::update('contact', ['rel' => Contact::NOTHING], ['id' => $contact['id']]);
                }
 
                return $success;
@@ -258,9 +258,9 @@ class Transmitter
                $condition = array_merge($condition,
                        ['author-id' => $public_contact,
                        'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
-                       'deleted' => false, 'visible' => true, 'moderated' => false]);
+                       'deleted' => false, 'visible' => true]);
 
-               $count = DBA::count('item', $condition);
+               $count = Post::count($condition);
 
                $data = ['@context' => ActivityPub::CONTEXT];
                $data['id'] = DI::baseUrl() . '/outbox/' . $owner['nickname'];
@@ -275,8 +275,8 @@ class Transmitter
 
                        $condition['parent-network'] = Protocol::NATIVE_SUPPORT;
 
-                       $items = Item::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]);
-                       while ($item = Item::fetch($items)) {
+                       $items = Post::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]);
+                       while ($item = Post::fetch($items)) {
                                $activity = self::createActivityFromItem($item['id'], true);
                                $activity['type'] = $activity['type'] == 'Update' ? 'Create' : $activity['type'];
 
@@ -285,6 +285,7 @@ class Transmitter
                                        $list[] = $activity['object'];
                                }
                        }
+                       DBA::close($items);
 
                        if (!empty($list)) {
                                $data['next'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1);
@@ -432,7 +433,12 @@ class Transmitter
                $activity = json_decode($conversation['source'], true);
 
                $actor = JsonLD::fetchElement($activity, 'actor', 'id');
-               $profile = APContact::getByURL($actor);
+               if (!empty($actor)) {
+                       $permissions['to'][] = $actor;
+                       $profile = APContact::getByURL($actor);
+               } else {
+                       $profile = [];
+               }
 
                $item_profile = APContact::getByURL($item['author-link']);
                $exclude[] = $item['author-link'];
@@ -441,8 +447,6 @@ class Transmitter
                        $exclude[] = $item['owner-link'];
                }
 
-               $permissions['to'][] = $actor;
-
                foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
                        if (empty($activity[$element])) {
                                continue;
@@ -466,6 +470,21 @@ class Transmitter
                return $permissions;
        }
 
+       /**
+        * Check if the given item id is from ActivityPub
+        *
+        * @param integer $item_id
+        * @return boolean "true" if the post is from ActivityPub
+        */
+       private static function isAPPost(int $item_id)
+       {
+               if (empty($item_id)) {
+                       return false;
+               }
+
+               return Post::exists(['id' => $item_id, 'network' => Protocol::ACTIVITYPUB]);
+       }
+
        /**
         * Creates an array of permissions from an item thread
         *
@@ -498,7 +517,7 @@ class Transmitter
                        $always_bcc = true;
                }
 
-               if (self::isAnnounce($item) || DI::config()->get('debug', 'total_ap_delivery')) {
+               if (self::isAnnounce($item) || DI::config()->get('debug', 'total_ap_delivery') || self::isAPPost($last_id)) {
                        // Will be activated in a later step
                        $networks = Protocol::FEDERATED;
                } else {
@@ -547,8 +566,8 @@ class Transmitter
                        foreach ($terms as $term) {
                                $cid = Contact::getIdForURL($term['url'], $item['uid']);
                                if (!empty($cid) && in_array($cid, $receiver_list)) {
-                                       $contact = DBA::selectFirst('contact', ['url', 'network', 'protocol'], ['id' => $cid]);
-                                       if (!DBA::isResult($contact) || (!in_array($contact['network'], $networks) && ($contact['protocol'] != Protocol::ACTIVITYPUB))) {
+                                       $contact = DBA::selectFirst('contact', ['url', 'network', 'protocol', 'gsid'], ['id' => $cid, 'network' => Protocol::FEDERATED]);
+                                       if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
                                                continue;
                                        }
 
@@ -559,8 +578,8 @@ class Transmitter
                        }
 
                        foreach ($receiver_list as $receiver) {
-                               $contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol'], ['id' => $receiver]);
-                               if (!DBA::isResult($contact) || (!in_array($contact['network'], $networks) && ($contact['protocol'] != Protocol::ACTIVITYPUB))) {
+                               $contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]);
+                               if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) {
                                        continue;
                                }
 
@@ -575,8 +594,8 @@ class Transmitter
                }
 
                if (!empty($item['parent'])) {
-                       $parents = Item::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']]);
-                       while ($parent = Item::fetch($parents)) {
+                       $parents = Post::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']]);
+                       while ($parent = Post::fetch($parents)) {
                                if ($parent['gravity'] == GRAVITY_PARENT) {
                                        $profile = APContact::getByURL($parent['owner-link'], false);
                                        if (!empty($profile)) {
@@ -667,22 +686,40 @@ class Transmitter
         *
         * @return boolean "true" if inbox is archived
         */
-       private static function archivedInbox($url)
+       public static function archivedInbox($url)
        {
                return DBA::exists('inbox-status', ['url' => $url, 'archive' => true]);
        }
 
+       /**
+        * Check if a given contact should be delivered via AP
+        *
+        * @param array $contact 
+        * @param array $networks 
+        * @return bool 
+        * @throws Exception 
+        */
+       private static function isAPContact(array $contact, array $networks)
+       {
+               if (in_array($contact['network'], $networks) || ($contact['protocol'] == Protocol::ACTIVITYPUB)) {
+                       return true;
+               }
+
+               return GServer::getProtocol($contact['gsid'] ?? 0) == Post\DeliveryData::ACTIVITYPUB;
+       }
+
        /**
         * Fetches a list of inboxes of followers of a given user
         *
         * @param integer $uid      User ID
         * @param boolean $personal fetch personal inboxes
+        * @param boolean $all_ap   Retrieve all AP enabled inboxes
         *
         * @return array of follower inboxes
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function fetchTargetInboxesforUser($uid, $personal = false)
+       public static function fetchTargetInboxesforUser($uid, $personal = false, bool $all_ap = false)
        {
                $inboxes = [];
 
@@ -695,7 +732,7 @@ class Transmitter
                        }
                }
 
-               if (DI::config()->get('debug', 'total_ap_delivery')) {
+               if (DI::config()->get('debug', 'total_ap_delivery') || $all_ap) {
                        // Will be activated in a later step
                        $networks = Protocol::FEDERATED;
                } else {
@@ -703,19 +740,19 @@ class Transmitter
                        $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
                }
 
-               $condition = ['uid' => $uid, 'archive' => false, 'pending' => false, 'blocked' => false];
+               $condition = ['uid' => $uid, 'archive' => false, 'pending' => false, 'blocked' => false, 'network' => Protocol::FEDERATED];
 
                if (!empty($uid)) {
                        $condition['rel'] = [Contact::FOLLOWER, Contact::FRIEND];
                }
 
-               $contacts = DBA::select('contact', ['url', 'network', 'protocol'], $condition);
+               $contacts = DBA::select('contact', ['id', 'url', 'network', 'protocol', 'gsid'], $condition);
                while ($contact = DBA::fetch($contacts)) {
                        if (Contact::isLocal($contact['url'])) {
                                continue;
                        }
 
-                       if (!in_array($contact['network'], $networks) && ($contact['protocol'] != Protocol::ACTIVITYPUB)) {
+                       if (!self::isAPContact($contact, $networks)) {
                                continue;
                        }
 
@@ -735,7 +772,7 @@ class Transmitter
                                        $target = $profile['sharedinbox'];
                                }
                                if (!self::archivedInbox($target)) {
-                                       $inboxes[$target] = $target;
+                                       $inboxes[$target][] = $contact['id'];
                                }
                        }
                }
@@ -790,7 +827,7 @@ class Transmitter
                                }
 
                                if ($item_profile && ($receiver == $item_profile['followers']) && ($uid == $profile_uid)) {
-                                       $inboxes = array_merge($inboxes, self::fetchTargetInboxesforUser($uid, $personal));
+                                       $inboxes = array_merge($inboxes, self::fetchTargetInboxesforUser($uid, $personal, self::isAPPost($last_id)));
                                } else {
                                        if (Contact::isLocal($receiver)) {
                                                continue;
@@ -798,13 +835,15 @@ class Transmitter
 
                                        $profile = APContact::getByURL($receiver, false);
                                        if (!empty($profile)) {
+                                               $contact = Contact::getByURLForUser($receiver, $uid, false, ['id']);
+
                                                if (empty($profile['sharedinbox']) || $personal || $blindcopy) {
                                                        $target = $profile['inbox'];
                                                } else {
                                                        $target = $profile['sharedinbox'];
                                                }
                                                if (!self::archivedInbox($target)) {
-                                                       $inboxes[$target] = $target;
+                                                       $inboxes[$target][] = $contact['id'] ?? 0;
                                                }
                                        }
                                }
@@ -1000,25 +1039,25 @@ class Transmitter
         * @param integer $item_id
         * @param boolean $object_mode Is the activity item is used inside another object?
         *
-        * @return array of activity
+        * @return false|array
         * @throws \Exception
         */
-       public static function createActivityFromItem($item_id, $object_mode = false)
+       public static function createActivityFromItem(int $item_id, bool $object_mode = false)
        {
                Logger::info('Fetching activity', ['item' => $item_id]);
-               $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
+               $item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]);
                if (!DBA::isResult($item)) {
                        return false;
                }
 
                // In case of a forum post ensure to return the original post if author and forum are on the same machine
-               if (!empty($item['forum_mode'])) {
+               if (($item['gravity'] == GRAVITY_PARENT) && !empty($item['forum_mode'])) {
                        $author = Contact::getById($item['author-id'], ['nurl']);
                        if (!empty($author['nurl'])) {
                                $self = Contact::selectFirst(['uid'], ['nurl' => $author['nurl'], 'self' => true]);
                                if (!empty($self['uid'])) {
-                                       $forum_item = Item::selectFirst([], ['uri-id' => $item['uri-id'], 'uid' => $self['uid']]);
-                                       if (DBA::isResult($item)) {
+                                       $forum_item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri-id' => $item['uri-id'], 'uid' => $self['uid']]);
+                                       if (DBA::isResult($forum_item)) {
                                                $item = $forum_item; 
                                        }
                                }
@@ -1030,10 +1069,10 @@ class Transmitter
                        return false;
                }
 
-               if (empty($type)) {
+               if (!$item['deleted']) {
                        $condition = ['item-uri' => $item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
                        $conversation = DBA::selectFirst('conversation', ['source'], $condition);
-                       if (DBA::isResult($conversation)) {
+                       if (!$item['origin'] && DBA::isResult($conversation)) {
                                $data = json_decode($conversation['source'], true);
                                if (!empty($data['type'])) {
                                        if (in_array($data['type'], ['Create', 'Update'])) {
@@ -1053,10 +1092,10 @@ class Transmitter
                                        }
                                }
                        }
-
-                       $type = self::getTypeOfItem($item);
                }
 
+               $type = self::getTypeOfItem($item);
+
                if (!$object_mode) {
                        $data = ['@context' => $context ?? ActivityPub::CONTEXT];
 
@@ -1069,7 +1108,9 @@ class Transmitter
                        $data = [];
                }
 
-               if (($item['gravity'] == GRAVITY_ACTIVITY) && ($type != 'Undo')) {
+               if ($type == 'Delete') {
+                       $data['id'] = Item::newURI($item['uid'], $item['guid']) . '/' . $type;;
+               } elseif (($item['gravity'] == GRAVITY_ACTIVITY) && ($type != 'Undo')) {
                        $data['id'] = $item['uri'];
                } else {
                        $data['id'] = $item['uri'] . '/' . $type;
@@ -1219,53 +1260,60 @@ class Transmitter
        {
                $attachments = [];
 
-               // Currently deactivated, since it creates side effects on Mastodon and Pleroma.
-               // It will be reactivated, once this cleared.
-               /*
-               $attach_data = BBCode::getAttachmentData($item['body']);
-               if (!empty($attach_data['url'])) {
-                       $attachment = ['type' => 'Page',
-                               'mediaType' => 'text/html',
-                               'url' => $attach_data['url']];
-
-                       if (!empty($attach_data['title'])) {
-                               $attachment['name'] = $attach_data['title'];
-                       }
-
-                       if (!empty($attach_data['description'])) {
-                               $attachment['summary'] = $attach_data['description'];
+               $uriids = [$item['uri-id']];
+               $shared = BBCode::fetchShareAttributes($item['body']);
+               if (!empty($shared['guid'])) {
+                       $shared_item = Post::selectFirst(['uri-id'], ['guid' => $shared['guid']]);
+                       if (!empty($shared_item['uri-id'])) {
+                               $uriids[] = $shared_item['uri-id'];
                        }
+               }
 
-                       if (!empty($attach_data['image'])) {
-                               $imgdata = Images::getInfoFromURLCached($attach_data['image']);
-                               if ($imgdata) {
-                                       $attachment['icon'] = ['type' => 'Image',
-                                               'mediaType' => $imgdata['mime'],
-                                               'width' => $imgdata[0],
-                                               'height' => $imgdata[1],
-                                               'url' => $attach_data['image']];
+               $urls = [];
+               foreach ($uriids as $uriid) {
+                       foreach (Post\Media::getByURIId($uriid, [Post\Media::DOCUMENT, Post\Media::TORRENT]) as $attachment) {
+                               if (in_array($attachment['url'], $urls)) {
+                                       continue;
                                }
-                       }
+                               $urls[] = $attachment['url'];
 
-                       $attachments[] = $attachment;
-               }
-               */
-               foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT, Post\Media::UNKNOWN]) as $attachment) {
-                       $attachments[] = ['type' => 'Document',
-                               'mediaType' => $attachment['mimetype'],
-                               'url' => $attachment['url'],
-                               'name' => $attachment['description']];
+                               $attachments[] = ['type' => 'Document',
+                                       'mediaType' => $attachment['mimetype'],
+                                       'url' => $attachment['url'],
+                                       'name' => $attachment['description']];
+                       }
                }
 
                if ($type != 'Note') {
                        return $attachments;
                }
 
-               foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]) as $attachment) {
-                       $attachments[] = ['type' => 'Document',
-                               'mediaType' => $attachment['mimetype'],
-                               'url' => $attachment['url'],
-                               'name' => $attachment['description']];
+               foreach ($uriids as $uriid) {
+                       foreach (Post\Media::getByURIId($uriid, [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]) as $attachment) {
+                               if (in_array($attachment['url'], $urls)) {
+                                       continue;
+                               }
+                               $urls[] = $attachment['url'];
+
+                               $attachments[] = ['type' => 'Document',
+                                       'mediaType' => $attachment['mimetype'],
+                                       'url' => $attachment['url'],
+                                       'name' => $attachment['description']];
+                       }
+                       // Currently deactivated, since it creates side effects on Mastodon and Pleroma.
+                       // It will be activated, once this cleared.
+                       /*
+                       foreach (Post\Media::getByURIId($uriid, [Post\Media::HTML]) as $attachment) {
+                               if (in_array($attachment['url'], $urls)) {
+                                       continue;
+                               }
+                               $urls[] = $attachment['url'];
+
+                               $attachments[] = ['type' => 'Page',
+                                       'mediaType' => $attachment['mimetype'],
+                                       'url' => $attachment['url'],
+                                       'name' => $attachment['description']];
+                       }*/
                }
 
                return $attachments;
@@ -1289,7 +1337,28 @@ class Transmitter
                        return $match[0];
                }
 
-               return '[url=' . ($data['alias'] ?: $data['url']) . ']@' . $data['nick'] . '[/url]';
+               return '[url=' . $data['url'] . ']@' . $data['nick'] . '[/url]';
+       }
+
+       /**
+        * Callback function to replace a Friendica style mention in a mention for a summary
+        *
+        * @param array $match Matching values for the callback
+        * @return string Replaced mention
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        */
+       private static function mentionAddrCallback($match)
+       {
+               if (empty($match[1])) {
+                       return '';
+               }
+
+               $data = Contact::getByURL($match[1], false, ['addr']);
+               if (empty($data['addr'])) {
+                       return $match[0];
+               }
+
+               return '@' . $data['addr'];
        }
 
        /**
@@ -1367,7 +1436,7 @@ class Transmitter
         * @return array with the event data
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
-       public static function createEvent($item)
+       private static function createEvent($item)
        {
                $event = [];
                $event['name'] = $item['event-summary'];
@@ -1383,6 +1452,8 @@ class Transmitter
                        $event['location'] = self::createLocation($item);
                }
 
+               $event['dfrn:adjust'] = (bool)$item['event-adjust'];
+
                return $event;
        }
 
@@ -1452,7 +1523,9 @@ class Transmitter
                if ($type == 'Note') {
                        $body = $item['raw-body'] ?? self::removePictures($body);
                } elseif (($type == 'Article') && empty($data['summary'])) {
-                       $data['summary'] = BBCode::toPlaintext(Plaintext::shorten(self::removePictures($body), 1000));
+                       $regexp = "/[@!]\[url\=([^\[\]]*)\].*?\[\/url\]/ism";
+                       $summary = preg_replace_callback($regexp, ['self', 'mentionAddrCallback'], $body);
+                       $data['summary'] = BBCode::toPlaintext(Plaintext::shorten(self::removePictures($summary), 1000));
                }
 
                if (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) {
@@ -1477,7 +1550,7 @@ class Transmitter
                        $richbody = preg_replace_callback($regexp, ['self', 'mentionCallback'], $item['body']);
                        $richbody = BBCode::removeAttachment($richbody);
 
-                       $data['contentMap'][$language] = BBCode::convert($richbody, false);
+                       $data['contentMap'][$language] = BBCode::convert($richbody, false, BBCode::EXTERNAL);
                }
 
                $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"];
@@ -1608,7 +1681,7 @@ class Transmitter
                        return [];
                }
 
-               $reshared_item = Item::selectFirst([], ['guid' => $reshared['guid']]);
+               $reshared_item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['guid' => $reshared['guid']]);
                if (!DBA::isResult($reshared_item)) {
                        return [];
                }
@@ -1867,7 +1940,7 @@ class Transmitter
 
                $condition = ['verb' => Activity::FOLLOW, 'uid' => 0, 'parent-uri' => $object,
                        'author-id' => Contact::getPublicIdByUserId($uid)];
-               if (Item::exists($condition)) {
+               if (Post::exists($condition)) {
                        Logger::log('Follow for ' . $object . ' for user ' . $uid . ' does already exist.', Logger::DEBUG);
                        return false;
                }