From 07ae002a2d9bb09a17b63b1ac43944649d82cacd Mon Sep 17 00:00:00 2001 From: BentiGorlich Date: Mon, 7 Oct 2024 20:55:25 +0200 Subject: [PATCH] Introduce the activity table Whenever an actor makes an activity (like, announce, etc.) we now save it in our db, so the url we pass to other activity pub servers is valid and can return the correct json. All the factory and wrapper classes now return an `Activity` entity that can be converted to json using the new `ActivityJsonBuilder` --- migrations/Version20240820201944.php | 61 +++ .../ActivityPub/ObjectController.php | 18 +- src/Entity/Activity.php | 136 +++++++ .../ActivityPubActivityInterface.php | 4 + src/Entity/Message.php | 5 + .../Entry/EntryDeleteSubscriber.php | 6 +- .../Entry/EntryPinSubscriber.php | 2 +- .../EntryCommentDeleteSubscriber.php | 6 +- .../Magazine/MagazineUpdatedSubscriber.php | 2 +- .../Post/PostDeleteSubscriber.php | 6 +- .../PostCommentDeleteSubscriber.php | 6 +- src/Factory/ActivityPub/AddRemoveFactory.php | 75 +--- src/Factory/ActivityPub/FlagFactory.php | 122 +----- .../Outbox/AnnounceLikeMessage.php | 1 + .../Outbox/GenericAnnounceMessage.php | 5 +- .../ActivityPub/Inbox/FollowHandler.php | 11 +- .../ActivityPub/Inbox/LikeHandler.php | 2 +- .../ActivityPub/Inbox/UpdateHandler.php | 8 +- .../ActivityPub/Outbox/AddHandler.php | 5 +- .../ActivityPub/Outbox/AnnounceHandler.php | 28 +- .../Outbox/AnnounceLikeHandler.php | 41 +- .../ActivityPub/Outbox/CreateHandler.php | 7 +- .../Outbox/EntryPinMessageHandler.php | 5 +- .../ActivityPub/Outbox/FlagHandler.php | 7 +- .../ActivityPub/Outbox/FollowHandler.php | 17 +- .../Outbox/GenericAnnounceHandler.php | 21 +- .../ActivityPub/Outbox/LikeHandler.php | 9 +- .../ActivityPub/Outbox/RemoveHandler.php | 4 +- .../ActivityPub/Outbox/UpdateHandler.php | 9 +- src/MessageHandler/DeleteUserHandler.php | 5 +- src/Repository/ActivityRepository.php | 78 ++++ src/Repository/EntryRepository.php | 12 +- .../ActivityPub/ActivityJsonBuilder.php | 370 ++++++++++++++++++ .../ActivityPub/Wrapper/AnnounceWrapper.php | 63 +-- .../ActivityPub/Wrapper/CreateWrapper.php | 51 +-- .../ActivityPub/Wrapper/DeleteWrapper.php | 66 ++-- .../Wrapper/FollowResponseWrapper.php | 43 +- .../ActivityPub/Wrapper/FollowWrapper.php | 40 +- .../ActivityPub/Wrapper/LikeWrapper.php | 34 +- .../ActivityPub/Wrapper/UndoWrapper.php | 45 ++- .../ActivityPub/Wrapper/UpdateWrapper.php | 88 +---- 41 files changed, 1020 insertions(+), 504 deletions(-) create mode 100644 migrations/Version20240820201944.php create mode 100644 src/Entity/Activity.php create mode 100644 src/Repository/ActivityRepository.php create mode 100644 src/Service/ActivityPub/ActivityJsonBuilder.php diff --git a/migrations/Version20240820201944.php b/migrations/Version20240820201944.php new file mode 100644 index 0000000000..a78e7f97c3 --- /dev/null +++ b/migrations/Version20240820201944.php @@ -0,0 +1,61 @@ +addSql('CREATE TABLE activity (uuid UUID NOT NULL, user_actor_id INT DEFAULT NULL, magazine_actor_id INT DEFAULT NULL, audience_id INT DEFAULT NULL, inner_activity_id UUID DEFAULT NULL, object_entry_id INT DEFAULT NULL, object_entry_comment_id INT DEFAULT NULL, object_post_id INT DEFAULT NULL, object_post_comment_id INT DEFAULT NULL, object_message_id INT DEFAULT NULL, object_user_id INT DEFAULT NULL, object_magazine_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, inner_activity_url TEXT DEFAULT NULL, object_generic TEXT DEFAULT NULL, target_string TEXT DEFAULT NULL, content_string TEXT DEFAULT NULL, activity_json TEXT DEFAULT NULL, PRIMARY KEY(uuid))'); + $this->addSql('CREATE INDEX IDX_AC74095AF057164A ON activity (user_actor_id)'); + $this->addSql('CREATE INDEX IDX_AC74095A2F5FA0A4 ON activity (magazine_actor_id)'); + $this->addSql('CREATE INDEX IDX_AC74095A848CC616 ON activity (audience_id)'); + $this->addSql('CREATE INDEX IDX_AC74095A1B4C3858 ON activity (inner_activity_id)'); + $this->addSql('CREATE INDEX IDX_AC74095A6CE0A42A ON activity (object_entry_id)'); + $this->addSql('CREATE INDEX IDX_AC74095AC3683D33 ON activity (object_entry_comment_id)'); + $this->addSql('CREATE INDEX IDX_AC74095A4BC7838C ON activity (object_post_id)'); + $this->addSql('CREATE INDEX IDX_AC74095ACC1812B0 ON activity (object_post_comment_id)'); + $this->addSql('CREATE INDEX IDX_AC74095A20E5BA95 ON activity (object_message_id)'); + $this->addSql('CREATE INDEX IDX_AC74095AA7205335 ON activity (object_user_id)'); + $this->addSql('CREATE INDEX IDX_AC74095AFC1C2A13 ON activity (object_magazine_id)'); + $this->addSql('COMMENT ON COLUMN activity.uuid IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN activity.inner_activity_id IS \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095AF057164A FOREIGN KEY (user_actor_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A2F5FA0A4 FOREIGN KEY (magazine_actor_id) REFERENCES magazine (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A848CC616 FOREIGN KEY (audience_id) REFERENCES magazine (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A1B4C3858 FOREIGN KEY (inner_activity_id) REFERENCES activity (uuid) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A6CE0A42A FOREIGN KEY (object_entry_id) REFERENCES entry (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095AC3683D33 FOREIGN KEY (object_entry_comment_id) REFERENCES entry_comment (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A4BC7838C FOREIGN KEY (object_post_id) REFERENCES post (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095ACC1812B0 FOREIGN KEY (object_post_comment_id) REFERENCES post_comment (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095A20E5BA95 FOREIGN KEY (object_message_id) REFERENCES message (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095AA7205335 FOREIGN KEY (object_user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE activity ADD CONSTRAINT FK_AC74095AFC1C2A13 FOREIGN KEY (object_magazine_id) REFERENCES magazine (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095AF057164A'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A2F5FA0A4'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A848CC616'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A1B4C3858'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A6CE0A42A'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095AC3683D33'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A4BC7838C'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095ACC1812B0'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095A20E5BA95'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095AA7205335'); + $this->addSql('ALTER TABLE activity DROP CONSTRAINT FK_AC74095AFC1C2A13'); + $this->addSql('DROP TABLE activity'); + } +} diff --git a/src/Controller/ActivityPub/ObjectController.php b/src/Controller/ActivityPub/ObjectController.php index 39bba81f24..1a8519afda 100644 --- a/src/Controller/ActivityPub/ObjectController.php +++ b/src/Controller/ActivityPub/ObjectController.php @@ -4,15 +4,29 @@ namespace App\Controller\ActivityPub; +use App\Repository\ActivityRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Uid\Uuid; class ObjectController { - public function __invoke(string|int $id, Request $request): JsonResponse + public function __construct( + private readonly ActivityRepository $activityRepository, + private readonly ActivityJsonBuilder $activityJsonBuilder, + ) { + } + + public function __invoke(string $id, Request $request): JsonResponse { - $response = new JsonResponse(); + $uuid = Uuid::fromString($id); + $activity = $this->activityRepository->findOneBy(['uuid' => $uuid]); + if (null === $activity) { + return new JsonResponse(status: 404); + } + $response = new JsonResponse($this->activityJsonBuilder->buildActivityJson($activity)); $response->headers->set('Content-Type', 'application/activity+json'); return $response; diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php new file mode 100644 index 0000000000..bfa0a2a41c --- /dev/null +++ b/src/Entity/Activity.php @@ -0,0 +1,136 @@ + null])] + public ?Activity $innerActivity = null; + + #[Column(type: 'text', nullable: true)] + public ?string $innerActivityUrl = null; + + #[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')] + public ?Entry $objectEntry = null; + + #[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')] + public ?EntryComment $objectEntryComment = null; + + #[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')] + public ?Post $objectPost = null; + + #[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')] + public ?PostComment $objectPostComment = null; + + #[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')] + public ?Message $objectMessage = null; + + #[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')] + public ?User $objectUser = null; + + #[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')] + public ?Magazine $objectMagazine = null; + + #[Column(type: 'text', nullable: true)] + public ?string $objectGeneric = null; + + #[Column(type: 'text', nullable: true)] + public ?string $targetString = null; + + #[Column(type: 'text', nullable: true)] + public ?string $contentString = null; + + /** + * This should only be set when the json should not get compiled. + */ + #[Column(type: 'text', nullable: true)] + public ?string $activityJson = null; + + public function __construct(string $type) + { + $this->type = $type; + } + + public function setObject(ActivityPubActivityInterface|Entry|EntryComment|Post|PostComment|ActivityPubActorInterface|User|Magazine|array|string $object): void + { + if ($object instanceof Entry) { + $this->objectEntry = $object; + } elseif ($object instanceof EntryComment) { + $this->objectEntryComment = $object; + } elseif ($object instanceof Post) { + $this->objectPost = $object; + } elseif ($object instanceof PostComment) { + $this->objectPostComment = $object; + } elseif ($object instanceof Message) { + $this->objectMessage = $object; + } elseif ($object instanceof User) { + $this->objectUser = $object; + } elseif ($object instanceof Magazine) { + $this->objectMagazine = $object; + } elseif (\is_array($object)) { + $this->objectGeneric = json_encode($object); + } elseif (\is_string($object)) { + $this->objectGeneric = $object; + } else { + throw new \LogicException(\get_class($object)); + } + } + + public function getObject(): Post|EntryComment|PostComment|Entry|Message|User|Magazine|array|string|null + { + $o = $this->objectEntry ?? $this->objectEntryComment ?? $this->objectPost ?? $this->objectPostComment ?? $this->objectMessage ?? $this->objectUser ?? $this->objectMagazine; + if (null !== $o) { + return $o; + } + $o = json_decode($this->objectGeneric ?? ''); + if (JSON_ERROR_NONE === json_last_error()) { + return $o; + } + + return $this->objectGeneric; + } + + public function setActor(Magazine|User $actor): void + { + if ($actor instanceof User) { + $this->userActor = $actor; + } else { + $this->magazineActor = $actor; + } + } + + public function getActor(): Magazine|User|null + { + return $this->userActor ?? $this->magazineActor; + } +} diff --git a/src/Entity/Contracts/ActivityPubActivityInterface.php b/src/Entity/Contracts/ActivityPubActivityInterface.php index 1b31d12f85..90e0aaa996 100644 --- a/src/Entity/Contracts/ActivityPubActivityInterface.php +++ b/src/Entity/Contracts/ActivityPubActivityInterface.php @@ -4,6 +4,8 @@ namespace App\Entity\Contracts; +use App\Entity\User; + interface ActivityPubActivityInterface { public const FOLLOWERS = 'followers'; @@ -37,4 +39,6 @@ interface ActivityPubActivityInterface 'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods', 'stickied' => 'lemmy:stickied', ]; + + public function getUser(): ?User; } diff --git a/src/Entity/Message.php b/src/Entity/Message.php index e323a2cb43..1ea030890d 100644 --- a/src/Entity/Message.php +++ b/src/Entity/Message.php @@ -82,4 +82,9 @@ public function getTitle(): string return grapheme_substr($firstLine, 0, 80).'…'; } + + public function getUser(): User + { + return $this->sender; + } } diff --git a/src/EventSubscriber/Entry/EntryDeleteSubscriber.php b/src/EventSubscriber/Entry/EntryDeleteSubscriber.php index 0c9a3836f1..7a9d203936 100644 --- a/src/EventSubscriber/Entry/EntryDeleteSubscriber.php +++ b/src/EventSubscriber/Entry/EntryDeleteSubscriber.php @@ -12,10 +12,10 @@ use App\Message\ActivityPub\Outbox\DeleteMessage; use App\Message\Notification\EntryDeletedNotificationMessage; use App\Repository\EntryRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\DeleteWrapper; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Uid\Uuid; class EntryDeleteSubscriber implements EventSubscriberInterface { @@ -23,6 +23,7 @@ public function __construct( private readonly MessageBusInterface $bus, private readonly EntryRepository $entryRepository, private readonly DeleteWrapper $deleteWrapper, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { } @@ -58,7 +59,8 @@ public function onEntryBeforeDeleteImpl(?User $user, Entry $entry): void $this->bus->dispatch(new EntryDeletedNotificationMessage($entry->getId())); if (!$entry->apId || !$entry->magazine->apId || (null !== $user && $entry->magazine->userIsModerator($user))) { - $payload = $this->deleteWrapper->adjustDeletePayload($user, $entry, Uuid::v4()->toRfc4122()); + $activity = $this->deleteWrapper->adjustDeletePayload($user, $entry); + $payload = $this->activityJsonBuilder->buildActivityJson($activity); $this->bus->dispatch(new DeleteMessage($payload, $entry->user->getId(), $entry->magazine->getId())); } } diff --git a/src/EventSubscriber/Entry/EntryPinSubscriber.php b/src/EventSubscriber/Entry/EntryPinSubscriber.php index b9f5a5703a..8336454c59 100644 --- a/src/EventSubscriber/Entry/EntryPinSubscriber.php +++ b/src/EventSubscriber/Entry/EntryPinSubscriber.php @@ -41,7 +41,7 @@ public function onEntryPin(EntryPinEvent $event): void $activity = $this->addRemoveFactory->buildRemovePinnedPost($event->actor, $event->entry); } $this->logger->debug('dispatching announce for add pin post {e} by {u} in {m}', ['e' => $event->entry->title, 'u' => $event->actor->apId, 'm' => $event->entry->magazine->name]); - $this->bus->dispatch(new GenericAnnounceMessage($event->entry->magazine->getId(), $activity, $event->actor->apInboxUrl)); + $this->bus->dispatch(new GenericAnnounceMessage($event->entry->magazine->getId(), null, $event->actor->apInboxUrl, $activity->uuid->toString(), null)); } else { $this->logger->debug('entry {e} got {p} by {u}, dispatching new EntryPinMessage', ['e' => $event->entry->title, 'p' => $event->entry->sticky ? 'pinned' : 'unpinned', 'u' => $event->actor?->username ?? 'system']); $this->bus->dispatch(new EntryPinMessage($event->entry->getId(), $event->entry->sticky, $event->actor?->getId())); diff --git a/src/EventSubscriber/EntryComment/EntryCommentDeleteSubscriber.php b/src/EventSubscriber/EntryComment/EntryCommentDeleteSubscriber.php index 2347183abc..e48e11daed 100644 --- a/src/EventSubscriber/EntryComment/EntryCommentDeleteSubscriber.php +++ b/src/EventSubscriber/EntryComment/EntryCommentDeleteSubscriber.php @@ -11,10 +11,10 @@ use App\Event\EntryComment\EntryCommentDeletedEvent; use App\Message\ActivityPub\Outbox\DeleteMessage; use App\Message\Notification\EntryCommentDeletedNotificationMessage; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\DeleteWrapper; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Uid\Uuid; use Symfony\Contracts\Cache\CacheInterface; class EntryCommentDeleteSubscriber implements EventSubscriberInterface @@ -23,6 +23,7 @@ public function __construct( private readonly CacheInterface $cache, private readonly MessageBusInterface $bus, private readonly DeleteWrapper $deleteWrapper, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { } @@ -59,7 +60,8 @@ public function onEntryCommentBeforeDeleteImpl(?User $user, EntryComment $commen $this->bus->dispatch(new EntryCommentDeletedNotificationMessage($comment->getId())); if (!$comment->apId || !$comment->magazine->apId || (null !== $user && $comment->magazine->userIsModerator($user))) { - $payload = $this->deleteWrapper->adjustDeletePayload($user, $comment, Uuid::v4()->toRfc4122()); + $activity = $this->deleteWrapper->adjustDeletePayload($user, $comment); + $payload = $this->activityJsonBuilder->buildActivityJson($activity); $this->bus->dispatch(new DeleteMessage($payload, $comment->user->getId(), $comment->magazine->getId())); } } diff --git a/src/EventSubscriber/Magazine/MagazineUpdatedSubscriber.php b/src/EventSubscriber/Magazine/MagazineUpdatedSubscriber.php index 40e1dfc657..6558db392b 100644 --- a/src/EventSubscriber/Magazine/MagazineUpdatedSubscriber.php +++ b/src/EventSubscriber/Magazine/MagazineUpdatedSubscriber.php @@ -32,7 +32,7 @@ public function onMagazineUpdated(MagazineUpdatedEvent $event): void $mag = $event->magazine; if (null === $mag->apId) { $activity = $this->updateWrapper->buildForActor($mag, $event->editedBy); - $this->bus->dispatch(new GenericAnnounceMessage($mag->getId(), $activity, $event->editedBy->apDomain)); + $this->bus->dispatch(new GenericAnnounceMessage($mag->getId(), null, $event->editedBy->apDomain, $activity->uuid->toString(), null)); } elseif (null !== $event->editedBy && null === $event->editedBy->apId) { $this->bus->dispatch(new UpdateMessage($mag->getId(), Magazine::class, $event->editedBy->getId())); } diff --git a/src/EventSubscriber/Post/PostDeleteSubscriber.php b/src/EventSubscriber/Post/PostDeleteSubscriber.php index bc76c9d0a3..6e5c8ba4a7 100644 --- a/src/EventSubscriber/Post/PostDeleteSubscriber.php +++ b/src/EventSubscriber/Post/PostDeleteSubscriber.php @@ -12,10 +12,10 @@ use App\Message\ActivityPub\Outbox\DeleteMessage; use App\Message\Notification\PostDeletedNotificationMessage; use App\Repository\PostRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\DeleteWrapper; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Uid\Uuid; class PostDeleteSubscriber implements EventSubscriberInterface { @@ -23,6 +23,7 @@ public function __construct( private readonly MessageBusInterface $bus, private readonly PostRepository $postRepository, private readonly DeleteWrapper $deleteWrapper, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { } @@ -56,7 +57,8 @@ public function onPostBeforeDeleteImpl(?User $user, Post $post): void $this->bus->dispatch(new PostDeletedNotificationMessage($post->getId())); if (!$post->apId || !$post->magazine->apId || (null !== $user && $post->magazine->userIsModerator($user))) { - $payload = $this->deleteWrapper->adjustDeletePayload($user, $post, Uuid::v4()->toRfc4122()); + $activity = $this->deleteWrapper->adjustDeletePayload($user, $post); + $payload = $this->activityJsonBuilder->buildActivityJson($activity); $this->bus->dispatch(new DeleteMessage($payload, $post->user->getId(), $post->magazine->getId())); } } diff --git a/src/EventSubscriber/PostComment/PostCommentDeleteSubscriber.php b/src/EventSubscriber/PostComment/PostCommentDeleteSubscriber.php index f8529cc3b4..8717a3f28b 100644 --- a/src/EventSubscriber/PostComment/PostCommentDeleteSubscriber.php +++ b/src/EventSubscriber/PostComment/PostCommentDeleteSubscriber.php @@ -11,10 +11,10 @@ use App\Event\PostComment\PostCommentDeletedEvent; use App\Message\ActivityPub\Outbox\DeleteMessage; use App\Message\Notification\PostCommentDeletedNotificationMessage; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\DeleteWrapper; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Uid\Uuid; use Symfony\Contracts\Cache\CacheInterface; class PostCommentDeleteSubscriber implements EventSubscriberInterface @@ -23,6 +23,7 @@ public function __construct( private readonly CacheInterface $cache, private readonly MessageBusInterface $bus, private readonly DeleteWrapper $deleteWrapper, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { } @@ -65,7 +66,8 @@ public function onPostCommentBeforeDeleteImpl(?User $user, PostComment $comment) $this->bus->dispatch(new PostCommentDeletedNotificationMessage($comment->getId())); if (!$comment->apId || !$comment->magazine->apId || (null !== $user && $comment->magazine->userIsModerator($user))) { - $payload = $this->deleteWrapper->adjustDeletePayload($user, $comment, Uuid::v4()->toRfc4122()); + $activity = $this->deleteWrapper->adjustDeletePayload($user, $comment); + $payload = $this->activityJsonBuilder->buildActivityJson($activity); $this->bus->dispatch(new DeleteMessage($payload, $comment->user->getId(), $comment->magazine->getId())); } } diff --git a/src/Factory/ActivityPub/AddRemoveFactory.php b/src/Factory/ActivityPub/AddRemoveFactory.php index 13df001af1..8b397faffb 100644 --- a/src/Factory/ActivityPub/AddRemoveFactory.php +++ b/src/Factory/ActivityPub/AddRemoveFactory.php @@ -4,106 +4,63 @@ namespace App\Factory\ActivityPub; -use App\Entity\Contracts\ActivityPubActivityInterface; +use App\Entity\Activity; use App\Entity\Entry; use App\Entity\Magazine; use App\Entity\User; -use App\Service\ActivityPub\ContextsProvider; -use JetBrains\PhpStorm\ArrayShape; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; class AddRemoveFactory { public function __construct( private readonly UrlGeneratorInterface $urlGenerator, - private readonly ContextsProvider $contextProvider, ) { } - public function buildAddModerator(User $actor, User $added, Magazine $magazine): array + public function buildAddModerator(User $actor, User $added, Magazine $magazine): Activity { $url = null !== $magazine->apId ? $magazine->apAttributedToUrl : $this->urlGenerator->generate( 'ap_magazine_moderators', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL ); - $addedUserUrl = null !== $added->apId ? $added->apPublicUrl : $this->urlGenerator->generate( - 'ap_user', ['username' => $added->username], UrlGeneratorInterface::ABSOLUTE_URL - ); - return $this->build($actor, $addedUserUrl, $magazine, 'Add', $url); + return $this->build($actor, $added, $magazine, 'Add', $url); } - public function buildRemoveModerator(User $actor, User $removed, Magazine $magazine): array + public function buildRemoveModerator(User $actor, User $removed, Magazine $magazine): Activity { $url = null !== $magazine->apId ? $magazine->apAttributedToUrl : $this->urlGenerator->generate( 'ap_magazine_moderators', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL ); - $removedUserUrl = null !== $removed->apId ? $removed->apPublicUrl : $this->urlGenerator->generate( - 'ap_user', ['username' => $removed->username], UrlGeneratorInterface::ABSOLUTE_URL - ); - return $this->build($actor, $removedUserUrl, $magazine, 'Remove', $url); + return $this->build($actor, $removed, $magazine, 'Remove', $url); } - public function buildAddPinnedPost(User $actor, Entry $added): array + public function buildAddPinnedPost(User $actor, Entry $added): Activity { $url = null !== $added->magazine->apId ? $added->magazine->apFeaturedUrl : $this->urlGenerator->generate( 'ap_magazine_pinned', ['name' => $added->magazine->name], UrlGeneratorInterface::ABSOLUTE_URL ); - $entryUrl = $added->apId ?? $this->urlGenerator->generate( - 'ap_entry', ['entry_id' => $added->getId(), 'magazine_name' => $added->magazine->name], UrlGeneratorInterface::ABSOLUTE_URL - ); - return $this->build($actor, $entryUrl, $added->magazine, 'Add', $url); + return $this->build($actor, $added, $added->magazine, 'Add', $url); } - public function buildRemovePinnedPost(User $actor, Entry $removed): array + public function buildRemovePinnedPost(User $actor, Entry $removed): Activity { $url = null !== $removed->magazine->apId ? $removed->magazine->apFeaturedUrl : $this->urlGenerator->generate( 'ap_magazine_pinned', ['name' => $removed->magazine->name], UrlGeneratorInterface::ABSOLUTE_URL ); - $entryUrl = $removed->apId ?? $this->urlGenerator->generate( - 'ap_entry', ['entry_id' => $removed->getId(), 'magazine_name' => $removed->magazine->name], UrlGeneratorInterface::ABSOLUTE_URL - ); - return $this->build($actor, $entryUrl, $removed->magazine, 'Remove', $url); + return $this->build($actor, $removed, $removed->magazine, 'Remove', $url); } - #[ArrayShape([ - '@context' => 'array', - 'id' => 'string', - 'actor' => 'string', - 'to' => 'array', - 'object' => 'string', - 'cc' => 'array', - 'type' => 'string', - 'target' => 'string', - 'audience' => 'string', - ])] - private function build(User $actor, string $targetObjectUrl, Magazine $magazine, string $type, string $collectionUrl): array + private function build(User $actor, User|Entry $targetObject, Magazine $magazine, string $type, string $collectionUrl): Activity { - $id = Uuid::v4()->toRfc4122(); + $activity = new Activity($type); + $activity->audience = $magazine; + $activity->setActor($actor); + $activity->setObject($targetObject); + $activity->targetString = $collectionUrl; - return [ - '@context' => $this->contextProvider->referencedContexts(), - 'id' => $this->urlGenerator->generate( - 'ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL - ), - 'actor' => null !== $actor->apId ? $actor->apPublicUrl : $this->urlGenerator->generate( - 'ap_user', ['username' => $actor->username], UrlGeneratorInterface::ABSOLUTE_URL - ), - 'to' => [ActivityPubActivityInterface::PUBLIC_URL], - 'object' => $targetObjectUrl, - 'cc' => [ - null !== $magazine->apId ? $magazine->apPublicUrl : $this->urlGenerator->generate( - 'ap_magazine', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL - ), - ], - 'type' => $type, - 'target' => $collectionUrl, - 'audience' => null !== $magazine->apId ? $magazine->apPublicUrl : $this->urlGenerator->generate( - 'ap_magazine', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL - ), - ]; + return $activity; } } diff --git a/src/Factory/ActivityPub/FlagFactory.php b/src/Factory/ActivityPub/FlagFactory.php index 9af50b6a62..94f94e8550 100644 --- a/src/Factory/ActivityPub/FlagFactory.php +++ b/src/Factory/ActivityPub/FlagFactory.php @@ -4,128 +4,22 @@ namespace App\Factory\ActivityPub; -use App\Entity\Contracts\ActivityPubActivityInterface; -use App\Entity\Contracts\ReportInterface; -use App\Entity\Entry; -use App\Entity\EntryComment; -use App\Entity\Post; -use App\Entity\PostComment; +use App\Entity\Activity; use App\Entity\Report; -use JetBrains\PhpStorm\ArrayShape; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class FlagFactory { - public function __construct(private readonly UrlGeneratorInterface $urlGenerator) + public function __construct() { } - #[ArrayShape([ - '@context' => 'mixed', - 'id' => 'string', - 'type' => 'string', - 'actor' => 'mixed', - 'to' => 'mixed', - 'object' => 'string', - 'audience' => 'string', - 'summary' => 'string', - 'content' => 'string', - ])] - public function build(Report $report, string $objectUrl): array + public function build(Report $report): Activity { - // mastodon does not accept a report that does not have an array as object. - // I created an issue for it: https://github.com/mastodon/mastodon/issues/28159 - $mastodonObject = [ - $objectUrl, - $report->reported->apPublicUrl ?? $this->urlGenerator->generate( - 'ap_user', - ['username' => $report->reported->username], - UrlGeneratorInterface::ABSOLUTE_URL - ), - ]; + $activity = new Activity('Flag'); + $activity->setObject($report->getSubject()); + $activity->setActor($report->reporting); + $activity->contentString = $report->reason; - // lemmy does not accept a report that does have an array as object. - // I created an issue for it: https://github.com/LemmyNet/lemmy/issues/4217 - $lemmyObject = $objectUrl; - - if ('random' !== $report->magazine->name or $report->magazine->apId) { - // apAttributedToUrl is not a standardized field, - // so it is not implemented by every software that supports groups. - // Some don't have moderation at all, so it will probably remain optional in the future. - $audience = $report->magazine->apPublicUrl ?? $this->urlGenerator->generate( - 'ap_magazine', - ['name' => $report->magazine->name], - UrlGeneratorInterface::ABSOLUTE_URL - ); - $object = $lemmyObject; - } else { - $audience = $report->reported->apPublicUrl ?? $this->urlGenerator->generate( - 'ap_user', - ['username' => $report->reported->username], - UrlGeneratorInterface::ABSOLUTE_URL - ); - $object = $mastodonObject; - } - - $result = [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $this->urlGenerator->generate( - 'ap_report', - ['report_id' => $report->uuid], - UrlGeneratorInterface::ABSOLUTE_URL - ), - 'type' => 'Flag', - 'actor' => $report->reporting->apPublicUrl ?? $this->urlGenerator->generate( - 'ap_user', - ['username' => $report->reporting->username], - UrlGeneratorInterface::ABSOLUTE_URL - ), - 'object' => $object, - 'audience' => $audience, - 'summary' => $report->reason, - 'content' => $report->reason, - ]; - - if ('random' !== $report->magazine->name or $report->magazine->apId) { - $result['to'] = [ - $report->magazine->apPublicUrl ?? $this->urlGenerator->generate( - 'ap_magazine', - ['name' => $report->magazine->name], - UrlGeneratorInterface::ABSOLUTE_URL - ), - ]; - } - - return $result; - } - - public function getPublicUrl(ReportInterface $subject): string - { - $publicUrl = $subject->getApId(); - if ($publicUrl) { - return $publicUrl; - } - - return match (str_replace('Proxies\\__CG__\\', '', \get_class($subject))) { - Entry::class => $this->urlGenerator->generate('ap_entry', [ - 'magazine_name' => $subject->magazine->name, - 'entry_id' => $subject->getId(), - ], UrlGeneratorInterface::ABSOLUTE_URL), - EntryComment::class => $this->urlGenerator->generate('ap_entry_comment', [ - 'magazine_name' => $subject->magazine->name, - 'entry_id' => $subject->entry->getId(), - 'comment_id' => $subject->getId(), - ], UrlGeneratorInterface::ABSOLUTE_URL), - Post::class => $this->urlGenerator->generate('ap_post', [ - 'magazine_name' => $subject->magazine->name, - 'post_id' => $subject->getId(), - ], UrlGeneratorInterface::ABSOLUTE_URL), - PostComment::class => $this->urlGenerator->generate('ap_post_comment', [ - 'magazine_name' => $subject->magazine->name, - 'post_id' => $subject->post->getId(), - 'comment_id' => $subject->getId(), - ], UrlGeneratorInterface::ABSOLUTE_URL), - default => throw new \LogicException("can't handle ".\get_class($subject)), - }; + return $activity; } } diff --git a/src/Message/ActivityPub/Outbox/AnnounceLikeMessage.php b/src/Message/ActivityPub/Outbox/AnnounceLikeMessage.php index 8aeec6b9fa..70f016c8be 100644 --- a/src/Message/ActivityPub/Outbox/AnnounceLikeMessage.php +++ b/src/Message/ActivityPub/Outbox/AnnounceLikeMessage.php @@ -13,6 +13,7 @@ public function __construct( public int $objectId, public string $objectType, public bool $undo = false, + public ?string $likeMessageId = null, ) { } } diff --git a/src/Message/ActivityPub/Outbox/GenericAnnounceMessage.php b/src/Message/ActivityPub/Outbox/GenericAnnounceMessage.php index 1a7ae2229a..3fe5cab307 100644 --- a/src/Message/ActivityPub/Outbox/GenericAnnounceMessage.php +++ b/src/Message/ActivityPub/Outbox/GenericAnnounceMessage.php @@ -8,7 +8,10 @@ class GenericAnnounceMessage implements ActivityPubOutboxInterface { - public function __construct(public int $announcingMagazineId, public array $payloadToAnnounce, public ?string $sourceInstance) + /** + * @param array|null $payloadToAnnounce THIS IS NOT USED ANYMORE, ONLY THERE FOR BACKWARDS COMPATIBILITY + */ + public function __construct(public int $announcingMagazineId, public ?array $payloadToAnnounce, public ?string $sourceInstance, public ?string $innerActivityUUID, public ?string $innerActivityUrl) { } } diff --git a/src/MessageHandler/ActivityPub/Inbox/FollowHandler.php b/src/MessageHandler/ActivityPub/Inbox/FollowHandler.php index a31f0651cf..dd7755e51d 100644 --- a/src/MessageHandler/ActivityPub/Inbox/FollowHandler.php +++ b/src/MessageHandler/ActivityPub/Inbox/FollowHandler.php @@ -9,6 +9,7 @@ use App\Message\ActivityPub\Inbox\FollowMessage; use App\Message\Contracts\MessageInterface; use App\MessageHandler\MbinMessageHandler; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\ApHttpClient; use App\Service\ActivityPub\Wrapper\FollowResponseWrapper; use App\Service\ActivityPubManager; @@ -31,6 +32,7 @@ public function __construct( private readonly ApHttpClient $client, private readonly LoggerInterface $logger, private readonly FollowResponseWrapper $followResponseWrapper, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -106,13 +108,8 @@ private function handleFollow(User|Magazine $object, User $actor): void private function handleFollowRequest(array $payload, User|Magazine $object, bool $isReject = false): void { - $response = $this->followResponseWrapper->build( - $payload['object'], - $payload['actor'], - $payload['id'], - $isReject - ); - + $activity = $this->followResponseWrapper->build($object, $payload['object'], $isReject); + $response = $this->activityJsonBuilder->buildActivityJson($activity); $this->client->post($this->client->getInboxUrl($payload['actor']), $object, $response); } diff --git a/src/MessageHandler/ActivityPub/Inbox/LikeHandler.php b/src/MessageHandler/ActivityPub/Inbox/LikeHandler.php index 6cff548df8..e7c436a526 100644 --- a/src/MessageHandler/ActivityPub/Inbox/LikeHandler.php +++ b/src/MessageHandler/ActivityPub/Inbox/LikeHandler.php @@ -89,7 +89,7 @@ public function doWork(MessageInterface $message): void if (isset($entity) and isset($actor) and ($entity instanceof Entry or $entity instanceof EntryComment or $entity instanceof Post or $entity instanceof PostComment)) { if (!$entity->magazine->apId and $actor->apId and 'random' !== $entity->magazine->name) { // local magazine, but remote user. Don't announce for random magazine - $this->bus->dispatch(new AnnounceLikeMessage($actor->getId(), $entity->getId(), \get_class($entity), 'Undo' === $message->payload['type'])); + $this->bus->dispatch(new AnnounceLikeMessage($actor->getId(), $entity->getId(), \get_class($entity), 'Undo' === $message->payload['type'], $message->payload['id'])); } } } diff --git a/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php b/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php index 4cbfd9d9ab..02f5a48279 100644 --- a/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php +++ b/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php @@ -120,22 +120,22 @@ private function editActivity(array $object, User $actor, array $payload): void if ($object instanceof Entry) { $this->editEntry($object, $actor, $payload); if (null === $object->magazine->apId) { - $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), $payload, $actor->apInboxUrl)); + $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), null, $actor->apInboxUrl, null, $payload['id'])); } } elseif ($object instanceof EntryComment) { $this->editEntryComment($object, $actor, $payload); if (null === $object->magazine->apId) { - $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), $payload, $actor->apInboxUrl)); + $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), null, $actor->apInboxUrl, null, $payload['id'])); } } elseif ($object instanceof Post) { $this->editPost($object, $actor, $payload); if (null === $object->magazine->apId) { - $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), $payload, $actor->apInboxUrl)); + $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), null, $actor->apInboxUrl, null, $payload['id'])); } } elseif ($object instanceof PostComment) { $this->editPostComment($object, $actor, $payload); if (null === $object->magazine->apId) { - $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), $payload, $actor->apInboxUrl)); + $this->bus->dispatch(new GenericAnnounceMessage($object->magazine->getId(), null, $actor->apInboxUrl, null, $payload['id'])); } } elseif ($object instanceof Message) { $this->editMessage($object, $actor, $payload); diff --git a/src/MessageHandler/ActivityPub/Outbox/AddHandler.php b/src/MessageHandler/ActivityPub/Outbox/AddHandler.php index 94d52aa4ed..bd30370f3b 100644 --- a/src/MessageHandler/ActivityPub/Outbox/AddHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/AddHandler.php @@ -10,6 +10,7 @@ use App\MessageHandler\MbinMessageHandler; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\DeliverManager; use App\Service\SettingsManager; use Doctrine\ORM\EntityManagerInterface; @@ -27,6 +28,7 @@ public function __construct( private readonly SettingsManager $settingsManager, private readonly AddRemoveFactory $factory, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -59,6 +61,7 @@ public function doWork(MessageInterface $message): void } $activity = $this->factory->buildAddModerator($actor, $added, $magazine); - $this->deliverManager->deliver($audience, $activity); + $json = $this->activityJsonBuilder->buildActivityJson($activity); + $this->deliverManager->deliver($audience, $json); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/AnnounceHandler.php b/src/MessageHandler/ActivityPub/Outbox/AnnounceHandler.php index dddc68ed45..996cf658f5 100644 --- a/src/MessageHandler/ActivityPub/Outbox/AnnounceHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/AnnounceHandler.php @@ -10,12 +10,13 @@ use App\Entity\Post; use App\Entity\PostComment; use App\Entity\User; -use App\Factory\ActivityPub\ActivityFactory; use App\Message\ActivityPub\Outbox\AnnounceMessage; use App\Message\Contracts\MessageInterface; use App\MessageHandler\MbinMessageHandler; +use App\Repository\ActivityRepository; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\AnnounceWrapper; use App\Service\ActivityPub\Wrapper\CreateWrapper; use App\Service\ActivityPub\Wrapper\UndoWrapper; @@ -39,9 +40,10 @@ public function __construct( private readonly UndoWrapper $undoWrapper, private readonly CreateWrapper $createWrapper, private readonly ActivityPubManager $activityPubManager, - private readonly ActivityFactory $activityFactory, private readonly DeliverManager $deliverManager, private readonly SettingsManager $settingsManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, + private readonly ActivityRepository $activityRepository, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -70,16 +72,14 @@ public function doWork(MessageInterface $message): void $object = $this->entityManager->getRepository($message->objectType)->find($message->objectId); - $activity = $this->announceWrapper->build( - $this->activityPubManager->getActorProfileId($actor), - $this->activityFactory->create($object), - true - ); - if ($actor instanceof Magazine && ($object instanceof Entry || $object instanceof Post || $object instanceof EntryComment || $object instanceof PostComment)) { - $wrapperObject = $this->createWrapper->build($object); - unset($wrapperObject['@context']); - $activity['object'] = $wrapperObject; + $createActivity = $this->activityRepository->findFirstActivitiesByTypeAndObject('Create', $object); + if (null === $createActivity) { + $createActivity = $this->createWrapper->build($object); + } + $activity = $this->announceWrapper->build($actor, $createActivity, true); + } else { + $activity = $this->announceWrapper->build($actor, $object, true); } if ($message->removeAnnounce) { @@ -91,11 +91,13 @@ public function doWork(MessageInterface $message): void [$object->user->apInboxUrl, $object->magazine->apId ? $object->magazine->apInboxUrl : null] ); + $json = $this->activityJsonBuilder->buildActivityJson($activity); + if ($actor instanceof User) { $inboxes = array_merge( $inboxes, $this->userRepository->findAudience($actor), - $this->activityPubManager->createInboxesFromCC($activity, $actor), + $this->activityPubManager->createInboxesFromCC($json, $actor), ); } elseif ($actor instanceof Magazine) { if ('random' === $actor->name) { @@ -110,6 +112,6 @@ public function doWork(MessageInterface $message): void } $inboxes = array_filter(array_unique($inboxes)); - $this->deliverManager->deliver($inboxes, $activity); + $this->deliverManager->deliver($inboxes, $json); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/AnnounceLikeHandler.php b/src/MessageHandler/ActivityPub/Outbox/AnnounceLikeHandler.php index ac532971ff..60fffca371 100644 --- a/src/MessageHandler/ActivityPub/Outbox/AnnounceLikeHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/AnnounceLikeHandler.php @@ -8,22 +8,22 @@ use App\Entity\EntryComment; use App\Entity\Post; use App\Entity\PostComment; -use App\Factory\ActivityPub\ActivityFactory; -use App\Factory\ActivityPub\PersonFactory; use App\Message\ActivityPub\Outbox\AnnounceLikeMessage; use App\Message\Contracts\MessageInterface; use App\MessageHandler\MbinMessageHandler; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; +use App\Service\ActivityPub\ApHttpClient; use App\Service\ActivityPub\Wrapper\AnnounceWrapper; use App\Service\ActivityPub\Wrapper\LikeWrapper; use App\Service\ActivityPub\Wrapper\UndoWrapper; use App\Service\DeliverManager; use App\Service\SettingsManager; use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; #[AsMessageHandler] class AnnounceLikeHandler extends MbinMessageHandler @@ -36,11 +36,11 @@ public function __construct( private readonly AnnounceWrapper $announceWrapper, private readonly UndoWrapper $undoWrapper, private readonly LikeWrapper $likeWrapper, - private readonly ActivityFactory $activityFactory, private readonly SettingsManager $settingsManager, - private readonly UrlGeneratorInterface $urlGenerator, - private readonly PersonFactory $personFactory, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, + private readonly ApHttpClient $apHttpClient, + private readonly LoggerInterface $logger, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -73,23 +73,34 @@ public function doWork(MessageInterface $message): void return; } - $activityObject = $this->activityFactory->create($object); - $likeActivity = $this->likeWrapper->build($this->personFactory->getActivityPubId($user), $activityObject); + if (null === $message->likeMessageId) { + $this->logger->warning('Got an AnnounceLikeMessage without a remote like id, probably an old message though'); - if ($message->undo) { - $likeActivity = $this->undoWrapper->build($likeActivity); + return; + } + if (false === filter_var($message->likeMessageId, FILTER_VALIDATE_URL)) { + $this->logger->warning('Got an AnnounceLikeMessage without a valid remote like id: {url}', ['url' => $message->likeMessageId]); + + return; + } + + $this->logger->debug('got AnnounceLikeMessage: {m}', ['m' => json_encode($message)]); + $this->logger->debug('building like activity for: {a}', ['a' => json_encode($object)]); + + if (!$message->undo) { + $likeActivity = $message->likeMessageId; + } else { + $likeActivity = $this->undoWrapper->build($message->likeMessageId, $user); } - $activity = $this->announceWrapper->build( - $this->urlGenerator->generate('ap_magazine', ['name' => $object->magazine->name], UrlGeneratorInterface::ABSOLUTE_URL), - $likeActivity - ); + $activity = $this->announceWrapper->build($object->magazine, $likeActivity); + $json = $this->activityJsonBuilder->buildActivityJson($activity); $inboxes = array_filter(array_unique(array_merge( $this->magazineRepository->findAudience($object->magazine), $this->userRepository->findAudience($user), [$object->user->apInboxUrl] ))); - $this->deliverManager->deliver($inboxes, $activity); + $this->deliverManager->deliver($inboxes, $json); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/CreateHandler.php b/src/MessageHandler/ActivityPub/Outbox/CreateHandler.php index 9373318761..892c7edf4e 100644 --- a/src/MessageHandler/ActivityPub/Outbox/CreateHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/CreateHandler.php @@ -10,6 +10,7 @@ use App\MessageHandler\MbinMessageHandler; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\CreateWrapper; use App\Service\ActivityPubManager; use App\Service\DeliverManager; @@ -34,6 +35,7 @@ public function __construct( private readonly LoggerInterface $logger, private readonly DeliverManager $deliverManager, private readonly KernelInterface $kernel, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -55,6 +57,7 @@ public function doWork(MessageInterface $message): void $entity = $this->entityManager->getRepository($message->type)->find($message->id); $activity = $this->createWrapper->build($entity); + $json = $this->activityJsonBuilder->buildActivityJson($activity); if ($entity instanceof Message) { $receivers = $this->messageManager->findAudience($entity->thread); @@ -62,7 +65,7 @@ public function doWork(MessageInterface $message): void } else { $receivers = [ ...$this->userRepository->findAudience($entity->user), - ...$this->activityPubManager->createInboxesFromCC($activity, $entity->user), + ...$this->activityPubManager->createInboxesFromCC($json, $entity->user), ]; if ('random' !== $entity->magazine->name) { // only add the magazine subscribers if it is not the random magazine @@ -70,6 +73,6 @@ public function doWork(MessageInterface $message): void } $this->logger->debug('[CreateHandler::doWork] Sending create activity to {p}', ['p' => $receivers]); } - $this->deliverManager->deliver(array_filter(array_unique($receivers)), $activity); + $this->deliverManager->deliver(array_filter(array_unique($receivers)), $json); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/EntryPinMessageHandler.php b/src/MessageHandler/ActivityPub/Outbox/EntryPinMessageHandler.php index 79c528b093..cadf5699fd 100644 --- a/src/MessageHandler/ActivityPub/Outbox/EntryPinMessageHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/EntryPinMessageHandler.php @@ -11,6 +11,7 @@ use App\Repository\EntryRepository; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\DeliverManager; use App\Service\SettingsManager; use Doctrine\ORM\EntityManagerInterface; @@ -31,6 +32,7 @@ public function __construct( private readonly MagazineRepository $magazineRepository, private readonly DeliverManager $deliverManager, private readonly LoggerInterface $logger, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -74,6 +76,7 @@ public function doWork(MessageInterface $message): void $audience = $this->magazineRepository->findAudience($entry->magazine); } - $this->deliverManager->deliver($audience, $activity); + $json = $this->activityJsonBuilder->buildActivityJson($activity); + $this->deliverManager->deliver($audience, $json); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php b/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php index c0acf405b3..f095f494b4 100644 --- a/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php @@ -11,6 +11,7 @@ use App\Message\Contracts\MessageInterface; use App\MessageHandler\MbinMessageHandler; use App\Repository\ReportRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\DeliverManager; use App\Service\SettingsManager; use Doctrine\ORM\EntityManagerInterface; @@ -29,6 +30,7 @@ public function __construct( private readonly FlagFactory $factory, private readonly LoggerInterface $logger, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -56,8 +58,9 @@ public function doWork(MessageInterface $message): void return; } - $activity = $this->factory->build($report, $this->factory->getPublicUrl($report->getSubject())); - $this->deliverManager->deliver($inboxes, $activity); + $activity = $this->factory->build($report); + $json = $this->activityJsonBuilder->buildActivityJson($activity); + $this->deliverManager->deliver($inboxes, $json); } /** diff --git a/src/MessageHandler/ActivityPub/Outbox/FollowHandler.php b/src/MessageHandler/ActivityPub/Outbox/FollowHandler.php index 4280c1b1f7..7278e42410 100644 --- a/src/MessageHandler/ActivityPub/Outbox/FollowHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/FollowHandler.php @@ -7,8 +7,10 @@ use App\Message\ActivityPub\Outbox\FollowMessage; use App\Message\Contracts\MessageInterface; use App\MessageHandler\MbinMessageHandler; +use App\Repository\ActivityRepository; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\ApHttpClient; use App\Service\ActivityPub\Wrapper\FollowWrapper; use App\Service\ActivityPub\Wrapper\UndoWrapper; @@ -33,6 +35,8 @@ public function __construct( private readonly ApHttpClient $apHttpClient, private readonly SettingsManager $settingsManager, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, + private readonly ActivityRepository $activityRepository, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -58,17 +62,16 @@ public function doWork(MessageInterface $message): void $following = $this->userRepository->find($message->followingId); } - $followObject = $this->followWrapper->build( - $this->activityPubManager->getActorProfileId($follower), - $followingProfileId = $this->activityPubManager->getActorProfileId($following), - ); + $followObject = $this->activityRepository->findFirstActivitiesByTypeAndObject('Follow', $following); + if (null === $followObject) { + $followObject = $this->followWrapper->build($follower, $following); + } if ($message->unfollow) { $followObject = $this->undoWrapper->build($followObject); } - $inbox = $this->apHttpClient->getInboxUrl($followingProfileId); - - $this->deliverManager->deliver([$inbox], $followObject); + $json = $this->activityJsonBuilder->buildActivityJson($followObject); + $this->deliverManager->deliver([$following->apInboxUrl], $json); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/GenericAnnounceHandler.php b/src/MessageHandler/ActivityPub/Outbox/GenericAnnounceHandler.php index 48b0210352..f56976cb32 100644 --- a/src/MessageHandler/ActivityPub/Outbox/GenericAnnounceHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/GenericAnnounceHandler.php @@ -7,7 +7,9 @@ use App\Message\ActivityPub\Outbox\GenericAnnounceMessage; use App\Message\Contracts\MessageInterface; use App\MessageHandler\MbinMessageHandler; +use App\Repository\ActivityRepository; use App\Repository\MagazineRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\AnnounceWrapper; use App\Service\DeliverManager; use App\Service\SettingsManager; @@ -15,6 +17,7 @@ use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Uid\Uuid; #[AsMessageHandler] class GenericAnnounceHandler extends MbinMessageHandler @@ -27,6 +30,8 @@ public function __construct( private readonly MagazineRepository $magazineRepository, private readonly AnnounceWrapper $announceWrapper, private readonly DeliverManager $deliverManager, + private readonly ActivityRepository $activityRepository, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -44,6 +49,7 @@ public function doWork(MessageInterface $message): void if (!($message instanceof GenericAnnounceMessage)) { throw new \LogicException(); } + $magazine = $this->magazineRepository->find($message->announcingMagazineId); if (null !== $magazine->apId) { return; @@ -53,9 +59,18 @@ public function doWork(MessageInterface $message): void // do not federate the random magazine return; } - $magazineUrl = $this->urlGenerator->generate('ap_magazine', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL); - $announce = $this->announceWrapper->build($magazineUrl, $message->payloadToAnnounce); + + if (null !== $message->innerActivityUUID) { + $object = $this->activityRepository->findOneBy(['uuid' => Uuid::fromString($message->innerActivityUUID)]); + } elseif (null !== $message->innerActivityUrl) { + $object = $message->innerActivityUrl; + } else { + throw new \LogicException('expect at least one of innerActivityUUID or innerActivityUrl to not be null'); + } + + $announce = $this->announceWrapper->build($magazine, $object); + $json = $this->activityJsonBuilder->buildActivityJson($announce); $inboxes = array_filter($this->magazineRepository->findAudience($magazine), fn ($item) => null !== $item && $item !== $message->sourceInstance); - $this->deliverManager->deliver($inboxes, $announce); + $this->deliverManager->deliver($inboxes, $json); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/LikeHandler.php b/src/MessageHandler/ActivityPub/Outbox/LikeHandler.php index 0dccfedd0c..2f10cc9538 100644 --- a/src/MessageHandler/ActivityPub/Outbox/LikeHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/LikeHandler.php @@ -14,6 +14,7 @@ use App\MessageHandler\MbinMessageHandler; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\LikeWrapper; use App\Service\ActivityPub\Wrapper\UndoWrapper; use App\Service\ActivityPubManager; @@ -37,6 +38,7 @@ public function __construct( private readonly ActivityFactory $activityFactory, private readonly SettingsManager $settingsManager, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -59,10 +61,7 @@ public function doWork(MessageInterface $message): void /** @var Entry|EntryComment|Post|PostComment $object */ $object = $this->entityManager->getRepository($message->objectType)->find($message->objectId); - $activity = $this->likeWrapper->build( - $this->activityPubManager->getActorProfileId($user), - $this->activityFactory->create($object), - ); + $activity = $this->likeWrapper->build($user, $object); if ($message->removeLike) { $activity = $this->undoWrapper->build($activity); @@ -78,6 +77,6 @@ public function doWork(MessageInterface $message): void $inboxes = array_merge($inboxes, $this->magazineRepository->findAudience($object->magazine)); } - $this->deliverManager->deliver(array_filter(array_unique($inboxes)), $activity); + $this->deliverManager->deliver(array_filter(array_unique($inboxes)), $this->activityJsonBuilder->buildActivityJson($activity)); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/RemoveHandler.php b/src/MessageHandler/ActivityPub/Outbox/RemoveHandler.php index 029967f232..478d2e3910 100644 --- a/src/MessageHandler/ActivityPub/Outbox/RemoveHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/RemoveHandler.php @@ -10,6 +10,7 @@ use App\MessageHandler\MbinMessageHandler; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\DeliverManager; use App\Service\SettingsManager; use Doctrine\ORM\EntityManagerInterface; @@ -27,6 +28,7 @@ public function __construct( private readonly SettingsManager $settingsManager, private readonly AddRemoveFactory $factory, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -61,6 +63,6 @@ public function doWork(MessageInterface $message): void } $activity = $this->factory->buildRemoveModerator($actor, $removed, $magazine); - $this->deliverManager->deliver($audience, $activity); + $this->deliverManager->deliver($audience, $this->activityJsonBuilder->buildActivityJson($activity)); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php b/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php index 441c849146..dee8dfaa61 100644 --- a/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/UpdateHandler.php @@ -18,6 +18,7 @@ use App\MessageHandler\MbinMessageHandler; use App\Repository\MagazineRepository; use App\Repository\UserRepository; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\UpdateWrapper; use App\Service\ActivityPubManager; use App\Service\DeliverManager; @@ -38,6 +39,7 @@ public function __construct( private readonly DeliverManager $deliverManager, private readonly UpdateWrapper $updateWrapper, private readonly KernelInterface $kernel, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -63,7 +65,8 @@ public function doWork(MessageInterface $message): void } if ($entity instanceof ActivityPubActivityInterface) { - $activity = $this->updateWrapper->buildForActivity($entity, $editedByUser); + $activityObject = $this->updateWrapper->buildForActivity($entity, $editedByUser); + $activity = $this->activityJsonBuilder->buildActivityJson($activityObject); if ($entity instanceof Entry || $entity instanceof EntryComment || $entity instanceof Post || $entity instanceof PostComment) { if ('random' === $entity->magazine->name) { @@ -85,7 +88,9 @@ public function doWork(MessageInterface $message): void throw new \LogicException('unknown activity type: '.\get_class($entity)); } } elseif ($entity instanceof ActivityPubActorInterface) { - $activity = $this->updateWrapper->buildForActor($entity, $editedByUser); + $activityObject = $this->updateWrapper->buildForActor($entity, $editedByUser); + $activity = $this->activityJsonBuilder->buildActivityJson($activityObject); + if ($entity instanceof User) { $inboxes = $this->userRepository->findAudience($entity); } elseif ($entity instanceof Magazine) { diff --git a/src/MessageHandler/DeleteUserHandler.php b/src/MessageHandler/DeleteUserHandler.php index 8379c1f17e..326a165d85 100644 --- a/src/MessageHandler/DeleteUserHandler.php +++ b/src/MessageHandler/DeleteUserHandler.php @@ -9,6 +9,7 @@ use App\Message\ActivityPub\Outbox\DeliverMessage; use App\Message\Contracts\MessageInterface; use App\Message\DeleteUserMessage; +use App\Service\ActivityPub\ActivityJsonBuilder; use App\Service\ActivityPub\Wrapper\DeleteWrapper; use App\Service\ImageManager; use App\Service\UserManager; @@ -30,6 +31,7 @@ public function __construct( private readonly DeleteWrapper $deleteWrapper, private readonly MessageBusInterface $bus, private readonly EntityManagerInterface $entityManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager, $this->kernel); } @@ -127,7 +129,8 @@ private function sendDeleteMessages(array $targetInboxes, User $deletedUser): vo return; } - $message = $this->deleteWrapper->buildForUser($deletedUser); + $activity = $this->deleteWrapper->buildForUser($deletedUser); + $message = $this->activityJsonBuilder->buildActivityJson($activity); foreach ($targetInboxes as $inbox) { $this->bus->dispatch(new DeliverMessage($inbox, $message)); diff --git a/src/Repository/ActivityRepository.php b/src/Repository/ActivityRepository.php new file mode 100644 index 0000000000..bea964e26d --- /dev/null +++ b/src/Repository/ActivityRepository.php @@ -0,0 +1,78 @@ +findAllActivitiesByTypeAndObject($type, $object); + if (!empty($results)) { + return $results[0]; + } + + return null; + } + + /** + * @return Activity[]|null + */ + public function findAllActivitiesByTypeAndObject(string $type, ActivityPubActivityInterface|ActivityPubActorInterface $object): ?array + { + $qb = $this->createQueryBuilder('a'); + $qb->where('a.type = :type'); + $qb->setParameter('type', $type); + + if ($object instanceof Entry) { + $qb->andWhere('a.objectEntry = :entry') + ->setParameter('entry', $object); + } elseif ($object instanceof EntryComment) { + $qb->andWhere('a.objectEntryComment = :entryComment') + ->setParameter('entryComment', $object); + } elseif ($object instanceof Post) { + $qb->andWhere('a.objectPost = :post') + ->setParameter('post', $object); + } elseif ($object instanceof PostComment) { + $qb->andWhere('a.objectPostComment = :postComment') + ->setParameter('postComment', $object); + } elseif ($object instanceof Message) { + $qb->andWhere('a.objectMessage = :message') + ->setParameter('message', $object); + } elseif ($object instanceof User) { + $qb->andWhere('a.objectUser = :user') + ->setParameter('user', $object); + } elseif ($object instanceof Magazine) { + $qb->andWhere('a.objectMagazine = :magazine') + ->setParameter('magazine', $object); + } + + return $qb->getQuery()->getResult(); + } +} diff --git a/src/Repository/EntryRepository.php b/src/Repository/EntryRepository.php index 33466bff9b..d5c06e28e4 100644 --- a/src/Repository/EntryRepository.php +++ b/src/Repository/EntryRepository.php @@ -12,7 +12,6 @@ use App\Entity\DomainBlock; use App\Entity\DomainSubscription; use App\Entity\Entry; -use App\Entity\EntryFavourite; use App\Entity\HashtagLink; use App\Entity\Magazine; use App\Entity\MagazineBlock; @@ -29,6 +28,7 @@ use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\NoResultException; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Pagerfanta\Exception\NotValidCurrentPageException; @@ -215,9 +215,7 @@ private function filter(QueryBuilder $qb, EntryPageView $criteria): QueryBuilder } if ($criteria->favourite) { - $qb->andWhere( - 'e.id IN (SELECT IDENTITY(mf.entry) FROM '.EntryFavourite::class.' mf WHERE mf.user = :user)' - ); + $qb->innerJoin('e.favourites', 'mf', Join::ON, 'mf.user = :user AND mf.entry = e'); $qb->setParameter('user', $this->security->getUser()); } @@ -261,7 +259,11 @@ private function filter(QueryBuilder $qb, EntryPageView $criteria): QueryBuilder default: } - $qb->addOrderBy('e.createdAt', Criteria::SORT_OLD === $criteria->sortOption ? 'ASC' : 'DESC'); + if (!$criteria->favourite) { + $qb->addOrderBy('e.createdAt', Criteria::SORT_OLD === $criteria->sortOption ? 'ASC' : 'DESC'); + } else { + $qb->addOrderBy('mf.createdAt', Criteria::SORT_OLD === $criteria->sortOption ? 'ASC' : 'DESC'); + } $qb->addOrderBy('e.id', 'DESC'); return $qb; diff --git a/src/Service/ActivityPub/ActivityJsonBuilder.php b/src/Service/ActivityPub/ActivityJsonBuilder.php new file mode 100644 index 0000000000..1fa579098b --- /dev/null +++ b/src/Service/ActivityPub/ActivityJsonBuilder.php @@ -0,0 +1,370 @@ +logger->debug('activity json: build for {id}', ['id' => $activity->uuid->toString()]); + if (null !== $activity->activityJson) { + $json = json_decode($activity->activityJson, true); + $this->logger->debug('activity json: {json}', ['json' => json_encode($json, JSON_PRETTY_PRINT)]); + + return $json; + } + + $json = match ($activity->type) { + 'Create' => $this->buildCreateFromActivity($activity), + 'Like' => $this->buildLikeFromActivity($activity), + 'Undo' => $this->buildUndoFromActivity($activity), + 'Announce' => $this->buildAnnounceFromActivity($activity), + 'Delete' => $this->buildDeleteFromActivity($activity), + 'Add', 'Remove' => $this->buildAddRemoveFromActivity($activity), + 'Flag' => $this->buildFlagFromActivity($activity), + 'Follow' => $this->buildFollowFromActivity($activity), + 'Accept', 'Reject' => $this->buildAcceptRejectFromActivity($activity), + 'Update' => $this->buildUpdateFromActivity($activity), + default => new \LogicException(), + }; + $this->logger->debug('activity json: {json}', ['json' => json_encode($json, JSON_PRETTY_PRINT)]); + + return $json; + } + + public function buildCreateFromActivity(Activity $activity): array + { + $o = $activity->objectEntry ?? $activity->objectEntryComment ?? $activity->objectPost ?? $activity->objectPostComment ?? $activity->objectMessage; + $item = $this->activityFactory->create($o, true); + + unset($item['@context']); + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Create', + 'actor' => $item['attributedTo'], + 'published' => $item['published'], + 'to' => $item['to'], + 'cc' => $item['cc'], + 'object' => $item, + ]; + } + + public function buildLikeFromActivity(Activity $activity): array + { + $actor = $this->personFactory->getActivityPubId($activity->userActor); + if (null !== $activity->userActor->apId) { + throw new \LogicException('activities cannot be build for remote users'); + } + $object = $activity->getObject(); + if (!\is_string($object)) { + throw new \LogicException('object must be a string'); + } + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Like', + 'actor' => $actor, + 'to' => [ActivityPubActivityInterface::PUBLIC_URL], + 'cc' => [ + $this->urlGenerator->generate('ap_user_followers', ['username' => $activity->userActor->username], UrlGeneratorInterface::ABSOLUTE_URL), + ], + 'object' => $object, + ]; + } + + public function buildUndoFromActivity(Activity $activity): array + { + if (null !== $activity->innerActivity) { + $object = $this->buildActivityJson($activity->innerActivity); + } elseif (null !== $activity->innerActivityUrl) { + $object = $this->apHttpClient->getActivityObject($activity->innerActivityUrl); + if (!\is_array($object)) { + throw new \LogicException('object must be another activity'); + } + } else { + throw new \LogicException('undo activity must have an inner activity / -url'); + } + + unset($object['@context']); + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Undo', + 'actor' => $object['actor'], + 'object' => $object, + ]; + } + + public function buildAnnounceFromActivity(Activity $activity): array + { + $actor = $activity->getActor(); + $to = [ActivityPubActivityInterface::PUBLIC_URL]; + + $object = $activity->getObject(); + + if (null !== $activity->innerActivity) { + $object = $this->buildActivityJson($activity->innerActivity); + } elseif (null !== $activity->innerActivityUrl) { + $object = $this->apHttpClient->getActivityObject($activity->innerActivityUrl); + } elseif ($object instanceof ActivityPubActivityInterface) { + $object = $this->activityFactory->create($object); + if (isset($object['attributedTo'])) { + $to[] = $object['attributedTo']; + } + + unset($object['@context']); + } + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Announce', + 'actor' => $actor instanceof User ? $this->personFactory->getActivityPubId($actor) : $this->groupFactory->getActivityPubId($actor), + 'object' => $object, + 'to' => $to, + 'cc' => $object['cc'] ?? [], + 'published' => (new \DateTime())->format(DATE_ATOM), + ]; + } + + public function buildDeleteFromActivity(Activity $activity): array + { + $item = $activity->getObject(); + if (!\is_array($item)) { + throw new \LogicException(); + } + + $activityActor = $activity->getActor(); + if ($activityActor instanceof User) { + $userUrl = $this->personFactory->getActivityPubId($activityActor); + } elseif ($activityActor instanceof Magazine) { + $userUrl = $this->groupFactory->getActivityPubId($activityActor); + } else { + throw new \LogicException(); + } + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Delete', + 'actor' => $userUrl, + 'object' => [ + 'id' => $item['id'], + 'type' => 'Tombstone', + ], + 'to' => $item['to'], + 'cc' => $item['cc'], + ]; + } + + public function buildAddRemoveFromActivity(Activity $activity): array + { + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'actor' => $this->personFactory->getActivityPubId($activity->userActor), + 'to' => [ActivityPubActivityInterface::PUBLIC_URL], + 'object' => $this->personFactory->getActivityPubId($activity->objectUser), + 'cc' => [$this->groupFactory->getActivityPubId($activity->audience)], + 'type' => $activity->type, + 'target' => $activity->targetString, + 'audience' => $this->groupFactory->getActivityPubId($activity->audience), + ]; + } + + public function buildFlagFromActivity(Activity $activity): array + { + // mastodon does not accept a report that does not have an array as object. + // I created an issue for it: https://github.com/mastodon/mastodon/issues/28159 + $mastodonObject = [ + $this->getPublicUrl($activity->getObject()), + $this->personFactory->getActivityPubId($activity->objectUser), + ]; + + // lemmy does not accept a report that does have an array as object. + // I created an issue for it: https://github.com/LemmyNet/lemmy/issues/4217 + $lemmyObject = $this->getPublicUrl($activity->getObject()); + + if ('random' !== $activity->audience || $activity->audience->apId) { + // apAttributedToUrl is not a standardized field, + // so it is not implemented by every software that supports groups. + // Some don't have moderation at all, so it will probably remain optional in the future. + $audience = $this->groupFactory->getActivityPubId($activity->audience); + $object = $lemmyObject; + } else { + $audience = $this->personFactory->getActivityPubId($activity->objectUser); + $object = $mastodonObject; + } + + $result = [ + '@context' => ActivityPubActivityInterface::CONTEXT_URL, + 'id' => $this->urlGenerator->generate('ap_object', ['report_id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Flag', + 'actor' => $this->personFactory->getActivityPubId($activity->userActor), + 'object' => $object, + 'audience' => $audience, + 'summary' => $activity->contentString, + 'content' => $activity->contentString, + ]; + + if ('random' !== $activity->audience->name || $activity->audience->apId) { + $result['to'] = [$this->groupFactory->getActivityPubId($activity->audience)]; + } + + return $result; + } + + public function buildFollowFromActivity(Activity $activity): array + { + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Follow', + 'actor' => $this->personFactory->getActivityPubId($activity->userActor), + 'object' => $this->personFactory->getActivityPubId($activity->objectUser), + ]; + } + + public function buildAcceptRejectFromActivity(Activity $activity): array + { + $activityActor = $activity->getActor(); + if ($activityActor instanceof User) { + $actor = $this->personFactory->getActivityPubId($activityActor); + } elseif ($activityActor instanceof Magazine) { + $actor = $this->groupFactory->getActivityPubId($activityActor); + } else { + throw new \LogicException(); + } + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => $activity->type, + 'actor' => $actor, + 'object' => $activity->getObject(), + ]; + } + + public function buildUpdateFromActivity(Activity $activity): array + { + $object = $activity->getObject(); + if ($object instanceof ActivityPubActivityInterface) { + return $this->buildUpdateForContentFromActivity($activity, $object); + } elseif ($object instanceof ActivityPubActorInterface) { + return $this->buildUpdateForActorFromActivity($activity, $object); + } else { + throw new \LogicException(); + } + } + + public function buildUpdateForContentFromActivity(Activity $activity, ActivityPubActivityInterface $content): array + { + $entity = $this->activityFactory->create($content); + + $entity['object']['updated'] = $content->editedAt ? $content->editedAt->format(DATE_ATOM) : (new \DateTime())->format(DATE_ATOM); + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Update', + 'actor' => $this->personFactory->getActivityPubId($activity->userActor), + 'published' => $entity['published'], + 'to' => $entity['to'], + 'cc' => $entity['cc'], + 'object' => $entity, + ]; + } + + public function buildUpdateForActorFromActivity(Activity $activity, ActivityPubActorInterface $object): array + { + if ($object instanceof User) { + $activityObject = $this->personFactory->create($object, false); + if (null === $object->apId) { + $cc = [$this->urlGenerator->generate('ap_user_followers', ['username' => $object->username], UrlGeneratorInterface::ABSOLUTE_URL)]; + } else { + $cc = [$object->apFollowersUrl]; + } + } elseif ($object instanceof Magazine) { + $activityObject = $this->groupFactory->create($object, false); + if (null === $object->apId) { + $cc = [$this->urlGenerator->generate('ap_magazine_followers', ['name' => $object->name], UrlGeneratorInterface::ABSOLUTE_URL)]; + } else { + $cc = [$object->apFollowersUrl]; + } + } else { + throw new \LogicException('Unknown actor type: '.\get_class($object)); + } + + $actorUrl = $this->personFactory->getActivityPubId($activity->userActor); + + return [ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'Update', + 'actor' => $actorUrl, + 'published' => $activityObject['published'], + 'to' => [ActivityPubActivityInterface::PUBLIC_URL], + 'cc' => $cc, + 'object' => $activityObject, + ]; + } + + public function getPublicUrl(ReportInterface|ActivityPubActivityInterface $subject): string + { + if ($subject instanceof Entry) { + return $this->entryPageFactory->getActivityPubId($subject); + } elseif ($subject instanceof EntryComment) { + return $this->entryCommentNoteFactory->getActivityPubId($subject); + } elseif ($subject instanceof Post) { + return $this->postNoteFactory->getActivityPubId($subject); + } elseif ($subject instanceof PostComment) { + return $this->postCommentNoteFactory->getActivityPubId($subject); + } elseif ($subject instanceof Message) { + return $this->urlGenerator->generate('ap_message', ['uuid' => $subject->uuid], UrlGeneratorInterface::ABSOLUTE_URL); + } + + throw new \LogicException("can't handle ".\get_class($subject)); + } +} diff --git a/src/Service/ActivityPub/Wrapper/AnnounceWrapper.php b/src/Service/ActivityPub/Wrapper/AnnounceWrapper.php index 3c6a3689a1..0aa4112332 100644 --- a/src/Service/ActivityPub/Wrapper/AnnounceWrapper.php +++ b/src/Service/ActivityPub/Wrapper/AnnounceWrapper.php @@ -4,43 +4,54 @@ namespace App\Service\ActivityPub\Wrapper; +use App\Entity\Activity; use App\Entity\Contracts\ActivityPubActivityInterface; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; +use App\Entity\Magazine; +use App\Entity\User; +use App\Factory\ActivityPub\ActivityFactory; +use Doctrine\ORM\EntityManagerInterface; class AnnounceWrapper { - public function __construct(private readonly UrlGeneratorInterface $urlGenerator) - { + public function __construct( + private readonly ActivityFactory $activityFactory, + private readonly EntityManagerInterface $entityManager, + ) { } /** - * @param string $user the actor doing the announce - * @param array $object the thing the actor is announcing - * @param bool $idAsObject use only the id of $object as the 'object' in the payload. - * This should only be true for user boosts + * @param User|Magazine $actor the actor doing the announce + * @param ActivityPubActivityInterface|Activity|string $object the thing the actor is announcing. + * If it is a string it will be treated as a url to the activity this is announcing + * @param bool $idAsObject use only the id of $object as the 'object' in the payload. + * This should only be true for user boosts * - * @return array an announce activity + * @return Activity an announce activity */ - public function build(string $user, array $object, bool $idAsObject = false): array + public function build(User|Magazine $actor, ActivityPubActivityInterface|Activity|string $object, bool $idAsObject = false): Activity { - $id = Uuid::v4()->toRfc4122(); - - $to = [ActivityPubActivityInterface::PUBLIC_URL]; - - if (isset($object['attributedTo'])) { - $to[] = $object['attributedTo']; + $activity = new Activity('Announce'); + $activity->setActor($actor); + if ($object instanceof Activity) { + $activity->innerActivity = $object; + } elseif ($object instanceof ActivityPubActivityInterface) { + if ($idAsObject) { + $arr = $this->activityFactory->create($object); + $activity->setObject($arr['id']); + } else { + $activity->setObject($object); + } + } else { + $url = filter_var($object, FILTER_VALIDATE_URL); + if (false === $url) { + throw new \LogicException('expecting the object to be an url if it is a string'); + } + $activity->innerActivityUrl = $url; } - return [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => 'Announce', - 'actor' => $user, - 'object' => $idAsObject ? $object['id'] : $object, - 'to' => $to, - 'cc' => $object['cc'] ?? [], - 'published' => (new \DateTime())->format(DATE_ATOM), - ]; + $this->entityManager->persist($activity); + $this->entityManager->flush(); + + return $activity; } } diff --git a/src/Service/ActivityPub/Wrapper/CreateWrapper.php b/src/Service/ActivityPub/Wrapper/CreateWrapper.php index 6465bd23b6..985e221686 100644 --- a/src/Service/ActivityPub/Wrapper/CreateWrapper.php +++ b/src/Service/ActivityPub/Wrapper/CreateWrapper.php @@ -4,47 +4,34 @@ namespace App\Service\ActivityPub\Wrapper; +use App\Entity\Activity; use App\Entity\Contracts\ActivityPubActivityInterface; -use App\Factory\ActivityPub\ActivityFactory; -use JetBrains\PhpStorm\ArrayShape; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; +use App\Entity\Entry; +use App\Entity\EntryComment; +use App\Entity\Message; +use App\Entity\Post; +use App\Entity\PostComment; +use Doctrine\ORM\EntityManagerInterface; class CreateWrapper { public function __construct( - private readonly ActivityFactory $factory, - private readonly UrlGeneratorInterface $urlGenerator, + private readonly EntityManagerInterface $entityManager, ) { } - #[ArrayShape([ - '@context' => 'mixed', - 'id' => 'mixed', - 'type' => 'string', - 'actor' => 'mixed', - 'published' => 'mixed', - 'to' => 'mixed', - 'cc' => 'mixed', - 'object' => 'array', - ])] - public function build(ActivityPubActivityInterface $item): array + public function build(ActivityPubActivityInterface $item): Activity { - $item = $this->factory->create($item, true); - $id = Uuid::v4()->toRfc4122(); + $activity = new Activity('Create'); + $activity->setObject($item); + if ($item instanceof Entry || $item instanceof EntryComment || $item instanceof Post || $item instanceof PostComment) { + $activity->userActor = $item->getUser(); + } elseif ($item instanceof Message) { + $activity->userActor = $item->sender; + } + $this->entityManager->persist($activity); + $this->entityManager->flush(); - $context = $item['@context']; - unset($item['@context']); - - return [ - '@context' => $context, - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => 'Create', - 'actor' => $item['attributedTo'], - 'published' => $item['published'], - 'to' => $item['to'], - 'cc' => $item['cc'], - 'object' => $item, - ]; + return $activity; } } diff --git a/src/Service/ActivityPub/Wrapper/DeleteWrapper.php b/src/Service/ActivityPub/Wrapper/DeleteWrapper.php index a4a2c5c310..01d6824829 100644 --- a/src/Service/ActivityPub/Wrapper/DeleteWrapper.php +++ b/src/Service/ActivityPub/Wrapper/DeleteWrapper.php @@ -4,6 +4,7 @@ namespace App\Service\ActivityPub\Wrapper; +use App\Entity\Activity; use App\Entity\Contracts\ActivityPubActivityInterface; use App\Entity\Entry; use App\Entity\EntryComment; @@ -11,9 +12,9 @@ use App\Entity\PostComment; use App\Entity\User; use App\Factory\ActivityPub\ActivityFactory; -use JetBrains\PhpStorm\ArrayShape; +use App\Service\ActivityPub\ContextsProvider; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; class DeleteWrapper { @@ -21,20 +22,14 @@ public function __construct( private readonly ActivityFactory $factory, private readonly AnnounceWrapper $announceWrapper, private readonly UrlGeneratorInterface $urlGenerator, + private readonly EntityManagerInterface $entityManager, + private readonly ContextsProvider $contextsProvider, ) { } - #[ArrayShape([ - '@context' => 'string', - 'id' => 'string', - 'type' => 'string', - 'object' => 'mixed', - 'actor' => 'mixed', - 'to' => 'mixed', - 'cc' => 'mixed', - ])] - public function build(ActivityPubActivityInterface $item, string $id, ?User $deletingUser = null): array + public function build(ActivityPubActivityInterface $item, ?User $deletingUser = null): Activity { + $activity = new Activity('Delete'); $item = $this->factory->create($item); $userUrl = $item['attributedTo']; @@ -48,9 +43,12 @@ public function build(ActivityPubActivityInterface $item, string $id, ?User $del } } - return [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), + $this->entityManager->persist($activity); + $this->entityManager->flush(); + + $activity->activityJson = json_encode([ + '@context' => $this->contextsProvider->referencedContexts(), + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), 'type' => 'Delete', 'actor' => $userUrl, 'object' => [ @@ -59,17 +57,22 @@ public function build(ActivityPubActivityInterface $item, string $id, ?User $del ], 'to' => $item['to'], 'cc' => $item['cc'], - ]; + ]); + $this->entityManager->persist($activity); + $this->entityManager->flush(); + + return $activity; } - public function buildForUser(User $user): array + public function buildForUser(User $user): Activity { - $id = Uuid::v4()->toRfc4122(); + $activity = new Activity('Delete'); + $this->entityManager->persist($activity); + $this->entityManager->flush(); $userId = $this->urlGenerator->generate('ap_user', ['username' => $user->username], UrlGeneratorInterface::ABSOLUTE_URL); - return [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), + $activity->activityJson = json_encode([ + 'id' => $this->urlGenerator->generate('ap_object', ['id' => $activity->uuid], UrlGeneratorInterface::ABSOLUTE_URL), 'type' => 'Delete', 'actor' => $userId, 'object' => $userId, @@ -77,24 +80,31 @@ public function buildForUser(User $user): array 'cc' => [$this->urlGenerator->generate('ap_user_followers', ['username' => $user->username], UrlGeneratorInterface::ABSOLUTE_URL)], // this is a lemmy specific tag, that should cause the deletion of the data of a user (see this issue https://github.com/LemmyNet/lemmy/issues/4544) 'removeData' => true, - ]; + ]); + $this->entityManager->persist($activity); + $this->entityManager->flush(); + + return $activity; } - public function adjustDeletePayload(?User $actor, Entry|EntryComment|Post|PostComment $content, string $id): array + public function adjustDeletePayload(?User $actor, Entry|EntryComment|Post|PostComment $content): Activity { - $payload = $this->build($content, $id, $actor); + $payload = $this->build($content, $actor); + $json = json_decode($payload->activityJson, true); if (null !== $actor && $content->user->getId() !== $actor->getId()) { // if the user is different, then this is a mod action. Lemmy requires a mod action to have a summary - $payload['summary'] = ' '; + $json['summary'] = ' '; } if (null !== $actor?->apId) { - // wrap the `Delete` in an `Announce` activity if the deleting user is not a local one - $magazineUrl = $this->urlGenerator->generate('ap_magazine', ['name' => $content->magazine->name], UrlGeneratorInterface::ABSOLUTE_URL); - $payload = $this->announceWrapper->build($magazineUrl, $payload); + $json = $this->announceWrapper->build($content->magazine, $payload); } + $payload->activityJson = json_encode($json); + $this->entityManager->persist($payload); + $this->entityManager->flush(); + return $payload; } } diff --git a/src/Service/ActivityPub/Wrapper/FollowResponseWrapper.php b/src/Service/ActivityPub/Wrapper/FollowResponseWrapper.php index 429f43201b..1950291bc3 100644 --- a/src/Service/ActivityPub/Wrapper/FollowResponseWrapper.php +++ b/src/Service/ActivityPub/Wrapper/FollowResponseWrapper.php @@ -4,44 +4,27 @@ namespace App\Service\ActivityPub\Wrapper; -use App\Entity\Contracts\ActivityPubActivityInterface; -use JetBrains\PhpStorm\ArrayShape; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; +use App\Entity\Activity; +use App\Entity\Magazine; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; class FollowResponseWrapper { public function __construct( - private readonly UrlGeneratorInterface $urlGenerator, + private readonly EntityManagerInterface $entityManager, ) { } - #[ArrayShape([ - '@context' => 'string', - 'id' => 'string', - 'type' => 'string', - 'actor' => 'string', - 'object' => 'string', - ])] - public function build(string $user, string $actor, string $remoteId, bool $isReject = false): array + public function build(User|Magazine $actor, array $request, bool $isReject = false): Activity { - $id = Uuid::v4()->toRfc4122(); + $activity = new Activity($isReject ? 'Reject' : 'Accept'); + $activity->setActor($actor); + $activity->setObject($request); - return [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $this->urlGenerator->generate( - 'ap_object', - ['id' => $id], - UrlGeneratorInterface::ABSOLUTE_URL - ).($isReject ? '#reject' : '#accept'), - 'type' => $isReject ? 'Reject' : 'Accept', - 'actor' => $user, - 'object' => [ - 'id' => $remoteId, - 'type' => 'Follow', - 'actor' => $actor, - 'object' => $user, - ], - ]; + $this->entityManager->persist($activity); + $this->entityManager->flush(); + + return $activity; } } diff --git a/src/Service/ActivityPub/Wrapper/FollowWrapper.php b/src/Service/ActivityPub/Wrapper/FollowWrapper.php index fe2bd1b183..850b001d3f 100644 --- a/src/Service/ActivityPub/Wrapper/FollowWrapper.php +++ b/src/Service/ActivityPub/Wrapper/FollowWrapper.php @@ -4,36 +4,26 @@ namespace App\Service\ActivityPub\Wrapper; -use App\Entity\Contracts\ActivityPubActivityInterface; -use JetBrains\PhpStorm\ArrayShape; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; +use App\Entity\Activity; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; class FollowWrapper { - public function __construct(private readonly UrlGeneratorInterface $urlGenerator) - { + public function __construct( + private readonly EntityManagerInterface $entityManager, + ) { } - #[ArrayShape([ - '@context' => 'string', - 'id' => 'string', - 'type' => 'string', - 'actor' => 'string', - 'object' => 'string', - ])] - public function build( - string $follower, - string $following, - ): array { - $id = Uuid::v4()->toRfc4122(); + public function build(User $follower, User $following): Activity + { + $activity = new Activity('Follow'); + $activity->setActor($follower); + $activity->setObject($following); + + $this->entityManager->persist($activity); + $this->entityManager->flush(); - return [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => 'Follow', - 'actor' => $follower, - 'object' => $following, - ]; + return $activity; } } diff --git a/src/Service/ActivityPub/Wrapper/LikeWrapper.php b/src/Service/ActivityPub/Wrapper/LikeWrapper.php index 3be35ae187..e33e199178 100644 --- a/src/Service/ActivityPub/Wrapper/LikeWrapper.php +++ b/src/Service/ActivityPub/Wrapper/LikeWrapper.php @@ -4,28 +4,30 @@ namespace App\Service\ActivityPub\Wrapper; +use App\Entity\Activity; use App\Entity\Contracts\ActivityPubActivityInterface; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; +use App\Entity\User; +use App\Factory\ActivityPub\ActivityFactory; +use Doctrine\ORM\EntityManagerInterface; class LikeWrapper { - public function __construct(private readonly UrlGeneratorInterface $urlGenerator) - { + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly ActivityFactory $activityFactory, + ) { } - public function build( - string $user, - array $object, - ): array { - $id = Uuid::v4()->toRfc4122(); + public function build(User $user, ActivityPubActivityInterface $object): Activity + { + $activityObject = $this->activityFactory->create($object); + $activity = new Activity('Like'); + $activity->setObject($activityObject['id']); + $activity->userActor = $user; + + $this->entityManager->persist($activity); + $this->entityManager->flush(); - return [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => 'Like', - 'actor' => $user, - 'object' => $object['id'], - ]; + return $activity; } } diff --git a/src/Service/ActivityPub/Wrapper/UndoWrapper.php b/src/Service/ActivityPub/Wrapper/UndoWrapper.php index 5a4f60a40b..96bd343394 100644 --- a/src/Service/ActivityPub/Wrapper/UndoWrapper.php +++ b/src/Service/ActivityPub/Wrapper/UndoWrapper.php @@ -4,29 +4,34 @@ namespace App\Service\ActivityPub\Wrapper; -use App\Entity\Contracts\ActivityPubActivityInterface; -use JetBrains\PhpStorm\ArrayShape; +use App\Entity\Activity; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; class UndoWrapper { - #[ArrayShape([ - '@context' => 'string', - 'id' => 'string', - 'type' => 'string', - 'actor' => 'mixed', - 'object' => 'array', - ])] - public function build( - array $object, - ): array { - unset($object['@context']); + public function __construct( + private readonly EntityManagerInterface $entityManager, + ) { + } + + public function build(Activity|string $object, ?User $actor = null): Activity + { + $activity = new Activity('Undo'); + if ($object instanceof Activity) { + $activity->innerActivity = $object; + $activity->setActor($object->getActor()); + } else { + if (null === $actor) { + throw new \LogicException('actor must not be null if the object is a url'); + } + $activity->innerActivityUrl = $object; + $activity->setActor($actor); + } + + $this->entityManager->persist($activity); + $this->entityManager->flush(); - return [ - '@context' => ActivityPubActivityInterface::CONTEXT_URL, - 'id' => $object['id'].'#unfollow', - 'type' => 'Undo', - 'actor' => $object['actor'], - 'object' => $object, - ]; + return $activity; } } diff --git a/src/Service/ActivityPub/Wrapper/UpdateWrapper.php b/src/Service/ActivityPub/Wrapper/UpdateWrapper.php index 85ebd38b51..452b6e6cd2 100644 --- a/src/Service/ActivityPub/Wrapper/UpdateWrapper.php +++ b/src/Service/ActivityPub/Wrapper/UpdateWrapper.php @@ -4,16 +4,16 @@ namespace App\Service\ActivityPub\Wrapper; +use App\Entity\Activity; use App\Entity\Contracts\ActivityPubActivityInterface; use App\Entity\Contracts\ActivityPubActorInterface; -use App\Entity\Magazine; use App\Entity\User; use App\Factory\ActivityPub\ActivityFactory; use App\Factory\ActivityPub\GroupFactory; use App\Factory\ActivityPub\PersonFactory; +use Doctrine\ORM\EntityManagerInterface; use JetBrains\PhpStorm\ArrayShape; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Uid\Uuid; class UpdateWrapper { @@ -22,44 +22,20 @@ public function __construct( private readonly UrlGeneratorInterface $urlGenerator, private readonly GroupFactory $groupFactory, private readonly PersonFactory $personFactory, + private readonly EntityManagerInterface $entityManager, ) { } - #[ArrayShape([ - '@context' => 'mixed', - 'id' => 'mixed', - 'type' => 'string', - 'actor' => 'mixed', - 'published' => 'mixed', - 'to' => 'mixed', - 'cc' => 'mixed', - 'object' => 'array', - ])] - public function buildForActivity(ActivityPubActivityInterface $activity, ?User $editedBy = null): array + public function buildForActivity(ActivityPubActivityInterface $content, ?User $editedBy = null): Activity { - $entity = $this->factory->create($activity, true); - $id = Uuid::v4()->toRfc4122(); - - $context = $entity['@context']; - unset($entity['@context']); - - $entity['object']['updated'] = $activity->editedAt ? $activity->editedAt->format(DATE_ATOM) : (new \DateTime())->format(DATE_ATOM); + $activity = new Activity('Update'); + $activity->setActor($editedBy ?? $content->getUser()); + $activity->setObject($content); - $actorUrl = $entity['attributedTo']; - if (null !== $editedBy) { - $actorUrl = $this->urlGenerator->generate('ap_user', ['username' => $editedBy->username], UrlGeneratorInterface::ABSOLUTE_URL); - } + $this->entityManager->persist($activity); + $this->entityManager->flush(); - return [ - '@context' => $context, - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => 'Update', - 'actor' => $actorUrl, - 'published' => $entity['published'], - 'to' => $entity['to'], - 'cc' => $entity['cc'], - 'object' => $entity, - ]; + return $activity; } #[ArrayShape([ @@ -72,45 +48,15 @@ public function buildForActivity(ActivityPubActivityInterface $activity, ?User $ 'cc' => 'mixed', 'object' => 'array', ])] - public function buildForActor(ActivityPubActorInterface $item, ?User $editedBy = null): array + public function buildForActor(ActivityPubActorInterface $item, ?User $editedBy = null): Activity { - if ($item instanceof User) { - $activity = $this->personFactory->create($item, false); - if (null === $item->apId) { - $cc = [$this->urlGenerator->generate('ap_user_followers', ['username' => $item->username], UrlGeneratorInterface::ABSOLUTE_URL)]; - } else { - $cc = [$item->apFollowersUrl]; - } - } elseif ($item instanceof Magazine) { - $activity = $this->groupFactory->create($item, false); - if (null === $item->apId) { - $cc = [$this->urlGenerator->generate('ap_magazine_followers', ['name' => $item->name], UrlGeneratorInterface::ABSOLUTE_URL)]; - } else { - $cc = [$item->apFollowersUrl]; - } - } else { - throw new \LogicException('Unknown actor type: '.\get_class($item)); - } - $id = Uuid::v4()->toRfc4122(); + $activity = new Activity('Update'); + $activity->setActor($editedBy ?? $item); + $activity->setObject($item); - $actorUrl = $activity['id']; - if (null !== $editedBy) { - if (null === $editedBy->apId) { - $actorUrl = $this->urlGenerator->generate('ap_user', ['username' => $editedBy->username], UrlGeneratorInterface::ABSOLUTE_URL); - } else { - $actorUrl = $editedBy->apProfileId; - } - } + $this->entityManager->persist($activity); + $this->entityManager->flush(); - return [ - '@context' => [$this->urlGenerator->generate('ap_contexts', [], UrlGeneratorInterface::ABSOLUTE_URL)], - 'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), - 'type' => 'Update', - 'actor' => $actorUrl, - 'published' => $activity['published'], - 'to' => [ActivityPubActivityInterface::PUBLIC_URL], - 'cc' => $cc, - 'object' => $activity, - ]; + return $activity; } }