use Friendica\Protocol\Relay;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\HTTPSignature;
-use Friendica\Util\JsonLD;
use Friendica\Util\LDSignature;
use Friendica\Util\Map;
use Friendica\Util\Network;
*/
class Transmitter
{
+ const CACHEKEY_FEATURED = 'transmitter:getFeatured:';
+ const CACHEKEY_CONTACTS = 'transmitter:getContacts:';
+ const CACHEKEY_OUTBOX = 'transmitter:getOutbox:';
+
/**
* Add relay servers to the list of inboxes
*
/**
* Collects a list of contacts of the given owner
*
- * @param array $owner Owner array
- * @param int|array $rel The relevant value(s) contact.rel should match
- * @param string $module The name of the relevant AP endpoint module (followers|following)
- * @param integer $page Page number
- * @param string $requester URL of the requester
+ * @param array $owner Owner array
+ * @param array $rel The relevant value(s) contact.rel should match
+ * @param string $module The name of the relevant AP endpoint module (followers|following)
+ * @param integer $page Page number
+ * @param string $requester URL of the requester
+ * @param boolean $nocache Wether to bypass caching
*
* @return array of owners
* @throws \Exception
*/
- public static function getContacts($owner, $rel, $module, $page = null, string $requester = null)
+ public static function getContacts(array $owner, array $rel, string $module, int $page = null, string $requester = null, $nocache = false)
{
+ if (empty($page)) {
+ $cachekey = self::CACHEKEY_CONTACTS . $module . ':'. $owner['uid'];
+ $result = DI::cache()->get($cachekey);
+ if (!$nocache && !is_null($result)) {
+ return $result;
+ }
+ }
+
$parameters = [
'rel' => $rel,
'uid' => $owner['uid'],
$data['type'] = 'OrderedCollection';
$data['totalItems'] = $total;
+ if (!empty($page)) {
+ $data['id'] .= '?' . http_build_query(['page' => $page]);
+ }
+
// When we hide our friends we will only show the pure number but don't allow more.
$show_contacts = empty($owner['hide-friends']);
}
if (!$show_contacts) {
+ if (!empty($cachekey)) {
+ DI::cache()->set($cachekey, $data, Duration::DAY);
+ }
+
return $data;
}
}
DBA::close($contacts);
- if (!empty($list)) {
+ if (count($list) == 100) {
$data['next'] = DI::baseUrl() . $modulePath . $owner['nickname'] . '?page=' . ($page + 1);
}
$data['orderedItems'] = $list;
}
+ if (!empty($cachekey)) {
+ DI::cache()->set($cachekey, $data, Duration::DAY);
+ }
+
return $data;
}
* @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($owner, $page = null, $requester = '')
+ public static function getOutbox(array $owner, int $page = null, string $requester = '', $nocache = false)
{
+ if (empty($page)) {
+ $cachekey = self::CACHEKEY_OUTBOX . $owner['uid'];
+ $result = DI::cache()->get($cachekey);
+ if (!$nocache && !is_null($result)) {
+ return $result;
+ }
+ }
+
$condition = ['private' => [Item::PUBLIC, Item::UNLISTED]];
if (!empty($requester)) {
$data['type'] = 'OrderedCollection';
$data['totalItems'] = $count;
+ if (!empty($page)) {
+ $data['id'] .= '?' . http_build_query(['page' => $page]);
+ }
+
if (empty($page)) {
$data['first'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=1';
} else {
}
DBA::close($items);
- if (!empty($list)) {
+ if (count($list) == 20) {
$data['next'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1);
}
$data['orderedItems'] = $list;
}
+ if (!empty($cachekey)) {
+ DI::cache()->set($cachekey, $data, Duration::DAY);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Public posts for the given owner
+ *
+ * @param array $owner Owner array
+ * @param integer $page Page number
+ * @param boolean $nocache Wether to bypass caching
+ *
+ * @return array of posts
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ * @throws \ImagickException
+ */
+ public static function getFeatured(array $owner, int $page = null, $nocache = false)
+ {
+ if (empty($page)) {
+ $cachekey = self::CACHEKEY_FEATURED . $owner['uid'];
+ $result = DI::cache()->get($cachekey);
+ if (!$nocache && !is_null($result)) {
+ return $result;
+ }
+ }
+
+ $owner_cid = Contact::getIdForURL($owner['url'], 0, false);
+
+ $condition = ["`uri-id` IN (SELECT `uri-id` FROM `collection-view` WHERE `cid` = ? AND `type` = ?)",
+ $owner_cid, Post\Collection::FEATURED];
+
+ $condition = DBA::mergeConditions($condition,
+ ['uid' => $owner['uid'],
+ 'author-id' => $owner_cid,
+ 'private' => [Item::PUBLIC, Item::UNLISTED],
+ 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT],
+ 'network' => Protocol::FEDERATED,
+ 'parent-network' => Protocol::FEDERATED,
+ 'origin' => true,
+ 'deleted' => false,
+ 'visible' => true]);
+
+ $count = Post::count($condition);
+
+ $data = ['@context' => ActivityPub::CONTEXT];
+ $data['id'] = DI::baseUrl() . '/featured/' . $owner['nickname'];
+ $data['type'] = 'OrderedCollection';
+ $data['totalItems'] = $count;
+
+ if (!empty($page)) {
+ $data['id'] .= '?' . http_build_query(['page' => $page]);
+ }
+
+ if (empty($page)) {
+ $items = Post::select(['id'], $condition, ['limit' => 20, 'order' => ['created' => true]]);
+ } else {
+ $data['type'] = 'OrderedCollectionPage';
+ $items = Post::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]);
+ }
+ $list = [];
+
+ 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() . '/featured/' . $owner['nickname'] . '?page=' . ($page + 1);
+ }
+
+ if (!empty($page)) {
+ $data['partOf'] = DI::baseUrl() . '/featured/' . $owner['nickname'];
+ }
+
+ $data['orderedItems'] = $list;
+
+ if (!empty($cachekey)) {
+ DI::cache()->set($cachekey, $data, Duration::DAY);
+ }
+
return $data;
}
if ($uid != 0) {
$data['following'] = DI::baseUrl() . '/following/' . $owner['nick'];
$data['followers'] = DI::baseUrl() . '/followers/' . $owner['nick'];
- $data['inbox'] = DI::baseUrl() . '/inbox/' . $owner['nick'];
- $data['outbox'] = DI::baseUrl() . '/outbox/' . $owner['nick'];
+ $data['inbox'] = DI::baseUrl() . '/inbox/' . $owner['nick'];
+ $data['outbox'] = DI::baseUrl() . '/outbox/' . $owner['nick'];
+ $data['featured'] = DI::baseUrl() . '/featured/' . $owner['nick'];
} else {
$data['inbox'] = DI::baseUrl() . '/friendica/inbox';
}
* Returns an array with permissions of the thread parent of the given item array
*
* @param array $item
+ * @param bool $is_forum_thread
*
* @return array with permissions
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
- public static function fetchPermissionBlockFromThreadParent($item)
+ private static function fetchPermissionBlockFromThreadParent(array $item, bool $is_forum_thread)
{
if (empty($item['thr-parent-id'])) {
return [];
$type = [Tag::TO => 'to', Tag::CC => 'cc', Tag::BTO => 'bto', Tag::BCC => 'bcc'];
foreach (Tag::getByURIId($item['thr-parent-id'], [Tag::TO, Tag::CC, Tag::BTO, Tag::BCC]) as $receiver) {
if (!empty($parent_profile['followers']) && $receiver['url'] == $parent_profile['followers'] && !empty($item_profile['followers'])) {
- $permissions[$type[$receiver['type']]][] = $item_profile['followers'];
+ if (!$is_forum_thread) {
+ $permissions[$type[$receiver['type']]][] = $item_profile['followers'];
+ }
} elseif (!in_array($receiver['url'], $exclude)) {
$permissions[$type[$receiver['type']]][] = $receiver['url'];
}
$always_bcc = true;
}
+ $parent = Post::selectFirst(['causer-link', 'post-reason'], ['id' => $item['parent']]);
+ if (($parent['post-reason'] == Item::PR_ANNOUNCEMENT) && !empty($parent['causer-link'])) {
+ $profile = APContact::getByURL($parent['causer-link'], false);
+ $is_forum_thread = isset($profile['type']) && $profile['type'] == 'Group';
+ } else {
+ $is_forum_thread = false;
+ }
+
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;
$data['cc'][] = $announce['actor']['url'];
}
- $data = array_merge($data, self::fetchPermissionBlockFromThreadParent($item));
+ $data = array_merge($data, self::fetchPermissionBlockFromThreadParent($item, $is_forum_thread));
// Check if the item is completely public or unlisted
if ($item['private'] == Item::PUBLIC) {
}
if (!empty($item['parent'])) {
- $parents = Post::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $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) {
$profile = APContact::getByURL($parent['owner-link'], false);
$data['to'][] = $profile['url'];
} else {
$data['cc'][] = $profile['url'];
- if (($item['private'] != Item::PRIVATE) && !empty($actor_profile['followers'])) {
+ if (($item['private'] != Item::PRIVATE) && !empty($actor_profile['followers'])&& !$is_forum_thread) {
$data['cc'][] = $actor_profile['followers'];
}
}
- } elseif (!$exclusive) {
+ } elseif (!$exclusive && !$is_forum_thread) {
// Public thread parent post always are directed to the followers.
if ($item['private'] != Item::PRIVATE) {
$data['cc'][] = $actor_profile['followers'];
if (in_array($data['type'], ['Create', 'Update', 'Delete'])) {
$data['object'] = $object ?? self::createNote($item);
+ $data['published'] = DateTimeFormat::utcNow(DateTimeFormat::ATOM);
} elseif ($data['type'] == 'Add') {
$data = self::createAddTag($item, $data);
} elseif ($data['type'] == 'Announce') {
return [];
}
+ // 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) {
+ $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)) {
+ $link = $links[0]['url'];
+ }
+ } else {
+ $isCommunityPost = false;
+ }
+
if ($item['event-type'] == 'event') {
$type = 'Event';
} elseif (!empty($item['title'])) {
- $type = 'Article';
+ if (!$isCommunityPost || empty($link)) {
+ $type = 'Article';
+ } else {
+ // "Page" is used by Lemmy for posts that contain an external link
+ $type = 'Page';
+ }
} else {
$type = 'Note';
}
$data['updated'] = DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM);
}
- $data['url'] = $item['plink'];
+ $data['url'] = $link ?? $item['plink'];
$data['attributedTo'] = $item['author-link'];
$data['sensitive'] = self::isSensitive($item['uri-id']);
$data['context'] = self::fetchContextURLForItem($item);
if ($type == 'Event') {
$data = array_merge($data, self::createEvent($item));
} else {
+ if ($isCommunityPost) {
+ // For community posts we remove the visible "!user@domain.tld".
+ // This improves the look at systems like Lemmy.
+ // Also in the future we should control the community delivery via other methods.
+ $body = preg_replace("/!\[url\=[^\[\]]*\][^\[\]]*\[\/url\]/ism", '', $body);
+ }
+
+ 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::setMentionsToNicknames($body);
$data['content'] = BBCode::convertForUriId($item['uri-id'], $body, BBCode::ACTIVITYPUB);