Skip to content

Commit

Permalink
Introduce the activity table
Browse files Browse the repository at this point in the history
Whenever an actor makes an activity (like, announce, etc.) we now save it in our db, so the url we pass to other activity pub servers is valid and can return the correct json.
All the factory and wrapper classes now return an `Activity` entity that can be converted to json using the new `ActivityJsonBuilder`
  • Loading branch information
BentiGorlich committed Oct 7, 2024
1 parent d07c5c3 commit 57ec00a
Show file tree
Hide file tree
Showing 41 changed files with 1,024 additions and 508 deletions.
61 changes: 61 additions & 0 deletions migrations/Version20240820201944.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20240820201944 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add the activity table';
}

public function up(Schema $schema): void
{
$this->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');
}
}
18 changes: 16 additions & 2 deletions src/Controller/ActivityPub/ObjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
136 changes: 136 additions & 0 deletions src/Entity/Activity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

declare(strict_types=1);

namespace App\Entity;

use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Contracts\ActivityPubActorInterface;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\CustomIdGenerator;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Symfony\Component\Uid\Uuid;

#[Entity]
class Activity
{
#[Column(type: 'uuid'), Id, GeneratedValue(strategy: 'CUSTOM')]
#[CustomIdGenerator(class: 'doctrine.uuid_generator')]
public Uuid $uuid;

#[Column]
public string $type;

#[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')]
public ?User $userActor;

#[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')]
public ?Magazine $magazineActor;

#[ManyToOne, JoinColumn(nullable: true, onDelete: 'CASCADE')]
public ?Magazine $audience;

#[ManyToOne, JoinColumn(referencedColumnName: 'uuid', nullable: true, onDelete: 'CASCADE', options: ['default' => 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;
}
}
4 changes: 4 additions & 0 deletions src/Entity/Contracts/ActivityPubActivityInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace App\Entity\Contracts;

use App\Entity\User;

interface ActivityPubActivityInterface
{
public const FOLLOWERS = 'followers';
Expand Down Expand Up @@ -37,4 +39,6 @@ interface ActivityPubActivityInterface
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
'stickied' => 'lemmy:stickied',
];

public function getUser(): ?User;
}
5 changes: 5 additions & 0 deletions src/Entity/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ public function getTitle(): string

return grapheme_substr($firstLine, 0, 80).'';
}

public function getUser(): User
{
return $this->sender;
}
}
6 changes: 4 additions & 2 deletions src/EventSubscriber/Entry/EntryDeleteSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@
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
{
public function __construct(
private readonly MessageBusInterface $bus,
private readonly EntryRepository $entryRepository,
private readonly DeleteWrapper $deleteWrapper,
private readonly ActivityJsonBuilder $activityJsonBuilder,
) {
}

Expand Down Expand Up @@ -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()));
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/Entry/EntryPinSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +23,7 @@ public function __construct(
private readonly CacheInterface $cache,
private readonly MessageBusInterface $bus,
private readonly DeleteWrapper $deleteWrapper,
private readonly ActivityJsonBuilder $activityJsonBuilder,
) {
}

Expand Down Expand Up @@ -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()));
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/Magazine/MagazineUpdatedSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
Expand Down
6 changes: 4 additions & 2 deletions src/EventSubscriber/Post/PostDeleteSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@
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
{
public function __construct(
private readonly MessageBusInterface $bus,
private readonly PostRepository $postRepository,
private readonly DeleteWrapper $deleteWrapper,
private readonly ActivityJsonBuilder $activityJsonBuilder,
) {
}

Expand Down Expand Up @@ -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()));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +23,7 @@ public function __construct(
private readonly CacheInterface $cache,
private readonly MessageBusInterface $bus,
private readonly DeleteWrapper $deleteWrapper,
private readonly ActivityJsonBuilder $activityJsonBuilder,
) {
}

Expand Down Expand Up @@ -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()));
}
}
Expand Down
Loading

0 comments on commit 57ec00a

Please sign in to comment.