diff --git a/src/Module/ActivityPub/Outbox.php b/src/Module/ActivityPub/Outbox.php index f2e88cafe6..01061d8c67 100644 --- a/src/Module/ActivityPub/Outbox.php +++ b/src/Module/ActivityPub/Outbox.php @@ -80,6 +80,6 @@ class Outbox extends BaseApi throw new \Friendica\Network\HTTPException\BadRequestException(); } - System::jsonExit(ActivityPub\Receiver::processC2SActivity($activity, $uid, self::getCurrentApplication() ?? [])); + System::jsonExit(ActivityPub\ClientToServer::processActivity($activity, $uid, self::getCurrentApplication() ?? [])); } } diff --git a/src/Protocol/ActivityPub/ClientToServer.php b/src/Protocol/ActivityPub/ClientToServer.php new file mode 100644 index 0000000000..c8ddcbfe93 --- /dev/null +++ b/src/Protocol/ActivityPub/ClientToServer.php @@ -0,0 +1,310 @@ +. + * + */ + +namespace Friendica\Protocol\ActivityPub; + +use Friendica\Content\Text\Markdown; +use Friendica\Core\Logger; +use Friendica\Core\Protocol; +use Friendica\DI; +use Friendica\Model\APContact; +use Friendica\Model\Contact; +use Friendica\Model\Group; +use Friendica\Model\Item; +use Friendica\Model\Post; +use Friendica\Model\User; +use Friendica\Protocol\Activity; +use Friendica\Util\JsonLD; + +/** + * ActivityPub Client To Server class + */ +class ClientToServer +{ + /** + * Process client to server activities + * + * @param array $activity + * @param integer $uid + * @param array $application + * @return array + */ + public static function processActivity(array $activity, int $uid, array $application): array + { + $ldactivity = JsonLD::compact($activity); + if (empty($ldactivity)) { + Logger::notice('Invalid activity', ['activity' => $activity, 'uid' => $uid]); + return []; + } + + $type = JsonLD::fetchElement($ldactivity, '@type'); + if (!$type) { + Logger::notice('Empty type', ['activity' => $ldactivity, 'uid' => $uid]); + return []; + } + + $object_id = JsonLD::fetchElement($ldactivity, 'as:object', '@id') ?? ''; + $object_type = Receiver::fetchObjectType($ldactivity, $object_id, $uid); + if (!$object_type && !$object_id) { + Logger::notice('Empty object type or id', ['activity' => $ldactivity, 'uid' => $uid]); + return []; + } + + Logger::debug('Processing activity', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'activity' => $ldactivity]); + return self::routeActivities($type, $object_type, $object_id, $uid, $application, $ldactivity); + } + + /** + * Route client to server activities + * + * @param string $type + * @param string $object_type + * @param string $object_id + * @param integer $uid + * @param array $application + * @param array $ldactivity + * @return array + */ + private static function routeActivities(string $type, string $object_type, string $object_id, int $uid, array $application, array $ldactivity): array + { + switch ($type) { + case 'as:Create': + if (in_array($object_type, Receiver::CONTENT_TYPES)) { + return self::createContent($uid, $application, $ldactivity); + } + break; + case 'as:Update': + if (in_array($object_type, Receiver::CONTENT_TYPES) && !empty($object_id)) { + return self::updateContent($uid, $object_id, $application, $ldactivity); + } + break; + case 'as:Follow': + if (in_array($object_type, Receiver::ACCOUNT_TYPES) && !empty($object_id)) { + return self::followAccount($uid, $object_id, $ldactivity); + } + break; + } + return []; + } + + /** + * Create a new post or comment + * + * @param integer $uid + * @param array $application + * @param array $ldactivity + * @return array + */ + private static function createContent(int $uid, array $application, array $ldactivity): array + { + $object_data = self::processObject($ldactivity['as:object']); + $item = ClientToServer::processContent($object_data, $application, $uid); + Logger::debug('Got data', ['item' => $item, 'object' => $object_data]); + + $id = Item::insert($item, true); + if (!empty($id)) { + $item = Post::selectFirst(['uri-id'], ['id' => $id]); + if (!empty($item['uri-id'])) { + return Transmitter::createActivityFromItem($id); + } + } + return []; + } + + /** + * Update an existing post or comment + * + * @param integer $uid + * @param string $object_id + * @param array $application + * @param array $ldactivity + * @return array + */ + private static function updateContent(int $uid, string $object_id, array $application, array $ldactivity):array + { + $id = Item::fetchByLink($object_id, $uid); + $original_post = Post::selectFirst(['uri-id'], ['uid' => $uid, 'origin' => true, 'id' => $id]); + if (empty($original_post)) { + Logger::debug('Item not found or does not belong to the user', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); + return []; + } + + $object_data = self::processObject($ldactivity['as:object']); + $item = ClientToServer::processContent($object_data, $application, $uid); + if (empty($item['title']) && empty($item['body'])) { + Logger::debug('Empty body and title', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); + return []; + } + $post = ['title' => $item['title'], 'body' => $item['body']]; + Logger::debug('Got data', ['id' => $id, 'uid' => $uid, 'item' => $post]); + Item::update($post, ['id' => $id]); + Item::updateDisplayCache($original_post['uri-id']); + + return Transmitter::createActivityFromItem($id); + } + + /** + * Follow a given account + * @todo Check the expected return value + * + * @param integer $uid + * @param string $object_id + * @param array $ldactivity + * @return array + */ + private static function followAccount(int $uid, string $object_id, array $ldactivity): array + { + return []; + } + + /** + * Fetches data from the object part of an client to server activity + * + * @param array $object + * + * @return array Object data + */ + private static function processObject(array $object): array + { + $object_data = Receiver::getObjectDataFromActivity($object); + + $object_data['target'] = self::getTargets($object, $object_data['actor'] ?? ''); + $object_data['receiver'] = []; + + return $object_data; + } + + /** + * Accumulate the targets and visibility of this post + * + * @param array $object + * @param string $actor + * @return array + */ + private static function getTargets(array $object, string $actor): array + { + $profile = APContact::getByURL($actor); + $followers = $profile['followers']; + + $targets = []; + + foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) { + switch ($element) { + case 'as:to': + $type = Receiver::TARGET_TO; + break; + case 'as:cc': + $type = Receiver::TARGET_CC; + break; + case 'as:bto': + $type = Receiver::TARGET_BTO; + break; + case 'as:bcc': + $type = Receiver::TARGET_BCC; + break; + } + $receiver_list = JsonLD::fetchElementArray($object, $element, '@id'); + if (empty($receiver_list)) { + continue; + } + + foreach ($receiver_list as $receiver) { + if ($receiver == Receiver::PUBLIC_COLLECTION) { + $targets[Receiver::TARGET_GLOBAL] = ($element == 'as:to'); + continue; + } + + if ($receiver == $followers) { + $targets[Receiver::TARGET_FOLLOWER] = true; + continue; + } + $targets[$type][] = Contact::getIdForURL($receiver); + } + } + return $targets; + } + + /** + * Create an item array from client to server object data + * + * @param array $object_data + * @param array $application + * @param integer $uid + * @return array + */ + private static function processContent(array $object_data, array $application, int $uid): array + { + $owner = User::getOwnerDataById($uid); + + $item = []; + + $item['network'] = Protocol::DFRN; + $item['uid'] = $uid; + $item['verb'] = Activity::POST; + $item['contact-id'] = $owner['id']; + $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); + $item['title'] = $object_data['name']; + $item['body'] = Markdown::toBBCode($object_data['content']); + $item['app'] = $application['name'] ?? 'API'; + + if (!empty($object_data['target'][Receiver::TARGET_GLOBAL])) { + $item['allow_cid'] = ''; + $item['allow_gid'] = ''; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + $item['private'] = Item::PUBLIC; + } elseif (isset($object_data['target'][Receiver::TARGET_GLOBAL])) { + $item['allow_cid'] = ''; + $item['allow_gid'] = ''; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + $item['private'] = Item::UNLISTED; + } elseif (!empty($object_data['target'][Receiver::TARGET_FOLLOWER])) { + $item['allow_cid'] = ''; + $item['allow_gid'] = '<' . Group::FOLLOWERS . '>'; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + $item['private'] = Item::PRIVATE; + } else { + // @todo Set permissions via the $object_data['target'] array + $item['allow_cid'] = '<' . $owner['id'] . '>'; + $item['allow_gid'] = ''; + $item['deny_cid'] = ''; + $item['deny_gid'] = ''; + $item['private'] = Item::PRIVATE; + } + + if (!empty($object_data['summary'])) { + $item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $object_data['summary'] . "[/abstract]\n" . $item['body']; + } + + if ($object_data['reply-to-id']) { + $item['thr-parent'] = $object_data['reply-to-id']; + $item['gravity'] = Item::GRAVITY_COMMENT; + } else { + $item['gravity'] = Item::GRAVITY_PARENT; + } + + $item = DI::contentItem()->expandTags($item); + + return $item; + } +} diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index dc618370a0..859e7a2451 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -2144,70 +2144,4 @@ class Processor return $body; } - - /** - * Create an item array from client to server object data - * - * @param array $object_data - * @param array $application - * @param integer $uid - * @return array - */ - public static function processC2SContent(array $object_data, array $application, int $uid): array - { - $owner = User::getOwnerDataById($uid); - - $item = []; - - $item['network'] = Protocol::DFRN; - $item['uid'] = $uid; - $item['verb'] = Activity::POST; - $item['contact-id'] = $owner['id']; - $item['author-id'] = $item['owner-id'] = Contact::getPublicIdByUserId($uid); - $item['title'] = $object_data['name']; - $item['body'] = Markdown::toBBCode($object_data['content']); - $item['app'] = $application['name'] ?? 'API'; - - if (!empty($object_data['target'][Receiver::TARGET_GLOBAL])) { - $item['allow_cid'] = ''; - $item['allow_gid'] = ''; - $item['deny_cid'] = ''; - $item['deny_gid'] = ''; - $item['private'] = Item::PUBLIC; - } elseif (isset($object_data['target'][Receiver::TARGET_GLOBAL])) { - $item['allow_cid'] = ''; - $item['allow_gid'] = ''; - $item['deny_cid'] = ''; - $item['deny_gid'] = ''; - $item['private'] = Item::UNLISTED; - } elseif (!empty($object_data['target'][Receiver::TARGET_FOLLOWER])) { - $item['allow_cid'] = ''; - $item['allow_gid'] = '<' . Group::FOLLOWERS . '>'; - $item['deny_cid'] = ''; - $item['deny_gid'] = ''; - $item['private'] = Item::PRIVATE; - } else { - // @todo Set permissions via the $object_data['target'] array - $item['allow_cid'] = '<' . $owner['id'] . '>'; - $item['allow_gid'] = ''; - $item['deny_cid'] = ''; - $item['deny_gid'] = ''; - $item['private'] = Item::PRIVATE; - } - - if (!empty($object_data['summary'])) { - $item['body'] = '[abstract=' . Protocol::ACTIVITYPUB . ']' . $object_data['summary'] . "[/abstract]\n" . $item['body']; - } - - if ($object_data['reply-to-id']) { - $item['thr-parent'] = $object_data['reply-to-id']; - $item['gravity'] = Item::GRAVITY_COMMENT; - } else { - $item['gravity'] = Item::GRAVITY_PARENT; - } - - $item = DI::contentItem()->expandTags($item); - - return $item; - } } diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index 42fcb50865..16257c2f95 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -255,7 +255,7 @@ class Receiver * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function fetchObjectType(array $activity, string $object_id, int $uid = 0) + public static function fetchObjectType(array $activity, string $object_id, int $uid = 0) { if (!empty($activity['as:object'])) { $object_type = JsonLD::fetchElement($activity['as:object'], '@type'); @@ -1873,23 +1873,6 @@ class Receiver return $object_data; } - /** - * Fetches data from the object part of an client to server activity - * - * @param array $object - * - * @return array Object data - */ - private static function processC2SObject(array $object): array - { - $object_data = self::getObjectDataFromActivity($object); - - $object_data['target'] = self::getTargets($object, $object_data['actor'] ?? ''); - $object_data['receiver'] = []; - - return $object_data; - } - /** * Create an object data array from a given activity * @@ -1897,7 +1880,7 @@ class Receiver * * @return array Object data */ - private static function getObjectDataFromActivity(array $object): array + public static function getObjectDataFromActivity(array $object): array { $object_data = []; $object_data['object_type'] = JsonLD::fetchElement($object, '@type'); @@ -2054,190 +2037,4 @@ class Receiver { return DBA::exists('arrived-activity', ['object-id' => $id]); } - - /** - * Process client to server activities - * - * @param array $activity - * @param integer $uid - * @param array $application - * @return array - */ - public static function processC2SActivity(array $activity, int $uid, array $application): array - { - $ldactivity = JsonLD::compact($activity); - if (empty($ldactivity)) { - Logger::notice('Invalid activity', ['activity' => $activity, 'uid' => $uid]); - return []; - } - - $type = JsonLD::fetchElement($ldactivity, '@type'); - if (!$type) { - Logger::notice('Empty type', ['activity' => $ldactivity, 'uid' => $uid]); - return []; - } - - $object_id = JsonLD::fetchElement($ldactivity, 'as:object', '@id') ?? ''; - $object_type = self::fetchObjectType($ldactivity, $object_id, $uid); - if (!$object_type && !$object_id) { - Logger::notice('Empty object type or id', ['activity' => $ldactivity, 'uid' => $uid]); - return []; - } - - Logger::debug('Processing activity', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'activity' => $ldactivity]); - return self::routeC2SActivities($type, $object_type, $object_id, $uid, $application, $ldactivity); - } - - /** - * Accumulate the targets and visibility of this post - * - * @param array $object - * @param string $actor - * @return array - */ - private static function getTargets(array $object, string $actor): array - { - $profile = APContact::getByURL($actor); - $followers = $profile['followers']; - - $targets = []; - - foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) { - switch ($element) { - case 'as:to': - $type = self::TARGET_TO; - break; - case 'as:cc': - $type = self::TARGET_CC; - break; - case 'as:bto': - $type = self::TARGET_BTO; - break; - case 'as:bcc': - $type = self::TARGET_BCC; - break; - } - $receiver_list = JsonLD::fetchElementArray($object, $element, '@id'); - if (empty($receiver_list)) { - continue; - } - - foreach ($receiver_list as $receiver) { - if ($receiver == self::PUBLIC_COLLECTION) { - $targets[self::TARGET_GLOBAL] = ($element == 'as:to'); - continue; - } - - if ($receiver == $followers) { - $targets[self::TARGET_FOLLOWER] = true; - continue; - } - $targets[$type][] = Contact::getIdForURL($receiver); - } - } - return $targets; - } - - /** - * Route client to server activities - * - * @param string $type - * @param string $object_type - * @param string $object_id - * @param integer $uid - * @param array $application - * @param array $ldactivity - * @return array - */ - private static function routeC2SActivities(string $type, string $object_type, string $object_id, int $uid, array $application, array $ldactivity): array - { - switch ($type) { - case 'as:Create': - if (in_array($object_type, self::CONTENT_TYPES)) { - return self::createContent($uid, $application, $ldactivity); - } - break; - case 'as:Update': - if (in_array($object_type, self::CONTENT_TYPES) && !empty($object_id)) { - return self::updateContent($uid, $object_id, $application, $ldactivity); - } - break; - case 'as:Follow': - if (in_array($object_type, self::ACCOUNT_TYPES) && !empty($object_id)) { - return self::followAccount($uid, $object_id, $ldactivity); - } - break; - } - return []; - } - - /** - * Create a new post or comment - * - * @param integer $uid - * @param array $application - * @param array $ldactivity - * @return array - */ - private static function createContent(int $uid, array $application, array $ldactivity): array - { - $object_data = self::processC2SObject($ldactivity['as:object']); - $item = Processor::processC2SContent($object_data, $application, $uid); - Logger::debug('Got data', ['item' => $item, 'object' => $object_data]); - - $id = Item::insert($item, true); - if (!empty($id)) { - $item = Post::selectFirst(['uri-id'], ['id' => $id]); - if (!empty($item['uri-id'])) { - return Transmitter::createActivityFromItem($id); - } - } - return []; - } - - /** - * Update an existing post or comment - * - * @param integer $uid - * @param string $object_id - * @param array $application - * @param array $ldactivity - * @return array - */ - private static function updateContent(int $uid, string $object_id, array $application, array $ldactivity):array - { - $id = Item::fetchByLink($object_id, $uid); - $original_post = Post::selectFirst(['uri-id'], ['uid' => $uid, 'origin' => true, 'id' => $id]); - if (empty($original_post)) { - Logger::debug('Item not found or does not belong to the user', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); - return []; - } - - $object_data = self::processC2SObject($ldactivity['as:object']); - $item = Processor::processC2SContent($object_data, $application, $uid); - if (empty($item['title']) && empty($item['body'])) { - Logger::debug('Empty body and title', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); - return []; - } - $post = ['title' => $item['title'], 'body' => $item['body']]; - Logger::debug('Got data', ['id' => $id, 'uid' => $uid, 'item' => $post]); - Item::update($post, ['id' => $id]); - Item::updateDisplayCache($original_post['uri-id']); - - return Transmitter::createActivityFromItem($id); - } - - /** - * Follow a given account - * @todo Check the expected return value - * - * @param integer $uid - * @param string $object_id - * @param array $ldactivity - * @return array - */ - private static function followAccount(int $uid, string $object_id, array $ldactivity): array - { - return []; - } }