use Friendica\Model\Item;
use Friendica\Model\ItemURI;
use Friendica\Model\Mail;
+use Friendica\Model\Search;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Protocol\Activity;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\JsonLD;
use Friendica\Util\Strings;
+use Text_LanguageDetect;
/**
* ActivityPub Processor Protocol class
} else {
$item['gravity'] = GRAVITY_COMMENT;
$item['object-type'] = Activity\ObjectType::COMMENT;
-
- // Ensure that the comment reaches all receivers of the referring post
- $activity['receiver'] = self::addReceivers($activity);
}
if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) {
$item['isForum'] = false;
if (!empty($activity['thread-completion'])) {
- // Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
- $item['causer-link'] = $item['owner-link'];
- $item['causer-id'] = $item['owner-id'];
+ if ($activity['thread-completion'] != $item['owner-id']) {
+ $actor = Contact::getById($activity['thread-completion'], ['url']);
+ $item['causer-link'] = $actor['url'];
+ $item['causer-id'] = $activity['thread-completion'];
+ Logger::info('Use inherited actor as causer.', ['id' => $item['owner-id'], 'activity' => $activity['thread-completion'], 'owner' => $item['owner-link'], 'actor' => $actor['url']]);
+ } else {
+ // Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
+ $item['causer-link'] = $item['owner-link'];
+ $item['causer-id'] = $item['owner-id'];
+ Logger::info('Use actor as causer.', ['id' => $item['owner-id'], 'actor' => $item['owner-link']]);
+ }
- Logger::info('Ignoring actor because of thread completion.', ['actor' => $item['owner-link']]);
$item['owner-link'] = $item['author-link'];
$item['owner-id'] = $item['author-id'];
} else {
$item = self::processContent($activity, $item);
if (empty($item)) {
+ Logger::info('Message was not processed');
return [];
}
}
}
- /**
- * Add users to the receiver list of the given public activity.
- * This is used to ensure that the activity will be stored in every thread.
- *
- * @param array $activity Activity array
- * @return array Modified receiver list
- */
- private static function addReceivers(array $activity)
- {
- if (!in_array(0, $activity['receiver'])) {
- // Private activities will not be modified
- return $activity['receiver'];
- }
-
- // Add all owners of the referring item to the receivers
- $original = $receivers = $activity['receiver'];
- $items = Item::select(['uid'], ['uri' => $activity['object_id']]);
- while ($item = DBA::fetch($items)) {
- $receivers['uid:' . $item['uid']] = $item['uid'];
- }
- DBA::close($items);
-
- if (count($original) != count($receivers)) {
- Logger::info('Improved data', ['id' => $activity['id'], 'object' => $activity['object_id'], 'original' => $original, 'improved' => $receivers]);
- }
-
- return $receivers;
- }
-
/**
* Prepare the item array for an activity
*
$item['diaspora_signed_text'] = $activity['diaspora:like'] ?? '';
- $activity['receiver'] = self::addReceivers($activity);
-
self::postItem($activity, $item);
}
}
$stored = false;
+ ksort($activity['receiver']);
foreach ($activity['receiver'] as $receiver) {
if ($receiver == -1) {
$item['uid'] = $receiver;
+ $type = $activity['reception_type'][$receiver] ?? Receiver::TARGET_UNKNOWN;
+ switch($type) {
+ case Receiver::TARGET_TO:
+ $item['post-type'] = Item::PT_TO;
+ break;
+ case Receiver::TARGET_CC:
+ $item['post-type'] = Item::PT_CC;
+ break;
+ case Receiver::TARGET_BTO:
+ $item['post-type'] = Item::PT_BTO;
+ break;
+ case Receiver::TARGET_BCC:
+ $item['post-type'] = Item::PT_BCC;
+ break;
+ case Receiver::TARGET_FOLLOWER:
+ $item['post-type'] = Item::PT_FOLLOWER;
+ break;
+ case Receiver::TARGET_ANSWER:
+ $item['post-type'] = Item::PT_COMMENT;
+ break;
+ case Receiver::TARGET_GLOBAL:
+ $item['post-type'] = Item::PT_GLOBAL;
+ break;
+ default:
+ $item['post-type'] = Item::PT_ARTICLE;
+ }
+
+ if (!empty($activity['from-relay'])) {
+ $item['post-type'] = Item::PT_RELAY;
+ } elseif (!empty($activity['thread-completion'])) {
+ $item['post-type'] = Item::PT_FETCHED;
+ }
+
if ($item['isForum'] ?? false) {
$item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver);
} else {
/**
* Fetches missing posts
*
- * @param string $url message URL
- * @param array $child activity array with the child of this message
+ * @param string $url message URL
+ * @param array $child activity array with the child of this message
+ * @param string $relay_actor Relay actor
* @return string fetched message URL
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
- public static function fetchMissingActivity(string $url, array $child = [])
+ public static function fetchMissingActivity(string $url, array $child = [], string $relay_actor = '')
{
if (!empty($child['receiver'])) {
$uid = ActivityPub\Receiver::getFirstUserFromReceivers($child['receiver']);
return '';
}
- if (!empty($child['author'])) {
- $actor = $child['author'];
- } elseif (!empty($object['actor'])) {
- $actor = $object['actor'];
+ if (!empty($object['actor'])) {
+ $object_actor = $object['actor'];
} elseif (!empty($object['attributedTo'])) {
- $actor = $object['attributedTo'];
+ $object_actor = $object['attributedTo'];
} else {
// Shouldn't happen
- $actor = '';
+ $object_actor = '';
+ }
+
+ $signer = [$object_actor];
+
+ if (!empty($child['author'])) {
+ $actor = $child['author'];
+ $signer[] = $actor;
+ } else {
+ $actor = $object_actor;
}
if (!empty($object['published'])) {
$ldactivity = JsonLD::compact($activity);
- $ldactivity['thread-completion'] = true;
+ if (!empty($relay_actor)) {
+ $ldactivity['thread-completion'] = $ldactivity['from-relay'] = Contact::getIdForURL($relay_actor);
+ } elseif (!empty($child['thread-completion'])) {
+ $ldactivity['thread-completion'] = $child['thread-completion'];
+ } else {
+ $ldactivity['thread-completion'] = Contact::getIdForURL($actor);
+ }
- ActivityPub\Receiver::processActivity($ldactivity, json_encode($activity));
+ if (!empty($relay_actor) && !self::acceptIncomingMessage($ldactivity, $object['id'])) {
+ return '';
+ }
+
+ ActivityPub\Receiver::processActivity($ldactivity, json_encode($activity), $uid, true, false, $signer);
Logger::notice('Activity had been fetched and processed.', ['url' => $url, 'object' => $activity['id']]);
return $activity['id'];
}
+ /**
+ * Test if incoming relay messages should be accepted
+ *
+ * @param array $activity activity array
+ * @param string $id object ID
+ * @return boolean true if message is accepted
+ */
+ private static function acceptIncomingMessage(array $activity, string $id)
+ {
+ if (empty($activity['as:object'])) {
+ Logger::info('No object field in activity - accepted', ['id' => $id]);
+ return true;
+ }
+
+ $config = DI::config();
+
+ $subscribe = $config->get('system', 'relay_subscribe', false);
+ if ($subscribe) {
+ $scope = $config->get('system', 'relay_scope', SR_SCOPE_ALL);
+ } else {
+ $scope = SR_SCOPE_NONE;
+ }
+
+ if ($scope == SR_SCOPE_ALL) {
+ Logger::info('Server accept all posts - accepted', ['id' => $id]);
+ return true;
+ }
+
+ $replyto = JsonLD::fetchElement($activity['as:object'], 'as:inReplyTo', '@id');
+ if (Item::exists(['uri' => $replyto])) {
+ Logger::info('Post is a reply to an existing post - accepted', ['id' => $id, 'replyto' => $replyto]);
+ return true;
+ }
+
+ if ($scope == SR_SCOPE_NONE) {
+ Logger::info('Server does not accept relay posts - rejected', ['id' => $id]);
+ return false;
+ }
+
+ $messageTags = [];
+ $tags = Receiver::processTags(JsonLD::fetchElementArray($activity['as:object'], 'as:tag') ?? []);
+ if (!empty($tags)) {
+ foreach ($tags as $tag) {
+ if ($tag['type'] != 'Hashtag') {
+ continue;
+ }
+ $messageTags[] = ltrim(mb_strtolower($tag['name']), '#');
+ }
+ }
+
+ $systemTags = [];
+ $userTags = [];
+
+ if ($scope == SR_SCOPE_TAGS) {
+ $server_tags = $config->get('system', 'relay_server_tags', []);
+ $tagitems = explode(',', mb_strtolower($server_tags));
+
+ foreach ($tagitems AS $tag) {
+ $systemTags[] = trim($tag, '# ');
+ }
+
+ if ($config->get('system', 'relay_user_tags')) {
+ $userTags = Search::getUserTags();
+ }
+ }
+
+ $content = mb_strtolower(BBCode::toPlaintext(HTML::toBBCode(JsonLD::fetchElement($activity['as:object'], 'as:content', '@value')), false));
+
+ $tagList = array_unique(array_merge($systemTags, $userTags));
+ foreach ($messageTags as $tag) {
+ if (in_array($tag, $tagList)) {
+ Logger::info('Subscribed hashtag found - accepted', ['id' => $id, 'hashtag' => $tag]);
+ return true;
+ }
+ // We check with "strpos" for performance issues. Only when this is true, the regular expression check is used
+ // RegExp is taken from here: https://medium.com/@shiba1014/regex-word-boundaries-with-unicode-207794f6e7ed
+ if ((strpos($content, $tag) !== false) && preg_match('/(?<=[\s,.:;"\']|^)' . preg_quote($tag, '/') . '(?=[\s,.:;"\']|$)/', $content)) {
+ Logger::info('Subscribed hashtag found in content - accepted', ['id' => $id, 'hashtag' => $tag]);
+ return true;
+ }
+ }
+
+ Logger::info('No matching hashtags found - rejected', ['id' => $id]);
+ return false;
+ }
+
/**
* perform a "follow" request
*
}
$owner = User::getOwnerDataById($uid);
+ if (empty($owner)) {
+ return;
+ }
$cid = Contact::getIdForURL($activity['actor'], $uid);
if (!empty($cid)) {
}
$owner = User::getOwnerDataById($uid);
+ if (empty($owner)) {
+ return;
+ }
$cid = Contact::getIdForURL($activity['actor'], $uid);
if (empty($cid)) {