X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FTransmitter.php;h=7da110f6716dfd5fb6a2e284e727e19d85e551b8;hb=5f5298125530857f009ca236139853954511a94e;hp=38482d3ec95f83dab1191745305e794b49b824dc;hpb=844ed720b249d729a3f1d41eccf77a76cf48d016;p=friendica.git diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 38482d3ec9..7da110f671 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -1,6 +1,6 @@ Contact::FRIEND], ['id' => $contact['id']]); } @@ -136,7 +137,7 @@ class Transmitter return false; } - $success = self::sendContactUndo($url, $contact['id'], 0); + $success = self::sendContactUndo($url, $contact['id'], User::getSystemAccount()); if ($success || $force) { Contact::update(['rel' => Contact::NOTHING], ['id' => $contact['id']]); @@ -237,90 +238,6 @@ class Transmitter return $data; } - /** - * Public posts for the given owner - * - * @param array $owner Owner array - * @param integer $page Page number - * @param string $requester URL of requesting account - * @param boolean $nocache Wether to bypass caching - * @return array of posts - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function getOutbox(array $owner, int $page = null, string $requester = '', bool $nocache = false): array - { - $condition = ['private' => [Item::PUBLIC, Item::UNLISTED]]; - - if (!empty($requester)) { - $requester_id = Contact::getIdForURL($requester, $owner['uid']); - if (!empty($requester_id)) { - $permissionSets = DI::permissionSet()->selectByContactId($requester_id, $owner['uid']); - if (!empty($permissionSets)) { - $condition = ['psid' => array_merge($permissionSets->column('id'), - [DI::permissionSet()->selectPublicForUser($owner['uid'])])]; - } - } - } - - $condition = array_merge($condition, [ - 'uid' => $owner['uid'], - 'author-id' => Contact::getIdForURL($owner['url'], 0, false), - 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], - 'network' => Protocol::FEDERATED, - 'parent-network' => Protocol::FEDERATED, - 'origin' => true, - 'deleted' => false, - 'visible' => true - ]); - - $apcontact = APContact::getByURL($owner['url']); - - $data = ['@context' => ActivityPub::CONTEXT]; - $data['id'] = DI::baseUrl() . '/outbox/' . $owner['nickname']; - $data['type'] = 'OrderedCollection'; - $data['totalItems'] = $apcontact['statuses_count'] ?? 0; - - if (!empty($page)) { - $data['id'] .= '?' . http_build_query(['page' => $page]); - } - - if (empty($page)) { - $data['first'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=1'; - } else { - $data['type'] = 'OrderedCollectionPage'; - $list = []; - - $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']; - - // Only list "Create" activity objects here, no reshares - if (!empty($activity['object']) && ($activity['type'] == 'Create')) { - $list[] = $activity['object']; - } - } - DBA::close($items); - - if (count($list) == 20) { - $data['next'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1); - } - - // Fix the cached total item count when it is lower than the real count - $total = (($page - 1) * 20) + $data['totalItems']; - if ($total > $data['totalItems']) { - $data['totalItems'] = $total; - } - - $data['partOf'] = DI::baseUrl() . '/outbox/' . $owner['nickname']; - - $data['orderedItems'] = $list; - } - - return $data; - } - /** * Public posts for the given owner * @@ -351,7 +268,7 @@ class Transmitter 'uid' => $owner['uid'], 'author-id' => $owner_cid, 'private' => [Item::PUBLIC, Item::UNLISTED], - 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], + 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT], 'network' => Protocol::FEDERATED, 'parent-network' => Protocol::FEDERATED, 'origin' => true, @@ -380,11 +297,8 @@ class Transmitter while ($item = Post::fetch($items)) { $activity = self::createActivityFromItem($item['id'], true); - $activity['type'] = $activity['type'] == 'Update' ? 'Create' : $activity['type']; - - // Only list "Create" activity objects here, no reshares - if (!empty($activity['object']) && ($activity['type'] == 'Create')) { - $list[] = $activity['object']; + if (!empty($activity)) { + $list[] = $activity; } } DBA::close($items); @@ -411,12 +325,12 @@ class Transmitter * * @return array with service data */ - private static function getService(): array + public static function getService(): array { return [ 'type' => 'Service', - 'name' => FRIENDICA_PLATFORM . " '" . FRIENDICA_CODENAME . "' " . FRIENDICA_VERSION . '-' . DB_UPDATE_VERSION, - 'url' => DI::baseUrl()->get() + 'name' => App::PLATFORM . " '" . App::CODENAME . "' " . App::VERSION . '-' . DB_UPDATE_VERSION, + 'url' => DI::baseUrl() ]; } @@ -485,40 +399,42 @@ class Transmitter 'owner' => $owner['url'], 'publicKeyPem' => $owner['pubkey']]; $data['endpoints'] = ['sharedInbox' => DI::baseUrl() . '/inbox']; - $data['icon'] = ['type' => 'Image', 'url' => User::getAvatarUrl($owner)]; - - $resourceid = Photo::ridFromURI($owner['photo']); - if (!empty($resourceid)) { - $photo = Photo::selectFirst(['type'], ["resource-id" => $resourceid]); - if (!empty($photo['type'])) { - $data['icon']['mediaType'] = $photo['type']; - } - } - - if (!empty($owner['header'])) { - $data['image'] = ['type' => 'Image', 'url' => Contact::getHeaderUrlForId($owner['id'], '', $owner['updated'])]; + if ($uid != 0) { + $data['icon'] = ['type' => 'Image', 'url' => User::getAvatarUrl($owner)]; - $resourceid = Photo::ridFromURI($owner['header']); + $resourceid = Photo::ridFromURI($owner['photo']); if (!empty($resourceid)) { $photo = Photo::selectFirst(['type'], ["resource-id" => $resourceid]); if (!empty($photo['type'])) { - $data['image']['mediaType'] = $photo['type']; + $data['icon']['mediaType'] = $photo['type']; } } - } - $custom_fields = []; + if (!empty($owner['header'])) { + $data['image'] = ['type' => 'Image', 'url' => Contact::getHeaderUrlForId($owner['id'], '', $owner['updated'])]; - foreach (DI::profileField()->selectByContactId(0, $uid) as $profile_field) { - $custom_fields[] = [ - 'type' => 'PropertyValue', - 'name' => $profile_field->label, - 'value' => BBCode::convertForUriId($owner['uri-id'], $profile_field->value) - ]; - }; + $resourceid = Photo::ridFromURI($owner['header']); + if (!empty($resourceid)) { + $photo = Photo::selectFirst(['type'], ["resource-id" => $resourceid]); + if (!empty($photo['type'])) { + $data['image']['mediaType'] = $photo['type']; + } + } + } + + $custom_fields = []; - if (!empty($custom_fields)) { - $data['attachment'] = $custom_fields; + foreach (DI::profileField()->selectByContactId(0, $uid) as $profile_field) { + $custom_fields[] = [ + 'type' => 'PropertyValue', + 'name' => $profile_field->label, + 'value' => BBCode::convertForUriId($owner['uri-id'], $profile_field->value) + ]; + }; + + if (!empty($custom_fields)) { + $data['attachment'] = $custom_fields; + } } $data['generator'] = self::getService(); @@ -527,6 +443,34 @@ class Transmitter return $data; } + /** + * Get a minimal actror array for the C2S API + * + * @param integer $cid + * @return array + */ + private static function getActorArrayByCid(int $cid): array + { + $contact = Contact::getById($cid); + $data = [ + 'id' => $contact['url'], + 'type' => $data['type'] = ActivityPub::ACCOUNT_TYPES[$contact['contact-type']], + 'url' => $contact['alias'], + 'preferredUsername' => $contact['nick'], + 'name' => $contact['name'], + 'icon' => ['type' => 'Image', 'url' => Contact::getAvatarUrlForId($cid, '', $contact['updated'])], + 'image' => ['type' => 'Image', 'url' => Contact::getHeaderUrlForId($cid, '', $contact['updated'])], + 'manuallyApprovesFollowers' => (bool)$contact['manually-approve'], + 'discoverable' => !$contact['unsearchable'], + ]; + + if (empty($data['url'])) { + $data['url'] = $data['id']; + } + + return $data; + } + /** * @param string $username * @return array @@ -577,7 +521,7 @@ class Transmitter $item_profile = APContact::getByURL($item['author-link']); $exclude[] = $item['author-link']; - if ($item['gravity'] == GRAVITY_PARENT) { + if ($item['gravity'] == Item::GRAVITY_PARENT) { $exclude[] = $item['owner-link']; } @@ -655,7 +599,7 @@ class Transmitter $is_forum_thread = false; } - if (self::isAnnounce($item) || DI::config()->get('debug', 'total_ap_delivery') || self::isAPPost($last_id)) { + if (self::isAnnounce($item) || self::isAPPost($last_id)) { // Will be activated in a later step $networks = Protocol::FEDERATED; } else { @@ -665,13 +609,27 @@ class Transmitter $data = ['to' => [], 'cc' => [], 'bcc' => []]; - if ($item['gravity'] == GRAVITY_PARENT) { + if ($item['gravity'] == Item::GRAVITY_PARENT) { $actor_profile = APContact::getByURL($item['owner-link']); } else { $actor_profile = APContact::getByURL($item['author-link']); } $exclusive = false; + $mention = false; + + if ($is_forum_thread) { + foreach (Tag::getByURIId($item['parent-uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]) as $term) { + $profile = APContact::getByURL($term['url'], false); + if (!empty($profile) && ($profile['type'] == 'Group')) { + if ($term['type'] == Tag::EXCLUSIVE_MENTION) { + $exclusive = true; + } elseif ($term['type'] == Tag::MENTION) { + $mention = true; + } + } + } + } $terms = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); @@ -702,6 +660,8 @@ class Transmitter if (!empty($profile['followers']) && ($profile['type'] == 'Group')) { $data['cc'][] = $profile['followers']; } + } elseif (($term['type'] == Tag::MENTION) && ($profile['type'] == 'Group')) { + $mention = true; } $data['to'][] = $profile['url']; } @@ -724,12 +684,18 @@ class Transmitter if (!empty($profile['followers']) && ($profile['type'] == 'Group')) { $data['cc'][] = $profile['followers']; } + } elseif (($term['type'] == Tag::MENTION) && ($profile['type'] == 'Group')) { + $mention = true; } $data['to'][] = $profile['url']; } } } + if ($mention) { + $exclusive = false; + } + if ($is_forum && !$exclusive && !empty($follower)) { $data['cc'][] = $follower; } elseif (!$exclusive) { @@ -753,10 +719,10 @@ class Transmitter if (!empty($item['parent'])) { $parents = Post::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']], ['order' => ['id']]); while ($parent = Post::fetch($parents)) { - if ($parent['gravity'] == GRAVITY_PARENT) { + if ($parent['gravity'] == Item::GRAVITY_PARENT) { $profile = APContact::getByURL($parent['owner-link'], false); if (!empty($profile)) { - if ($item['gravity'] != GRAVITY_PARENT) { + if ($item['gravity'] != Item::GRAVITY_PARENT) { // Comments to forums are directed to the forum // But comments to forums aren't directed to the followers collection // This rule is only valid when the actor isn't the forum. @@ -900,7 +866,7 @@ class Transmitter } } - if (DI::config()->get('debug', 'total_ap_delivery') || $all_ap) { + if ($all_ap) { // Will be activated in a later step $networks = Protocol::FEDERATED; } else { @@ -971,7 +937,7 @@ class Transmitter $inboxes = []; - if ($item['gravity'] == GRAVITY_ACTIVITY) { + if ($item['gravity'] == Item::GRAVITY_ACTIVITY) { $item_profile = APContact::getByURL($item['author-link'], false); } else { $item_profile = APContact::getByURL($item['owner-link'], false); @@ -1060,7 +1026,7 @@ class Transmitter $mail['parent-uri'] = $reply['uri']; $mail['parent-uri-id'] = $reply['uri-id']; $mail['parent-author-id'] = Contact::getIdForURL($reply['from-url'], 0, false); - $mail['gravity'] = ($mail['reply'] ? GRAVITY_COMMENT: GRAVITY_PARENT); + $mail['gravity'] = ($mail['reply'] ? Item::GRAVITY_COMMENT: Item::GRAVITY_PARENT); $mail['event-type'] = ''; $mail['language'] = ''; $mail['parent'] = 0; @@ -1207,36 +1173,75 @@ class Transmitter * * @param integer $item_id * @param boolean $object_mode Is the activity item is used inside another object? + * @param boolean $api_mode "true" if used for the API * @return false|array * @throws \Exception */ - public static function createActivityFromItem(int $item_id, bool $object_mode = false) + public static function createActivityFromItem(int $item_id, bool $object_mode = false, $api_mode = false) { - Logger::info('Fetching activity', ['item' => $item_id]); - $item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]); + $condition = ['id' => $item_id]; + if (!$api_mode) { + $condition['parent-network'] = Protocol::NATIVE_SUPPORT; + } + Logger::info('Fetching activity', $condition); + $item = Post::selectFirst(Item::DELIVER_FIELDLIST, $condition); if (!DBA::isResult($item)) { return false; } + return self::createActivityFromArray($item, $object_mode, $api_mode); + } - if (empty($item['uri-id'])) { - Logger::warning('Item without uri-id', ['item' => $item]); + /** + * Creates an activity array for a given URI-Id and uid + * + * @param integer $uri_id + * @param integer $uid + * @param boolean $object_mode Is the activity item is used inside another object? + * @param boolean $api_mode "true" if used for the API + * @return false|array + * @throws \Exception + */ + public static function createActivityFromUriId(int $uri_id, int $uid, bool $object_mode = false, $api_mode = false) + { + $condition = ['uri-id' => $uri_id, 'uid' => [0, $uid]]; + if (!$api_mode) { + $condition['parent-network'] = Protocol::NATIVE_SUPPORT; + } + Logger::info('Fetching activity', $condition); + $item = Post::selectFirst(Item::DELIVER_FIELDLIST, $condition, ['order' => ['uid' => true]]); + if (!DBA::isResult($item)) { return false; } - if (!$item['deleted']) { + return self::createActivityFromArray($item, $object_mode, $api_mode); + } + + /** + * Creates an activity array for a given item id + * + * @param integer $item_id + * @param boolean $object_mode Is the activity item is used inside another object? + * @param boolean $api_mode "true" if used for the API + * @return false|array + * @throws \Exception + */ + private static function createActivityFromArray(array $item, bool $object_mode = false, $api_mode = false) + { + if (!$api_mode && !$item['deleted'] && $item['network'] == Protocol::ACTIVITYPUB) { $data = Post\Activity::getByURIId($item['uri-id']); if (!$item['origin'] && !empty($data)) { - if ($object_mode) { - unset($data['@context']); - unset($data['signature']); + if (!$object_mode) { + Logger::info('Return stored conversation', ['item' => $item['id']]); + return $data; + } elseif (!empty($data['object'])) { + Logger::info('Return stored conversation object', ['item' => $item['id']]); + return $data['object']; } - Logger::info('Return stored conversation', ['item' => $item_id]); - return $data; } } - if (!$item['origin'] && empty($object)) { - Logger::debug('Post is not ours and is not stored', ['id' => $item_id, 'uri-id' => $item['uri-id']]); + if (!$api_mode && !$item['origin']) { + Logger::debug('Post is not ours and is not stored', ['id' => $item['id'], 'uri-id' => $item['uri-id']]); return false; } @@ -1245,7 +1250,7 @@ class Transmitter if (!$object_mode) { $data = ['@context' => $context ?? ActivityPub::CONTEXT]; - if ($item['deleted'] && ($item['gravity'] == GRAVITY_ACTIVITY)) { + if ($item['deleted'] && ($item['gravity'] == Item::GRAVITY_ACTIVITY)) { $type = 'Undo'; } elseif ($item['deleted']) { $type = 'Delete'; @@ -1256,7 +1261,7 @@ class Transmitter if ($type == 'Delete') { $data['id'] = Item::newURI($item['guid']) . '/' . $type;; - } elseif (($item['gravity'] == GRAVITY_ACTIVITY) && ($type != 'Undo')) { + } elseif (($item['gravity'] == Item::GRAVITY_ACTIVITY) && ($type != 'Undo')) { $data['id'] = $item['uri']; } else { $data['id'] = $item['uri'] . '/' . $type; @@ -1264,10 +1269,18 @@ class Transmitter $data['type'] = $type; - if (($type != 'Announce') || ($item['gravity'] != GRAVITY_PARENT)) { - $data['actor'] = $item['author-link']; + if (($type != 'Announce') || ($item['gravity'] != Item::GRAVITY_PARENT)) { + $link = $item['author-link']; + $id = $item['author-id']; + } else { + $link = $item['owner-link']; + $id = $item['owner-id']; + } + + if ($api_mode) { + $data['actor'] = self::getActorArrayByCid($id); } else { - $data['actor'] = $item['owner-link']; + $data['actor'] = $link; } $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM); @@ -1277,20 +1290,19 @@ class Transmitter $data = array_merge($data, self::createPermissionBlockForItem($item, false)); if (in_array($data['type'], ['Create', 'Update', 'Delete'])) { - $data['object'] = $object ?? self::createNote($item); - $data['published'] = DateTimeFormat::utcNow(DateTimeFormat::ATOM); + $data['object'] = self::createNote($item, $api_mode); } elseif ($data['type'] == 'Add') { $data = self::createAddTag($item, $data); } elseif ($data['type'] == 'Announce') { if ($item['verb'] == ACTIVITY::ANNOUNCE) { $data['object'] = $item['thr-parent']; } else { - $data = self::createAnnounce($item, $data); + $data = self::createAnnounce($item, $data, $api_mode); } } elseif ($data['type'] == 'Follow') { $data['object'] = $item['parent-uri']; } elseif ($data['type'] == 'Undo') { - $data['object'] = self::createActivityFromItem($item_id, true); + $data['object'] = self::createActivityFromItem($item['id'], true); } else { $data['diaspora:guid'] = $item['guid']; if (!empty($item['signed_text'])) { @@ -1305,12 +1317,11 @@ class Transmitter $uid = $item['uid']; } - $owner = User::getOwnerDataById($uid); - - Logger::info('Fetched activity', ['item' => $item_id, 'uid' => $uid]); + Logger::info('Fetched activity', ['item' => $item['id'], 'uid' => $uid]); - // We don't sign if we aren't the actor. This is important for relaying content especially for forums - if (!$object_mode && !empty($owner) && ($data['actor'] == $owner['url'])) { + // We only sign our own activities + if (!$api_mode && !$object_mode && $item['origin']) { + $owner = User::getOwnerDataById($uid); return LDSignature::sign($data, $owner); } else { return $data; @@ -1475,28 +1486,28 @@ class Transmitter */ private static function removePictures(string $body): string { - // Simplify image codes - $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body); - $body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body); - - // Now remove local links - $body = preg_replace_callback( - '/\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]/Usi', - function ($match) { - // We remove the link when it is a link to a local photo page - if (Photo::isLocalPage($match[1])) { - return ''; - } - // otherwise we just return the link - return '[url]' . $match[1] . '[/url]'; - }, - $body - ); - - // Remove all pictures - $body = preg_replace("/\[img\]([^\[\]]*)\[\/img\]/Usi", '', $body); - - return $body; + return BBCode::performWithEscapedTags($body, ['code', 'noparse', 'nobb', 'pre'], function ($text) { + // Simplify image codes + $text = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $text); + $text = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $text); + + // Now remove local links + $text = preg_replace_callback( + '/\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]/Usi', + function ($match) { + // We remove the link when it is a link to a local photo page + if (Photo::isLocalPage($match[1])) { + return ''; + } + // otherwise we just return the link + return '[url]' . $match[1] . '[/url]'; + }, + $text + ); + + // Remove all pictures + return preg_replace("/\[img\]([^\[\]]*)\[\/img\]/Usi", '', $text); + }); } /** @@ -1544,11 +1555,12 @@ class Transmitter * Creates a note/article object array * * @param array $item + * @param bool $api_mode * @return array with the object data * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function createNote(array $item): array + public static function createNote(array $item, bool $api_mode = false): array { if (empty($item)) { return []; @@ -1557,7 +1569,7 @@ class Transmitter // We are treating posts differently when they are directed to a community. // This is done to better support Lemmy. Most of the changes should work with other systems as well. // But to not risk compatibility issues we currently perform the changes only for communities. - if ($item['gravity'] == GRAVITY_PARENT) { + if ($item['gravity'] == Item::GRAVITY_PARENT) { $isCommunityPost = !empty(Tag::getByURIId($item['uri-id'], [Tag::EXCLUSIVE_MENTION])); $links = Post\Media::getByURIId($item['uri-id'], [Post\Media::HTML]); if ($isCommunityPost && (count($links) == 1)) { @@ -1608,7 +1620,11 @@ class Transmitter } $data['url'] = $link ?? $item['plink']; - $data['attributedTo'] = $item['author-link']; + if ($api_mode) { + $data['attributedTo'] = self::getActorArrayByCid($item['author-id']); + } else { + $data['attributedTo'] = $item['author-link']; + } $data['sensitive'] = self::isSensitive($item['uri-id']); if (!empty($item['conversation']) && ($item['conversation'] != './')) { @@ -1621,6 +1637,10 @@ class Transmitter $permission_block = self::createPermissionBlockForItem($item, false); + $real_quote = false; + + $item = Post\Media::addHTMLAttachmentToItem($item); + $body = $item['body']; if ($type == 'Note') { @@ -1635,7 +1655,7 @@ class Transmitter * * } elseif (($type == 'Article') && empty($data['summary'])) { * $regexp = "/[@!]\[url\=([^\[\]]*)\].*?\[\/url\]/ism"; - * $summary = preg_replace_callback($regexp, ['self', 'mentionAddrCallback'], $body); + * $summary = preg_replace_callback($regexp, [self::class, 'mentionAddrCallback'], $body); * $data['summary'] = BBCode::toPlaintext(Plaintext::shorten(self::removePictures($summary), 1000)); * } */ @@ -1657,15 +1677,19 @@ class Transmitter if ($type == 'Page') { // When we transmit "Page" posts we have to remove the attachment. // The attachment contains the link that we already transmit in the "url" field. - $body = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", '', $body); + $body = BBCode::removeAttachment($body); } $body = BBCode::setMentionsToNicknames($body); - $shared = BBCode::fetchShareAttributes($body); - if (!empty($shared['link']) && !empty($shared['guid']) && !empty($shared['comment'])) { - $body = self::replaceSharedData($body); - $data['quoteUrl'] = $shared['link']; + if (!empty($item['quote-uri-id'])) { + if (Post::exists(['uri-id' => $item['quote-uri-id'], 'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN]])) { + $real_quote = true; + $data['quoteUrl'] = $item['quote-uri']; + $body = DI::contentItem()->addShareLink($body, $item['quote-uri-id']); + } else { + $body = DI::contentItem()->addSharedPost($item, $body); + } } $data['content'] = BBCode::convertForUriId($item['uri-id'], $body, BBCode::ACTIVITYPUB); @@ -1677,25 +1701,33 @@ class Transmitter $language = self::getLanguage($item); if (!empty($language)) { $richbody = BBCode::setMentionsToNicknames($item['body'] ?? ''); - - $shared = BBCode::fetchShareAttributes($richbody); - if (!empty($shared['link']) && !empty($shared['guid']) && !empty($shared['comment'])) { - $richbody = self::replaceSharedData($richbody); + $richbody = Post\Media::removeFromEndOfBody($richbody); + if (!empty($item['quote-uri-id'])) { + if ($real_quote) { + $richbody = DI::contentItem()->addShareLink($richbody, $item['quote-uri-id']); + } else { + $richbody = DI::contentItem()->addSharedPost($item, $richbody); + } } - - $richbody = BBCode::removeAttachment($richbody); + $richbody = BBCode::replaceAttachment($richbody); $data['contentMap'][$language] = BBCode::convertForUriId($item['uri-id'], $richbody, BBCode::EXTERNAL); } - $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"]; + if (!empty($item['quote-uri-id'])) { + $source = DI::contentItem()->addSharedPost($item, $item['body']); + } else { + $source = $item['body']; + } + + $data['source'] = ['content' => $source, 'mediaType' => "text/bbcode"]; if (!empty($item['signed_text']) && ($item['uri'] != $item['thr-parent'])) { $data['diaspora:comment'] = $item['signed_text']; } $data['attachment'] = self::createAttachmentList($item); - $data['tag'] = self::createTagList($item, $data['quoteUrl'] ?? ''); + $data['tag'] = self::createTagList($item, $data['quoteUrl'] ?? ''); if (empty($data['location']) && (!empty($item['coord']) || !empty($item['location']))) { $data['location'] = self::createLocation($item); @@ -1710,22 +1742,6 @@ class Transmitter return $data; } - /** - * Replace the share block with a link - * - * @param string $body - * @return string - */ - private static function replaceSharedData(string $body): string - { - return BBCode::convertShare( - $body, - function (array $attributes) { - return '♲ ' . $attributes['link']; - } - ); - } - /** * Fetches the language from the post, the user or the system. * @@ -1782,17 +1798,18 @@ class Transmitter * * @param array $item Item array * @param array $activity activity data + * @param bool $api_mode * @return array with activity data * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function createAnnounce(array $item, array $activity): array + private static function createAnnounce(array $item, array $activity, bool $api_mode = false): array { $orig_body = $item['body']; $announce = self::getAnnounceArray($item); if (empty($announce)) { $activity['type'] = 'Create'; - $activity['object'] = self::createNote($item); + $activity['object'] = self::createNote($item, $api_mode); return $activity; } @@ -1806,7 +1823,7 @@ class Transmitter // Quote $activity['type'] = 'Create'; $item['body'] = $announce['comment'] . "\n" . $announce['object']['plink']; - $activity['object'] = self::createNote($item); + $activity['object'] = self::createNote($item, $api_mode); /// @todo Finally descide how to implement this in AP. This is a possible way: $activity['object']['attachment'][] = self::createNote($announce['object']); @@ -1821,28 +1838,23 @@ class Transmitter * @param array $item * @return array Announcement array */ - public static function getAnnounceArray(array $item): array + private static function getAnnounceArray(array $item): array { - $reshared = Item::getShareArray($item); - if (empty($reshared['guid'])) { + $reshared = DI::contentItem()->getSharedPost($item, Item::DELIVER_FIELDLIST); + if (empty($reshared)) { return []; } - $reshared_item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['guid' => $reshared['guid']]); - if (!DBA::isResult($reshared_item)) { + if (!in_array($reshared['post']['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) { return []; } - if (!in_array($reshared_item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) { - return []; - } - - $profile = APContact::getByURL($reshared_item['author-link'], false); + $profile = APContact::getByURL($reshared['post']['author-link'], false); if (empty($profile)) { return []; } - return ['object' => $reshared_item, 'actor' => $profile, 'comment' => $reshared['comment']]; + return ['object' => $reshared['post'], 'actor' => $profile, 'comment' => $reshared['comment']]; } /** @@ -1887,16 +1899,14 @@ class Transmitter /** * Transmits a contact suggestion to a given inbox * - * @param integer $uid User ID + * @param array $owner Sender owner-view record * @param string $inbox Target inbox * @param integer $suggestion_id Suggestion ID * @return boolean was the transmission successful? - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Exception */ - public static function sendContactSuggestion(int $uid, string $inbox, int $suggestion_id): bool + public static function sendContactSuggestion(array $owner, string $inbox, int $suggestion_id): bool { - $owner = User::getOwnerDataById($uid); - $suggestion = DI::fsuggest()->selectOneById($suggestion_id); $data = [ @@ -1913,22 +1923,20 @@ class Transmitter $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile deletion for user ' . $uid . ' to ' . $inbox . ' via ActivityPub'); - return HTTPSignature::transmit($signed, $inbox, $uid); + Logger::info('Deliver profile deletion for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + return HTTPSignature::transmit($signed, $inbox, $owner); } /** * Transmits a profile relocation to a given inbox * - * @param integer $uid User ID - * @param string $inbox Target inbox + * @param array $owner Sender owner-view record + * @param string $inbox Target inbox * @return boolean was the transmission successful? - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Exception */ - public static function sendProfileRelocation(int $uid, string $inbox): bool + public static function sendProfileRelocation(array $owner, string $inbox): bool { - $owner = User::getOwnerDataById($uid); - $data = [ '@context' => ActivityPub::CONTEXT, 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), @@ -1943,29 +1951,22 @@ class Transmitter $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile relocation for user ' . $uid . ' to ' . $inbox . ' via ActivityPub'); - return HTTPSignature::transmit($signed, $inbox, $uid); + Logger::info('Deliver profile relocation for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + return HTTPSignature::transmit($signed, $inbox, $owner); } /** * Transmits a profile deletion to a given inbox * - * @param integer $uid User ID - * @param string $inbox Target inbox + * @param array $owner Sender owner-view record + * @param string $inbox Target inbox * @return boolean was the transmission successful? - * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Exception */ - public static function sendProfileDeletion(int $uid, string $inbox): bool + public static function sendProfileDeletion(array $owner, string $inbox): bool { - $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]); + Logger::error('No private key for owner found, the deletion message cannot be processed.', ['user' => $owner['uid']]); return false; } @@ -1981,30 +1982,29 @@ class Transmitter $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile deletion for user ' . $uid . ' to ' . $inbox . ' via ActivityPub'); - return HTTPSignature::transmit($signed, $inbox, $uid); + Logger::info('Deliver profile deletion for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + return HTTPSignature::transmit($signed, $inbox, $owner); } /** * Transmits a profile change to a given inbox * - * @param integer $uid User ID - * @param string $inbox Target inbox + * @param array $owner Sender owner-view record + * @param string $inbox Target inbox * @return boolean was the transmission successful? * @throws HTTPException\InternalServerErrorException * @throws HTTPException\NotFoundException * @throws \ImagickException */ - public static function sendProfileUpdate(int $uid, string $inbox): bool + public static function sendProfileUpdate(array $owner, string $inbox): bool { - $owner = User::getOwnerDataById($uid); $profile = APContact::getByURL($owner['url']); $data = ['@context' => ActivityPub::CONTEXT, 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), 'type' => 'Update', 'actor' => $owner['url'], - 'object' => self::getProfile($uid), + 'object' => self::getProfile($owner['uid']), 'published' => DateTimeFormat::utcNow(DateTimeFormat::ATOM), 'instrument' => self::getService(), 'to' => [$profile['followers']], @@ -2012,8 +2012,8 @@ class Transmitter $signed = LDSignature::sign($data, $owner); - Logger::info('Deliver profile update for user ' . $uid . ' to ' . $inbox . ' via ActivityPub'); - return HTTPSignature::transmit($signed, $inbox, $uid); + Logger::info('Deliver profile update for user ' . $owner['uid'] . ' to ' . $inbox . ' via ActivityPub'); + return HTTPSignature::transmit($signed, $inbox, $owner); } /** @@ -2037,6 +2037,10 @@ class Transmitter } $owner = User::getOwnerDataById($uid); + if (empty($owner)) { + Logger::warning('No user found for actor, aborting', ['uid' => $uid]); + return false; + } if (empty($id)) { $id = DI::baseUrl() . '/activity/' . System::createGUID(); @@ -2055,7 +2059,7 @@ class Transmitter Logger::info('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid); $signed = LDSignature::sign($data, $owner); - return HTTPSignature::transmit($signed, $profile['inbox'], $uid); + return HTTPSignature::transmit($signed, $profile['inbox'], $owner); } /** @@ -2079,13 +2083,14 @@ class Transmitter } if (empty($uid)) { - // Fetch the list of administrators - $admin_mail = explode(',', str_replace(' ', '', DI::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']; + $admin = User::getFirstAdmin(['uid']); + if (!$admin) { + Logger::warning('No available admin user for transmission', ['target' => $target]); + return false; + } + + $uid = $admin['uid']; } $condition = ['verb' => Activity::FOLLOW, 'uid' => 0, 'parent-uri' => $object, @@ -2110,7 +2115,7 @@ class Transmitter Logger::info('Sending follow ' . $object . ' to ' . $target . ' for user ' . $uid); $signed = LDSignature::sign($data, $owner); - return HTTPSignature::transmit($signed, $profile['inbox'], $uid); + return HTTPSignature::transmit($signed, $profile['inbox'], $owner); } /** @@ -2132,6 +2137,11 @@ class Transmitter } $owner = User::getOwnerDataById($uid); + if (!$owner) { + Logger::notice('No user found for actor', ['uid' => $uid]); + return; + } + $data = [ '@context' => ActivityPub::CONTEXT, 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), @@ -2150,7 +2160,7 @@ class Transmitter Logger::debug('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id); $signed = LDSignature::sign($data, $owner); - HTTPSignature::transmit($signed, $profile['inbox'], $uid); + HTTPSignature::transmit($signed, $profile['inbox'], $owner); } /** @@ -2158,12 +2168,12 @@ class Transmitter * * @param string $target Target profile * @param string $objectId Object id - * @param int $uid User ID + * @param array $owner Sender owner-view record * @return bool Operation success * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function sendContactReject(string $target, string $objectId, int $uid): bool + public static function sendContactReject(string $target, string $objectId, array $owner): bool { $profile = APContact::getByURL($target); if (empty($profile['inbox'])) { @@ -2171,7 +2181,6 @@ class Transmitter return false; } - $owner = User::getOwnerDataById($uid); $data = [ '@context' => ActivityPub::CONTEXT, 'id' => DI::baseUrl() . '/activity/' . System::createGUID(), @@ -2187,10 +2196,10 @@ class Transmitter 'to' => [$profile['url']], ]; - Logger::debug('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $objectId); + Logger::debug('Sending reject to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId); $signed = LDSignature::sign($data, $owner); - return HTTPSignature::transmit($signed, $profile['inbox'], $uid); + return HTTPSignature::transmit($signed, $profile['inbox'], $owner); } /** @@ -2198,13 +2207,13 @@ class Transmitter * * @param string $target Target profile * @param integer $cid Contact id - * @param integer $uid User ID + * @param array $owner Sender owner-view record * @return bool success * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException * @throws \Exception */ - public static function sendContactUndo(string $target, int $cid, int $uid): bool + public static function sendContactUndo(string $target, int $cid, array $owner): bool { $profile = APContact::getByURL($target); if (empty($profile['inbox'])) { @@ -2219,7 +2228,6 @@ class Transmitter $objectId = DI::baseUrl() . '/activity/' . System::createGUID(); - $owner = User::getOwnerDataById($uid); $data = [ '@context' => ActivityPub::CONTEXT, 'id' => $objectId, @@ -2235,10 +2243,10 @@ class Transmitter 'to' => [$profile['url']], ]; - Logger::info('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $objectId); + Logger::info('Sending undo to ' . $target . ' for user ' . $owner['uid'] . ' with id ' . $objectId); $signed = LDSignature::sign($data, $owner); - return HTTPSignature::transmit($signed, $profile['inbox'], $uid); + return HTTPSignature::transmit($signed, $profile['inbox'], $owner); } /**