X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FTransmitter.php;h=7da110f6716dfd5fb6a2e284e727e19d85e551b8;hb=5f5298125530857f009ca236139853954511a94e;hp=9a68bc33512efbd69a485e5e077dc166f9b55bf7;hpb=9825f12b16c4f4ccf9c1ddca234e693810e054e3;p=friendica.git diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 9a68bc3351..7da110f671 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -1,6 +1,6 @@ Contact::FRIEND], ['id' => $contact['id']]); } @@ -239,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' => [Item::GRAVITY_PARENT, Item::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 * @@ -382,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); @@ -413,12 +325,12 @@ class Transmitter * * @return array with service data */ - private static function getService(): array + public static function getService(): array { return [ 'type' => 'Service', 'name' => App::PLATFORM . " '" . App::CODENAME . "' " . App::VERSION . '-' . DB_UPDATE_VERSION, - 'url' => DI::baseUrl()->get() + 'url' => DI::baseUrl() ]; } @@ -487,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']; + } + } + + if (!empty($owner['header'])) { + $data['image'] = ['type' => 'Image', 'url' => Contact::getHeaderUrlForId($owner['id'], '', $owner['updated'])]; + + $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 = []; + $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) - ]; - }; + 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; + if (!empty($custom_fields)) { + $data['attachment'] = $custom_fields; + } } $data['generator'] = self::getService(); @@ -529,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 @@ -1231,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; } @@ -1289,9 +1270,17 @@ class Transmitter $data['type'] = $type; if (($type != 'Announce') || ($item['gravity'] != Item::GRAVITY_PARENT)) { - $data['actor'] = $item['author-link']; + $link = $item['author-link']; + $id = $item['author-id']; } else { - $data['actor'] = $item['owner-link']; + $link = $item['owner-link']; + $id = $item['owner-id']; + } + + if ($api_mode) { + $data['actor'] = self::getActorArrayByCid($id); + } else { + $data['actor'] = $link; } $data['published'] = DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM); @@ -1301,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'])) { @@ -1329,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; @@ -1568,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 []; @@ -1632,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'] != './')) { @@ -1647,6 +1639,8 @@ class Transmitter $real_quote = false; + $item = Post\Media::addHTMLAttachmentToItem($item); + $body = $item['body']; if ($type == 'Note') { @@ -1661,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)); * } */ @@ -1683,7 +1677,7 @@ 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); @@ -1707,6 +1701,7 @@ class Transmitter $language = self::getLanguage($item); if (!empty($language)) { $richbody = BBCode::setMentionsToNicknames($item['body'] ?? ''); + $richbody = Post\Media::removeFromEndOfBody($richbody); if (!empty($item['quote-uri-id'])) { if ($real_quote) { $richbody = DI::contentItem()->addShareLink($richbody, $item['quote-uri-id']); @@ -1714,7 +1709,7 @@ class Transmitter $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); } @@ -1803,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; } @@ -1827,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']);