diff --git a/mod/display.php b/mod/display.php index 047f752c9..1b4508c18 100644 --- a/mod/display.php +++ b/mod/display.php @@ -80,7 +80,7 @@ function display_init(App $a) if (ActivityPub::isRequest()) { $wall_item = Item::selectFirst(['id', 'uid'], ['guid' => $item['guid'], 'wall' => true]); if ($wall_item['uid'] == 180) { - $data = ActivityPub::createActivityFromItem($wall_item['id']); + $data = ActivityPub::createObjectFromItemID($wall_item['id']); echo json_encode($data); exit(); } diff --git a/src/Model/Item.php b/src/Model/Item.php index b9cc6c2b9..e590c0c5c 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2851,7 +2851,7 @@ class Item extends BaseObject } // returns an array of contact-ids that are allowed to see this object - private static function enumeratePermissions($obj) + public static function enumeratePermissions($obj) { $allow_people = expand_acl($obj['allow_cid']); $allow_groups = Group::expand(expand_acl($obj['allow_gid'])); diff --git a/src/Model/Term.php b/src/Model/Term.php index a81241cb4..262312532 100644 --- a/src/Model/Term.php +++ b/src/Model/Term.php @@ -33,6 +33,17 @@ class Term return $tag_text; } + public static function tagArrayFromItemId($itemid) + { + $condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_HASHTAG, TERM_MENTION]]; + $tags = DBA::select('term', ['type', 'term', 'url'], $condition); + if (!DBA::isResult($tags)) { + return []; + } + + return DBA::toArray($tags); + } + public static function fileTextFromItemId($itemid) { $file_text = ''; diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 9fb22b2bd..c148e26c6 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -13,6 +13,7 @@ use Friendica\Core\Protocol; use Friendica\Model\Conversation; use Friendica\Model\Contact; use Friendica\Model\Item; +use Friendica\Model\Term; use Friendica\Model\User; use Friendica\Util\DateTimeFormat; use Friendica\Util\Crypto; @@ -153,6 +154,111 @@ class ActivityPub return $data; } + public static function createPermissionBlockForItem($item) + { + $data = ['to' => [], 'cc' => []]; + + $terms = Term::tagArrayFromItemId($item['id']); + + if (!$item['private']) { + $data['to'][] = self::PUBLIC; + $data['cc'][] = System::baseUrl() . '/followers/' . $item['author-nick']; + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $profile = Probe::uri($term['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $data['cc'][] = $profile['url']; + } + } + } else { + $receiver_list = Item::enumeratePermissions($item); + + $mentioned = []; + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $cid = Contact::getIdForURL($term['url'], $item['uid']); + if (!empty($cid) && in_array($cid, $receiver_list)) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]); + $data['to'][] = $contact['url']; + } + } + + foreach ($receiver_list as $receiver) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); + $data['cc'][] = $contact['url']; + } + + if (empty($data['to'])) { + $data['to'] = $data['cc']; + unset($data['cc']); + } + } + + return $data; + } + + public static function fetchTargetInboxes($item) + { + $inboxes = []; + + $terms = Term::tagArrayFromItemId($item['id']); + if (!$item['private']) { + $contacts = DBA::select('contact', ['notify', 'batch'], ['uid' => $item['uid'], 'network' => Protocol::ACTIVITYPUB]); + while ($contact = DBA::fetch($contacts)) { + $contact = defaults($contact, 'batch', $contact['notify']); + $inboxes[$contact] = $contact; + } + DBA::close($contacts); + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $profile = Probe::uri($term['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $target = defaults($profile, 'batch', $profile['notify']); + $inboxes[$target] = $target; + } + } + } else { + $receiver_list = Item::enumeratePermissions($item); + + $mentioned = []; + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $cid = Contact::getIdForURL($term['url'], $item['uid']); + if (!empty($cid) && in_array($cid, $receiver_list)) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]); + $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $target = defaults($profile, 'batch', $profile['notify']); + $inboxes[$target] = $target; + } + } + } + + foreach ($receiver_list as $receiver) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); + $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $target = defaults($profile, 'batch', $profile['notify']); + $inboxes[$target] = $target; + } + } + } + + return $inboxes; + } + public static function createActivityFromItem($item_id) { $item = Item::selectFirst([], ['id' => $item_id]); @@ -177,13 +283,34 @@ class ActivityPub 'toot' => 'http://joinmastodon.org/ns#']]]; $data['type'] = 'Create'; - $data['id'] = $item['uri'] . '/activity'; + $data['id'] = $item['uri'] . '#activity'; $data['actor'] = $item['author-link']; - $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; + $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item)); + $data['object'] = self::createNote($item); return $data; } + public static function createObjectFromItemID($item_id) + { + $item = Item::selectFirst([], ['id' => $item_id]); + + if (!DBA::isResult($item)) { + return false; + } + + $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', + ['Emoji' => 'toot:Emoji', 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', + 'conversation' => 'ostatus:conversation', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', + 'ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive', + 'toot' => 'http://joinmastodon.org/ns#']]]; + + $data = array_merge($data, self::createNote($item)); + + + return $data; + } + public static function createNote($item) { $data = []; @@ -203,9 +330,7 @@ class ActivityPub $data['context'] = $data['conversation'] = $conversation_uri; $data['actor'] = $item['author-link']; - if (!$item['private']) { - $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; - } + $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item)); $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); $data['attributedTo'] = $item['author-link']; diff --git a/src/Worker/APDelivery.php b/src/Worker/APDelivery.php new file mode 100644 index 000000000..b7e881c7a --- /dev/null +++ b/src/Worker/APDelivery.php @@ -0,0 +1,28 @@ + $item_id]); + $data = ActivityPub::createActivityFromItem($item_id); + ActivityPub::transmit($data, $inbox, $item['uid']); + } + + return; + } +} diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index 8ee00af63..e0a5c09c2 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -15,7 +15,6 @@ use Friendica\Model\Item; use Friendica\Model\Queue; use Friendica\Model\User; use Friendica\Protocol\DFRN; -use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; use Friendica\Protocol\Email; @@ -166,10 +165,6 @@ class Delivery extends BaseObject switch ($contact['network']) { - case Protocol::ACTIVITYPUB: - self::deliverActivityPub($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); - break; - case Protocol::DFRN: self::deliverDFRN($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); break; @@ -388,80 +383,6 @@ class Delivery extends BaseObject logger('Unknown mode ' . $cmd . ' for ' . $loc); } - /** - * @brief Deliver content via ActivityPub -q * - * @param string $cmd Command - * @param array $contact Contact record of the receiver - * @param array $owner Owner record of the sender - * @param array $items Item record of the content and the parent - * @param array $target_item Item record of the content - * @param boolean $public_message Is the content public? - * @param boolean $top_level Is it a thread starter? - * @param boolean $followup Is it an answer to a remote post? - */ - private static function deliverActivityPub($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup) - { - // We don't treat Forum posts as "wall-to-wall" to be able to post them via ActivityPub - $walltowall = $top_level && ($owner['id'] != $items[0]['contact-id']) & ($owner['account-type'] != Contact::ACCOUNT_TYPE_COMMUNITY); - - if ($public_message) { - $loc = 'public batch ' . $contact['batch']; - } else { - $loc = $contact['addr']; - } - - logger('Deliver ' . $target_item["guid"] . ' via ActivityPub to ' . $loc); - -// if (Config::get('system', 'dfrn_only') || !Config::get('system', 'diaspora_enabled')) { -// return; -// } - if ($cmd == self::MAIL) { -// ActivityPub::sendMail($target_item, $owner, $contact); - return; - } - - if ($cmd == self::SUGGESTION) { - return; - } -// if (!$contact['pubkey'] && !$public_message) { -// logger('No public key, no delivery.'); -// return; -// } - if (($target_item['deleted']) && (($target_item['uri'] === $target_item['parent-uri']) || $followup)) { - // top-level retraction - logger('ActivityPub retract: ' . $loc); -// ActivityPub::sendRetraction($target_item, $owner, $contact, $public_message); - return; - } elseif ($cmd == self::RELOCATION) { -// ActivityPub::sendAccountMigration($owner, $contact, $owner['uid']); - return; - } elseif ($followup) { - // send comments and likes to owner to relay - logger('ActivityPub followup: ' . $loc); - $data = ActivityPub::createActivityFromItem($target_item['id']); - ActivityPub::transmit($data, $contact['notify'], $owner['uid']); -// ActivityPub::sendFollowup($target_item, $owner, $contact, $public_message); - return; - } elseif ($target_item['uri'] !== $target_item['parent-uri']) { - // we are the relay - send comments, likes and relayable_retractions to our conversants - logger('ActivityPub relay: ' . $loc); - $data = ActivityPub::createActivityFromItem($target_item['id']); - ActivityPub::transmit($data, $contact['notify'], $owner['uid']); -// ActivityPub::sendRelay($target_item, $owner, $contact, $public_message); - return; - } elseif ($top_level && !$walltowall) { - // currently no workable solution for sending walltowall - logger('ActivityPub status: ' . $loc); - $data = ActivityPub::createActivityFromItem($target_item['id']); - ActivityPub::transmit($data, $contact['notify'], $owner['uid']); -// ActivityPub::sendStatus($target_item, $owner, $contact, $public_message); - return; - } - - logger('Unknown mode ' . $cmd . ' for ' . $loc); - } - /** * @brief Deliver content via mail * diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index 6a3718618..693a9e343 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -16,6 +16,7 @@ use Friendica\Model\Item; use Friendica\Model\PushSubscriber; use Friendica\Model\User; use Friendica\Network\Probe; +use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; use Friendica\Protocol\OStatus; use Friendica\Protocol\Salmon; @@ -363,9 +364,9 @@ class Notifier } // It only makes sense to distribute answers to OStatus messages to Friendica and OStatus - but not Diaspora - $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DFRN]; + $networks = [Protocol::OSTATUS, Protocol::DFRN]; } else { - $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL]; + $networks = [Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL]; } } else { $public_message = false; @@ -413,6 +414,14 @@ class Notifier } } + $inboxes = ActivityPub::fetchTargetInboxes($target_item); + foreach ($inboxes as $inbox) { + logger('Deliver ' . $item_id .' to ' . $inbox .' via ActivityPub', LOGGER_DEBUG); + + Worker::add(['priority' => $a->queue['priority'], 'created' => $a->queue['created'], 'dont_fork' => true], + 'APDelivery', $cmd, $item_id, $inbox); + } + // send salmon slaps to mentioned remote tags (@foo@example.com) in OStatus posts // They are especially used for notifications to OStatus users that don't follow us. if (!Config::get('system', 'dfrn_only') && count($url_recipients) && ($public_message || $push_notify) && $normal_mode) { @@ -448,7 +457,7 @@ class Notifier } } - $condition = ['network' => [Protocol::DFRN, Protocol::ACTIVITYPUB], 'uid' => $owner['uid'], 'blocked' => false, + $condition = ['network' => Protocol::DFRN, 'uid' => $owner['uid'], 'blocked' => false, 'pending' => false, 'archive' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]; $r2 = DBA::toArray(DBA::select('contact', ['id', 'name', 'network'], $condition));