X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FItem.php;h=e31009753b8933c3fb98e6fd66a02dbe7748b6ac;hb=0da2391c4911ceb423a83bccfdbf3fff2a076a5c;hp=89390317fe42694f9888421ae65032536a614de6;hpb=f0c29edcde0ab15150147a3ebae40ca628197fde;p=friendica.git diff --git a/src/Model/Item.php b/src/Model/Item.php index 89390317fe..e31009753b 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -31,6 +31,7 @@ use Friendica\Core\System; use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Delivery; @@ -78,6 +79,7 @@ class Item const PR_DISTRIBUTE = 79; const PR_PUSHED = 80; const PR_LOCAL = 81; + const PR_AUDIENCE = 82; // system.accept_only_sharer setting values const COMPLETION_NONE = 1; @@ -92,9 +94,9 @@ class Item 'wall', 'private', 'starred', 'origin', 'parent-origin', 'title', 'body', 'language', 'content-warning', 'location', 'coord', 'app', 'rendered-hash', 'rendered-html', 'object', 'quote-uri', 'quote-uri-id', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'mention', 'global', - 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', 'author-updated', 'author-gsid', 'author-addr', 'author-uri-id', - 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'owner-network', 'owner-contact-type', 'owner-updated', - 'causer-id', 'causer-link', 'causer-name', 'causer-avatar', 'causer-contact-type', 'causer-network', + 'author-id', 'author-link', 'author-alias', 'author-name', 'author-avatar', 'author-network', 'author-updated', 'author-gsid', 'author-addr', 'author-uri-id', + 'owner-id', 'owner-link', 'owner-alias', 'owner-name', 'owner-avatar', 'owner-network', 'owner-contact-type', 'owner-updated', + 'causer-id', 'causer-link', 'causer-alias', 'causer-name', 'causer-avatar', 'causer-contact-type', 'causer-network', 'contact-id', 'contact-uid', 'contact-link', 'contact-name', 'contact-avatar', 'writable', 'self', 'cid', 'alias', 'event-created', 'event-edited', 'event-start', 'event-finish', @@ -106,31 +108,35 @@ class Item ]; // Field list that is used to deliver items via the protocols - const DELIVER_FIELDLIST = ['uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', - 'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target', - 'private', 'title', 'body', 'raw-body', 'location', 'coord', 'app', - 'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity', - 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', - 'author-id', 'author-addr', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid', - 'signed_text', 'network', 'wall', 'contact-id', 'plink', 'origin', - 'thr-parent-id', 'parent-uri-id', 'quote-uri', 'quote-uri-id', 'postopts', 'pubmail', - 'event-created', 'event-edited', 'event-start', 'event-finish', - 'event-summary', 'event-desc', 'event-location', 'event-type', - 'event-nofinish', 'event-ignore', 'event-id']; + const DELIVER_FIELDLIST = [ + 'uid', 'id', 'parent', 'uri-id', 'uri', 'thr-parent', 'parent-uri', 'guid', + 'parent-guid', 'conversation', 'received', 'created', 'edited', 'verb', 'object-type', 'object', 'target', + 'private', 'title', 'body', 'raw-body', 'location', 'coord', 'app', + 'inform', 'deleted', 'extid', 'post-type', 'post-reason', 'gravity', + 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', + 'author-id', 'author-addr', 'author-link', 'author-name', 'author-avatar', 'owner-id', 'owner-link', 'contact-uid', + 'signed_text', 'network', 'wall', 'contact-id', 'plink', 'origin', + 'thr-parent-id', 'parent-uri-id', 'quote-uri', 'quote-uri-id', 'postopts', 'pubmail', + 'event-created', 'event-edited', 'event-start', 'event-finish', + 'event-summary', 'event-desc', 'event-location', 'event-type', + 'event-nofinish', 'event-ignore', 'event-id' + ]; // All fields in the item table - const ITEM_FIELDLIST = ['id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', - 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'conversation', 'vid', - 'quote-uri', 'quote-uri-id', 'contact-id', 'wall', 'gravity', 'extid', 'psid', - 'created', 'edited', 'commented', 'received', 'changed', 'verb', - 'postopts', 'plink', 'resource-id', 'event-id', 'inform', - 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', 'post-reason', - 'private', 'pubmail', 'visible', 'starred', - 'unseen', 'deleted', 'origin', 'mention', 'global', 'network', - 'title', 'content-warning', 'body', 'location', 'coord', 'app', - 'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target', - 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', - 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'causer-id']; + const ITEM_FIELDLIST = [ + 'id', 'uid', 'parent', 'uri', 'parent-uri', 'thr-parent', + 'guid', 'uri-id', 'parent-uri-id', 'thr-parent-id', 'conversation', 'vid', + 'quote-uri', 'quote-uri-id', 'contact-id', 'wall', 'gravity', 'extid', 'psid', + 'created', 'edited', 'commented', 'received', 'changed', 'verb', + 'postopts', 'plink', 'resource-id', 'event-id', 'inform', + 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'post-type', 'post-reason', + 'private', 'pubmail', 'visible', 'starred', + 'unseen', 'deleted', 'origin', 'mention', 'global', 'network', + 'title', 'content-warning', 'body', 'location', 'coord', 'app', + 'rendered-hash', 'rendered-html', 'object-type', 'object', 'target-type', 'target', + 'author-id', 'author-link', 'author-name', 'author-avatar', 'author-network', + 'owner-id', 'owner-link', 'owner-name', 'owner-avatar', 'causer-id' + ]; // List of all verbs that don't need additional content data. // Never reorder or remove entries from this list. Just add new ones at the end, if needed. @@ -138,7 +144,8 @@ class Item Activity::LIKE, Activity::DISLIKE, Activity::ATTEND, Activity::ATTENDNO, Activity::ATTENDMAYBE, Activity::FOLLOW, - Activity::ANNOUNCE]; + Activity::ANNOUNCE + ]; // Privacy levels const PUBLIC = 0; @@ -189,8 +196,10 @@ class Item } // We only need to call the line by line update for specific fields - if (empty($fields['body']) && empty($fields['file']) && - empty($fields['attach']) && empty($fields['edited'])) { + if ( + empty($fields['body']) && empty($fields['file']) && + empty($fields['attach']) && empty($fields['edited']) + ) { return $rows; } @@ -210,8 +219,6 @@ class Item } } - Post\Media::insertFromAttachmentData($item['uri-id'], $fields['body']); - $content_fields = ['raw-body' => trim($fields['raw-body'] ?? $fields['body'])]; // Remove all media attachments from the body and store them in the post-media table @@ -220,6 +227,10 @@ class Item $content_fields['raw-body'] = self::setHashtags($content_fields['raw-body']); Post\Media::insertFromRelevantUrl($item['uri-id'], $content_fields['raw-body'], $fields['body'], $item['author-network']); + + Post\Media::insertFromAttachmentData($item['uri-id'], $fields['body']); + $content_fields['raw-body'] = BBCode::removeAttachment($content_fields['raw-body']); + Post\Content::update($item['uri-id'], $content_fields); } @@ -231,7 +242,7 @@ class Item Post\Media::insertFromAttachment($item['uri-id'], $fields['attach']); } - // We only need to notfiy others when it is an original entry from us. + // We only need to notify others when it is an original entry from us. // Only call the notifier when the item had been edited and records had been changed. if ($item['origin'] && !empty($fields['edited']) && ($previous['edited'] != $fields['edited'])) { $notify_items[] = $item['id']; @@ -241,7 +252,15 @@ class Item DBA::close($items); foreach ($notify_items as $notify_item) { - $post = Post::selectFirst(['uri-id', 'uid'], ['id' => $notify_item]); + $post = Post::selectFirst([], ['id' => $notify_item]); + + if ($post['gravity'] != self::GRAVITY_PARENT) { + $signed = Diaspora::createCommentSignature($post); + if (!empty($signed)) { + DBA::replace('diaspora-interaction', ['uri-id' => $post['uri-id'], 'interaction' => json_encode($signed)]); + } + } + Worker::add(Worker::PRIORITY_HIGH, 'Notifier', Delivery::POST, (int)$post['uri-id'], (int)$post['uid']); } @@ -307,9 +326,11 @@ class Item { Logger::info('Mark item for deletion by id', ['id' => $item_id, 'callstack' => System::callstack()]); // locate item to be deleted - $fields = ['id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri-id', 'origin', + $fields = [ + 'id', 'uri', 'uri-id', 'uid', 'parent', 'parent-uri-id', 'origin', 'deleted', 'resource-id', 'event-id', - 'verb', 'object-type', 'object', 'target', 'contact-id', 'psid', 'gravity']; + 'verb', 'object-type', 'object', 'target', 'contact-id', 'psid', 'gravity' + ]; $item = Post::selectFirst($fields, ['id' => $item_id]); if (!DBA::isResult($item)) { Logger::info('Item not found.', ['id' => $item_id]); @@ -347,7 +368,7 @@ class Item // If item has attachments, drop them $attachments = Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT]); - foreach($attachments as $attachment) { + foreach ($attachments as $attachment) { if (preg_match('|attach/(\d+)|', $attachment['url'], $matches)) { Attach::delete(['id' => $matches[1], 'uid' => $item['uid']]); } @@ -461,7 +482,7 @@ class Item private static function contactId(array $item): int { if ($item['uid'] == 0) { - return $item['author-id']; + return $item['owner-id']; } if ($item['origin']) { @@ -469,31 +490,31 @@ class Item return $owner['id']; } - if (!empty($item['causer-id']) && Contact::isSharing($item['causer-id'], $item['uid'], true)) { - $cdata = Contact::getPublicAndUserContactID($item['causer-id'], $item['uid']); - if (!empty($cdata['user'])) { - return $cdata['user']; + $contact_id = 0; + $user_contact_id = 0; + foreach (['group-link', 'causer-link', 'owner-link', 'author-link'] as $field) { + if (empty($item[$field])) { + continue; + } + if (!$user_contact_id && Contact::isSharingByURL($item[$field], $item['uid'], true)) { + $user_contact_id = Contact::getIdForURL($item[$field], $item['uid']); + } elseif (!$contact_id) { + $contact_id = Contact::getIdForURL($item[$field]); } } - if ($item['gravity'] == self::GRAVITY_PARENT) { - if (Contact::isSharingByURL($item['owner-link'], $item['uid'], true)) { - $contact_id = Contact::getIdForURL($item['owner-link'], $item['uid']); - } else { - $contact_id = Contact::getIdForURL($item['owner-link']); - } - if (!empty($contact_id)) { - return $contact_id; - } + if ($user_contact_id) { + return $user_contact_id; } - if (Contact::isSharingByURL($item['author-link'], $item['uid'], true)) { - $contact_id = Contact::getIdForURL($item['author-link'], $item['uid']); - } else { - $contact_id = Contact::getIdForURL($item['author-link']); + if (!empty($item['causer-id']) && Contact::isSharing($item['causer-id'], $item['uid'], true)) { + $cdata = Contact::getPublicAndUserContactID($item['causer-id'], $item['uid']); + if (!empty($cdata['user'])) { + return $cdata['user']; + } } - if (!empty($contact_id)) { + if ($contact_id) { return $contact_id; } @@ -539,8 +560,10 @@ class Item return true; } - $condition = ['uri-id' => $item['uri-id'], 'uid' => $item['uid'], - 'network' => [$item['network'], Protocol::DFRN]]; + $condition = [ + 'uri-id' => $item['uri-id'], 'uid' => $item['uid'], + 'network' => [$item['network'], Protocol::DFRN] + ]; if (Post::exists($condition)) { Logger::notice('duplicated item with the same uri found.', $condition); return true; @@ -555,8 +578,10 @@ class Item } } elseif ($item['network'] == Protocol::OSTATUS) { // Check for an existing post with the same content. There seems to be a problem with OStatus. - $condition = ["`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?", - $item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid']]; + $condition = [ + "`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?", + $item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid'] + ]; if (Post::exists($condition)) { Logger::notice('duplicated item with the same body found.', $item); return true; @@ -585,7 +610,7 @@ class Item public static function isValid(array $item): bool { // When there is no content then we don't post it - if (($item['body'] . $item['title'] == '') && empty($item['quote-uri-id']) && (empty($item['uri-id']) || !Post\Media::existsByURIId($item['uri-id']))) { + if (($item['body'] . $item['title'] == '') && empty($item['quote-uri-id']) && empty($item['attachments']) && (empty($item['uri-id']) || !Post\Media::existsByURIId($item['uri-id']))) { Logger::notice('No body, no title.'); return false; } @@ -630,8 +655,10 @@ class Item return false; } - $condition = ['verb' => Activity::FOLLOW, 'uid' => $item['uid'], - 'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id']]; + $condition = [ + 'verb' => Activity::FOLLOW, 'uid' => $item['uid'], + 'parent-uri' => $item['parent-uri'], 'author-id' => $item['author-id'] + ]; if (Post::exists($condition)) { // It happens that we receive multiple follow requests by the same author - we only store one. Logger::info('Follow: Found existing follow request from author', ['author-id' => $item['author-id'], 'parent-uri' => $item['parent-uri']]); @@ -683,7 +710,8 @@ class Item private static function getDuplicateID(array $item): int { if (empty($item['network']) || in_array($item['network'], Protocol::FEDERATED)) { - $condition = ['`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?, ?)', + $condition = [ + '`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?, ?)', $item['uri-id'], $item['uid'], Protocol::ACTIVITYPUB, @@ -739,10 +767,12 @@ class Item */ private static function getTopLevelParent(array $item): array { - $fields = ['uid', 'uri', 'parent-uri', 'id', 'deleted', + $fields = [ + 'uid', 'uri', 'parent-uri', 'id', 'deleted', 'uri-id', 'parent-uri-id', 'allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', - 'wall', 'private', 'origin', 'author-id']; + 'wall', 'private', 'origin', 'author-id' + ]; $condition = ['uri-id' => [$item['thr-parent-id'], $item['parent-uri-id']], 'uid' => $item['uid']]; $params = ['order' => ['id' => false]]; $parent = Post::selectFirst($fields, $condition, $params); @@ -767,9 +797,11 @@ class Item return $parent; } - $condition = ['uri-id' => $parent['parent-uri-id'], + $condition = [ + 'uri-id' => $parent['parent-uri-id'], 'parent-uri-id' => $parent['parent-uri-id'], - 'uid' => $parent['uid']]; + 'uid' => $parent['uid'] + ]; $params = ['order' => ['id' => false]]; $toplevel_parent = Post::selectFirst($fields, $condition, $params); @@ -873,7 +905,7 @@ class Item /* * Do we already have this item? * We have to check several networks since Friendica posts could be repeated - * via OStatus (maybe Diasporsa as well) + * via OStatus (maybe Diaspora as well) */ $duplicate = self::getDuplicateID($item); if ($duplicate) { @@ -890,6 +922,8 @@ class Item $item['post-type'] = empty($item['title']) ? self::PT_NOTE : self::PT_ARTICLE; } + $defined_permissions = isset($item['allow_cid']) && isset($item['allow_gid']) && isset($item['deny_cid']) && isset($item['deny_gid']) && isset($item['private']); + $item['wall'] = intval($item['wall'] ?? 0); $item['extid'] = trim($item['extid'] ?? ''); $item['author-name'] = trim($item['author-name'] ?? ''); @@ -929,10 +963,10 @@ class Item $item['inform'] = trim($item['inform'] ?? ''); $item['file'] = trim($item['file'] ?? ''); - // Communities aren't working with the Diaspora protoccol + // Communities aren't working with the Diaspora protocol if (($uid != 0) && ($item['network'] == Protocol::DIASPORA)) { $user = User::getById($uid, ['account-type']); - if ($user['account-type'] == Contact::TYPE_COMMUNITY) { + if ($user['account-type'] == Contact::TYPE_COMMUNITY) { Logger::info('Community posts are not supported via Diaspora'); return 0; } @@ -952,12 +986,16 @@ class Item $item['gravity'] = self::getGravity($item); - $default = ['url' => $item['author-link'], 'name' => $item['author-name'], - 'photo' => $item['author-avatar'], 'network' => $item['network']]; + $default = [ + 'url' => $item['author-link'], 'name' => $item['author-name'], + 'photo' => $item['author-avatar'], 'network' => $item['network'] + ]; $item['author-id'] = ($item['author-id'] ?? 0) ?: Contact::getIdForURL($item['author-link'], 0, null, $default); - $default = ['url' => $item['owner-link'], 'name' => $item['owner-name'], - 'photo' => $item['owner-avatar'], 'network' => $item['network']]; + $default = [ + 'url' => $item['owner-link'], 'name' => $item['owner-name'], + 'photo' => $item['owner-avatar'], 'network' => $item['network'] + ]; $item['owner-id'] = ($item['owner-id'] ?? 0) ?: Contact::getIdForURL($item['owner-link'], 0, null, $default); $item['post-reason'] = self::getPostReason($item); @@ -968,8 +1006,10 @@ class Item $item['contact-id'] = self::contactId($item); - if (!empty($item['direction']) && in_array($item['direction'], [Conversation::PUSH, Conversation::RELAY]) && - empty($item['origin']) && self::isTooOld($item)) { + if ( + !empty($item['direction']) && in_array($item['direction'], [Conversation::PUSH, Conversation::RELAY]) && + empty($item['origin']) && self::isTooOld($item) + ) { Logger::info('Item is too old', ['item' => $item]); return 0; } @@ -990,8 +1030,8 @@ class Item $item['deleted'] = $toplevel_parent['deleted']; $item['wall'] = $toplevel_parent['wall']; - // Reshares have to keep their permissions to allow forums to work - if (!$item['origin'] || ($item['verb'] != Activity::ANNOUNCE)) { + // Reshares have to keep their permissions to allow groups to work + if (!$defined_permissions && (!$item['origin'] || ($item['verb'] != Activity::ANNOUNCE))) { $item['allow_cid'] = $toplevel_parent['allow_cid']; $item['allow_gid'] = $toplevel_parent['allow_gid']; $item['deny_cid'] = $toplevel_parent['deny_cid']; @@ -1014,7 +1054,7 @@ class Item * This differs from the above settings as it subtly allows comments from * email correspondents to be private even if the overall thread is not. */ - if ($toplevel_parent['private']) { + if (!$defined_permissions && $toplevel_parent['private']) { $item['private'] = $toplevel_parent['private']; } @@ -1061,7 +1101,7 @@ class Item } // ACL settings - if (!empty($item['allow_cid'] . $item['allow_gid'] . $item['deny_cid'] . $item['deny_gid'])) { + if (!$defined_permissions && !empty($item['allow_cid'] . $item['allow_gid'] . $item['deny_cid'] . $item['deny_gid'])) { $item['private'] = self::PRIVATE; } @@ -1110,7 +1150,8 @@ class Item $item['allow_gid'], $item['deny_cid'], $item['deny_gid'] - ))->id; + ) + )->id; if (!empty($item['extid'])) { $item['external-id'] = ItemURI::getIdByURI($item['extid']); @@ -1143,8 +1184,6 @@ class Item $item['body'] = BBCode::removeSharedData($item['body']); } - Post\Media::insertFromAttachmentData($item['uri-id'], $item['body']); - // Remove all media attachments from the body and store them in the post-media table $item['raw-body'] = Post\Media::insertFromBody($item['uri-id'], $item['raw-body']); $item['raw-body'] = self::setHashtags($item['raw-body']); @@ -1152,6 +1191,10 @@ class Item $author = Contact::getById($item['author-id'], ['network']); Post\Media::insertFromRelevantUrl($item['uri-id'], $item['raw-body'], $item['body'], $author['network'] ?? ''); + Post\Media::insertFromAttachmentData($item['uri-id'], $item['body']); + $item['body'] = BBCode::removeAttachment($item['body']); + $item['raw-body'] = BBCode::removeAttachment($item['raw-body']); + // Check for hashtags in the body and repair or add hashtag links $item['body'] = self::setHashtags($item['body']); @@ -1211,8 +1254,8 @@ class Item Post\Thread::insert($item['uri-id'], $item); } - // The content of activities normally doesn't matter - except for likes from Misskey - if (!in_array($item['verb'], self::ACTIVITIES) || in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE]) && !empty($item['body']) && ($item['body'] != $item['verb'])) { + // The content of activities normally doesn't matter - except for likes from Misskey + if (!in_array($item['verb'], self::ACTIVITIES) || in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE]) && !empty($item['body']) && (mb_strlen($item['body']) == 1)) { Post\Content::insert($item['uri-id'], $item); } @@ -1291,7 +1334,7 @@ class Item Post::update($fields, ['uri-id' => $posted_item['parent-uri-id'], 'uid' => $posted_item['uid']]); - // In that function we check if this is a forum post. Additionally we delete the item under certain circumstances + // In that function we check if this is a group post. Additionally we delete the item under certain circumstances if (self::tagDeliver($posted_item['uid'], $post_user_id)) { // Get the user information for the logging $user = User::getById($uid); @@ -1340,7 +1383,8 @@ class Item // Don't relay participation messages if (($posted_item['verb'] == Activity::FOLLOW) && - (!$posted_item['origin'] || ($posted_item['author-id'] != Contact::getPublicIdByUserId($uid)))) { + (!$posted_item['origin'] || ($posted_item['author-id'] != Contact::getPublicIdByUserId($uid))) + ) { Logger::info('Participation messages will not be relayed', ['item' => $posted_item['id'], 'uri' => $posted_item['uri'], 'verb' => $posted_item['verb']]); $transmit = false; } @@ -1388,27 +1432,35 @@ class Item * * @param integer $uri_id * @return void + * @throws InternalServerErrorException + * @throws \ImagickException */ public static function updateDisplayCache(int $uri_id) { $item = Post::selectFirst(self::DISPLAY_FIELDLIST, ['uri-id' => $uri_id]); + if (!$item) { + return; + } + self::prepareBody($item, false, false, true); } /** - * Change the owner of a parent item if it had been shared by a forum + * Change the owner of a parent item if it had been shared by a group * - * (public) forum posts in the new format consist of the regular post by the author - * followed by an announce message sent from the forum account. - * Changing the owner helps in grouping forum posts. + * (public) group posts in the new format consist of the regular post by the author + * followed by an announce message sent from the group account. + * Changing the owner helps in grouping group posts. * * @param array $item * @return void */ private static function setOwnerforResharedItem(array $item) { - $parent = Post::selectFirst(['id', 'causer-id', 'owner-id', 'author-id', 'author-link', 'origin', 'post-reason'], - ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); + $parent = Post::selectFirst( + ['id', 'causer-id', 'owner-id', 'author-id', 'author-link', 'origin', 'post-reason'], + ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']] + ); if (!DBA::isResult($parent)) { Logger::error('Parent not found', ['uri-id' => $item['thr-parent-id'], 'uid' => $item['uid']]); return; @@ -1436,7 +1488,7 @@ class Item } if (Contact::isSharing($parent['owner-id'], $item['uid'])) { - Logger::info('The resharer is no forum: quit', ['resharer' => $item['author-id'], 'owner' => $parent['owner-id'], 'author' => $parent['author-id'], 'uid' => $item['uid']]); + Logger::info('The resharer is no group: quit', ['resharer' => $item['author-id'], 'owner' => $parent['owner-id'], 'author' => $parent['author-id'], 'uid' => $item['uid']]); return; } } @@ -1480,9 +1532,11 @@ class Item } // Only distribute public items from native networks - $condition = ['id' => $itemid, 'uid' => 0, - 'network' => array_merge(Protocol::FEDERATED ,['']), - 'visible' => true, 'deleted' => false, 'private' => [self::PUBLIC, self::UNLISTED]]; + $condition = [ + 'id' => $itemid, 'uid' => 0, + 'network' => array_merge(Protocol::FEDERATED, ['']), + 'visible' => true, 'deleted' => false, 'private' => [self::PUBLIC, self::UNLISTED] + ]; $item = Post::selectFirst(array_merge(self::ITEM_FIELDLIST, ['protocol']), $condition); if (!DBA::isResult($item)) { Logger::warning('Item not found', ['condition' => $condition]); @@ -1493,7 +1547,7 @@ class Item $users = []; - /// @todo add a field "pcid" in the contact table that referrs to the public contact id. + /// @todo add a field "pcid" in the contact table that refers to the public contact id. $owner = DBA::selectFirst('contact', ['url', 'nurl', 'alias'], ['id' => $parent['owner-id']]); if (!DBA::isResult($owner)) { return; @@ -1581,7 +1635,7 @@ class Item if (($uid != 0) && ($item['gravity'] == self::GRAVITY_PARENT)) { $owner = User::getOwnerDataById($uid); if (($owner['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY) && !Tag::isMentioned($uri_id, $owner['url'])) { - Logger::info('Target user is a forum but is not mentioned here, thread will not be stored', ['uid' => $uid, 'uri-id' => $uri_id]); + Logger::info('Target user is a group but is not mentioned here, thread will not be stored', ['uid' => $uid, 'uri-id' => $uri_id]); return 0; } } @@ -1603,7 +1657,8 @@ class Item if (($uid != 0) && (($item['gravity'] == self::GRAVITY_PARENT) || $is_reshare) && DI::pConfig()->get($uid, 'system', 'accept_only_sharer') == self::COMPLETION_NONE && - !in_array($item['post-reason'], [self::PR_FOLLOWER, self::PR_TAG, self::PR_TO, self::PR_CC, self::PR_ACTIVITY])) { + !in_array($item['post-reason'], [self::PR_FOLLOWER, self::PR_TAG, self::PR_TO, self::PR_CC, self::PR_ACTIVITY, self::PR_AUDIENCE]) + ) { Logger::info('Contact is not a follower, thread will not be stored', ['author' => $item['author-link'], 'uid' => $uid, 'uri-id' => $uri_id, 'post-reason' => $item['post-reason']]); return 0; } @@ -1687,7 +1742,7 @@ class Item return 0; } - // When the post belongs to a a forum then all forum users are allowed to access it + // When the post belongs to a a group then all group users are allowed to access it foreach (Tag::getByURIId($uriid, [Tag::MENTION, Tag::EXCLUSIVE_MENTION]) as $tag) { if (DBA::exists('contact', ['uid' => $uid, 'nurl' => Strings::normaliseLink($tag['url']), 'contact-type' => Contact::TYPE_COMMUNITY])) { $target_uid = User::getIdForURL($tag['url']); @@ -1715,7 +1770,7 @@ class Item if (!empty($item['event-id'])) { $event_post = Post::selectFirst(['event-id'], ['uri-id' => $item['uri-id'], 'uid' => $uid]); if (!empty($event_post['event-id'])) { - $event = DBA::selectFirst('event', ['edited', 'start', 'finish', 'summary', 'desc', 'location', 'nofinish', 'adjust'], ['id' => $item['event-id']]); + $event = DBA::selectFirst('event', ['edited', 'start', 'finish', 'summary', 'desc', 'location', 'nofinish'], ['id' => $item['event-id']]); if (!empty($event)) { // We aren't using "Event::store" here, since we don't want to trigger any further action $ret = DBA::update('event', $event, ['id' => $event_post['event-id']]); @@ -1799,7 +1854,7 @@ class Item } // is it an entry from a connector? Only add an entry for natively connected networks - if (!in_array($item["network"], array_merge(Protocol::FEDERATED ,['']))) { + if (!in_array($item["network"], array_merge(Protocol::FEDERATED, ['']))) { return; } @@ -2022,13 +2077,17 @@ class Item // Remove the scheme to make sure that "https" and "http" doesn't make a difference unset($parsed['scheme']); - $hostPart = $host ?? $parsed['host'] ?? ''; + $hostPart = $host ?: $parsed['host'] ?? ''; if (!$hostPart) { Logger::warning('Empty host GUID part', ['uri' => $uri, 'host' => $host, 'parsed' => $parsed, 'callstack' => System::callstack(10)]); } // Glue it together to be able to make a hash from it - $host_id = implode('/', $parsed); + if (!empty($parsed)) { + $host_id = implode('/', $parsed); + } else { + $host_id = $uri; + } // Use a mixture of several hashes to provide some GUID like experience return hash('crc32', $hostPart) . '-' . hash('joaat', $host_id) . '-' . hash('fnv164', $host_id); @@ -2078,7 +2137,7 @@ class Item /// @todo On private posts we could obfuscate the date $update = ($arr['private'] != self::PRIVATE) || in_array($arr['network'], Protocol::FEDERATED); - // Is it a forum? Then we don't care about the rules from above + // Is it a group? Then we don't care about the rules from above if (!$update && in_array($arr["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN]) && ($arr["parent-uri-id"] === $arr["uri-id"])) { if (DBA::exists('contact', ['id' => $arr['contact-id'], 'forum' => true])) { $update = true; @@ -2098,12 +2157,16 @@ class Item } // Now do the same for the system wide contacts with uid=0 if ($arr['private'] != self::PRIVATE) { - Contact::update(['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], - ['id' => $arr['owner-id']]); + Contact::update( + ['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], + ['id' => $arr['owner-id']] + ); if ($arr['owner-id'] != $arr['author-id']) { - Contact::update(['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], - ['id' => $arr['author-id']]); + Contact::update( + ['failed' => false, 'local-data' => true, 'success_update' => $arr['received'], 'last-item' => $arr['received']], + ['id' => $arr['author-id']] + ); } } } @@ -2129,29 +2192,44 @@ class Item // All hashtags should point to the home server if "local_tags" is activated if (DI::config()->get('system', 'local_tags')) { - $body = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - "#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]", $body); + $body = preg_replace( + "/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", + "#[url=" . DI::baseUrl() . "/search?tag=$2]$2[/url]", + $body + ); } // mask hashtags inside of url, bookmarks and attachments to avoid urls in urls - $body = preg_replace_callback("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", + $body = preg_replace_callback( + "/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", function ($match) { return ("[url=" . str_replace("#", "#", $match[1]) . "]" . str_replace("#", "#", $match[2]) . "[/url]"); - }, $body); + }, + $body + ); - $body = preg_replace_callback("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", + $body = preg_replace_callback( + "/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", function ($match) { return ("[bookmark=" . str_replace("#", "#", $match[1]) . "]" . str_replace("#", "#", $match[2]) . "[/bookmark]"); - }, $body); + }, + $body + ); - $body = preg_replace_callback("/\[attachment (.*?)\](.*?)\[\/attachment\]/ism", + $body = preg_replace_callback( + "/\[attachment (.*?)\](.*?)\[\/attachment\]/ism", function ($match) { return ("[attachment " . str_replace("#", "#", $match[1]) . "]" . $match[2] . "[/attachment]"); - }, $body); + }, + $body + ); // Repair recursive urls - $body = preg_replace("/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", - "#$2", $body); + $body = preg_replace( + "/#\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", + "#$2", + $body + ); foreach ($tags as $tag) { if ((strpos($tag, '#') !== 0) || strpos($tag, '[url=') || strlen($tag) < 2 || $tag[1] == '#') { @@ -2174,7 +2252,7 @@ class Item } /** - * look for mention tags and setup a second delivery chain for forum/community posts if appropriate + * look for mention tags and setup a second delivery chain for group/community posts if appropriate * * @param int $uid * @param int $item_id @@ -2184,8 +2262,6 @@ class Item */ private static function tagDeliver(int $uid, int $item_id): bool { - $mention = false; - $owner = User::getOwnerDataById($uid); if (!DBA::isResult($owner)) { Logger::warning('User not found, quitting here.', ['uid' => $uid]); @@ -2228,7 +2304,7 @@ class Item if ($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) { $allow_cid = ''; - $allow_gid = '<' . Group::FOLLOWERS . '>'; + $allow_gid = '<' . Circle::FOLLOWERS . '>'; $deny_cid = ''; $deny_gid = ''; self::performActivity($item['id'], 'announce', $uid, $allow_cid, $allow_gid, $deny_cid, $deny_gid); @@ -2305,10 +2381,13 @@ class Item } $datarray2 = $datarray; - Logger::info('remote-self start', ['contact' => $contact['url'], 'remote_self'=> $contact['remote_self'], 'item' => $datarray]); + Logger::info('remote-self start', ['contact' => $contact['url'], 'remote_self' => $contact['remote_self'], 'item' => $datarray]); - $self = DBA::selectFirst('contact', ['id', 'name', 'url', 'thumb'], - ['uid' => $contact['uid'], 'self' => true]); + $self = DBA::selectFirst( + 'contact', + ['id', 'name', 'url', 'thumb'], + ['uid' => $contact['uid'], 'self' => true] + ); if (!DBA::isResult($self)) { Logger::error('Self contact not found', ['uid' => $contact['uid']]); return false; @@ -2344,7 +2423,7 @@ class Item // Store the original post $result = self::insert($datarray2); - Logger::info('remote-self post original item', ['contact' => $contact['url'], 'result'=> $result, 'item' => $datarray2]); + Logger::info('remote-self post original item', ['contact' => $contact['url'], 'result' => $result, 'item' => $datarray2]); } else { $datarray['private'] = self::PUBLIC; $datarray['app'] = 'Feed'; @@ -2471,7 +2550,8 @@ class Item if (($obj1['allow_cid'] == $obj2['allow_cid']) && ($obj1['allow_gid'] == $obj2['allow_gid']) && ($obj1['deny_cid'] == $obj2['deny_cid']) - && ($obj1['deny_gid'] == $obj2['deny_gid'])) { + && ($obj1['deny_gid'] == $obj2['deny_gid']) + ) { return true; } @@ -2488,22 +2568,27 @@ class Item /** * Returns an array of contact-ids that are allowed to see this object * - * @param array $obj Item array with at least uid, allow_cid, allow_gid, deny_cid and deny_gid - * @param bool $check_dead Prunes unavailable contacts from the result + * @param array $obj Item array with at least uid, allow_cid, allow_gid, deny_cid and deny_gid + * @param bool $check_dead Prunes unavailable contacts from the result + * @param bool $expand_followers Expand the list of followers * @return array * @throws \Exception */ - public static function enumeratePermissions(array $obj, bool $check_dead = false): array + public static function enumeratePermissions(array $obj, bool $check_dead = false, bool $expand_followers = true): array { - $aclFormater = DI::aclFormatter(); + $aclFormatter = DI::aclFormatter(); + + if (!$expand_followers && (!empty($obj['deny_cid']) || !empty($obj['deny_gid']))) { + $expand_followers = true; + } - $allow_people = $aclFormater->expand($obj['allow_cid']); - $allow_groups = Group::expand($obj['uid'], $aclFormater->expand($obj['allow_gid']), $check_dead); - $deny_people = $aclFormater->expand($obj['deny_cid']); - $deny_groups = Group::expand($obj['uid'], $aclFormater->expand($obj['deny_gid']), $check_dead); - $recipients = array_unique(array_merge($allow_people, $allow_groups)); - $deny = array_unique(array_merge($deny_people, $deny_groups)); - $recipients = array_diff($recipients, $deny); + $allow_people = $aclFormatter->expand($obj['allow_cid']); + $allow_circles = Circle::expand($obj['uid'], $aclFormatter->expand($obj['allow_gid']), $check_dead, $expand_followers); + $deny_people = $aclFormatter->expand($obj['deny_cid']); + $deny_circles = Circle::expand($obj['uid'], $aclFormatter->expand($obj['deny_gid']), $check_dead); + $recipients = array_unique(array_merge($allow_people, $allow_circles)); + $deny = array_unique(array_merge($deny_people, $deny_circles)); + $recipients = array_diff($recipients, $deny); return $recipients; } @@ -2513,8 +2598,10 @@ class Item return; } - $condition = ["`uid` = ? AND NOT `deleted` AND `gravity` = ?", - $uid, self::GRAVITY_PARENT]; + $condition = [ + "`uid` = ? AND NOT `deleted` AND `gravity` = ?", + $uid, self::GRAVITY_PARENT + ]; /* * $expire_network_only = save your own wall posts @@ -2588,8 +2675,10 @@ class Item return false; } - $condition = ["`uid` = ? AND `wall` = ? AND NOT `deleted` AND `visible` AND `received` >= ?", - $uid, $wall, $user['register_date']]; + $condition = [ + "`uid` = ? AND `wall` = ? AND NOT `deleted` AND `visible` AND `received` >= ?", + $uid, $wall, $user['register_date'] + ]; $params = ['order' => ['received' => false]]; $thread = Post::selectFirstThread(['received'], $condition, $params); if (DBA::isResult($thread)) { @@ -2609,7 +2698,7 @@ class Item * Activity verb. One of * like, unlike, dislike, undislike, attendyes, unattendyes, * attendno, unattendno, attendmaybe, unattendmaybe, - * announce, unannouce + * announce, unannounce * @param int $uid * @param string $allow_cid * @param string $allow_gid @@ -2721,8 +2810,10 @@ class Item $vids = Verb::getID($activity); } - $condition = ['vid' => $vids, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY, - 'author-id' => $author_id, 'uid' => $uid, 'thr-parent-id' => $uri_id]; + $condition = [ + 'vid' => $vids, 'deleted' => false, 'gravity' => self::GRAVITY_ACTIVITY, + 'author-id' => $author_id, 'uid' => $uid, 'thr-parent-id' => $uri_id + ]; $like_item = Post::selectFirst(['id', 'guid', 'verb'], $condition); if (DBA::isResult($like_item)) { @@ -2826,12 +2917,14 @@ class Item // Profile owner - everything is visible $condition = []; } elseif ($remote_user) { - // Authenticated visitor - fetch the matching permissionsets + // Authenticated visitor - fetch the matching permissionsets $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $owner_id); if (!empty($set)) { - $condition = ["(`private` != ? OR (`private` = ? AND `wall` + $condition = [ + "(`private` != ? OR (`private` = ? AND `wall` AND `psid` IN (" . implode(', ', array_fill(0, count($set), '?')) . ")))", - self::PRIVATE, self::PRIVATE]; + self::PRIVATE, self::PRIVATE + ]; $condition = array_merge($condition, $permissionSets->column('id')); } } @@ -2869,9 +2962,9 @@ class Item /* * Authenticated visitor. Unless pre-verified, * check that the contact belongs to this $owner_id - * and load the groups the visitor belongs to. + * and load the circles the visitor belongs to. * If pre-verified, the caller is expected to have already - * done this and passed the groups into this function. + * done this and passed the circles into this function. */ $permissionSets = DI::permissionSet()->selectByContactId($remote_user, $owner_id); @@ -2927,7 +3020,8 @@ class Item $rendered_hash = $item['rendered-hash'] ?? ''; $rendered_html = $item['rendered-html'] ?? ''; - if ($rendered_hash == '' + if ( + $rendered_hash == '' || $rendered_html == '' || $rendered_hash != hash('md5', BBCode::VERSION . '::' . $body) || DI::config()->get('system', 'ignore_cache') @@ -2994,6 +3088,7 @@ class Item if (!$is_preview) { $item['body'] = preg_replace("#\s*\[attachment .*?].*?\[/attachment]\s*#ism", "\n", $item['body']); $item['body'] = Post\Media::removeFromEndOfBody($item['body'] ?? ''); + $item['body'] = Post\Media::replaceImage($item['body']); } $body = $item['body']; @@ -3003,32 +3098,50 @@ class Item $fields = ['uri-id', 'uri', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink', 'network', 'has-media', 'quote-uri-id', 'post-type']; - $shared_uri_id = 0; - $shared_links = []; + $shared_uri_id = 0; + $shared_links = []; + $quote_shared_links = []; $shared = DI::contentItem()->getSharedPost($item, $fields); if (!empty($shared['post'])) { $shared_item = $shared['post']; $shared_item['body'] = Post\Media::removeFromEndOfBody($shared_item['body']); + $shared_item['body'] = Post\Media::replaceImage($shared_item['body']); $quote_uri_id = $shared['post']['uri-id']; $shared_links[] = strtolower($shared['post']['uri']); $item['body'] = BBCode::removeSharedData($item['body']); } elseif (empty($shared_item['uri-id']) && empty($item['quote-uri-id']) && ($item['network'] != Protocol::DIASPORA)) { $media = Post\Media::getByURIId($item['uri-id'], [Post\Media::ACTIVITY]); if (!empty($media)) { - $shared_item = Post::selectFirst($fields, ['plink' => $media[0]['url'], 'uid' => [$item['uid'], 0]]); + $shared_item = Post::selectFirst($fields, ['uri-id' => $media[0]['media-uri-id'], 'uid' => [$item['uid'], 0]]); + if (empty($shared_item['uri-id'])) { + $shared_item = Post::selectFirst($fields, ['plink' => $media[0]['url'], 'uid' => [$item['uid'], 0]]); + } elseif (!in_array(strtolower($media[0]['url']), $shared_links)) { + $shared_links[] = strtolower($media[0]['url']); + } if (empty($shared_item['uri-id'])) { $shared_item = Post::selectFirst($fields, ['uri' => $media[0]['url'], 'uid' => [$item['uid'], 0]]); $shared_links[] = strtolower($media[0]['url']); } - $quote_uri_id = $shared_item['uri-id'] ?? 0; + if (!empty($shared_item['uri-id'])) { + $data = BBCode::getAttachmentData($shared_item['body']); + if (!empty($data['url'])) { + $quote_shared_links[] = $data['url']; + } + + $quote_uri_id = $shared_item['uri-id']; + } } } if (!empty($quote_uri_id)) { - $item['body'] .= "\n" . DI::contentItem()->createSharedBlockByArray($shared_item); + if (isset($shared_item['plink'])) { + $item['body'] .= "\n" . DI::contentItem()->createSharedBlockByArray($shared_item); + } else { + DI::logger()->warning('Missing plink in shared item', ['item' => $item, 'shared' => $shared, 'quote_uri_id' => $quote_uri_id, 'shared_item' => $shared_item]); + } } if (!empty($shared_item['uri-id'])) { @@ -3097,12 +3210,14 @@ class Item } if (!empty($shared_attachments)) { + $s = self::addGallery($s, $shared_attachments, $item['uri-id']); $s = self::addVisualAttachments($shared_attachments, $shared_item, $s, true); - $s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $shared_attachments, $body, $s, true, []); + $s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $shared_attachments, $body, $s, true, $quote_shared_links); $s = self::addNonVisualAttachments($shared_attachments, $item, $s, true); $body = BBCode::removeSharedData($body); } + $s = self::addGallery($s, $attachments, $item['uri-id']); $s = self::addVisualAttachments($attachments, $item, $s, false); $s = self::addLinkAttachment($item['uri-id'], $attachments, $body, $s, false, $shared_links); $s = self::addNonVisualAttachments($attachments, $item, $s, false); @@ -3152,6 +3267,24 @@ class Item ]); } + /** + * Modify links to pictures to links for the "Fancybox" gallery + * + * @param string $s + * @param array $attachments + * @param integer $uri_id + * @return string + */ + private static function addGallery(string $s, array $attachments, int $uri_id): string + { + foreach ($attachments['visual'] as $attachment) { + if (empty($attachment['preview']) || ($attachment['type'] != Post\Media::IMAGE)) { + continue; + } + $s = str_replace(' $src_url, 'preview' => $preview_url, 'attachment' => $attachment]; + $images[] = ['src' => $src_url, 'preview' => $preview_url, 'attachment' => $attachment, 'uri_id' => $item['uri-id']]; } } @@ -3385,7 +3526,8 @@ class Item 'text' => '', 'title' => $attachment['name'] ?? '', 'type' => 'link', - 'url' => $attachment['url']]; + 'url' => $attachment['url'] + ]; if ($preview && !empty($attachment['preview'])) { if ($attachment['preview-width'] >= 500) { @@ -3418,7 +3560,7 @@ class Item DI::profiler()->stopRecording(); if (isset($data['url']) && !in_array(strtolower($data['url']), $ignore_links)) { - if (!empty($data['description']) || !empty($data['image']) || !empty($data['preview'])) { + if (!empty($data['description']) || !empty($data['image']) || !empty($data['preview']) || (!empty($data['title']) && !Strings::compareLink($data['title'], $data['url']))) { $parts = parse_url($data['url']); if (!empty($parts['scheme']) && !empty($parts['host'])) { if (empty($data['provider_name'])) { @@ -3475,8 +3617,13 @@ class Item continue; } - $author = ['uid' => 0, 'id' => $item['author-id'], - 'network' => $item['author-network'], 'url' => $item['author-link']]; + $author = [ + 'uid' => 0, + 'id' => $item['author-id'], + 'network' => $item['author-network'], + 'url' => $item['author-link'], + 'alias' => $item['author-alias'] + ]; $the_url = Contact::magicLinkByContact($author, $attachment['url']); $title = Strings::escapeHtml(trim(($attachment['description'] ?? '') ?: $attachment['url'])); @@ -3525,7 +3672,7 @@ class Item $summary = DI::l10n()->tt('%d voter.', '%d voters.', $question['voters']); } elseif (!empty($question['endtime'])) { $summary = DI::l10n()->t('Poll end: %s', Temporal::getRelativeDate($question['endtime'])); - } else { + } else { $summary = ''; } @@ -3534,7 +3681,7 @@ class Item '$options' => $options, '$summary' => $summary, ]); - } + } DI::profiler()->stopRecording(); return $content; } @@ -3563,8 +3710,13 @@ class Item ]; if (!empty($plink) && ($item['private'] == self::PRIVATE)) { - $author = ['uid' => 0, 'id' => $item['author-id'], - 'network' => $item['author-network'], 'url' => $item['author-link']]; + $author = [ + 'uid' => 0, + 'id' => $item['author-id'], + 'network' => $item['author-network'], + 'url' => $item['author-link'], + 'alias' => $item['author-alias'], + ]; $plink = Contact::magicLinkByContact($author, $plink); } @@ -3587,15 +3739,22 @@ class Item } /** - * Does the given uri-id belongs to a post that is sent as starting post to a forum? + * Does the given uri-id belongs to a post that is sent as starting post to a group? + * This does apply to posts that are sent via ! and not in parallel to a group via @ * * @param int $uri_id * - * @return boolean "true" when it is a forum post + * @return boolean "true" when it is a group post */ - public static function isForumPost(int $uri_id): bool + public static function isGroupPost(int $uri_id): bool { - foreach (Tag::getByURIId($uri_id, [Tag::EXCLUSIVE_MENTION]) as $tag) { + if (Post::exists(['private' => Item::PUBLIC, 'uri-id' => $uri_id])) { + return false; + } + + foreach (Tag::getByURIId($uri_id, [Tag::EXCLUSIVE_MENTION, Tag::AUDIENCE]) as $tag) { + // @todo Possibly check for a public audience in the future, see https://socialhub.activitypub.rocks/t/fep-1b12-group-federation/2724 + // and https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-1b12.md if (DBA::exists('contact', ['uid' => 0, 'nurl' => Strings::normaliseLink($tag['url']), 'contact-type' => Contact::TYPE_COMMUNITY])) { return true; }