/**
* Prepare the object array
*
- * @param array $activity Array with activity data
- * @param integer $uid User ID
- * @param boolean $push Message had been pushed to our system
- * @param boolean $trust_source Do we trust the source?
+ * @param array $activity Array with activity data
+ * @param integer $uid User ID
+ * @param boolean $push Message had been pushed to our system
+ * @param boolean $trust_source Do we trust the source?
+ * @param string $original_actor Actor of the original activity. Used for receiver detection. (Optional)
*
* @return array with object data
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
- public static function prepareObjectData(array $activity, int $uid, bool $push, bool &$trust_source): array
+ public static function prepareObjectData(array $activity, int $uid, bool $push, bool &$trust_source, string $original_actor = ''): array
{
$id = JsonLD::fetchElement($activity, '@id');
$type = JsonLD::fetchElement($activity, '@type');
$fetched = false;
if (!empty($id) && !$trust_source) {
- $fetch_uid = $uid ?: self::getBestUserForActivity($activity);
+ $fetch_uid = $uid ?: self::getBestUserForActivity($activity, $original_actor);
$fetched_activity = Processor::fetchCachedActivity($fetch_id, $fetch_uid);
if (!empty($fetched_activity)) {
$type = JsonLD::fetchElement($activity, '@type');
// Fetch all receivers from to, cc, bto and bcc
- $receiverdata = self::getReceivers($activity, $actor, [], false, $push || $fetched);
+ $receiverdata = self::getReceivers($activity, $original_actor ?: $actor, [], false, $push || $fetched);
$receivers = $reception_types = [];
foreach ($receiverdata as $key => $data) {
$receivers[$key] = $data['uid'];
// We possibly need some user to fetch private content,
// so we fetch one out of the receivers if no uid is provided.
- $fetch_uid = $uid ?: self::getBestUserForActivity($activity);
+ $fetch_uid = $uid ?: self::getBestUserForActivity($activity, $original_actor);
$object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
if (empty($object_id)) {
$object_type = self::fetchObjectType($activity, $object_id, $fetch_uid);
- // Fetch the activity on Lemmy "Announce" messages (announces of activities)
- if (($type == 'as:Announce') && in_array($object_type, array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) {
- Logger::debug('Fetch announced activity', ['object' => $object_id, 'uid' => $fetch_uid]);
- $data = Processor::fetchCachedActivity($object_id, $fetch_uid);
- if (!empty($data)) {
- $type = $object_type;
- $announced_activity = JsonLD::compact($data);
-
- // Some variables need to be refetched since the activity changed
- $actor = JsonLD::fetchElement($announced_activity, 'as:actor', '@id');
- $announced_id = JsonLD::fetchElement($announced_activity, 'as:object', '@id');
- if (empty($announced_id)) {
- Logger::warning('No object id in announced activity', ['id' => $object_id, 'activity' => $activity, 'announced' => $announced_activity]);
- return [];
- } else {
- $activity = $announced_activity;
- $object_id = $announced_id;
- }
- $object_type = self::fetchObjectType($activity, $object_id, $fetch_uid);
- }
- }
-
// Any activities on account types must not be altered
if (in_array($type, ['as:Flag'])) {
$object_data = [];
} elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Announce', 'as:Follow'])) && in_array($object_type, self::CONTENT_TYPES)) {
// Create a mostly empty array out of the activity data (instead of the object).
// This way we later don't have to check for the existence of each individual array element.
- $object_data = self::processObject($activity);
+ $object_data = self::processObject($activity, $original_actor);
$object_data['name'] = $type;
$object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id');
$object_data['object_id'] = $object_id;
}
}
+ // Lemmy announces activities.
+ // To simplify the further processing, we modify the received object.
+ // For announced "create" activities we remove the middle layer.
+ // For the rest (like, dislike, update, ...) we just process the activity directly.
+ $original_actor = '';
+ $object_type = JsonLD::fetchElement($activity['as:object'] ?? [], '@type');
+ if (($type == 'as:Announce') && !empty($object_type) && !in_array($object_type, self::CONTENT_TYPES) && self::isGroup($actor)) {
+ $object_object_type = JsonLD::fetchElement($activity['as:object']['as:object'] ?? [], '@type');
+ if (in_array($object_type, ['as:Create']) && in_array($object_object_type, self::CONTENT_TYPES)) {
+ Logger::debug('Replace "create" activity with inner object', ['type' => $object_type, 'object_type' => $object_object_type]);
+ $activity['as:object'] = $activity['as:object']['as:object'];
+ } elseif (in_array($object_type, array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) {
+ Logger::debug('Change announced activity to activity', ['type' => $object_type]);
+ $original_actor = $actor;
+ $type = $object_type;
+ $activity = $activity['as:object'];
+ } else {
+ Logger::info('Unhandled announced activity', ['type' => $object_type, 'object_type' => $object_object_type]);
+ }
+ }
+
// $trust_source is called by reference and is set to true if the content was retrieved successfully
- $object_data = self::prepareObjectData($activity, $uid, $push, $trust_source);
+ $object_data = self::prepareObjectData($activity, $uid, $push, $trust_source, $original_actor);
if (empty($object_data)) {
- Logger::info('No object data found', ['activity' => $activity]);
+ Logger::info('No object data found', ['activity' => $activity, 'callstack' => System::callstack(20)]);
return true;
}
- // Lemmy is announcing activities.
- // We are changing the announces into regular activities.
- if (($type == 'as:Announce') && in_array($object_data['type'] ?? '', array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) {
- Logger::debug('Change type of announce to activity', ['type' => $object_data['type']]);
- $type = $object_data['type'];
- }
-
if (!empty($body) && empty($object_data['raw'])) {
$object_data['raw'] = $body;
}
return true;
}
+ /**
+ * Checks if the provided actor is a group account
+ *
+ * @param string $actor
+ * @return boolean
+ */
+ private static function isGroup(string $actor): bool
+ {
+ $profile = APContact::getByURL($actor);
+ return ($profile['type'] ?? '') == 'Group';
+ }
+
/**
* Route activities
*
*
* @return int user id
*/
- public static function getBestUserForActivity(array $activity): int
+ public static function getBestUserForActivity(array $activity, string $actor = ''): int
{
$uid = 0;
- $actor = JsonLD::fetchElement($activity, 'as:actor', '@id') ?? '';
+ $actor = $actor ?: JsonLD::fetchElement($activity, 'as:actor', '@id') ?? '';
$receivers = self::getReceivers($activity, $actor, [], false, false);
foreach ($receivers as $receiver) {
}
// Fetch the receivers for the public and the followers collection
- if ((($receiver == $followers) || (($receiver == self::PUBLIC_COLLECTION) && !$isGroup)) && !empty($actor)) {
+ if ((($receiver == $followers) || (($receiver == self::PUBLIC_COLLECTION) && !$isGroup) || ($isGroup && ($element == 'as:audience'))) && !empty($actor)) {
$receivers = self::getReceiverForActor($actor, $tags, $receivers, $follower_target, $profile);
continue;
}
// "birdsitelive" is a service that mirrors tweets into the fediverse
// These posts can be fetched without authentication, but are not marked as public
// We treat them as unlisted posts to be able to handle them.
+ // We always process deletion activities.
+ $activity_type = JsonLD::fetchElement($activity, '@type');
if (empty($receivers) && $fetch_unlisted && Contact::isPlatform($actor, 'birdsitelive')) {
$receivers[0] = ['uid' => 0, 'type' => self::TARGET_GLOBAL];
$receivers[-1] = ['uid' => -1, 'type' => self::TARGET_GLOBAL];
Logger::notice('Post from "birdsitelive" is set to "unlisted"', ['id' => JsonLD::fetchElement($activity, '@id')]);
+ } elseif (empty($receivers) && in_array($activity_type, ['as:Delete', 'as:Undo'])) {
+ $receivers[0] = ['uid' => 0, 'type' => self::TARGET_GLOBAL];
} elseif (empty($receivers)) {
- Logger::notice('Post has got no receivers', ['fetch_unlisted' => $fetch_unlisted, 'actor' => $actor, 'id' => JsonLD::fetchElement($activity, '@id'), 'type' => JsonLD::fetchElement($activity, '@type')]);
+ Logger::notice('Post has got no receivers', ['fetch_unlisted' => $fetch_unlisted, 'actor' => $actor, 'id' => JsonLD::fetchElement($activity, '@id'), 'type' => $activity_type, 'callstack' => System::callstack(20)]);
}
return $receivers;
return false;
}
- // Lemmy is resharing "create" activities instead of content
- // We fetch the content from the activity.
- if (in_array($type, ['as:Create'])) {
- $object = $object['as:object'];
- $type = JsonLD::fetchElement($object, '@type');
- if (empty($type)) {
- Logger::info('Empty type');
- return false;
- }
- $object_data = self::processObject($object);
- }
-
// We currently don't handle 'pt:CacheFile', but with this step we avoid logging
if (in_array($type, self::CONTENT_TYPES) || ($type == 'pt:CacheFile')) {
- $object_data = self::processObject($object);
+ $object_data = self::processObject($object, '');
if (!empty($data)) {
$object_data['raw-object'] = json_encode($data);
/**
* Fetches data from the object part of an activity
*
- * @param array $object
+ * @param array $object
+ * @param string $actor
*
* @return array|bool Object data or FALSE if $object does not contain @id element
* @throws \Exception
*/
- private static function processObject(array $object)
+ private static function processObject(array $object, string $actor)
{
if (!JsonLD::fetchElement($object, '@id')) {
return false;
$object_data = self::getObjectDataFromActivity($object);
- $receiverdata = self::getReceivers($object, $object_data['actor'] ?? '', $object_data['tags'], true, false);
+ $receiverdata = self::getReceivers($object, $actor ?: $object_data['actor'] ?? '', $object_data['tags'], true, false);
$receivers = $reception_types = [];
foreach ($receiverdata as $key => $data) {
$receivers[$key] = $data['uid'];
* Returns an array with permissions of the thread parent of the given item array
*
* @param array $item
- * @param bool $is_group_thread
*
* @return array with permissions
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
- private static function fetchPermissionBlockFromThreadParent(array $item, bool $is_group_thread): array
+ private static function fetchPermissionBlockFromThreadParent(array $item): array
{
if (empty($item['thr-parent-id'])) {
return [];
'cc' => [],
'bto' => [],
'bcc' => [],
+ 'audience' => [],
];
$parent_profile = APContact::getByURL($parent['author-link']);
$exclude[] = $item['owner-link'];
}
- $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) {
+ $type = [Tag::TO => 'to', Tag::CC => 'cc', Tag::BTO => 'bto', Tag::BCC => 'bcc', Tag::AUDIENCE => 'audience'];
+ foreach (Tag::getByURIId($item['thr-parent-id'], [Tag::TO, Tag::CC, Tag::BTO, Tag::BCC, Tag::AUDIENCE]) as $receiver) {
if (!empty($parent_profile['followers']) && $receiver['url'] == $parent_profile['followers'] && !empty($item_profile['followers'])) {
- if (!$is_group_thread) {
- $permissions[$type[$receiver['type']]][] = $item_profile['followers'];
- }
+ $permissions[$type[$receiver['type']]][] = $item_profile['followers'];
} elseif (!in_array($receiver['url'], $exclude)) {
$permissions[$type[$receiver['type']]][] = $receiver['url'];
}
$is_group_thread = false;
}
+ $exclusive = false;
+ $mention = false;
+
+ $parent_tags = Tag::getByURIId($item['parent-uri-id'], [Tag::AUDIENCE, Tag::MENTION]);
+ if (!empty($parent_tags)) {
+ $is_group_thread = false;
+ foreach ($parent_tags as $tag) {
+ if ($tag['type'] != Tag::AUDIENCE) {
+ continue;
+ }
+ $profile = APContact::getByURL($tag['url'], false);
+ if (!empty($profile) && ($profile['type'] == 'Group')) {
+ $is_group_thread = true;
+ }
+ }
+ if ($is_group_thread) {
+ foreach ($parent_tags as $tag) {
+ if (($tag['type'] == Tag::MENTION) && ($tag['url'] == $profile['url'])) {
+ $mention = false;
+ }
+ }
+ $exclusive = !$mention;
+ }
+ } elseif ($is_group_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;
+ }
+ }
+ }
+ }
+
if (self::isAnnounce($item) || self::isAPPost($last_id)) {
// Will be activated in a later step
$networks = Protocol::FEDERATED;
$actor_profile = APContact::getByURL($item['author-link']);
}
- $exclusive = false;
- $mention = false;
-
- if ($is_group_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]);
$data['cc'][] = $announce['actor']['url'];
}
- $data = array_merge($data, self::fetchPermissionBlockFromThreadParent($item, $is_group_thread));
+ if (!$is_group_thread) {
+ $data = array_merge($data, self::fetchPermissionBlockFromThreadParent($item));
+ }
// Check if the item is completely public or unlisted
if ($item['private'] == Item::PUBLIC) {
}
}
- if (!empty($item['parent'])) {
+ if (!empty($item['parent']) && (!$is_group_thread || ($item['private'] == Item::PRIVATE))) {
if ($item['private'] == Item::PRIVATE) {
$condition = ['parent' => $item['parent'], 'uri-id' => $item['thr-parent-id']];
} else {
}
}
- $receivers = ['to' => array_values($data['to']), 'cc' => array_values($data['cc']), 'bcc' => array_values($data['bcc'])];
-
- if (!empty($data['audience'])) {
- $receivers['audience'] = array_values($data['audience']);
- if (count($receivers['audience']) == 1) {
- $receivers['audience'] = $receivers['audience'][0];
- }
- }
+ $receivers = ['to' => array_values($data['to']), 'cc' => array_values($data['cc']), 'bcc' => array_values($data['bcc']), 'audience' => array_values($data['audience'])];
if (!$blindcopy) {
unset($receivers['bcc']);
}
- foreach (['to' => Tag::TO, 'cc' => Tag::CC, 'bcc' => Tag::BCC] as $element => $type) {
+ foreach (['to' => Tag::TO, 'cc' => Tag::CC, 'bcc' => Tag::BCC, 'audience' => Tag::AUDIENCE] as $element => $type) {
if (!empty($receivers[$element])) {
foreach ($receivers[$element] as $receiver) {
if ($receiver == ActivityPub::PUBLIC_COLLECTION) {
}
}
+ if (!$blindcopy && count($receivers['audience']) == 1) {
+ $receivers['audience'] = $receivers['audience'][0];
+ } elseif (!$receivers['audience']) {
+ unset($receivers['audience']);
+ }
+
return $receivers;
}
$profile_uid = User::getIdForURL($item_profile['url']);
- foreach (['to', 'cc', 'bto', 'bcc'] as $element) {
+ foreach (['to', 'cc', 'bto', 'bcc', 'audience'] as $element) {
if (empty($permissions[$element])) {
continue;
}
} else {
$target = $profile['sharedinbox'];
}
- if (!self::archivedInbox($target)) {
+ if (!self::archivedInbox($target) && !in_array($contact['id'], $inboxes[$target] ?? [])) {
$inboxes[$target][] = $contact['id'] ?? 0;
}
}
unset($data['cc']);
unset($data['bcc']);
+ unset($data['audience']);
$object['to'] = $data['to'];
$object['tag'] = [['type' => 'Mention', 'href' => $object['to'][0], 'name' => '']];
unset($object['cc']);
unset($object['bcc']);
+ unset($object['audience']);
$data['directMessage'] = true;