]> git.mxchange.org Git - friendica.git/blobdiff - src/Protocol/ActivityPub/Transmitter.php
Merge pull request #7092 from MrPetovan/task/7047-theme-error-page
[friendica.git] / src / Protocol / ActivityPub / Transmitter.php
index 7481db9e740bd4cad511a5c997f6710064f74fcf..a06fbdcf19b532a0cad73877deddc8e05fd4672f 100644 (file)
@@ -5,7 +5,9 @@
 namespace Friendica\Protocol\ActivityPub;
 
 use Friendica\BaseObject;
+use Friendica\Content\Feature;
 use Friendica\Database\DBA;
+use Friendica\Core\Config;
 use Friendica\Core\Logger;
 use Friendica\Core\System;
 use Friendica\Util\HTTPSignature;
@@ -18,6 +20,7 @@ use Friendica\Model\Term;
 use Friendica\Model\User;
 use Friendica\Util\DateTimeFormat;
 use Friendica\Content\Text\BBCode;
+use Friendica\Content\Text\Plaintext;
 use Friendica\Util\JsonLD;
 use Friendica\Util\LDSignature;
 use Friendica\Model\Profile;
@@ -150,7 +153,7 @@ class Transmitter
 
                $condition = ['uid' => 0, 'contact-id' => $public_contact, 'author-id' => $public_contact,
                        'private' => false, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
-                       'deleted' => false, 'visible' => true];
+                       'deleted' => false, 'visible' => true, 'moderated' => false];
                $count = DBA::count('item', $condition);
 
                $data = ['@context' => ActivityPub::CONTEXT];
@@ -184,6 +187,18 @@ class Transmitter
                return $data;
        }
 
+       /**
+        * Return the service array containing information the used software and it's url
+        *
+        * @return array with service data
+        */
+       private static function getService()
+       {
+               return ['type' => 'Service',
+                       'name' =>  FRIENDICA_PLATFORM . " '" . FRIENDICA_CODENAME . "' " . FRIENDICA_VERSION . '-' . DB_UPDATE_VERSION,
+                       'url' => BaseObject::getApp()->getBaseURL()];
+       }
+
        /**
         * Return the ActivityPub profile of the given user
         *
@@ -240,10 +255,29 @@ class Transmitter
                $data['icon'] = ['type' => 'Image',
                        'url' => $contact['avatar']];
 
+               $data['generator'] = self::getService();
+
                // tags: https://kitty.town/@inmysocks/100656097926961126.json
                return $data;
        }
 
+       /**
+        * @param string $username
+        * @return array
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        */
+       public static function getDeletedUser($username)
+       {
+               return [
+                       '@context' => ActivityPub::CONTEXT,
+                       'id' => System::baseUrl() . '/profile/' . $username,
+                       'type' => 'Tombstone',
+                       'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
+                       'updated' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
+                       'deleted' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
+               ];
+       }
+
        /**
         * Returns an array with permissions of a given item array
         *
@@ -301,15 +335,16 @@ class Transmitter
        /**
         * Creates an array of permissions from an item thread
         *
-        * @param array   $item
-        * @param boolean $blindcopy
-        * @param boolean $last_id
+        * @param array   $item       Item array
+        * @param boolean $blindcopy  addressing via "bcc" or "cc"?
+        * @param integer $last_id    Last item id for adding receivers
+        * @param boolean $forum_mode "true" means that we are sending content to a forum
         *
         * @return array with permission data
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0)
+       private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0, $forum_mode = false)
        {
                if ($last_id == 0) {
                        $last_id = $item['id'];
@@ -325,11 +360,13 @@ class Transmitter
                        }
                }
 
-               // Will be activated in a later step
-               // $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
-
-               // For now only send to these contacts:
-               $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
+               if (Config::get('debug', 'total_ap_delivery')) {
+                       // Will be activated in a later step
+                       $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
+               } else {
+                       // For now only send to these contacts:
+                       $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
+               }
 
                $data = ['to' => [], 'cc' => [], 'bcc' => []];
 
@@ -339,7 +376,7 @@ class Transmitter
                        $actor_profile = APContact::getByURL($item['author-link']);
                }
 
-               $terms = Term::tagArrayFromItemId($item['id'], TERM_MENTION);
+               $terms = Term::tagArrayFromItemId($item['id'], [Term::MENTION, Term::IMPLICIT_MENTION]);
 
                if (!$item['private']) {
                        $data = array_merge($data, self::fetchPermissionBlockFromConversation($item));
@@ -395,7 +432,7 @@ class Transmitter
                                                }
                                        } else {
                                                // Public thread parent post always are directed to the followes
-                                               if (!$item['private']) {
+                                               if (!$item['private'] && !$forum_mode) {
                                                        $data['cc'][] = $actor_profile['followers'];
                                                }
                                        }
@@ -459,6 +496,18 @@ class Transmitter
                return $receivers;
        }
 
+       /**
+        * Check if an inbox is archived
+        *
+        * @param string $url Inbox url
+        *
+        * @return boolean "true" if inbox is archived
+        */
+       private static function archivedInbox($url)
+       {
+               return DBA::exists('inbox-status', ['url' => $url, 'archive' => true]);
+       }
+
        /**
         * Fetches a list of inboxes of followers of a given user
         *
@@ -473,11 +522,13 @@ class Transmitter
        {
                $inboxes = [];
 
-               // Will be activated in a later step
-               // $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
-
-               // For now only send to these contacts:
-               $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
+               if (Config::get('debug', 'total_ap_delivery')) {
+                       // Will be activated in a later step
+                       $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
+               } else {
+                       // For now only send to these contacts:
+                       $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
+               }
 
                $condition = ['uid' => $uid, 'network' => $networks, 'archive' => false, 'pending' => false];
 
@@ -498,7 +549,9 @@ class Transmitter
                                } else {
                                        $target = $profile['sharedinbox'];
                                }
-                               $inboxes[$target] = $target;
+                               if (!self::archivedInbox($target)) {
+                                       $inboxes[$target] = $target;
+                               }
                        }
                }
                DBA::close($contacts);
@@ -509,18 +562,18 @@ class Transmitter
        /**
         * Fetches an array of inboxes for the given item and user
         *
-        * @param array   $item
-        * @param integer $uid      User ID
-        * @param boolean $personal fetch personal inboxes
-        * @param integer $last_id Last item id for adding receivers
-        *
+        * @param array   $item       Item array
+        * @param integer $uid        User ID
+        * @param boolean $personal   fetch personal inboxes
+        * @param integer $last_id    Last item id for adding receivers
+        * @param boolean $forum_mode "true" means that we are sending content to a forum
         * @return array with inboxes
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0)
+       public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0, $forum_mode = false)
        {
-               $permissions = self::createPermissionBlockForItem($item, true, $last_id);
+               $permissions = self::createPermissionBlockForItem($item, true, $last_id, $forum_mode);
                if (empty($permissions)) {
                        return [];
                }
@@ -555,7 +608,9 @@ class Transmitter
                                                } else {
                                                        $target = $profile['sharedinbox'];
                                                }
-                                               $inboxes[$target] = $target;
+                                               if (!self::archivedInbox($target)) {
+                                                       $inboxes[$target] = $target;
+                                               }
                                        }
                                }
                        }
@@ -601,6 +656,8 @@ class Transmitter
                        $type = 'Reject';
                } elseif ($item['verb'] == ACTIVITY_ATTENDMAYBE) {
                        $type = 'TentativeAccept';
+               } elseif ($item['verb'] == ACTIVITY_FOLLOW) {
+                       $type = 'Follow';
                } else {
                        $type = '';
                }
@@ -651,7 +708,7 @@ class Transmitter
                        return false;
                }
 
-               if ($item['wall']) {
+               if ($item['wall'] && ($item['uri'] == $item['parent-uri'])) {
                        $owner = User::getOwnerDataById($item['uid']);
                        if (($owner['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) && ($item['author-link'] != $owner['url'])) {
                                $type = 'Announce';
@@ -666,7 +723,7 @@ class Transmitter
                        $condition = ['item-uri' => $item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB];
                        $conversation = DBA::selectFirst('conversation', ['source'], $condition);
                        if (DBA::isResult($conversation)) {
-                               $data = json_decode($conversation['source']);
+                               $data = json_decode($conversation['source'], true);
                                if (!empty($data)) {
                                        return $data;
                                }
@@ -689,11 +746,16 @@ class Transmitter
 
                $data['id'] = $item['uri'] . '#' . $type;
                $data['type'] = $type;
-               $data['actor'] = $item['owner-link'];
+
+               if (Item::isForumPost($item) && ($type != 'Announce')) {
+                       $data['actor'] = $item['author-link'];
+               } else {
+                       $data['actor'] = $item['owner-link'];
+               }
 
                $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM);
 
-               $data['instrument'] = ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()];
+               $data['instrument'] = self::getService();
 
                $data = array_merge($data, self::createPermissionBlockForItem($item, false));
 
@@ -701,6 +763,8 @@ class Transmitter
                        $data['object'] = self::createNote($item);
                } elseif ($data['type'] == 'Announce') {
                        $data = self::createAnnounce($item, $data);
+               } elseif ($data['type'] == 'Follow') {
+                       $data['object'] = $item['parent-uri'];
                } elseif ($data['type'] == 'Undo') {
                        $data['object'] = self::createActivityFromItem($item_id, true);
                } else {
@@ -797,12 +861,12 @@ class Transmitter
        {
                $tags = [];
 
-               $terms = Term::tagArrayFromItemId($item['id']);
+               $terms = Term::tagArrayFromItemId($item['id'], [Term::HASHTAG, Term::MENTION, Term::IMPLICIT_MENTION]);
                foreach ($terms as $term) {
-                       if ($term['type'] == TERM_HASHTAG) {
+                       if ($term['type'] == Term::HASHTAG) {
                                $url = System::baseUrl() . '/search?tag=' . urlencode($term['term']);
                                $tags[] = ['type' => 'Hashtag', 'href' => $url, 'name' => '#' . $term['term']];
-                       } elseif ($term['type'] == TERM_MENTION) {
+                       } elseif ($term['type'] == Term::MENTION || $term['type'] == Term::IMPLICIT_MENTION) {
                                $contact = Contact::getDetailsByURL($term['url']);
                                if (!empty($contact['addr'])) {
                                        $mention = '@' . $contact['addr'];
@@ -882,12 +946,12 @@ class Transmitter
        private static function mentionCallback($match)
        {
                if (empty($match[1])) {
-                       return;
+                       return '';
                }
 
                $data = Contact::getDetailsByURL($match[1]);
-               if (empty($data) || empty($data['nick'])) {
-                       return;
+               if (empty($data['nick'])) {
+                       return $match[0];
                }
 
                return '@[url=' . $data['url'] . ']' . $data['nick'] . '[/url]';
@@ -1004,7 +1068,7 @@ class Transmitter
                        return $data;
                }
 
-               $data['summary'] = null; // Ignore by now
+               $data['summary'] = BBCode::toPlaintext(BBCode::getAbstract($item['body'], Protocol::ACTIVITYPUB));
 
                if ($item['uri'] != $item['thr-parent']) {
                        $data['inReplyTo'] = $item['thr-parent'];
@@ -1028,10 +1092,18 @@ class Transmitter
                        $data['name'] = BBCode::toPlaintext($item['title'], false);
                }
 
+               $permission_block = self::createPermissionBlockForItem($item, false);
+
                $body = $item['body'];
 
+               if (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) {
+                       $body = self::prependMentions($body, $permission_block);
+               }
+
                if ($type == 'Note') {
                        $body = self::removePictures($body);
+               } elseif (($type == 'Article') && empty($data['summary'])) {
+                       $data['summary'] = BBCode::toPlaintext(Plaintext::shorten(self::removePictures($body), 1000));
                }
 
                if ($type == 'Event') {
@@ -1060,7 +1132,7 @@ class Transmitter
                        $data['generator'] = ['type' => 'Application', 'name' => $item['app']];
                }
 
-               $data = array_merge($data, self::createPermissionBlockForItem($item, false));
+               $data = array_merge($data, $permission_block);
 
                return $data;
        }
@@ -1141,7 +1213,7 @@ class Transmitter
                        'actor' => $owner['url'],
                        'object' => $suggestion['url'],
                        'content' => $suggestion['note'],
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [ActivityPub::PUBLIC_COLLECTION],
                        'cc' => []];
 
@@ -1170,7 +1242,7 @@ class Transmitter
                        'actor' => $owner['url'],
                        'object' => $owner['url'],
                        'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [ActivityPub::PUBLIC_COLLECTION],
                        'cc' => []];
 
@@ -1193,13 +1265,23 @@ class Transmitter
        {
                $owner = User::getOwnerDataById($uid);
 
+               if (empty($owner)) {
+                       Logger::error('No owner data found, the deletion message cannot be processed.', ['user' => $uid]);
+                       return false;
+               }
+
+               if (empty($owner['uprvkey'])) {
+                       Logger::error('No private key for owner found, the deletion message cannot be processed.', ['user' => $uid]);
+                       return false;
+               }
+
                $data = ['@context' => ActivityPub::CONTEXT,
                        'id' => System::baseUrl() . '/activity/' . System::createGUID(),
                        'type' => 'Delete',
                        'actor' => $owner['url'],
                        'object' => $owner['url'],
                        'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [ActivityPub::PUBLIC_COLLECTION],
                        'cc' => []];
 
@@ -1230,7 +1312,7 @@ class Transmitter
                        'actor' => $owner['url'],
                        'object' => self::getProfile($uid),
                        'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [$profile['followers']],
                        'cc' => []];
 
@@ -1266,7 +1348,7 @@ class Transmitter
                        'type' => $activity,
                        'actor' => $owner['url'],
                        'object' => $profile['url'],
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [$profile['url']]];
 
                Logger::log('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, Logger::DEBUG);
@@ -1275,6 +1357,55 @@ class Transmitter
                return HTTPSignature::transmit($signed, $profile['inbox'], $uid);
        }
 
+       /**
+        * Transmits a "follow object" activity to a target
+        * This is a preparation for sending automated "follow" requests when receiving "Announce" messages
+        *
+        * @param string  $object Object URL
+        * @param string  $target Target profile
+        * @param integer $uid    User ID
+        * @return bool
+        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        * @throws \ImagickException
+        * @throws \Exception
+        */
+       public static function sendFollowObject($object, $target, $uid = 0)
+       {
+               $profile = APContact::getByURL($target);
+
+               if (empty($uid)) {
+                       // Fetch the list of administrators
+                       $admin_mail = explode(',', str_replace(' ', '', Config::get('config', 'admin_email')));
+
+                       // We need to use some user as a sender. It doesn't care who it will send. We will use an administrator account.
+                       $condition = ['verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false, 'email' => $admin_mail];
+                       $first_user = DBA::selectFirst('user', ['uid'], $condition);
+                       $uid = $first_user['uid'];
+               }
+
+               $condition = ['verb' => ACTIVITY_FOLLOW, 'uid' => 0, 'parent-uri' => $object,
+                       'author-id' => Contact::getPublicIdByUserId($uid)];
+               if (Item::exists($condition)) {
+                       Logger::log('Follow for ' . $object . ' for user ' . $uid . ' does already exist.', Logger::DEBUG);
+                       return false;
+               }
+
+               $owner = User::getOwnerDataById($uid);
+
+               $data = ['@context' => ActivityPub::CONTEXT,
+                       'id' => System::baseUrl() . '/activity/' . System::createGUID(),
+                       'type' => 'Follow',
+                       'actor' => $owner['url'],
+                       'object' => $object,
+                       'instrument' => self::getService(),
+                       'to' => [$profile['url']]];
+
+               Logger::log('Sending follow ' . $object . ' to ' . $target . ' for user ' . $uid, Logger::DEBUG);
+
+               $signed = LDSignature::sign($data, $owner);
+               return HTTPSignature::transmit($signed, $profile['inbox'], $uid);
+       }
+
        /**
         * Transmit a message that the contact request had been accepted
         *
@@ -1296,7 +1427,7 @@ class Transmitter
                        'object' => ['id' => $id, 'type' => 'Follow',
                                'actor' => $profile['url'],
                                'object' => $owner['url']],
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [$profile['url']]];
 
                Logger::log('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG);
@@ -1326,7 +1457,7 @@ class Transmitter
                        'object' => ['id' => $id, 'type' => 'Follow',
                                'actor' => $profile['url'],
                                'object' => $owner['url']],
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [$profile['url']]];
 
                Logger::log('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG);
@@ -1363,7 +1494,7 @@ class Transmitter
                        'object' => ['id' => $object_id, 'type' => 'Follow',
                                'actor' => $owner['url'],
                                'object' => $profile['url']],
-                       'instrument' => ['type' => 'Service', 'name' => BaseObject::getApp()->getUserAgent()],
+                       'instrument' => self::getService(),
                        'to' => [$profile['url']]];
 
                Logger::log('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, Logger::DEBUG);
@@ -1371,4 +1502,28 @@ class Transmitter
                $signed = LDSignature::sign($data, $owner);
                HTTPSignature::transmit($signed, $profile['inbox'], $uid);
        }
+
+       private static function prependMentions($body, array $permission_block)
+       {
+               if (Config::get('system', 'disable_implicit_mentions')) {
+                       return $body;
+               }
+
+               $mentions = [];
+
+               foreach ($permission_block['to'] as $profile_url) {
+                       $profile = Contact::getDetailsByURL($profile_url);
+                       if (!empty($profile['addr'])
+                               && $profile['contact-type'] != Contact::TYPE_COMMUNITY
+                               && !strstr($body, $profile['addr'])
+                               && !strstr($body, $profile_url)
+                       ) {
+                               $mentions[] = '@[url=' . $profile_url . ']' . $profile['nick'] . '[/url]';
+                       }
+               }
+
+               $mentions[] = $body;
+
+               return implode(' ', $mentions);
+       }
 }