From be33c20614adc88301a6a06476de3ade0a03ff22 Mon Sep 17 00:00:00 2001 From: Diogo Peralta Cordeiro Date: Mon, 28 Mar 2022 20:58:48 +0100 Subject: [PATCH] [PLUGIN][ActivityPub] Improve flexibility of Type layer, accomodate more elaborate understanding of Group Announces after FEP-2100 development --- plugins/ActivityPub/ActivityPub.php | 15 +++++--- plugins/ActivityPub/Controller/Inbox.php | 2 +- plugins/ActivityPub/Util/Model.php | 34 +++++++++++++------ plugins/ActivityPub/Util/Model/Activity.php | 20 +++++++---- .../Util/Model/ActivityAnnounce.php | 26 +++++--------- .../ActivityPub/Util/Model/ActivityFollow.php | 3 ++ plugins/ActivityPub/Util/Model/Note.php | 5 +-- src/Util/Common.php | 10 ++---- 8 files changed, 64 insertions(+), 51 deletions(-) diff --git a/plugins/ActivityPub/ActivityPub.php b/plugins/ActivityPub/ActivityPub.php index f54b2f1e8b..3f5e955a17 100644 --- a/plugins/ActivityPub/ActivityPub.php +++ b/plugins/ActivityPub/ActivityPub.php @@ -324,12 +324,16 @@ class ActivityPub extends Plugin array &$retry_args, ): bool { try { - $data = Model::toJson($activity); - if ($sender->isGroup()) { - // When the sender is a group, we have to wrap it in an Announce activity - $data = Type::create('Announce', ['object' => $data])->toJson(); + $data = Model::toType($activity); + if ($sender->isGroup() && ($activity->getVerb() !== 'subscribe' || !($activity->getVerb() === 'undo' && $data->get('object')->get('type') === 'Follow'))) { + // When the sender is a group, we have to wrap it in a transient Announce activity + $data = Type::create('Announce', [ + '@context' => 'https:\/\/www.w3.org\/ns\/activitystreams', + 'actor' => $sender->getUri(type: Router::ABSOLUTE_URL), + 'object' => $data, + ]); } - $res = self::postman($sender, $data, $inbox); + $res = self::postman($sender, $data->toJson(), $inbox); // accumulate errors for later use, if needed $status_code = $res->getStatusCode(); @@ -377,6 +381,7 @@ class ActivityPub extends Plugin // the actor, that could for example mean that OStatus handled this actor while we were deactivated // On next interaction this should be resolved, for now continue if (\is_null($ap_target = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))) { + Log::info('FreeNetwork wrongly told ActivityPub that it can handle actor id: ' . $actor->getId() . ' you might want to keep an eye on it.'); continue; } $to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor; diff --git a/plugins/ActivityPub/Controller/Inbox.php b/plugins/ActivityPub/Controller/Inbox.php index d166486692..1d3359b27d 100644 --- a/plugins/ActivityPub/Controller/Inbox.php +++ b/plugins/ActivityPub/Controller/Inbox.php @@ -98,7 +98,7 @@ class Inbox extends Controller try { $resource_parts = parse_url($type->get('actor')); if ($resource_parts['host'] !== Common::config('site', 'server')) { - $actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor'))); + $actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor'))); $ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]); } else { throw new Exception('Only remote actors can use this endpoint.'); diff --git a/plugins/ActivityPub/Util/Model.php b/plugins/ActivityPub/Util/Model.php index 67715f3808..07e060c11e 100644 --- a/plugins/ActivityPub/Util/Model.php +++ b/plugins/ActivityPub/Util/Model.php @@ -114,24 +114,36 @@ abstract class Model */ abstract public static function fromJson(string|Type\AbstractObject $json, array $options = []): Entity; + /** + * Get a Type + * + * @throws \App\Util\Exception\ServerException + * @throws ClientException + */ + public static function toType(mixed $object): Type\AbstractObject + { + switch ($object::class) { + case \App\Entity\Activity::class: + return Activity::toType($object); + case \App\Entity\Note::class: + return Note::toType($object); + default: + $type = self::jsonToType($object); + Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); + return $type; + } + } + /** * Get a JSON * - * @param ?int $options PHP JSON options + * @param int $options PHP JSON options * + * @throws \App\Util\Exception\ServerException * @throws ClientException */ public static function toJson(mixed $object, int $options = \JSON_UNESCAPED_SLASHES): string { - switch ($object::class) { - case \App\Entity\Activity::class: - return Activity::toJson($object, $options); - case \App\Entity\Note::class: - return Note::toJson($object, $options); - default: - $type = self::jsonToType($object); - Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); - return $type->toJson($options); - } + return self::toType($object)->toJson($options); } } diff --git a/plugins/ActivityPub/Util/Model/Activity.php b/plugins/ActivityPub/Util/Model/Activity.php index 12f465d19f..5076ba23bf 100644 --- a/plugins/ActivityPub/Util/Model/Activity.php +++ b/plugins/ActivityPub/Util/Model/Activity.php @@ -39,6 +39,7 @@ use App\Core\Event; use App\Core\Log; use App\Core\Router\Router; use App\Entity\Activity as GSActivity; +use App\Util\Common; use App\Util\Exception\ClientException; use App\Util\Exception\NoSuchActorException; use App\Util\Exception\NotFoundException; @@ -46,7 +47,6 @@ use App\Util\Exception\NotImplementedException; use DateTimeInterface; use Exception; use InvalidArgumentException; -use const JSON_UNESCAPED_SLASHES; use Plugin\ActivityPub\ActivityPub; use Plugin\ActivityPub\Entity\ActivitypubActivity; use Plugin\ActivityPub\Util\Explorer; @@ -90,9 +90,14 @@ class Activity extends Model // Find Actor and Object $actor = Explorer::getOneFromUri($type_activity->get('actor')); $type_object = $type_activity->get('object'); - if (\is_string($type_object)) { // Retrieve it - $type_object = ActivityPub::getObjectByUri($type_object, try_online: true); - } else { // Encapsulated, if we have it locally, prefer it + if (\is_string($type_object)) { + if (Common::isValidHttpUrl($type_object)) { // Retrieve it + $type_object = ActivityPub::getObjectByUri($type_object, try_online: true); + } else { + $type_object = Type::fromJson($type_object); + } + } + if ($type_object instanceof AbstractObject) { // Encapsulated, if we have it locally, prefer it // TODO: Test authority of activity over object try { $type_object = ActivityPub::getObjectByUri($type_object->get('id'), try_online: false); @@ -153,7 +158,7 @@ class Activity extends Model * * @throws ClientException */ - public static function toJson(mixed $object, int $options = JSON_UNESCAPED_SLASHES): string + public static function toType(mixed $object): AbstractObject { if ($object::class !== GSActivity::class) { throw new InvalidArgumentException('First argument type must be an Activity.'); @@ -186,7 +191,8 @@ class Activity extends Model // Get object or Tombstone try { $child = $object->getObject(); // Throws NotFoundException - $attr['object'] = ($attr['type'] === 'Create') ? self::jsonToType(Model::toJson($child)) : ActivityPub::getUriByObject($child); + $prefer_embed = ['Create', 'Undo']; + $attr['object'] = \in_array($attr['type'], $prefer_embed) ? self::jsonToType(Model::toJson($child)) : ActivityPub::getUriByObject($child); } catch (NotFoundException) { // It seems this object was deleted, refer to it as a Tombstone $uri = match ($object->getObjectType()) { @@ -203,6 +209,6 @@ class Activity extends Model } $type = self::jsonToType($attr); Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); - return $type->toJson($options); + return $type; } } diff --git a/plugins/ActivityPub/Util/Model/ActivityAnnounce.php b/plugins/ActivityPub/Util/Model/ActivityAnnounce.php index e634069f86..7a3caf8eb5 100644 --- a/plugins/ActivityPub/Util/Model/ActivityAnnounce.php +++ b/plugins/ActivityPub/Util/Model/ActivityAnnounce.php @@ -33,6 +33,8 @@ declare(strict_types = 1); namespace Plugin\ActivityPub\Util\Model; use ActivityPhp\Type\AbstractObject; +use Exception; +use InvalidArgumentException; use Plugin\ActivityPub\Entity\ActivitypubActivity; /** @@ -45,27 +47,15 @@ class ActivityAnnounce extends Activity { protected static function handle_core_activity(\App\Entity\Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): ActivitypubActivity { - // The only core Announce we recognise is for (transitive) activities coming from Group actors + // The only core Announce we recognise is for (transient) activities coming from Group actors if ($actor->isGroup()) { if ($type_object instanceof AbstractObject) { - $actual_to = array_flip(\is_string($type_object->get('to')) ? [$type_object->get('to')] : $type_object->get('to')); - $actual_cc = array_flip(\is_string($type_object->get('cc')) ? [$type_object->get('cc')] : $type_object->get('cc')); - $actual_cc[$type_activity->get('actor')] = true; // Add group to targets - foreach (\is_string($type_activity->get('to')) ? [$type_activity->get('to')] : $type_activity->get('to') as $to) { - if ($to !== 'https://www.w3.org/ns/activitystreams#Public') { - $actual_to[$to] = true; - } - } - foreach (\is_string($type_activity->get('cc')) ? [$type_activity->get('cc')] : $type_activity->get('cc') as $cc) { - if ($cc !== 'https://www.w3.org/ns/activitystreams#Public') { - $actual_cc[$cc] = true; - } - } - $type_object->set('to', array_keys($actual_to)); - $type_object->set('cc', array_keys($actual_cc)); - $ap_act = self::fromJson($type_object); + return $ap_act = Activity::fromJson($type_object); + } else { + throw new Exception('Already handled.'); } + } else { + throw new InvalidArgumentException('Unsupported Announce Activity.'); } - return $ap_act ?? ($ap_act = $type_object); } } diff --git a/plugins/ActivityPub/Util/Model/ActivityFollow.php b/plugins/ActivityPub/Util/Model/ActivityFollow.php index 8c71d869dd..f788d417b5 100644 --- a/plugins/ActivityPub/Util/Model/ActivityFollow.php +++ b/plugins/ActivityPub/Util/Model/ActivityFollow.php @@ -37,6 +37,7 @@ use App\Core\DB\DB; use App\Entity\Activity as GSActivity; use App\Util\Exception\ClientException; use Component\Subscription\Subscription; +use Component\Subscription\Subscription as SubscriptionComponent; use DateTime; use InvalidArgumentException; use Plugin\ActivityPub\Entity\ActivitypubActivity; @@ -63,6 +64,8 @@ class ActivityFollow extends Activity if (\is_null($act)) { throw new ClientException('You are already subscribed to this actor.'); } + SubscriptionComponent::refreshSubscriptionCount($actor, $subscribed); + // Store ActivityPub Activity $ap_act = ActivitypubActivity::create([ 'activity_id' => $act->getId(), diff --git a/plugins/ActivityPub/Util/Model/Note.php b/plugins/ActivityPub/Util/Model/Note.php index 9ec59a99a5..23e5f1a95e 100644 --- a/plugins/ActivityPub/Util/Model/Note.php +++ b/plugins/ActivityPub/Util/Model/Note.php @@ -166,6 +166,7 @@ class Note extends Model 'reply_to' => $reply_to = $handleInReplyTo($type_note), 'modified' => new DateTime(), 'type' => match ($type_note->get('type')) { + 'Article' => 'article', 'Page' => 'page', default => 'note' }, @@ -361,7 +362,7 @@ class Note extends Model * @throws InvalidArgumentException * @throws ServerException */ - public static function toJson(mixed $object, int $options = \JSON_UNESCAPED_SLASHES): string + public static function toType(mixed $object): AbstractObject { if ($object::class !== GSNote::class) { throw new InvalidArgumentException('First argument type must be a Note.'); @@ -469,6 +470,6 @@ class Note extends Model $type = self::jsonToType($attr); Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); - return $type->toJson($options); + return $type; } } diff --git a/src/Util/Common.php b/src/Util/Common.php index abf520f4d5..df23f07b85 100644 --- a/src/Util/Common.php +++ b/src/Util/Common.php @@ -263,9 +263,9 @@ abstract class Common public static function getPreferredPhpUploadLimit(): int { return min( - self::sizeStrToInt(ini_get('post_max_size')), - self::sizeStrToInt(ini_get('upload_max_filesize')), - self::sizeStrToInt(ini_get('memory_limit')), + self::sizeStrToInt(\ini_get('post_max_size')), + self::sizeStrToInt(\ini_get('upload_max_filesize')), + self::sizeStrToInt(\ini_get('memory_limit')), ); } @@ -295,10 +295,6 @@ abstract class Common */ public static function isValidHttpUrl(string $url, bool $ensure_secure = false) { - if (empty($url)) { - return false; - } - // (if false, we use '?' in 'https?' to say the 's' is optional) $regex = $ensure_secure ? '/^https$/' : '/^https?$/'; return filter_var($url, \FILTER_VALIDATE_URL) !== false