Skip to content

Commit

Permalink
Add events for add/removing moderators
Browse files Browse the repository at this point in the history
- add handling of adding/removing of moderators (needs testing)
- add outgoing messages for adding/removing moderators
- add `addedByUser` field to moderator table (update all calls of the moderator creation)
- local followers/subscribers should immediately be added to the count now
  • Loading branch information
BentiGorlich committed Nov 12, 2023
1 parent d109356 commit 1a388a9
Show file tree
Hide file tree
Showing 27 changed files with 542 additions and 16 deletions.
30 changes: 30 additions & 0 deletions migrations/Version20231112133420.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

final class Version20231112133420 extends AbstractMigration
{
public function getDescription(): string
{
return 'add column "added_by_user_id" to moderator table';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE moderator ADD added_by_user_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE moderator ADD CONSTRAINT FK_6A30B268CA792C6B FOREIGN KEY (added_by_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_6A30B268CA792C6B ON moderator (added_by_user_id)');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE moderator DROP CONSTRAINT FK_6A30B268CA792C6B');
$this->addSql('DROP INDEX IDX_6A30B268CA792C6B');
$this->addSql('ALTER TABLE moderator DROP added_by_user_id');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function accept(Magazine $magazine, User $user, Request $request): Respon
{
$this->validateCsrf('admin_magazine_ownership_requests_accept', $request->request->get('token'));

$this->manager->acceptOwnershipRequest($magazine, $user);
$this->manager->acceptOwnershipRequest($magazine, $user, $this->getUserOrThrow());

return $this->redirectToRefererOrHome($request);
}
Expand Down
1 change: 1 addition & 0 deletions src/Controller/Admin/AdminModeratorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function moderators(Request $request): Response
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$dto->addedBy = $this->getUserOrThrow();
$this->manager->addModerator($dto);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function __invoke(
User $user,
MagazineManager $manager,
MagazineFactory $factory,
RateLimiterFactory $apiModerateLimiter
RateLimiterFactory $apiModerateLimiter,
): JsonResponse {
$headers = $this->rateLimit($apiModerateLimiter);

Expand All @@ -102,6 +102,7 @@ public function __invoke(
$dto = new ModeratorDto($magazine);

$dto->user = $user;
$dto->addedBy = $this->getUserOrThrow();

$manager->addModerator($dto);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public function accept(Magazine $magazine, Request $request): Response
{
$this->validateCsrf('magazine_ownership_request', $request->request->get('token'));

$this->manager->acceptOwnershipRequest($magazine, $this->getUserOrThrow());
$user = $this->getUserOrThrow();
$this->manager->acceptOwnershipRequest($magazine, $user, $user);

return $this->redirectToRefererOrHome($request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function moderators(Magazine $magazine, Request $request): Response
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$dto->addedBy = $this->getUserOrThrow();
$this->manager->addModerator($dto);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function accept(Magazine $magazine, User $user, Request $request): Respon
{
$this->validateCsrf('magazine_panel_moderator_request_accept', $request->request->get('token'));

$this->manager->acceptModeratorRequest($magazine, $user);
$this->manager->acceptModeratorRequest($magazine, $user, $this->getUserOrThrow());

return $this->redirectToRefererOrHome($request);
}
Expand Down
4 changes: 3 additions & 1 deletion src/DTO/ModeratorDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ class ModeratorDto
public ?Magazine $magazine = null;
#[Assert\NotBlank]
public ?User $user = null;
public ?User $addedBy = null;

public function __construct(?Magazine $magazine, User $user = null)
public function __construct(?Magazine $magazine, User $user = null, User $addedBy = null)
{
$this->magazine = $magazine;
$this->user = $user;
$this->addedBy = $addedBy;
}
}
44 changes: 42 additions & 2 deletions src/Entity/Magazine.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Entity\Traits\CreatedAtTrait;
use App\Entity\Traits\VisibilityTrait;
use App\Repository\MagazineRepository;
use App\Service\MagazineManager;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
Expand Down Expand Up @@ -137,11 +138,17 @@ public function __construct(
$this->logs = new ArrayCollection();
$this->awards = new ArrayCollection();

$this->addModerator(new Moderator($this, $user, true, true));
$this->addModerator(new Moderator($this, $user, null, true, true));

$this->createdAtTraitConstruct();
}

/**
* Only use this to add a moderator if you don't want that action to be federated.
* If you want this action to be federated, use @see MagazineManager::addModerator().
*
* @return $this
*/
public function addModerator(Moderator $moderator): self
{
if (!$this->moderators->contains($moderator)) {
Expand All @@ -168,6 +175,22 @@ public function userIsModerator(User $user): bool
return !$user->moderatorTokens->matching($criteria)->isEmpty();
}

public function getUserAsModeratorOrNull(User $user): ?Moderator
{
$user->moderatorTokens->get(-1);

$criteria = Criteria::create()
->where(Criteria::expr()->eq('magazine', $this))
->andWhere(Criteria::expr()->eq('isConfirmed', true));

$col = $user->moderatorTokens->matching($criteria);
if (!$col->isEmpty()) {
return $col->first();
}

return null;
}

public function removeUserAsModerator(User $user): void
{
$user->moderatorTokens->get(-1);
Expand Down Expand Up @@ -306,7 +329,11 @@ public function isSubscribed(User $user): bool
public function updateSubscriptionsCount(): void
{
if (null !== $this->apFollowersCount) {
$this->subscriptionsCount = $this->apFollowersCount;
$criteria = Criteria::create()
->where(Criteria::expr()->gt('createdAt', $this->apFetchedAt));

$newSubscribers = $this->subscriptions->matching($criteria)->count();
$this->subscriptionsCount = $this->apFollowersCount + $newSubscribers;
} else {
$this->subscriptionsCount = $this->subscriptions->count();
}
Expand Down Expand Up @@ -430,4 +457,17 @@ public function getApName(): string
{
return $this->name;
}

public function hasSameHostAsUser(User $actor): bool
{
if (!$actor->apId and !$this->apId) {
return true;
}

if ($actor->apId and $this->apId) {
return parse_url($actor->apId, PHP_URL_HOST) === parse_url($this->apId, PHP_URL_HOST);
}

return false;
}
}
6 changes: 5 additions & 1 deletion src/Entity/Moderator.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class Moderator
#[ManyToOne(targetEntity: Magazine::class, inversedBy: 'moderators')]
#[JoinColumn(nullable: false, onDelete: 'CASCADE')]
public Magazine $magazine;
#[ManyToOne(targetEntity: User::class, inversedBy: 'moderatorTokens')]
#[JoinColumn(nullable: true)]
public ?User $addedByUser;
#[Column(type: 'boolean', nullable: false)]
public bool $isOwner = false;
#[Column(type: 'boolean', nullable: false)]
Expand All @@ -38,10 +41,11 @@ class Moderator
#[Column(type: 'integer')]
private int $id;

public function __construct(Magazine $magazine, User $user, $isOwner = false, $isConfirmed = false)
public function __construct(Magazine $magazine, User $user, User $addedByUser = null, $isOwner = false, $isConfirmed = false)
{
$this->magazine = $magazine;
$this->user = $user;
$this->addedByUser = $addedByUser;
$this->isOwner = $isOwner;
$this->isConfirmed = $isConfirmed;

Expand Down
6 changes: 5 additions & 1 deletion src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,11 @@ public function isFollowing(User $user): bool
public function updateFollowCounts(): void
{
if (null !== $this->apFollowersCount) {
$this->followersCount = $this->apFollowersCount;
$criteria = Criteria::create()
->where(Criteria::expr()->gt('createdAt', $this->apFetchedAt));

$newFollowers = $this->followers->matching($criteria)->count();
$this->followersCount = $this->apFollowersCount + $newFollowers;
} else {
$this->followersCount = $this->followers->count();
}
Expand Down
15 changes: 15 additions & 0 deletions src/Event/Magazine/MagazineModeratorAddedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Event\Magazine;

use App\Entity\Magazine;
use App\Entity\User;

class MagazineModeratorAddedEvent
{
public function __construct(public Magazine $magazine, public User $user, public ?User $addedBy)
{
}
}
15 changes: 15 additions & 0 deletions src/Event/Magazine/MagazineModeratorRemovedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\Event\Magazine;

use App\Entity\Magazine;
use App\Entity\User;

class MagazineModeratorRemovedEvent
{
public function __construct(public Magazine $magazine, public User $user, public ?User $removedBy)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace App\EventSubscriber\ActivityPub;

use App\Event\Magazine\MagazineModeratorAddedEvent;
use App\Event\Magazine\MagazineModeratorRemovedEvent;
use App\Message\ActivityPub\Outbox\AddMessage;
use App\Message\ActivityPub\Outbox\RemoveMessage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBusInterface;

class MagazineModeratorAddedRemovedSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly MessageBusInterface $bus,
) {
}

public function onModeratorAdded(MagazineModeratorAddedEvent $event): void
{
// if the magazine is local then we have authority over it, otherwise the addedBy user has to be defined
if (!$event->magazine->apId or null !== $event->addedBy) {
$this->bus->dispatch(new AddMessage($event->addedBy->getId(), $event->magazine->getId(), $event->user->getId()));
}
}

public function onModeratorRemoved(MagazineModeratorRemovedEvent $event): void
{
// if the magazine is local then we have authority over it, otherwise the removedBy user has to be defined
if (!$event->magazine->apId or null !== $event->removedBy) {
$this->bus->dispatch(new RemoveMessage($event->removedBy->getId(), $event->magazine->getId(), $event->user->getId()));
}
}

public static function getSubscribedEvents(): array
{
return [
MagazineModeratorAddedEvent::class => 'onModeratorAdded',
MagazineModeratorRemovedEvent::class => 'onModeratorRemoved',
];
}
}
58 changes: 58 additions & 0 deletions src/Factory/ActivityPub/AddRemoveFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace App\Factory\ActivityPub;

use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Magazine;
use App\Entity\User;
use JetBrains\PhpStorm\ArrayShape;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Uid\Uuid;

class AddRemoveFactory
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
) {
}

public function buildAdd(User $actor, User $added, Magazine $magazine): array
{
return $this->build($actor, $added, $magazine, 'Add');
}

public function buildRemove(User $actor, User $removed, Magazine $magazine): array
{
return $this->build($actor, $removed, $magazine, 'Remove');
}

#[ArrayShape([
'@context' => 'array',
'id' => 'string',
'actor' => 'string',
'to' => 'array',
'object' => 'string',
'cc' => 'array',
'type' => 'string',
'target' => 'string',
'audience' => 'string',
])]
private function build(User $actor, User $targetUser, Magazine $magazine, string $type): array
{
$id = Uuid::v4()->toRfc4122();

return [
'@context' => [ActivityPubActivityInterface::CONTEXT_URL, ActivityPubActivityInterface::SECURITY_URL],
'id' => $this->urlGenerator->generate('ap_object', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL),
'actor' => $actor->apId ?? $this->urlGenerator->generate('ap_user', ['username' => $actor->username], UrlGeneratorInterface::ABSOLUTE_URL),
'to' => [ActivityPubActivityInterface::PUBLIC_URL],
'object' => $targetUser->apId ?? $this->urlGenerator->generate('ap_user', ['username' => $targetUser->username], UrlGeneratorInterface::ABSOLUTE_URL),
'cc' => [$magazine->apId ?? $this->urlGenerator->generate('ap_magazine', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL)],
'type' => $type,
'target' => $magazine->apAttributedToUrl ?? $this->urlGenerator->generate('ap_magazine_moderators', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL),
'audience' => $magazine->apId ?? $this->urlGenerator->generate('ap_magazine', ['name' => $magazine->name], UrlGeneratorInterface::ABSOLUTE_URL),
];
}
}
14 changes: 14 additions & 0 deletions src/Message/ActivityPub/Inbox/AddMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace App\Message\ActivityPub\Inbox;

use App\Message\Contracts\AsyncApMessageInterface;

class AddMessage implements AsyncApMessageInterface
{
public function __construct(public array $payload)
{
}
}
14 changes: 14 additions & 0 deletions src/Message/ActivityPub/Inbox/RemoveMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace App\Message\ActivityPub\Inbox;

use App\Message\Contracts\AsyncApMessageInterface;

class RemoveMessage implements AsyncApMessageInterface
{
public function __construct(public array $payload)
{
}
}
Loading

0 comments on commit 1a388a9

Please sign in to comment.