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; } }