From c444eebac55d960885680fd7b7701c3b4643ec22 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 | 3 +- .../Outbox/GenericAnnounceMessage.php | 5 +- .../ActivityPub/Inbox/FollowHandler.php | 13 +- .../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 | 7 +- 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 | 68 ++-- .../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, 1024 insertions(+), 508 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 000000000..a78e7f97c --- /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 39bba81f2..1a8519afd 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 000000000..bfa0a2a41 --- /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 1b31d12f8..90e0aaa99 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 e323a2cb4..1ea030890 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 0c9a3836f..7a9d20393 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 b9f5a5703..8336454c5 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 2347183ab..e48e11dae 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 40e1dfc65..6558db392 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 bc76c9d0a..6e5c8ba4a 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 f8529cc3b..8717a3f28 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 13df001af..8b397faff 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 9af50b6a6..94f94e855 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 fb38edef9..70f016c8b 100644 --- a/src/Message/ActivityPub/Outbox/AnnounceLikeMessage.php +++ b/src/Message/ActivityPub/Outbox/AnnounceLikeMessage.php @@ -12,7 +12,8 @@ public function __construct( public int $userId, public int $objectId, public string $objectType, - public bool $undo = false + 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 1a7ae2229..3fe5cab30 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 d3eeb2673..034924bd3 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; @@ -28,7 +29,8 @@ public function __construct( private readonly MagazineManager $magazineManager, private readonly ApHttpClient $client, private readonly LoggerInterface $logger, - private readonly FollowResponseWrapper $followResponseWrapper + private readonly FollowResponseWrapper $followResponseWrapper, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -104,13 +106,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 579f1d5ee..368192f3e 100644 --- a/src/MessageHandler/ActivityPub/Inbox/LikeHandler.php +++ b/src/MessageHandler/ActivityPub/Inbox/LikeHandler.php @@ -87,7 +87,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 fdb26e4af..593d0221d 100644 --- a/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php +++ b/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php @@ -118,22 +118,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 8e80c86db..a6ff7c0c2 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; @@ -25,6 +26,7 @@ public function __construct( private readonly SettingsManager $settingsManager, private readonly AddRemoveFactory $factory, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -53,6 +55,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 03e0c8bc6..4e195ea3a 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; @@ -37,9 +38,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); } @@ -68,16 +70,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) { @@ -89,11 +89,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) { $createHost = parse_url($object->apId, PHP_URL_HOST); @@ -104,6 +106,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 21fe71e13..a45ea95f9 100644 --- a/src/MessageHandler/ActivityPub/Outbox/AnnounceLikeHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/AnnounceLikeHandler.php @@ -8,21 +8,21 @@ 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\Messenger\Attribute\AsMessageHandler; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; #[AsMessageHandler] class AnnounceLikeHandler extends MbinMessageHandler @@ -34,11 +34,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); } @@ -71,23 +71,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 339dcef4d..e260e511a 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; @@ -32,6 +33,7 @@ public function __construct( private readonly MessageManager $messageManager, private readonly LoggerInterface $logger, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -53,6 +55,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); @@ -60,11 +63,11 @@ 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), ...$this->magazineRepository->findAudience($entity->magazine), ]; $this->logger->debug('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 2e237a282..8049cff65 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; @@ -29,6 +30,7 @@ public function __construct( private readonly MagazineRepository $magazineRepository, private readonly DeliverManager $deliverManager, private readonly LoggerInterface $logger, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -67,6 +69,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 283711f82..95f2b6e85 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; @@ -27,6 +28,7 @@ public function __construct( private readonly FlagFactory $factory, private readonly LoggerInterface $logger, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -54,8 +56,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 6df9eec11..e76a925b3 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; @@ -31,6 +33,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); } @@ -56,17 +60,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 e6f63886c..93bba3843 100644 --- a/src/MessageHandler/ActivityPub/Outbox/GenericAnnounceHandler.php +++ b/src/MessageHandler/ActivityPub/Outbox/GenericAnnounceHandler.php @@ -7,13 +7,16 @@ 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; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Uid\Uuid; #[AsMessageHandler] class GenericAnnounceHandler extends MbinMessageHandler @@ -25,6 +28,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); } @@ -42,13 +47,23 @@ public function doWork(MessageInterface $message): void if (!($message instanceof GenericAnnounceMessage)) { throw new \LogicException(); } + $magazine = $this->magazineRepository->find($message->announcingMagazineId); if (null !== $magazine->apId) { 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 493c519ad..945d8442c 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; @@ -35,6 +36,7 @@ public function __construct( private readonly ActivityFactory $activityFactory, private readonly SettingsManager $settingsManager, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -57,10 +59,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); @@ -71,6 +70,6 @@ public function doWork(MessageInterface $message): void $this->magazineRepository->findAudience($object->magazine), [$object->user->apInboxUrl, $object->magazine->apId ? $object->magazine->apInboxUrl : null] ))); - $this->deliverManager->deliver($inboxes, $activity); + $this->deliverManager->deliver($inboxes, $this->activityJsonBuilder->buildActivityJson($activity)); } } diff --git a/src/MessageHandler/ActivityPub/Outbox/RemoveHandler.php b/src/MessageHandler/ActivityPub/Outbox/RemoveHandler.php index 1c4843eaa..e77b471bb 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; @@ -25,6 +26,7 @@ public function __construct( private readonly SettingsManager $settingsManager, private readonly AddRemoveFactory $factory, private readonly DeliverManager $deliverManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -53,6 +55,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 5bcb70c5e..f8fb13463 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; @@ -36,6 +37,7 @@ public function __construct( private readonly SettingsManager $settingsManager, private readonly DeliverManager $deliverManager, private readonly UpdateWrapper $updateWrapper, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -61,7 +63,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) { $inboxes = array_filter(array_unique(array_merge( @@ -78,7 +81,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 c949a3027..8b1c6e6f1 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; @@ -27,7 +28,8 @@ public function __construct( private readonly UserManager $userManager, private readonly DeleteWrapper $deleteWrapper, private readonly MessageBusInterface $bus, - private readonly EntityManagerInterface $entityManager + private readonly EntityManagerInterface $entityManager, + private readonly ActivityJsonBuilder $activityJsonBuilder, ) { parent::__construct($this->entityManager); } @@ -125,7 +127,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 000000000..bea964e26 --- /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 2dfcbe7e8..79d544238 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; @@ -28,6 +27,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; @@ -210,9 +210,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()); } @@ -256,7 +254,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 000000000..05c1294b3 --- /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 3c6a3689a..0aa411233 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 6465bd23b..985e22168 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 4546b7b25..01d682482 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,30 +12,24 @@ 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 { public function __construct( private readonly ActivityFactory $factory, private readonly AnnounceWrapper $announceWrapper, - private readonly UrlGeneratorInterface $urlGenerator + 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 429f43201..1950291bc 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 fe2bd1b18..850b001d3 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 3be35ae18..e33e19917 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 5a4f60a40..96bd34339 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 85ebd38b5..452b6e6cd 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; } }