diff --git a/config/mbin_routes/message.yaml b/config/mbin_routes/message.yaml
index cc6bb1e367..7045ae64c0 100644
--- a/config/mbin_routes/message.yaml
+++ b/config/mbin_routes/message.yaml
@@ -4,12 +4,19 @@ messages_front:
methods: [ GET ]
messages_single:
- controller: App\Controller\Message\MessageThreadController
+ controller: App\Controller\Message\MessageThreadController::show
path: /profile/messages/{id}
methods: [ GET, POST ]
requirements:
id: \d+
+messages_remove_thread:
+ controller: App\Controller\Message\MessageThreadController::remove
+ path: /profile/messages/{id}/delete
+ methods: [ POST ]
+ requirements:
+ id: \d+
+
messages_create:
controller: App\Controller\Message\MessageCreateThreadController
path: /u/{username}/message
diff --git a/config/mbin_routes/message_api.yaml b/config/mbin_routes/message_api.yaml
index 746686e2c4..9d6a295d88 100644
--- a/config/mbin_routes/message_api.yaml
+++ b/config/mbin_routes/message_api.yaml
@@ -41,9 +41,16 @@ api_message_retrieve_thread:
methods: [ GET ]
format: json
+# Delete a thread with a user
+api_message_remove_thread:
+ controller: App\Controller\Api\Message\MessageRemoveApi::removeThread
+ path: /api/messages/thread/{thread_id}
+ methods: [ DELETE ]
+ format: json
+
# Create a thread with a user
api_message_create_thread:
controller: App\Controller\Api\Message\MessageThreadCreateApi
path: /api/users/{user_id}/message
methods: [ POST ]
- format: json
\ No newline at end of file
+ format: json
diff --git a/config/mbin_routes/message_reports.yaml b/config/mbin_routes/message_reports.yaml
new file mode 100644
index 0000000000..513557a229
--- /dev/null
+++ b/config/mbin_routes/message_reports.yaml
@@ -0,0 +1,22 @@
+message_reports:
+ controller: App\Controller\Message\MessageReportController::reports
+ path: /messages/reports/{status}
+ defaults: { status: !php/const \App\Entity\Report::STATUS_ANY }
+ methods: [ GET ]
+
+message_report_approve:
+ controller: App\Controller\Message\MessageReportController::reportApprove
+ path: /messages/reports/{report_id}/approve
+ methods: [ POST ]
+
+message_report_reject:
+ controller: App\Controller\Message\MessageReportController::reportReject
+ path: /messages/reports/{report_id}/reject
+ methods: [ POST ]
+
+message_report:
+ controller: App\Controller\Message\MessageReportController::reportMessage
+ path: /mr/{id}
+ methods: [ GET, POST ]
+ requirements:
+ id: \d+
diff --git a/config/packages/league_oauth2_server.yaml b/config/packages/league_oauth2_server.yaml
index f7388c121c..c456fb25fe 100644
--- a/config/packages/league_oauth2_server.yaml
+++ b/config/packages/league_oauth2_server.yaml
@@ -69,6 +69,7 @@ league_oauth2_server:
"user:message",
"user:message:read",
"user:message:create",
+ "user:message:delete",
"user:notification",
"user:notification:read",
"user:notification:delete",
diff --git a/config/packages/nelmio_api_doc.yaml b/config/packages/nelmio_api_doc.yaml
index f728838bc8..85744059b6 100644
--- a/config/packages/nelmio_api_doc.yaml
+++ b/config/packages/nelmio_api_doc.yaml
@@ -148,6 +148,7 @@ nelmio_api_doc:
user:message: Read your messages and send messages to other users.
user:message:read: Read your messages.
user:message:create: Send messages to other users.
+ user:message:delete: Delete your messages.
user:notification: Read and clear your notifications.
user:notification:read: Read your notifications, including message notifications.
user:notification:delete: Clear notifications.
@@ -265,6 +266,7 @@ nelmio_api_doc:
user:message: Read your messages and send messages to other users.
user:message:read: Read your messages.
user:message:create: Send messages to other users.
+ user:message:delete: Delete your messages.
user:notification: Read and clear your notifications.
user:notification:read: Read your notifications, including message notifications.
user:notification:delete: Clear notifications.
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index 37d5f8858e..234473b386 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -253,7 +253,7 @@ security:
'ROLE_OAUTH2_USER:PROFILE':
['ROLE_OAUTH2_USER:PROFILE:READ', 'ROLE_OAUTH2_USER:PROFILE:EDIT']
'ROLE_OAUTH2_USER:MESSAGE':
- ['ROLE_OAUTH2_USER:MESSAGE:READ', 'ROLE_OAUTH2_USER:MESSAGE:CREATE']
+ ['ROLE_OAUTH2_USER:MESSAGE:READ', 'ROLE_OAUTH2_USER:MESSAGE:CREATE', 'ROLE_OAUTH2_USER:MESSAGE:DELETE']
'ROLE_OAUTH2_USER:NOTIFICATION':
[
'ROLE_OAUTH2_USER:NOTIFICATION:READ',
diff --git a/config/services.yaml b/config/services.yaml
index ee1b428cb9..f3f0f97ca7 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -125,6 +125,11 @@ parameters:
mbin_use_federation_allow_list: '%env(bool:default::MBIN_USE_FEDERATION_ALLOW_LIST)%'
services:
+ _instanceof:
+ App\Service\Contracts\SwitchableService:
+ tags: ['switchable_service']
+ lazy: true
+
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
@@ -259,3 +264,7 @@ services:
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
arguments:
- '%env(DATABASE_URL)%'
+
+ App\Service\SwitchingServiceRegistry:
+ arguments:
+ - !tagged 'switchable_service'
diff --git a/docs/04-app_developers/README.md b/docs/04-app_developers/README.md
index 73547ae411..ab8f46f611 100644
--- a/docs/04-app_developers/README.md
+++ b/docs/04-app_developers/README.md
@@ -188,6 +188,8 @@ POST /api/client
- Also allows the client to mark unread messages as read or read messages as unread
- `user:message:create`
- Allows the client to create new messages to other users or reply to existing messages
+ - `user:message:delete`
+ - Allows the client to delete message-threads of the current user
- `user:notification`
- `user:notification:read`
- Allows the client to read notifications about threads, posts, or comments being replied to, as well as moderation notifications.
diff --git a/migrations/Version20260311182316.php b/migrations/Version20260311182316.php
new file mode 100644
index 0000000000..318d3f56de
--- /dev/null
+++ b/migrations/Version20260311182316.php
@@ -0,0 +1,38 @@
+addSql('ALTER TABLE message ALTER uuid DROP DEFAULT');
+ $this->addSql('ALTER TABLE message_thread ALTER updated_at DROP NOT NULL');
+
+ $this->addSql('ALTER TABLE report ADD message_id INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE report ALTER magazine_id DROP NOT NULL');
+ $this->addSql('ALTER TABLE report ADD CONSTRAINT FK_C42F7784537A1329 FOREIGN KEY (message_id) REFERENCES message (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
+ $this->addSql('CREATE INDEX IDX_C42F7784537A1329 ON report (message_id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE message_thread ALTER updated_at SET NOT NULL');
+ $this->addSql('ALTER TABLE message ALTER uuid SET DEFAULT \'gen_random_uuid()\'');
+
+ $this->addSql('ALTER TABLE report DROP CONSTRAINT FK_C42F7784537A1329');
+ $this->addSql('DROP INDEX IDX_C42F7784537A1329');
+ $this->addSql('ALTER TABLE report DROP message_id');
+ $this->addSql('ALTER TABLE report ALTER magazine_id SET NOT NULL');
+ }
+}
diff --git a/src/Controller/ActivityPub/ReportController.php b/src/Controller/ActivityPub/ReportController.php
index caac4252e3..44a6ffa726 100644
--- a/src/Controller/ActivityPub/ReportController.php
+++ b/src/Controller/ActivityPub/ReportController.php
@@ -29,7 +29,7 @@ public function __invoke(
throw new ArgumentException('there is no such report');
}
- $json = $this->factory->build($report, $this->factory->getPublicUrl($report->getSubject()));
+ $json = $this->factory->build($report);
$response = new JsonResponse($json);
$response->headers->set('Content-Type', 'application/activity+json');
diff --git a/src/Controller/Admin/AdminReportController.php b/src/Controller/Admin/AdminReportController.php
index 6eb0f3d8be..e4f246b389 100644
--- a/src/Controller/Admin/AdminReportController.php
+++ b/src/Controller/Admin/AdminReportController.php
@@ -25,6 +25,7 @@ public function __invoke(Request $request, string $status): Response
{
$page = (int) $request->get('p', 1);
+ //TODO rest api for this
$reports = $this->repository->findAllPaginated($page, $status);
$this->notificationRepository->markReportNotificationsAsRead($this->getUserOrThrow());
diff --git a/src/Controller/Api/Magazine/Moderate/MagazineReportsAcceptApi.php b/src/Controller/Api/Magazine/Moderate/MagazineReportsAcceptApi.php
index 05052110ab..0b9a2aebbe 100644
--- a/src/Controller/Api/Magazine/Moderate/MagazineReportsAcceptApi.php
+++ b/src/Controller/Api/Magazine/Moderate/MagazineReportsAcceptApi.php
@@ -10,7 +10,9 @@
use App\Entity\Magazine;
use App\Entity\Report;
use App\Factory\ContentManagerFactory;
+use App\Service\Contracts\ContentManagerInterface;
use App\Service\ReportManager;
+use App\Service\SwitchingServiceRegistry;
use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Attribute\Security;
use OpenApi\Attributes as OA;
@@ -84,17 +86,17 @@ public function __invoke(
#[MapEntity(id: 'report_id')]
Report $report,
ReportManager $reportManager,
- ContentManagerFactory $managerFactory,
+ SwitchingServiceRegistry $serviceRegistry,
RateLimiterFactoryInterface $apiModerateLimiter,
): JsonResponse {
$headers = $this->rateLimit($apiModerateLimiter);
- if ($magazine->getId() !== $report->magazine->getId()) {
+ //TODO create api endpoints for reports without magazine (or maybe not)
+ if ($magazine->getId() !== $report->magazine?->getId()) {
throw new NotFoundHttpException('Report not found in magazine');
}
- $manager = $managerFactory->createManager($report->getSubject());
-
+ $manager = $serviceRegistry->getService($report->getSubject(), ContentManagerInterface::class);
$manager->delete($this->getUserOrThrow(), $report->getSubject());
return new JsonResponse(
diff --git a/src/Controller/Api/Magazine/Moderate/MagazineReportsRejectApi.php b/src/Controller/Api/Magazine/Moderate/MagazineReportsRejectApi.php
index 6c5c5f0992..13662aeac4 100644
--- a/src/Controller/Api/Magazine/Moderate/MagazineReportsRejectApi.php
+++ b/src/Controller/Api/Magazine/Moderate/MagazineReportsRejectApi.php
@@ -87,7 +87,7 @@ public function __invoke(
): JsonResponse {
$headers = $this->rateLimit($apiModerateLimiter);
- if ($magazine->getId() !== $report->magazine->getId()) {
+ if ($magazine->getId() !== $report->magazine?->getId()) {
throw new NotFoundHttpException('Report not found in magazine');
}
diff --git a/src/Controller/Api/Magazine/Moderate/MagazineReportsRetrieveApi.php b/src/Controller/Api/Magazine/Moderate/MagazineReportsRetrieveApi.php
index 1f8c2ccc56..14cde30ce7 100644
--- a/src/Controller/Api/Magazine/Moderate/MagazineReportsRetrieveApi.php
+++ b/src/Controller/Api/Magazine/Moderate/MagazineReportsRetrieveApi.php
@@ -85,7 +85,7 @@ public function __invoke(
): JsonResponse {
$headers = $this->rateLimit($apiModerateLimiter);
- if ($magazine->getId() !== $report->magazine->getId()) {
+ if ($magazine->getId() !== $report->magazine?->getId()) {
throw new NotFoundHttpException('The report was not found in the magazine');
}
diff --git a/src/Controller/Api/Message/MessageRemoveApi.php b/src/Controller/Api/Message/MessageRemoveApi.php
new file mode 100644
index 0000000000..a5bad5755d
--- /dev/null
+++ b/src/Controller/Api/Message/MessageRemoveApi.php
@@ -0,0 +1,78 @@
+rateLimit($apiReadLimiter);
+
+ $manager->removeUserFromThread($thread, $this->getUserOrThrow());
+
+ return new Response(status: 204, headers: $headers);
+ }
+}
diff --git a/src/Controller/Api/Message/MessageReportApi.php b/src/Controller/Api/Message/MessageReportApi.php
new file mode 100644
index 0000000000..fc2d676da5
--- /dev/null
+++ b/src/Controller/Api/Message/MessageReportApi.php
@@ -0,0 +1,83 @@
+rateLimit($apiReportLimiter);
+
+ $this->reportContent($message);
+
+ return new JsonResponse(
+ status: 204,
+ headers: $headers
+ );
+ }
+}
diff --git a/src/Controller/Api/Message/MessageThreadCreateApi.php b/src/Controller/Api/Message/MessageThreadCreateApi.php
index f4ea432112..0029721680 100644
--- a/src/Controller/Api/Message/MessageThreadCreateApi.php
+++ b/src/Controller/Api/Message/MessageThreadCreateApi.php
@@ -90,10 +90,6 @@ public function __invoke(
): JsonResponse {
$headers = $this->rateLimit($apiMessageLimiter);
- if ($receiver->apId) {
- throw new AccessDeniedHttpException();
- }
-
$dto = $this->deserializeMessage();
$errors = $validator->validate($dto);
diff --git a/src/Controller/Api/Notification/NotificationBaseApi.php b/src/Controller/Api/Notification/NotificationBaseApi.php
index 1dd4a21aaa..9198d04687 100644
--- a/src/Controller/Api/Notification/NotificationBaseApi.php
+++ b/src/Controller/Api/Notification/NotificationBaseApi.php
@@ -9,6 +9,8 @@
use App\DTO\EntryResponseDto;
use App\DTO\PostCommentResponseDto;
use App\DTO\PostResponseDto;
+use App\Entity\Contracts\ContentInterface;
+use App\Entity\Contracts\HashtagableInterface;
use App\Entity\Contracts\ReportInterface;
use App\Entity\Entry;
use App\Entity\EntryComment;
@@ -19,12 +21,15 @@
use App\Entity\ReportApprovedNotification;
use App\Entity\ReportCreatedNotification;
use App\Entity\ReportRejectedNotification;
+use App\Factory\Contract\ContentDtoFactory;
use App\Factory\MessageFactory;
+use App\Service\SwitchingServiceRegistry;
use Symfony\Contracts\Service\Attribute\Required;
class NotificationBaseApi extends BaseApi
{
private MessageFactory $messageFactory;
+ private SwitchingServiceRegistry $serviceRegistry;
#[Required]
public function setMessageFactory(MessageFactory $messageFactory)
@@ -32,6 +37,11 @@ public function setMessageFactory(MessageFactory $messageFactory)
$this->messageFactory = $messageFactory;
}
+ #[Required]
+ public function setServiceRegistry(SwitchingServiceRegistry $serviceRegistry) {
+ $this->serviceRegistry = $serviceRegistry;
+ }
+
/**
* Serialize a single message to JSON.
*
@@ -139,15 +149,7 @@ protected function serializeNotification(Notification $dto)
private function createResponseDtoForReport(ReportInterface $subject): EntryCommentResponseDto|EntryResponseDto|PostCommentResponseDto|PostResponseDto
{
- if ($subject instanceof Entry) {
- return $this->entryFactory->createResponseDto($subject, $this->tagLinkRepository->getTagsOfContent($subject));
- } elseif ($subject instanceof EntryComment) {
- return $this->entryCommentFactory->createResponseDto($subject, $this->tagLinkRepository->getTagsOfContent($subject));
- } elseif ($subject instanceof Post) {
- return $this->postFactory->createResponseDto($subject, $this->tagLinkRepository->getTagsOfContent($subject));
- } elseif ($subject instanceof PostComment) {
- return $this->postCommentFactory->createResponseDto($subject, $this->tagLinkRepository->getTagsOfContent($subject));
- }
- throw new \InvalidArgumentException("cannot work with: '".\get_class($subject)."'");
+ $tags = $subject instanceof HashtagableInterface ? $this->tagLinkRepository->getTagsOfContent($subject) : [];
+ return $this->serviceRegistry->getService($subject, ContentDtoFactory::class)->createResponseDto($subject, $tags);
}
}
diff --git a/src/Controller/Message/MessageReportController.php b/src/Controller/Message/MessageReportController.php
new file mode 100644
index 0000000000..66b77d7f44
--- /dev/null
+++ b/src/Controller/Message/MessageReportController.php
@@ -0,0 +1,155 @@
+security->isGranted('ROLE_ADMIN') && !$this->security->isGranted('ROLE_MODERATOR')) {
+ throw new AccessDeniedException();
+ }
+
+ $reports = $this->repository->findReports($this->getPageNb($request), status: $status);
+
+ $reportIds = array_map(function (Report $report) { return $report->getId(); }, [...$reports->getCurrentPageResults()]);
+ $this->notificationRepository->markReportNotificationsOfMessagesAsRead($this->getUserOrThrow(), $reportIds);
+
+ return $this->render(
+ 'messages/reports.html.twig',
+ [
+ 'reports' => $reports,
+ ]
+ );
+ }
+
+ public function reportApprove(
+ #[MapEntity(id: 'report_id')]
+ Report $report,
+ Request $request,
+ ): Response {
+ if (!$this->security->isGranted('ROLE_ADMIN') && !$this->security->isGranted('ROLE_MODERATOR')) {
+ throw new AccessDeniedException();
+ }
+
+ $this->validateCsrf('report_approve', $request->getPayload()->get('token'));
+
+ $this->reportManager->accept($report, $this->getUserOrThrow());
+
+ return $this->redirectToRefererOrHome($request);
+ }
+
+ public function reportReject(
+ #[MapEntity(id: 'report_id')]
+ Report $report,
+ Request $request,
+ ): Response {
+ if (!$this->security->isGranted('ROLE_ADMIN') && !$this->security->isGranted('ROLE_MODERATOR')) {
+ throw new AccessDeniedException();
+ }
+
+ $this->validateCsrf('report_decline', $request->getPayload()->get('token'));
+
+ $this->reportManager->reject($report, $this->getUserOrThrow());
+
+ return $this->redirectToRefererOrHome($request);
+ }
+
+ #[IsGranted('ROLE_USER')]
+ public function reportMessage(
+ #[MapEntity]
+ Message $subject,
+ Request $request,
+ ): Response {
+ $user = $this->getUserOrThrow();
+ $thread = $subject->thread;
+ if(!$thread->userIsParticipant($user)) {
+ throw new AccessDeniedException();
+ }
+
+ $dto = ReportDto::create($subject);
+
+ $form = $this->createForm(
+ ReportType::class,
+ $dto,
+ ['action' => $this->generateUrl($dto->getRouteName(), ['id' => $subject->getId()])]
+ );
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ return $this->handleReportRequest($dto, $request);
+ }
+
+ if ($request->isXmlHttpRequest()) {
+ return $this->getJsonFormResponse($form, 'report/_form_report.html.twig');
+ }
+
+ return $this->render(
+ 'report/create.html.twig',
+ [
+ 'form' => $form->createView(),
+ 'magazine' => null,
+ 'subject' => $subject,
+ ]
+ );
+ }
+
+ private function handleReportRequest(ReportDto $dto, Request $request): Response
+ {
+ $reportError = false;
+ try {
+ $this->reportManager->report($dto, $this->getUserOrThrow());
+ $responseMessage = $this->translator->trans('subject_reported');
+
+ //TODO should the message be deleted directly or at report-accept?
+ } catch (SubjectHasBeenReportedException $exception) {
+ $reportError = true;
+ $responseMessage = $this->translator->trans('subject_reported_exists');
+ } finally {
+ if ($request->isXmlHttpRequest()) {
+ return new JsonResponse(
+ [
+ 'success' => true,
+ 'html' => \sprintf("
%s
", ($reportError) ? 'alert__danger' : 'alert__info', $responseMessage),
+ ]
+ );
+ }
+
+ $this->addFlash($reportError ? 'error' : 'info', $responseMessage);
+
+ return $this->redirectToRefererOrHome($request);
+ }
+ }
+}
diff --git a/src/Controller/Message/MessageThreadController.php b/src/Controller/Message/MessageThreadController.php
index 8c813f7ccf..311b54d116 100644
--- a/src/Controller/Message/MessageThreadController.php
+++ b/src/Controller/Message/MessageThreadController.php
@@ -7,6 +7,7 @@
use App\Controller\AbstractController;
use App\Entity\MessageThread;
use App\Form\MessageType;
+use App\Repository\NotificationRepository;
use App\Service\MessageManager;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Request;
@@ -15,13 +16,16 @@
class MessageThreadController extends AbstractController
{
- public function __construct(private readonly MessageManager $manager)
+ public function __construct(
+ private readonly MessageManager $manager,
+ private readonly NotificationRepository $notificationRepo,
+ )
{
}
#[IsGranted('ROLE_USER')]
#[IsGranted('show', subject: 'thread', statusCode: 403)]
- public function __invoke(#[MapEntity(id: 'id')] MessageThread $thread, Request $request): Response
+ public function show(#[MapEntity(id: 'id')] MessageThread $thread, Request $request): Response
{
$form = $this->createForm(MessageType::class);
$form->handleRequest($request);
@@ -32,7 +36,8 @@ public function __invoke(#[MapEntity(id: 'id')] MessageThread $thread, Request $
return $this->redirectToRoute('messages_single', ['id' => $thread->getId()]);
}
- $this->manager->readMessages($thread, $this->getUserOrThrow());
+ //$this->manager->readMessages($thread, $this->getUserOrThrow());
+ $this->notificationRepo->markMessageNotificationsAsRead($this->getUserOrThrow(), $thread);
return $this->render(
'messages/single.html.twig',
@@ -43,4 +48,15 @@ public function __invoke(#[MapEntity(id: 'id')] MessageThread $thread, Request $
]
);
}
+
+ #[IsGranted('ROLE_USER')]
+ #[IsGranted('show', subject: 'thread', statusCode: 403)]
+ public function remove(#[MapEntity(id: 'id')] MessageThread $thread): Response
+ {
+ $this->manager->removeUserFromThread($thread, $this->getUserOrThrow());
+
+ return $this->redirectToRoute(
+ 'messages_front'
+ );
+ }
}
diff --git a/src/DTO/OAuth2ClientDto.php b/src/DTO/OAuth2ClientDto.php
index d4f80d8a9c..ec82efc3f4 100644
--- a/src/DTO/OAuth2ClientDto.php
+++ b/src/DTO/OAuth2ClientDto.php
@@ -72,6 +72,7 @@ class OAuth2ClientDto extends ImageUploadDto implements \JsonSerializable
'user:message',
'user:message:read',
'user:message:create',
+ 'user:message:delete',
'user:notification',
'user:notification:read',
'user:notification:delete',
diff --git a/src/DTO/ReportDto.php b/src/DTO/ReportDto.php
index f0a7136c53..7e9ada02e7 100644
--- a/src/DTO/ReportDto.php
+++ b/src/DTO/ReportDto.php
@@ -27,8 +27,8 @@ public static function create(ReportInterface $subject, ?string $reason = null,
$dto->subject = $subject;
$dto->reason = $reason;
- $dto->magazine = $subject->magazine;
- $dto->reported = $subject->user;
+ $dto->magazine = $subject->magazine ?? null;
+ $dto->reported = $subject->getUser();
return $dto;
}
@@ -40,18 +40,7 @@ public function getId(): ?int
public function getRouteName(): string
{
- switch (\get_class($this->getSubject())) {
- case Entry::class:
- return 'entry_report';
- case EntryComment::class:
- return 'entry_comment_report';
- case Post::class:
- return 'post_report';
- case PostComment::class:
- return 'post_comment_report';
- }
-
- throw new \LogicException();
+ return $this->getSubject()->getReportType();
}
public function getSubject(): ReportInterface
diff --git a/src/DTO/ReportResponseDto.php b/src/DTO/ReportResponseDto.php
index 6d6dc25522..675610047b 100644
--- a/src/DTO/ReportResponseDto.php
+++ b/src/DTO/ReportResponseDto.php
@@ -5,6 +5,12 @@
namespace App\DTO;
use App\Entity\Contracts\VisibilityInterface;
+use App\Entity\Entry;
+use App\Entity\EntryComment;
+use App\Entity\EntryReport;
+use App\Entity\Message;
+use App\Entity\Post;
+use App\Entity\PostComment;
use Nelmio\ApiDocBundle\Attribute\Model;
use OpenApi\Attributes as OA;
@@ -59,10 +65,11 @@ public static function create(
#[OA\Property(
'type',
enum: [
- 'entry_report',
- 'entry_comment_report',
- 'post_report',
- 'post_comment_report',
+ Entry::REPORT_TYPE,
+ EntryComment::REPORT_TYPE,
+ Post::REPORT_TYPE,
+ PostComment::REPORT_TYPE,
+ Message::REPORT_TYPE,
'null_report',
]
)]
@@ -75,13 +82,15 @@ public function getType(): string
switch (\get_class($this->subject)) {
case EntryResponseDto::class:
- return 'entry_report';
+ return Entry::REPORT_TYPE;
case EntryCommentResponseDto::class:
- return 'entry_comment_report';
+ return EntryComment::REPORT_TYPE;
case PostResponseDto::class:
- return 'post_report';
+ return Post::REPORT_TYPE;
case PostCommentResponseDto::class:
- return 'post_comment_report';
+ return PostComment::REPORT_TYPE;
+ case MessageResponseDto::class:
+ return Message::REPORT_TYPE;
}
throw new \LogicException();
diff --git a/src/Entity/Contracts/HashtagableInterface.php b/src/Entity/Contracts/HashtagableInterface.php
new file mode 100644
index 0000000000..1b400baff8
--- /dev/null
+++ b/src/Entity/Contracts/HashtagableInterface.php
@@ -0,0 +1,8 @@
+get(EntryCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s: %s', $this->entryComment->user->username, $trans->trans('added_new_comment'), $this->entryComment->getShortTitle());
- $slash = $this->entryComment->user->avatar && !str_starts_with('/', $this->entryComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entryComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entryComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_comment_view', [
- 'entry_id' => $this->entryComment->entry->getId(),
- 'magazine_name' => $this->entryComment->magazine->name,
- 'slug' => $this->entryComment->entry->slug ?? '-',
- 'comment_id' => $this->entryComment->getId(),
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entryComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->entryComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_new_comment', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryCommentDeletedNotification.php b/src/Entity/EntryCommentDeletedNotification.php
index e8a90cc16b..c4337b42cb 100644
--- a/src/Entity/EntryCommentDeletedNotification.php
+++ b/src/Entity/EntryCommentDeletedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Entry\EntryCommentUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,17 +44,16 @@ public function getType(): string
return 'entry_comment_deleted_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(EntryCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $trans->trans('comment'), $this->entryComment->getShortTitle(), $this->entryComment->isTrashed() ? $trans->trans('removed') : $trans->trans('deleted'));
- $slash = $this->entryComment->user->avatar && !str_starts_with('/', $this->entryComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entryComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entryComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_comment_view', [
- 'entry_id' => $this->entryComment->entry->getId(),
- 'magazine_name' => $this->entryComment->magazine->name,
- 'slug' => $this->entryComment->entry->slug ?? '-',
- 'comment_id' => $this->entryComment->getId(),
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entryComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->entryComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_removed_comment', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryCommentEditedNotification.php b/src/Entity/EntryCommentEditedNotification.php
index 899fbf73bc..74e9ecfd3d 100644
--- a/src/Entity/EntryCommentEditedNotification.php
+++ b/src/Entity/EntryCommentEditedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Entry\EntryCommentUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,17 +44,16 @@ public function getType(): string
return 'entry_comment_edited_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(EntryCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->entryComment->user->username, $trans->trans('edited_comment'), $this->entryComment->getShortTitle());
- $slash = $this->entryComment->user->avatar && !str_starts_with('/', $this->entryComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entryComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entryComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_comment_view', [
- 'entry_id' => $this->entryComment->entry->getId(),
- 'magazine_name' => $this->entryComment->magazine->name,
- 'slug' => $this->entryComment->entry->slug ?? '-',
- 'comment_id' => $this->entryComment->getId(),
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entryComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->entryComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_edited_comment', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryCommentMentionedNotification.php b/src/Entity/EntryCommentMentionedNotification.php
index 4b7eff4cb8..ad7b9c9f57 100644
--- a/src/Entity/EntryCommentMentionedNotification.php
+++ b/src/Entity/EntryCommentMentionedNotification.php
@@ -4,9 +4,13 @@
namespace App\Entity;
+use App\Factory\Entry\EntryCommentUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -38,17 +42,16 @@ public function getType(): string
return 'entry_comment_mentioned_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(EntryCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->entryComment->user->username, $trans->trans('mentioned_you'), $this->entryComment->getShortTitle());
- $slash = $this->entryComment->user->avatar && !str_starts_with('/', $this->entryComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entryComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entryComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_comment_view', [
- 'entry_id' => $this->entryComment->entry->getId(),
- 'magazine_name' => $this->entryComment->magazine->name,
- 'slug' => $this->entryComment->entry->slug ?? '-',
- 'comment_id' => $this->entryComment->getId(),
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entryComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->entryComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_mention', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryCommentReplyNotification.php b/src/Entity/EntryCommentReplyNotification.php
index 08ba77b4ca..d0fb81f9cb 100644
--- a/src/Entity/EntryCommentReplyNotification.php
+++ b/src/Entity/EntryCommentReplyNotification.php
@@ -4,10 +4,15 @@
namespace App\Entity;
+use App\Factory\Entry\EntryCommentUrlFactory;
+use App\Factory\Entry\EntryUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,17 +45,16 @@ public function getType(): string
return 'entry_comment_reply_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(EntryCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->entryComment->user->username, $trans->trans('replied_to_your_comment'), $this->entryComment->getShortTitle());
- $slash = $this->entryComment->user->avatar && !str_starts_with('/', $this->entryComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entryComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entryComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_comment_view', [
- 'entry_id' => $this->entryComment->entry->getId(),
- 'magazine_name' => $this->entryComment->magazine->name,
- 'slug' => $this->entryComment->entry->slug ?? '-',
- 'comment_id' => $this->entryComment->getId(),
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entryComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->entryComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_new_reply', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryCreatedNotification.php b/src/Entity/EntryCreatedNotification.php
index fbb3854dbb..c9301d361f 100644
--- a/src/Entity/EntryCreatedNotification.php
+++ b/src/Entity/EntryCreatedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Entry\EntryUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,16 +39,16 @@ public function getType(): string
return 'entry_created_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryUrlFactory $entryUrlFactory */
+ $entryUrlFactory = $serviceContainer->get(EntryUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->entry->user->username, $trans->trans('added_new_thread'), $this->entry->getShortTitle());
- $slash = $this->entry->user->avatar && !str_starts_with('/', $this->entry->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entry->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entry->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_single', [
- 'entry_id' => $this->entry->getId(),
- 'magazine_name' => $this->entry->magazine->name,
- 'slug' => $this->entry->slug ?? '-',
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entry->user);
+ $url = $entryUrlFactory->getLocalUrl($this->entry);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_new_thread', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryDeletedNotification.php b/src/Entity/EntryDeletedNotification.php
index 7ac7dd47b7..f6e433c6ab 100644
--- a/src/Entity/EntryDeletedNotification.php
+++ b/src/Entity/EntryDeletedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Entry\EntryUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,16 +39,16 @@ public function getType(): string
return 'entry_deleted_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryUrlFactory $entryUrlFactory */
+ $entryUrlFactory = $serviceContainer->get(EntryUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s', $this->entry->getShortTitle(), $this->entry->isTrashed() ? $trans->trans('removed') : $trans->trans('deleted'));
- $slash = $this->entry->user->avatar && !str_starts_with('/', $this->entry->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entry->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entry->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_single', [
- 'entry_id' => $this->entry->getId(),
- 'magazine_name' => $this->entry->magazine->name,
- 'slug' => $this->entry->slug ?? '-',
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entry->user);
+ $url = $entryUrlFactory->getLocalUrl($this->entry);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_removed_thread', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryEditedNotification.php b/src/Entity/EntryEditedNotification.php
index 0d76734d4e..5a472cdbe6 100644
--- a/src/Entity/EntryEditedNotification.php
+++ b/src/Entity/EntryEditedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Entry\EntryUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,16 +39,16 @@ public function getType(): string
return 'entry_edited_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryUrlFactory $entryUrlFactory */
+ $entryUrlFactory = $serviceContainer->get(EntryUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->entry->user->username, $trans->trans('edited_thread'), $this->entry->getShortTitle());
- $slash = $this->entry->user->avatar && !str_starts_with('/', $this->entry->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entry->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entry->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_single', [
- 'entry_id' => $this->entry->getId(),
- 'magazine_name' => $this->entry->magazine->name,
- 'slug' => $this->entry->slug ?? '-',
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entry->user);
+ $url = $entryUrlFactory->getLocalUrl($this->entry);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_edited_thread', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/EntryMentionedNotification.php b/src/Entity/EntryMentionedNotification.php
index 9db1590d90..8e9cbfc091 100644
--- a/src/Entity/EntryMentionedNotification.php
+++ b/src/Entity/EntryMentionedNotification.php
@@ -4,9 +4,14 @@
namespace App\Entity;
+use App\Factory\Entry\EntryUrlFactory;
+use App\Factory\Magazine\MagazineUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -33,16 +38,16 @@ public function getType(): string
return 'entry_mentioned_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var EntryUrlFactory $entryUrlFactory */
+ $entryUrlFactory = $serviceContainer->get(EntryUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->entry->user->username, $trans->trans('mentioned_you'), $this->entry->getShortTitle());
- $slash = $this->entry->user->avatar && !str_starts_with('/', $this->entry->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->entry->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->entry->user->avatar->filePath : null;
- $url = $urlGenerator->generate('entry_single', [
- 'entry_id' => $this->entry->getId(),
- 'magazine_name' => $this->entry->magazine->name,
- 'slug' => $this->entry->slug ?? '-',
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->entry->user);
+ $url = $entryUrlFactory->getLocalUrl($this->entry);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_mention', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/MagazineBanNotification.php b/src/Entity/MagazineBanNotification.php
index 1b5cd1fec4..f43f45bc18 100644
--- a/src/Entity/MagazineBanNotification.php
+++ b/src/Entity/MagazineBanNotification.php
@@ -4,10 +4,13 @@
namespace App\Entity;
+use App\Factory\Magazine\MagazineUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,8 +38,11 @@ public function getType(): string
return 'magazine_ban_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var MagazineUrlFactory $magazineUrlFactory */
+ $magazineUrlFactory = $serviceContainer->get(MagazineUrlFactory::class);
+
$intl = new \IntlDateFormatter($locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT, calendar: \IntlDateFormatter::GREGORIAN);
if ($this->ban->expiredAt) {
@@ -54,8 +60,8 @@ public function getMessage(TranslatorInterface $trans, string $locale, UrlGenera
$this->ban->reason
);
}
- $slash = $this->ban->magazine->icon && !str_starts_with('/', $this->ban->magazine->icon->filePath) ? '/' : '';
- $avatarUrl = $this->ban->magazine->icon ? '/media/cache/resolve/avatar_thumb'.$slash.$this->ban->magazine->icon->filePath : null;
+
+ $avatarUrl = $magazineUrlFactory->getAvatarUrl($this->ban->magazine);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_ban', locale: $locale), avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/MagazineUnBanNotification.php b/src/Entity/MagazineUnBanNotification.php
index 205521181b..ac915c2125 100644
--- a/src/Entity/MagazineUnBanNotification.php
+++ b/src/Entity/MagazineUnBanNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Magazine\MagazineUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,11 +39,13 @@ public function getType(): string
return 'magazine_unban_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var MagazineUrlFactory $magazineUrlFactory */
+ $magazineUrlFactory = $serviceContainer->get(MagazineUrlFactory::class);
+
$message = $trans->trans('you_are_no_longer_banned_from_magazine', ['%m' => $this->ban->magazine->name], locale: $locale);
- $slash = $this->ban->magazine->icon && !str_starts_with('/', $this->ban->magazine->icon->filePath) ? '/' : '';
- $avatarUrl = $this->ban->magazine->icon ? '/media/cache/resolve/avatar_thumb'.$slash.$this->ban->magazine->icon->filePath : null;
+ $avatarUrl = $magazineUrlFactory->getAvatarUrl($this->ban->magazine);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_ban', locale: $locale), avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/Message.php b/src/Entity/Message.php
index 1ea030890d..cbc2539c0b 100644
--- a/src/Entity/Message.php
+++ b/src/Entity/Message.php
@@ -5,6 +5,7 @@
namespace App\Entity;
use App\Entity\Contracts\ActivityPubActivityInterface;
+use App\Entity\Contracts\ReportInterface;
use App\Entity\Traits\ActivityPubActivityTrait;
use App\Entity\Traits\CreatedAtTrait;
use App\Entity\Traits\EditedAtTrait;
@@ -20,13 +21,14 @@
use Symfony\Component\Uid\Uuid;
#[Entity]
-class Message implements ActivityPubActivityInterface
+class Message implements ActivityPubActivityInterface, ReportInterface
{
use ActivityPubActivityTrait;
use CreatedAtTrait {
CreatedAtTrait::__construct as createdAtTraitConstruct;
}
use EditedAtTrait;
+
public const STATUS_NEW = 'new';
public const STATUS_READ = 'read';
public const STATUS_OPTIONS = [
@@ -34,6 +36,8 @@ class Message implements ActivityPubActivityInterface
self::STATUS_READ,
];
+ public const string REPORT_TYPE = 'message_report';
+
#[ManyToOne(targetEntity: MessageThread::class, inversedBy: 'messages')]
#[JoinColumn(nullable: false, onDelete: 'CASCADE')]
public MessageThread $thread;
@@ -46,6 +50,8 @@ class Message implements ActivityPubActivityInterface
public string $status = self::STATUS_NEW;
#[Column(type: 'uuid', unique: true, nullable: false)]
public string $uuid;
+ #[OneToMany(mappedBy: 'message', targetEntity: MessageReport::class, cascade: ['remove'], fetch: 'EXTRA_LAZY', orphanRemoval: true)]
+ public Collection $reports;
#[Id]
#[GeneratedValue]
#[Column(type: 'integer')]
@@ -61,6 +67,7 @@ public function __construct(MessageThread $thread, User $sender, string $body, ?
$this->notifications = new ArrayCollection();
$this->uuid = Uuid::v4()->toRfc4122();
$this->apId = $apId;
+ $this->reports = new ArrayCollection();
$thread->addMessage($this);
@@ -87,4 +94,26 @@ public function getUser(): User
{
return $this->sender;
}
+
+
+ public function getApId(): ?string
+ {
+ return $this->apId;
+ }
+
+ public function getMagazine(): ?Magazine
+ {
+ return null;
+ }
+
+ public function getShortTitle(): string {
+ $body = wordwrap($this->body, 60);
+ $body = explode("\n", $body);
+ return trim($body[0]).(isset($body[1]) ? '...' : '');
+ }
+
+ public function getReportType(): string
+ {
+ return self::REPORT_TYPE;
+ }
}
diff --git a/src/Entity/MessageNotification.php b/src/Entity/MessageNotification.php
index aec3c4af15..f09c61ccf7 100644
--- a/src/Entity/MessageNotification.php
+++ b/src/Entity/MessageNotification.php
@@ -5,10 +5,13 @@
namespace App\Entity;
use App\Enums\EPushNotificationType;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -38,11 +41,15 @@ public function getType(): string
return 'message_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+ /** @var UrlGeneratorInterface $urlGenerator */
+ $urlGenerator = $serviceContainer->get(UrlGeneratorInterface::class);
+
$message = \sprintf('%s %s: %s', $this->message->sender->username, $trans->trans('wrote_message'), $this->message->body);
- $slash = $this->message->sender->avatar && !str_starts_with('/', $this->message->sender->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->message->sender->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->message->sender->avatar->filePath : null;
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->message->sender);
$url = $urlGenerator->generate('messages_single', ['id' => $this->message->thread->getId()]);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_message', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl, category: EPushNotificationType::Message);
diff --git a/src/Entity/MessageReport.php b/src/Entity/MessageReport.php
new file mode 100644
index 0000000000..aeccd65d7e
--- /dev/null
+++ b/src/Entity/MessageReport.php
@@ -0,0 +1,41 @@
+getUser(), null, $reason);
+
+ $this->message = $message;
+ }
+
+ public function getSubject(): Message
+ {
+ return $this->message;
+ }
+
+ public function clearSubject(): Report
+ {
+ $this->message = null;
+
+ return $this;
+ }
+
+ public function getType(): string
+ {
+ return 'message';
+ }
+}
diff --git a/src/Entity/MessageThread.php b/src/Entity/MessageThread.php
index 4dc20629ed..8fe3409b3b 100644
--- a/src/Entity/MessageThread.php
+++ b/src/Entity/MessageThread.php
@@ -34,7 +34,7 @@ class MessageThread
new JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete: 'CASCADE'),
]
)]
- #[ManyToMany(targetEntity: User::class, cascade: ['persist'], orphanRemoval: true)]
+ #[ManyToMany(targetEntity: User::class, cascade: ['persist'], orphanRemoval: false)]
public Collection $participants;
#[OneToMany(mappedBy: 'thread', targetEntity: Message::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[OrderBy(['createdAt' => 'ASC'])]
diff --git a/src/Entity/NewSignupNotification.php b/src/Entity/NewSignupNotification.php
index 66cfc0d3bc..9adf83c241 100644
--- a/src/Entity/NewSignupNotification.php
+++ b/src/Entity/NewSignupNotification.php
@@ -4,10 +4,13 @@
namespace App\Entity;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -28,13 +31,15 @@ public function getSubject(): ?User
return $this->newUser;
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = str_replace('%u%', $this->newUser->username, $trans->trans('notification_body_new_signup', locale: $locale));
$title = $trans->trans('notification_title_new_signup', locale: $locale);
- $url = $urlGenerator->generate('user_overview', ['username' => $this->newUser->username]);
- $slash = $this->newUser->avatar && !str_starts_with('/', $this->newUser->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->newUser->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->newUser->avatar->filePath : null;
+ $url = $userUrlFactory->getLocalUrl($this->newUser);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->newUser);
return new PushNotification($this->getId(), $message, $title, actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/Notification.php b/src/Entity/Notification.php
index 71836fb63a..ab732dbd08 100644
--- a/src/Entity/Notification.php
+++ b/src/Entity/Notification.php
@@ -6,6 +6,7 @@
use App\Entity\Traits\CreatedAtTrait;
use App\Payloads\PushNotification;
+use App\Service\SwitchingServiceRegistry;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\DiscriminatorColumn;
use Doctrine\ORM\Mapping\DiscriminatorMap;
@@ -15,6 +16,8 @@
use Doctrine\ORM\Mapping\InheritanceType;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -80,5 +83,5 @@ public function getId(): int
abstract public function getType(): string;
- abstract public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification;
+ abstract public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification;
}
diff --git a/src/Entity/OAuth2UserConsent.php b/src/Entity/OAuth2UserConsent.php
index f1ebe4ad04..bcd0364ac3 100644
--- a/src/Entity/OAuth2UserConsent.php
+++ b/src/Entity/OAuth2UserConsent.php
@@ -86,6 +86,7 @@ class OAuth2UserConsent
'user:message' => 'oauth2.grant.user.message.all',
'user:message:read' => 'oauth2.grant.user.message.read',
'user:message:create' => 'oauth2.grant.user.message.create',
+ 'user:message:delete' => 'oauth2.grant.user.message.delete',
'user:notification' => 'oauth2.grant.user.notification.all',
'user:notification:read' => 'oauth2.grant.user.notification.read',
'user:notification:delete' => 'oauth2.grant.user.notification.delete',
diff --git a/src/Entity/Post.php b/src/Entity/Post.php
index 19d1400218..de432eb42e 100644
--- a/src/Entity/Post.php
+++ b/src/Entity/Post.php
@@ -7,6 +7,7 @@
use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Contracts\CommentInterface;
use App\Entity\Contracts\FavouriteInterface;
+use App\Entity\Contracts\HashtagableInterface;
use App\Entity\Contracts\RankingInterface;
use App\Entity\Contracts\ReportInterface;
use App\Entity\Contracts\VisibilityInterface;
@@ -40,7 +41,7 @@
#[Index(columns: ['created_at'], name: 'post_created_at_idx')]
#[Index(columns: ['last_active'], name: 'post_last_active_at_idx')]
#[Index(columns: ['body_ts'], name: 'post_body_ts_idx')]
-class Post implements VotableInterface, CommentInterface, VisibilityInterface, RankingInterface, ReportInterface, FavouriteInterface, ActivityPubActivityInterface
+class Post implements VotableInterface, CommentInterface, VisibilityInterface, RankingInterface, ReportInterface, FavouriteInterface, ActivityPubActivityInterface, HashtagableInterface
{
use VotableTrait;
use RankingTrait;
@@ -51,6 +52,8 @@ class Post implements VotableInterface, CommentInterface, VisibilityInterface, R
CreatedAtTrait::__construct as createdAtTraitConstruct;
}
+ public const string REPORT_TYPE = 'post_report';
+
#[ManyToOne(targetEntity: User::class, inversedBy: 'posts')]
#[JoinColumn(nullable: false, onDelete: 'CASCADE')]
public User $user;
@@ -352,6 +355,11 @@ public function isAdult(): bool
return $this->isAdult || $this->magazine->isAdult;
}
+ public function getReportType(): string
+ {
+ return self::REPORT_TYPE;
+ }
+
public function __sleep()
{
return [];
diff --git a/src/Entity/PostComment.php b/src/Entity/PostComment.php
index 3f6cd88e0c..0eafdaa733 100644
--- a/src/Entity/PostComment.php
+++ b/src/Entity/PostComment.php
@@ -7,6 +7,7 @@
use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Contracts\ContentInterface;
use App\Entity\Contracts\FavouriteInterface;
+use App\Entity\Contracts\HashtagableInterface;
use App\Entity\Contracts\ReportInterface;
use App\Entity\Contracts\VisibilityInterface;
use App\Entity\Contracts\VotableInterface;
@@ -37,7 +38,7 @@
#[Index(columns: ['last_active'], name: 'post_comment_last_active_at_idx')]
#[Index(columns: ['created_at'], name: 'post_comment_created_at_idx')]
#[Index(columns: ['body_ts'], name: 'post_comment_body_ts_idx')]
-class PostComment implements VotableInterface, VisibilityInterface, ReportInterface, FavouriteInterface, ActivityPubActivityInterface
+class PostComment implements VotableInterface, VisibilityInterface, ReportInterface, FavouriteInterface, ActivityPubActivityInterface, HashtagableInterface
{
use VotableTrait;
use VisibilityTrait;
@@ -47,6 +48,8 @@ class PostComment implements VotableInterface, VisibilityInterface, ReportInterf
CreatedAtTrait::__construct as createdAtTraitConstruct;
}
+ public const string REPORT_TYPE = 'post_comment_report';
+
#[ManyToOne(targetEntity: User::class, inversedBy: 'postComments')]
#[JoinColumn(nullable: false, onDelete: 'CASCADE')]
public User $user;
@@ -312,4 +315,9 @@ public function getChildrenByCriteria(MbinCriteria $postCommentCriteria): array
return $children;
}
+
+ public function getReportType(): string
+ {
+ return self::REPORT_TYPE;
+ }
}
diff --git a/src/Entity/PostCommentCreatedNotification.php b/src/Entity/PostCommentCreatedNotification.php
index bde7838126..a79da4b9a6 100644
--- a/src/Entity/PostCommentCreatedNotification.php
+++ b/src/Entity/PostCommentCreatedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostCommentUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,16 +44,16 @@ public function getType(): string
return 'post_comment_created_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var PostCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(PostCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->postComment->user->username, $trans->trans('added_new_comment'), $this->postComment->getShortTitle());
- $slash = $this->postComment->user->avatar && !str_starts_with('/', $this->postComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->postComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->postComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->postComment->post->magazine->name,
- 'post_id' => $this->postComment->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]).'#post-comment-'.$this->postComment->getId();
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->postComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->postComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_new_comment', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostCommentDeletedNotification.php b/src/Entity/PostCommentDeletedNotification.php
index f2e9bfc6f4..b2617701c2 100644
--- a/src/Entity/PostCommentDeletedNotification.php
+++ b/src/Entity/PostCommentDeletedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostCommentUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,16 +44,20 @@ public function getType(): string
return 'post_comment_deleted_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
- $message = \sprintf('%s %s - %s', $trans->trans('comment'), $this->postComment->getShortTitle(), $this->postComment->isTrashed() ? $trans->trans('removed') : $trans->trans('deleted'));
- $slash = $this->postComment->user->avatar && !str_starts_with('/', $this->postComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->postComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$this->postComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->postComment->post->magazine->name,
- 'post_id' => $this->postComment->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]).'#post-comment-'.$this->postComment->getId();
+ /** @var PostCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(PostCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
+ $message = \sprintf('%s %s - %s',
+ $trans->trans('comment'),
+ $this->postComment->getShortTitle(),
+ $this->postComment->isTrashed() ? $trans->trans('removed') : $trans->trans('deleted')
+ );
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->postComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->postComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_removed_comment', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostCommentEditedNotification.php b/src/Entity/PostCommentEditedNotification.php
index 185c4c67c5..fbe5c81fd6 100644
--- a/src/Entity/PostCommentEditedNotification.php
+++ b/src/Entity/PostCommentEditedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostCommentUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,16 +44,16 @@ public function getType(): string
return 'post_comment_edited_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var PostCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(PostCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->postComment->user->username, $trans->trans('edited_comment'), $this->postComment->getShortTitle());
- $slash = $this->postComment->user->avatar && !str_starts_with('/', $this->postComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->postComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->postComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->postComment->post->magazine->name,
- 'post_id' => $this->postComment->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]).'#post-comment-'.$this->postComment->getId();
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->postComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->postComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_edited_comment', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostCommentMentionedNotification.php b/src/Entity/PostCommentMentionedNotification.php
index 33124996d3..a8fff81fdd 100644
--- a/src/Entity/PostCommentMentionedNotification.php
+++ b/src/Entity/PostCommentMentionedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostCommentUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,16 +44,16 @@ public function getType(): string
return 'post_comment_mentioned_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var PostCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(PostCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->postComment->user->username, $trans->trans('mentioned_you'), $this->postComment->getShortTitle());
- $slash = $this->postComment->user->avatar && !str_starts_with('/', $this->postComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->postComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->postComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->postComment->post->magazine->name,
- 'post_id' => $this->postComment->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]).'#post-comment-'.$this->postComment->getId();
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->postComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->postComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_mention', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostCommentReplyNotification.php b/src/Entity/PostCommentReplyNotification.php
index 81ebf5733d..850e2b80bd 100644
--- a/src/Entity/PostCommentReplyNotification.php
+++ b/src/Entity/PostCommentReplyNotification.php
@@ -4,10 +4,15 @@
namespace App\Entity;
+use App\Factory\Post\PostCommentUrlFactory;
+use App\Factory\Post\PostUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -40,16 +45,16 @@ public function getType(): string
return 'post_comment_reply_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var PostCommentUrlFactory $commentUrlFactory */
+ $commentUrlFactory = $serviceContainer->get(PostCommentUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->postComment->user->username, $trans->trans('replied_to_your_comment'), $this->postComment->getShortTitle());
- $slash = $this->postComment->user->avatar && !str_starts_with('/', $this->postComment->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->postComment->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->postComment->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->postComment->post->magazine->name,
- 'post_id' => $this->postComment->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]).'#post-comment-'.$this->postComment->getId();
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->postComment->user);
+ $url = $commentUrlFactory->getLocalUrl($this->postComment);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_new_reply', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostCreatedNotification.php b/src/Entity/PostCreatedNotification.php
index bbed49278c..370c16551a 100644
--- a/src/Entity/PostCreatedNotification.php
+++ b/src/Entity/PostCreatedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,16 +39,16 @@ public function getType(): string
return 'post_created_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var PostUrlFactory $postUrlFactory */
+ $postUrlFactory = $serviceContainer->get(PostUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->post->user->username, $trans->trans('added_new_post'), $this->post->getShortTitle());
- $slash = $this->post->user->avatar && !str_starts_with('/', $this->post->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->post->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->post->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->post->magazine->name,
- 'post_id' => $this->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->post->user);
+ $url = $postUrlFactory->getLocalUrl($this->post);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_new_post', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostDeletedNotification.php b/src/Entity/PostDeletedNotification.php
index 6c0341c9ac..af5de8d4ba 100644
--- a/src/Entity/PostDeletedNotification.php
+++ b/src/Entity/PostDeletedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,16 +39,16 @@ public function getType(): string
return 'post_deleted_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var PostUrlFactory $postUrlFactory */
+ $postUrlFactory = $serviceContainer->get(PostUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $trans->trans('post'), $this->post->getShortTitle(), $this->post->isTrashed() ? $trans->trans('removed') : $trans->trans('deleted'));
- $slash = $this->post->user->avatar && !str_starts_with('/', $this->post->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->post->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->post->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->post->magazine->name,
- 'post_id' => $this->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->post->user);
+ $url = $postUrlFactory->getLocalUrl($this->post);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_removed_post', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostEditedNotification.php b/src/Entity/PostEditedNotification.php
index 4a43b686c9..1f6bbc4d7b 100644
--- a/src/Entity/PostEditedNotification.php
+++ b/src/Entity/PostEditedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,16 +39,16 @@ public function getType(): string
return 'post_edited_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var PostUrlFactory $postUrlFactory */
+ $postUrlFactory = $serviceContainer->get(PostUrlFactory::class);
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->post->user->username, $trans->trans('edited_post'), $this->post->getShortTitle());
- $slash = $this->post->user->avatar && !str_starts_with('/', $this->post->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->post->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->post->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->post->magazine->name,
- 'post_id' => $this->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->post->user);
+ $url = $postUrlFactory->getLocalUrl($this->post);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_edited_post', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/PostMentionedNotification.php b/src/Entity/PostMentionedNotification.php
index e12251f800..24377d83ec 100644
--- a/src/Entity/PostMentionedNotification.php
+++ b/src/Entity/PostMentionedNotification.php
@@ -4,10 +4,14 @@
namespace App\Entity;
+use App\Factory\Post\PostUrlFactory;
+use App\Factory\User\UserUrlFactory;
use App\Payloads\PushNotification;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,16 +39,16 @@ public function getType(): string
return 'post_mentioned_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
+ /** @var UserUrlFactory $userUrlFactory */
+ $userUrlFactory = $serviceContainer->get(UserUrlFactory::class);
+ /** @var PostUrlFactory $userUrlFactory */
+ $postUrlFactory = $serviceContainer->get(PostUrlFactory::class);
+
$message = \sprintf('%s %s - %s', $this->post->user->username, $trans->trans('mentioned_you'), $this->post->getShortTitle());
- $slash = $this->post->user->avatar && !str_starts_with('/', $this->post->user->avatar->filePath) ? '/' : '';
- $avatarUrl = $this->post->user->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$this->post->user->avatar->filePath : null;
- $url = $urlGenerator->generate('post_single', [
- 'magazine_name' => $this->post->magazine->name,
- 'post_id' => $this->post->getId(),
- 'slug' => empty($this->postComment->post->slug) ? '-' : $this->postComment->post->slug,
- ]);
+ $url = $postUrlFactory->getLocalUrl($this->post);
+ $avatarUrl = $userUrlFactory->getAvatarUrl($this->post->user);
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_mention', locale: $locale), actionUrl: $url, avatarUrl: $avatarUrl);
}
diff --git a/src/Entity/Report.php b/src/Entity/Report.php
index 17df33426f..7d2247458d 100644
--- a/src/Entity/Report.php
+++ b/src/Entity/Report.php
@@ -28,6 +28,7 @@
'entry_comment' => 'EntryCommentReport',
'post' => 'PostReport',
'post_comment' => 'PostCommentReport',
+ 'message' => 'MessageReport',
])]
#[UniqueConstraint(name: 'report_uuid_idx', columns: ['uuid'])]
abstract class Report
@@ -54,8 +55,8 @@ abstract class Report
];
#[ManyToOne(targetEntity: Magazine::class, inversedBy: 'reports')]
- #[JoinColumn(nullable: false, onDelete: 'CASCADE')]
- public Magazine $magazine;
+ #[JoinColumn(nullable: true, onDelete: 'CASCADE')]
+ public ?Magazine $magazine;
#[ManyToOne(targetEntity: User::class, inversedBy: 'reports')]
#[JoinColumn(nullable: false, onDelete: 'CASCADE')]
public User $reporting;
@@ -80,7 +81,7 @@ abstract class Report
#[Column(type: 'integer')]
private int $id;
- public function __construct(User $reporting, User $reported, Magazine $magazine, ?string $reason = null)
+ public function __construct(User $reporting, User $reported, ?Magazine $magazine, ?string $reason = null)
{
$this->reporting = $reporting;
$this->reported = $reported;
diff --git a/src/Entity/ReportApprovedNotification.php b/src/Entity/ReportApprovedNotification.php
index 95fbe4c418..f57a9aec22 100644
--- a/src/Entity/ReportApprovedNotification.php
+++ b/src/Entity/ReportApprovedNotification.php
@@ -5,10 +5,15 @@
namespace App\Entity;
use App\Entity\Contracts\ReportInterface;
+use App\Factory\Contract\ContentUrlFactory;
+use App\Factory\Contract\ReportUrlFactory;
use App\Payloads\PushNotification;
+use App\Service\SwitchingServiceRegistry;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -31,12 +36,14 @@ public function getType(): string
return 'report_approved_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
- /** @var Entry|EntryComment|Post|PostComment $subject */
+ /** @var SwitchingServiceRegistry $serviceRegistry */
+ $serviceRegistry = $serviceContainer->get(SwitchingServiceRegistry::class);
+
$subject = $this->report->getSubject();
- $linkToSubject = $this->getSubjectLink($this->report->getSubject(), $urlGenerator);
- $linkToReport = $urlGenerator->generate('magazine_panel_reports', ['name' => $this->report->magazine->name, 'status' => Report::STATUS_APPROVED]);
+ $linkToSubject = $this->getSubjectLink($this->report->getSubject(), $serviceRegistry);
+ $linkToReport = $serviceRegistry->getService($this->report, ReportUrlFactory::class)->getReportUrl($this->report, Report::STATUS_APPROVED);
if ($this->report->reporting->getId() === $this->user->getId()) {
$title = $trans->trans('own_report_accepted', locale: $locale);
$message = \sprintf('%s: %s', $trans->trans('report_subject', locale: $locale), $subject->getShortTitle());
@@ -58,18 +65,12 @@ public function getMessage(TranslatorInterface $trans, string $locale, UrlGenera
return new PushNotification($this->getId(), $message, $title, actionUrl: $actionUrl);
}
- private function getSubjectLink(ReportInterface $subject, UrlGeneratorInterface $urlGenerator): string
+ private function getSubjectLink(ReportInterface $subject, SwitchingServiceRegistry $serviceRegistry): string
{
- if ($subject instanceof Entry) {
- return $urlGenerator->generate('entry_single', ['magazine_name' => $subject->magazine->name, 'entry_id' => $subject->getId(), 'slug' => $subject->slug]);
- } elseif ($subject instanceof EntryComment) {
- return $urlGenerator->generate('entry_comment_view', ['magazine_name' => $subject->magazine->name, 'entry_id' => $subject->entry->getId(), 'slug' => $subject->entry->slug, 'comment_id' => $subject->getId()]);
- } elseif ($subject instanceof Post) {
- return $urlGenerator->generate('post_single', ['magazine_name' => $subject->magazine->name, 'post_id' => $subject->getId(), 'slug' => $subject->slug]);
- } elseif ($subject instanceof PostComment) {
- return $urlGenerator->generate('post_single', ['magazine_name' => $subject->magazine->name, 'post_id' => $subject->post->getId(), 'slug' => $subject->post->slug]).'#post-comment-'.$subject->getId();
+ try {
+ return $serviceRegistry->getService($subject, ContentUrlFactory::class)->getLocalUrl($subject);
+ } catch (\Exception) {
+ return '';
}
-
- return '';
}
}
diff --git a/src/Entity/ReportCreatedNotification.php b/src/Entity/ReportCreatedNotification.php
index f805df3d56..b60bfa5f06 100644
--- a/src/Entity/ReportCreatedNotification.php
+++ b/src/Entity/ReportCreatedNotification.php
@@ -4,10 +4,15 @@
namespace App\Entity;
+use App\Entity\Contracts\ReportInterface;
+use App\Factory\Contract\ReportUrlFactory;
use App\Payloads\PushNotification;
+use App\Service\SwitchingServiceRegistry;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -30,13 +35,20 @@ public function getType(): string
return 'report_created_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
- /** @var Entry|EntryComment|Post|PostComment $subject */
+ /** @var SwitchingServiceRegistry $serviceRegistry */
+ $serviceRegistry = $serviceContainer->get(SwitchingServiceRegistry::class);
+
$subject = $this->report->getSubject();
- $reportLink = $urlGenerator->generate('magazine_panel_reports', ['name' => $this->report->magazine->name, 'status' => Report::STATUS_PENDING]).'#report-id-'.$this->report->getId();
- $message = \sprintf('%s %s %s\n%s: %s', $this->report->reporting->username, $trans->trans('reported', locale: $locale), $this->report->reported->username,
- $trans->trans('report_subject', locale: $locale), $subject->getShortTitle());
+ $reportLink = $serviceRegistry->getService($this->report, ReportUrlFactory::class)->getReportUrl($this->report, Report::STATUS_PENDING);
+ $message = \sprintf('%s %s %s\n%s: %s',
+ $this->report->reporting->username,
+ $trans->trans('reported', locale: $locale),
+ $this->report->reported->username,
+ $trans->trans('report_subject', locale: $locale),
+ $subject->getShortTitle()
+ );
return new PushNotification($this->getId(), $message, $trans->trans('notification_title_new_report'), actionUrl: $reportLink);
}
diff --git a/src/Entity/ReportRejectedNotification.php b/src/Entity/ReportRejectedNotification.php
index 23147b1aaf..3ad28e6e31 100644
--- a/src/Entity/ReportRejectedNotification.php
+++ b/src/Entity/ReportRejectedNotification.php
@@ -5,10 +5,14 @@
namespace App\Entity;
use App\Entity\Contracts\ReportInterface;
+use App\Factory\Contract\ContentUrlFactory;
use App\Payloads\PushNotification;
+use App\Service\SwitchingServiceRegistry;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -31,30 +35,31 @@ public function getType(): string
return 'report_rejected_notification';
}
- public function getMessage(TranslatorInterface $trans, string $locale, UrlGeneratorInterface $urlGenerator): PushNotification
+ public function getMessage(TranslatorInterface $trans, string $locale, ContainerInterface $serviceContainer): PushNotification
{
- /** @var Entry|EntryComment|Post|PostComment $subject */
+ /** @var SwitchingServiceRegistry $serviceRegistry */
+ $serviceRegistry = $serviceContainer->get(SwitchingServiceRegistry::class);
+
$subject = $this->report->getSubject();
$message = \sprintf('%s: %s\n%s: %s',
$trans->trans('reported_user', locale: $locale), $this->report->reported->username,
$trans->trans('report_subject', locale: $locale), $subject->getShortTitle()
);
- return new PushNotification($this->getId(), $message, $trans->trans('own_report_rejected', locale: $locale), actionUrl: $this->getSubjectLink($subject, $urlGenerator));
+ return new PushNotification(
+ $this->getId(),
+ $message,
+ $trans->trans('own_report_rejected', locale: $locale),
+ actionUrl: $this->getSubjectLink($subject, $serviceRegistry)
+ );
}
- private function getSubjectLink(ReportInterface $subject, UrlGeneratorInterface $urlGenerator): string
+ private function getSubjectLink(ReportInterface $subject, SwitchingServiceRegistry $serviceRegistry): string
{
- if ($subject instanceof Entry) {
- return $urlGenerator->generate('entry_single', ['magazine_name' => $subject->magazine->name, 'entry_id' => $subject->getId(), 'slug' => $subject->slug]);
- } elseif ($subject instanceof EntryComment) {
- return $urlGenerator->generate('entry_comment_view', ['magazine_name' => $subject->magazine->name, 'entry_id' => $subject->entry->getId(), 'slug' => $subject->entry->slug, 'comment_id' => $subject->getId()]);
- } elseif ($subject instanceof Post) {
- return $urlGenerator->generate('post_single', ['magazine_name' => $subject->magazine->name, 'post_id' => $subject->getId(), 'slug' => $subject->slug]);
- } elseif ($subject instanceof PostComment) {
- return $urlGenerator->generate('post_single', ['magazine_name' => $subject->magazine->name, 'post_id' => $subject->post->getId(), 'slug' => $subject->post->slug]).'#post-comment-'.$subject->getId();
+ try {
+ return $serviceRegistry->getService($subject, ContentUrlFactory::class)->getLocalUrl($subject);
+ } catch (\Exception) {
+ return '';
}
-
- return '';
}
}
diff --git a/src/EventListener/ContentNotificationPurgeListener.php b/src/EventListener/ContentNotificationPurgeListener.php
index 7533a46579..626033fb11 100644
--- a/src/EventListener/ContentNotificationPurgeListener.php
+++ b/src/EventListener/ContentNotificationPurgeListener.php
@@ -4,47 +4,36 @@
namespace App\EventListener;
+use App\Entity\Contracts\ContentInterface;
use App\Entity\Entry;
use App\Entity\EntryComment;
use App\Entity\Post;
use App\Entity\PostComment;
+use App\Entity\Report;
+use App\Service\Contracts\ContentNotificationManagerInterface;
use App\Service\Notification\EntryCommentNotificationManager;
use App\Service\Notification\EntryNotificationManager;
use App\Service\Notification\PostCommentNotificationManager;
use App\Service\Notification\PostNotificationManager;
+use App\Service\SwitchingServiceRegistry;
use Doctrine\Persistence\Event\LifecycleEventArgs;
readonly class ContentNotificationPurgeListener
{
public function __construct(
- private EntryNotificationManager $entryManager,
- private EntryCommentNotificationManager $entryCommentManager,
- private PostNotificationManager $postManager,
- private PostCommentNotificationManager $postCommentManager,
+ private SwitchingServiceRegistry $serviceRegistry,
) {
}
public function preRemove(LifecycleEventArgs $args): void
{
$object = $args->getObject();
-
- switch ($object) {
- case $object instanceof Entry:
- $this->entryManager->purgeNotifications($object);
- $this->entryManager->purgeMagazineLog($object);
- break;
- case $object instanceof EntryComment:
- $this->entryCommentManager->purgeNotifications($object);
- $this->entryCommentManager->purgeMagazineLog($object);
- break;
- case $object instanceof Post:
- $this->postManager->purgeNotifications($object);
- $this->postManager->purgeMagazineLog($object);
- break;
- case $object instanceof PostComment:
- $this->postCommentManager->purgeNotifications($object);
- $this->postCommentManager->purgeMagazineLog($object);
- break;
+ if (!($object instanceof ContentInterface)) {
+ return;
}
+
+ $manager = $this->serviceRegistry->getService($object, ContentNotificationManagerInterface::class);
+ $manager->purgeNotifications($object);
+ $manager->purgeMagazineLog($object);
}
}
diff --git a/src/EventSubscriber/SubjectReportedSubscriber.php b/src/EventSubscriber/SubjectReportedSubscriber.php
index bf53080196..b8aebbd5cf 100644
--- a/src/EventSubscriber/SubjectReportedSubscriber.php
+++ b/src/EventSubscriber/SubjectReportedSubscriber.php
@@ -4,9 +4,11 @@
namespace App\EventSubscriber;
+use App\Entity\Message;
use App\Event\Report\SubjectReportedEvent;
use App\Message\ActivityPub\Outbox\FlagMessage;
use App\Service\Notification\ReportNotificationManager;
+use App\Service\SettingsManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -17,6 +19,7 @@ public function __construct(
private readonly MessageBusInterface $bus,
private readonly LoggerInterface $logger,
private readonly ReportNotificationManager $notificationManager,
+ private readonly SettingsManager $settingsManager,
) {
}
@@ -24,16 +27,29 @@ public function onSubjectReported(SubjectReportedEvent $reportedEvent): void
{
$this->logger->debug($reportedEvent->report->reported->username.' was reported for '.$reportedEvent->report->reason);
$this->notificationManager->sendReportCreatedNotification($reportedEvent->report);
- if (!$reportedEvent->report->magazine->apId and 'random' !== $reportedEvent->report->magazine->name) {
- return;
- }
- if ($reportedEvent->report->magazine->apId) {
+ $sendFlag = false;
+ if($reportedEvent->report->magazine === null) {
+ // is a message -> check if remote
+ $message = $reportedEvent->report->getSubject();
+ if($message instanceof Message) {
+ if($message->getApId() !== null && !$this->settingsManager->isLocalUrl($message->getApId())) {
+ $this->logger->debug('was a message from a remote instance, dispatching a new FlagMessage');
+ $sendFlag = true;
+ }
+ } else {
+ $this->logger->error('got a report with magazine === null but it was not a MessageReport');
+ }
+ } elseif ($reportedEvent->report->magazine->apId) {
$this->logger->debug('was on a remote magazine, dispatching a new FlagMessage');
+ $sendFlag = true;
} elseif ('random' === $reportedEvent->report->magazine->name) {
$this->logger->debug('was on the random magazine, dispatching a new FlagMessage');
+ $sendFlag = true;
+ }
+ if ($sendFlag) {
+ $this->bus->dispatch(new FlagMessage($reportedEvent->report->getId()));
}
- $this->bus->dispatch(new FlagMessage($reportedEvent->report->getId()));
}
public static function getSubscribedEvents(): array
diff --git a/src/Factory/ActivityPub/ActivityFactory.php b/src/Factory/ActivityPub/ActivityFactory.php
index 764e3c870a..2a38ef6234 100644
--- a/src/Factory/ActivityPub/ActivityFactory.php
+++ b/src/Factory/ActivityPub/ActivityFactory.php
@@ -5,34 +5,28 @@
namespace App\Factory\ActivityPub;
use App\Entity\Contracts\ActivityPubActivityInterface;
+use App\Entity\Contracts\HashtagableInterface;
use App\Entity\Entry;
use App\Entity\EntryComment;
use App\Entity\Message;
use App\Entity\Post;
use App\Entity\PostComment;
+use App\Factory\Contract\ActivityFactoryInterface;
use App\Repository\TagLinkRepository;
+use App\Service\SwitchingServiceRegistry;
class ActivityFactory
{
public function __construct(
private readonly TagLinkRepository $tagLinkRepository,
- private readonly EntryPageFactory $pageFactory,
- private readonly EntryCommentNoteFactory $entryNoteFactory,
- private readonly PostNoteFactory $postNoteFactory,
- private readonly PostCommentNoteFactory $postCommentNoteFactory,
- private readonly MessageFactory $messageFactory,
+ private readonly SwitchingServiceRegistry $serviceRegistry,
) {
}
public function create(ActivityPubActivityInterface $activity, bool $context = false): array
{
- return match (true) {
- $activity instanceof Entry => $this->pageFactory->create($activity, $this->tagLinkRepository->getTagsOfContent($activity), $context),
- $activity instanceof EntryComment => $this->entryNoteFactory->create($activity, $this->tagLinkRepository->getTagsOfContent($activity), $context),
- $activity instanceof Post => $this->postNoteFactory->create($activity, $this->tagLinkRepository->getTagsOfContent($activity), $context),
- $activity instanceof PostComment => $this->postCommentNoteFactory->create($activity, $this->tagLinkRepository->getTagsOfContent($activity), $context),
- $activity instanceof Message => $this->messageFactory->build($activity, $context),
- default => throw new \LogicException('Cannot handle activity of type '.\get_class($activity)),
- };
+ $hashtags = $activity instanceof HashtagableInterface ? $this->tagLinkRepository->getTagsOfContent($activity) : [];
+ $factory = $this->serviceRegistry->getService($activity, ActivityFactoryInterface::class);
+ return $factory->create($activity, $hashtags, $context);
}
}
diff --git a/src/Factory/ActivityPub/EntryCommentNoteFactory.php b/src/Factory/ActivityPub/EntryCommentNoteFactory.php
index a6174024db..70d7d48e73 100644
--- a/src/Factory/ActivityPub/EntryCommentNoteFactory.php
+++ b/src/Factory/ActivityPub/EntryCommentNoteFactory.php
@@ -6,6 +6,8 @@
use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\EntryComment;
+use App\Factory\Contract\ActivityFactoryInterface;
+use App\Factory\Entry\EntryCommentUrlFactory;
use App\Markdown\MarkdownConverter;
use App\Markdown\RenderTarget;
use App\Service\ActivityPub\ApHttpClientInterface;
@@ -14,10 +16,15 @@
use App\Service\ActivityPub\Wrapper\MentionsWrapper;
use App\Service\ActivityPub\Wrapper\TagsWrapper;
use App\Service\ActivityPubManager;
+use App\Service\Contracts\SwitchableService;
use App\Service\MentionManager;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-class EntryCommentNoteFactory
+/**
+ * @implements SwitchableService
+ * @implements ActivityPubActivityInterface
+ */
+class EntryCommentNoteFactory implements SwitchableService, ActivityFactoryInterface
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
@@ -26,67 +33,76 @@ public function __construct(
private readonly ImageWrapper $imageWrapper,
private readonly TagsWrapper $tagsWrapper,
private readonly MentionsWrapper $mentionsWrapper,
- private readonly MentionManager $mentionManager,
private readonly EntryPageFactory $pageFactory,
private readonly ApHttpClientInterface $client,
private readonly ActivityPubManager $activityPubManager,
private readonly MarkdownConverter $markdownConverter,
+ private readonly EntryCommentUrlFactory $entryCommentUrlFactory,
) {
}
- public function create(EntryComment $comment, array $tags, bool $context = false): array
+ public function getSupportedTypes(): array
+ {
+ return [EntryComment::class];
+ }
+
+ /**
+ * @inheritDoc
+ * @param EntryComment $subject
+ */
+ public function create($subject, array $tags, bool $context = false): array
{
if ($context) {
$note['@context'] = $this->contextProvider->referencedContexts();
}
- if ('random' !== $comment->magazine->name && !$comment->magazine->apId) { // @todo
- $tags[] = $comment->magazine->name;
+ if ('random' !== $subject->magazine->name && !$subject->magazine->apId) { // @todo
+ $tags[] = $subject->magazine->name;
}
- $cc = [$this->groupFactory->getActivityPubId($comment->magazine)];
- if ($followersUrl = $comment->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $comment->apId)) {
+ $cc = [$this->groupFactory->getActivityPubId($subject->magazine)];
+ if ($followersUrl = $subject->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $subject->apId)) {
$cc[] = $followersUrl;
}
$note = array_merge($note ?? [], [
- 'id' => $this->getActivityPubId($comment),
+ 'id' => $this->getActivityPubId($subject),
'type' => 'Note',
- 'attributedTo' => $this->activityPubManager->getActorProfileId($comment->user),
- 'inReplyTo' => $this->getReplyTo($comment),
+ 'attributedTo' => $this->activityPubManager->getActorProfileId($subject->user),
+ 'inReplyTo' => $this->getReplyTo($subject),
'to' => [
ActivityPubActivityInterface::PUBLIC_URL,
],
'cc' => $cc,
- 'audience' => $this->groupFactory->getActivityPubId($comment->magazine),
- 'sensitive' => $comment->entry->isAdult(),
+ 'audience' => $this->groupFactory->getActivityPubId($subject->magazine),
+ 'sensitive' => $subject->entry->isAdult(),
'content' => $this->markdownConverter->convertToHtml(
- $comment->body,
+ $subject->body,
context: [MarkdownConverter::RENDER_TARGET => RenderTarget::ActivityPub]
),
'mediaType' => 'text/html',
- 'source' => $comment->body ? [
- 'content' => $comment->body,
+ 'source' => $subject->body ? [
+ 'content' => $subject->body,
'mediaType' => 'text/markdown',
] : null,
- 'url' => $this->getActivityPubId($comment),
+ 'url' => $this->getActivityPubId($subject),
'tag' => array_merge(
$this->tagsWrapper->build($tags),
- $this->mentionsWrapper->build($comment->mentions ?? [], $comment->body)
+ $this->mentionsWrapper->build($subject->mentions ?? [], $subject->body)
),
- 'published' => $comment->createdAt->format(DATE_ATOM),
+ 'published' => $subject->createdAt->format(DATE_ATOM),
]);
$note['contentMap'] = [
- $comment->lang => $note['content'],
+ $subject->lang => $note['content'],
];
- if ($comment->image) {
- $note = $this->imageWrapper->build($note, $comment->image, $comment->getShortTitle());
+ if ($subject->image) {
+ $note = $this->imageWrapper->build($note, $subject->image, $subject->getShortTitle());
}
$mentions = [];
- foreach ($comment->mentions ?? [] as $mention) {
+ foreach ($subject->mentions ?? [] as $mention) {
try {
$profileId = $this->activityPubManager->findActorOrCreate($mention)?->apProfileId;
if ($profileId) {
@@ -102,8 +118,8 @@ public function create(EntryComment $comment, array $tags, bool $context = false
array_merge(
$note['to'],
$mentions,
- $this->activityPubManager->createCcFromBody($comment->body),
- [$this->getReplyToAuthor($comment)],
+ $this->activityPubManager->createCcFromBody($subject->body),
+ [$this->getReplyToAuthor($subject)],
)
)
);
@@ -113,19 +129,7 @@ public function create(EntryComment $comment, array $tags, bool $context = false
public function getActivityPubId(EntryComment $comment): string
{
- if ($comment->apId) {
- return $comment->apId;
- }
-
- return $this->urlGenerator->generate(
- 'ap_entry_comment',
- [
- 'magazine_name' => $comment->magazine->name,
- 'entry_id' => $comment->entry->getId(),
- 'comment_id' => $comment->getId(),
- ],
- UrlGeneratorInterface::ABSOLUTE_URL
- );
+ return $this->entryCommentUrlFactory->getActivityPubId($comment);
}
private function getReplyTo(EntryComment $comment): string
diff --git a/src/Factory/ActivityPub/EntryPageFactory.php b/src/Factory/ActivityPub/EntryPageFactory.php
index ab1153e6b1..11a23ab50e 100644
--- a/src/Factory/ActivityPub/EntryPageFactory.php
+++ b/src/Factory/ActivityPub/EntryPageFactory.php
@@ -6,6 +6,8 @@
use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Entry;
+use App\Factory\Contract\ActivityFactoryInterface;
+use App\Factory\Entry\EntryUrlFactory;
use App\Markdown\MarkdownConverter;
use App\Markdown\RenderTarget;
use App\Service\ActivityPub\ApHttpClientInterface;
@@ -14,9 +16,14 @@
use App\Service\ActivityPub\Wrapper\MentionsWrapper;
use App\Service\ActivityPub\Wrapper\TagsWrapper;
use App\Service\ActivityPubManager;
+use App\Service\Contracts\SwitchableService;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-class EntryPageFactory
+/**
+ * @implements SwitchableService
+ * @implements ActivityPubActivityInterface
+ */
+class EntryPageFactory implements SwitchableService, ActivityFactoryInterface
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
@@ -28,80 +35,90 @@ public function __construct(
private readonly ApHttpClientInterface $client,
private readonly ActivityPubManager $activityPubManager,
private readonly MarkdownConverter $markdownConverter,
+ private readonly EntryUrlFactory $entryUrlFactory,
) {
}
- public function create(Entry $entry, array $tags, bool $context = false): array
+ public function getSupportedTypes(): array
+ {
+ return [Entry::class];
+ }
+
+ /**
+ * @inheritDoc
+ * @param Entry $subject
+ */
+ public function create($subject, array $tags, bool $context = false): array
{
if ($context) {
$page['@context'] = $this->contextProvider->referencedContexts();
}
- if ('random' !== $entry->magazine->name && !$entry->magazine->apId) { // @todo
- $tags[] = $entry->magazine->name;
+ if ('random' !== $subject->magazine->name && !$subject->magazine->apId) { // @todo
+ $tags[] = $subject->magazine->name;
}
$cc = [];
- if ($followersUrl = $entry->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $entry->apId)) {
+ if ($followersUrl = $subject->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $subject->apId)) {
$cc[] = $followersUrl;
}
$page = array_merge($page ?? [], [
- 'id' => $this->getActivityPubId($entry),
+ 'id' => $this->getActivityPubId($subject),
'type' => 'Page',
- 'attributedTo' => $this->activityPubManager->getActorProfileId($entry->user),
+ 'attributedTo' => $this->activityPubManager->getActorProfileId($subject->user),
'inReplyTo' => null,
'to' => [
- $this->groupFactory->getActivityPubId($entry->magazine),
+ $this->groupFactory->getActivityPubId($subject->magazine),
ActivityPubActivityInterface::PUBLIC_URL,
],
'cc' => $cc,
- 'name' => $entry->title,
- 'audience' => $this->groupFactory->getActivityPubId($entry->magazine),
- 'content' => $entry->body ? $this->markdownConverter->convertToHtml(
- $entry->body,
+ 'name' => $subject->title,
+ 'audience' => $this->groupFactory->getActivityPubId($subject->magazine),
+ 'content' => $subject->body ? $this->markdownConverter->convertToHtml(
+ $subject->body,
context: [MarkdownConverter::RENDER_TARGET => RenderTarget::ActivityPub]
) : null,
- 'summary' => $entry->getShortTitle().' '.implode(
+ 'summary' => $subject->getShortTitle().' '.implode(
' ',
array_map(fn ($val) => '#'.$val, $tags)
),
'mediaType' => 'text/html',
- 'source' => $entry->body ? [
- 'content' => $entry->body,
+ 'source' => $subject->body ? [
+ 'content' => $subject->body,
'mediaType' => 'text/markdown',
] : null,
'tag' => array_merge(
$this->tagsWrapper->build($tags),
- $this->mentionsWrapper->build($entry->mentions ?? [], $entry->body)
+ $this->mentionsWrapper->build($subject->mentions ?? [], $subject->body)
),
- 'commentsEnabled' => !$entry->isLocked,
- 'sensitive' => $entry->isAdult(),
- 'stickied' => $entry->sticky,
- 'published' => $entry->createdAt->format(DATE_ATOM),
+ 'commentsEnabled' => !$subject->isLocked,
+ 'sensitive' => $subject->isAdult(),
+ 'stickied' => $subject->sticky,
+ 'published' => $subject->createdAt->format(DATE_ATOM),
]);
$page['contentMap'] = [
- $entry->lang => $page['content'],
+ $subject->lang => $page['content'],
];
- if ($entry->url) {
- $page['source'] = $entry->url;
+ if ($subject->url) {
+ $page['source'] = $subject->url;
$page['attachment'][] = [
- 'href' => $entry->url,
+ 'href' => $subject->url,
'type' => 'Link',
];
}
- if ($entry->image) {
+ if ($subject->image) {
// We do not know whether the image comes from an embed.
// Even if $entry->hasEmbed is true that does not mean that the image is from the embed
- $page = $this->imageWrapper->build($page, $entry->image, $entry->title);
+ $page = $this->imageWrapper->build($page, $subject->image, $subject->title);
}
- if ($entry->body) {
+ if ($subject->body) {
$page['to'] = array_unique(
- array_merge($page['to'], $this->activityPubManager->createCcFromBody($entry->body))
+ array_merge($page['to'], $this->activityPubManager->createCcFromBody($subject->body))
);
}
@@ -110,14 +127,6 @@ public function create(Entry $entry, array $tags, bool $context = false): array
public function getActivityPubId(Entry $entry): string
{
- if ($entry->apId) {
- return $entry->apId;
- }
-
- return $this->urlGenerator->generate(
- 'ap_entry',
- ['magazine_name' => $entry->magazine->name, 'entry_id' => $entry->getId()],
- UrlGeneratorInterface::ABSOLUTE_URL
- );
+ return $this->entryUrlFactory->getActivityPubId($entry);
}
}
diff --git a/src/Factory/ActivityPub/MessageFactory.php b/src/Factory/ActivityPub/MessageFactory.php
index 34a63b99ae..cd6f5507f9 100644
--- a/src/Factory/ActivityPub/MessageFactory.php
+++ b/src/Factory/ActivityPub/MessageFactory.php
@@ -4,14 +4,21 @@
namespace App\Factory\ActivityPub;
+use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Message;
use App\Entity\User;
+use App\Factory\Contract\ActivityFactoryInterface;
use App\Markdown\MarkdownConverter;
use App\Markdown\RenderTarget;
use App\Service\ActivityPub\ContextsProvider;
+use App\Service\Contracts\SwitchableService;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-class MessageFactory
+/**
+ * @implements SwitchableService
+ * @implements ActivityPubActivityInterface
+ */
+class MessageFactory implements SwitchableService, ActivityFactoryInterface
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
@@ -20,6 +27,16 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [Message::class];
+ }
+
+ public function create($subject, array $tags, bool $context = false): array
+ {
+ return $this->build($subject, $context);
+ }
+
public function build(Message $message, bool $includeContext = true): array
{
$actorUrl = null === $message->sender->apId ? $this->urlGenerator->generate('ap_user', ['username' => $message->sender->username], UrlGeneratorInterface::ABSOLUTE_URL) : $message->sender->apPublicUrl;
diff --git a/src/Factory/ActivityPub/PostCommentNoteFactory.php b/src/Factory/ActivityPub/PostCommentNoteFactory.php
index 96ffabe4f6..2ee3bd95a6 100644
--- a/src/Factory/ActivityPub/PostCommentNoteFactory.php
+++ b/src/Factory/ActivityPub/PostCommentNoteFactory.php
@@ -6,6 +6,8 @@
use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\PostComment;
+use App\Factory\Contract\ActivityFactoryInterface;
+use App\Factory\Post\PostCommentUrlFactory;
use App\Markdown\MarkdownConverter;
use App\Markdown\RenderTarget;
use App\Service\ActivityPub\ApHttpClientInterface;
@@ -14,10 +16,15 @@
use App\Service\ActivityPub\Wrapper\MentionsWrapper;
use App\Service\ActivityPub\Wrapper\TagsWrapper;
use App\Service\ActivityPubManager;
+use App\Service\Contracts\SwitchableService;
use App\Service\MentionManager;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-class PostCommentNoteFactory
+/**
+ * @implements SwitchableService
+ * @implements ActivityPubActivityInterface
+ */
+class PostCommentNoteFactory implements SwitchableService, ActivityFactoryInterface
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
@@ -27,66 +34,75 @@ public function __construct(
private readonly GroupFactory $groupFactory,
private readonly TagsWrapper $tagsWrapper,
private readonly MentionsWrapper $mentionsWrapper,
- private readonly MentionManager $mentionManager,
private readonly ApHttpClientInterface $client,
private readonly ActivityPubManager $activityPubManager,
private readonly MarkdownConverter $markdownConverter,
+ private readonly PostCommentUrlFactory $postCommentUrlFactory,
) {
}
- public function create(PostComment $comment, array $tags, bool $context = false): array
+ public function getSupportedTypes(): array
+ {
+ return [PostComment::class];
+ }
+
+ /**
+ * @inheritDoc
+ * @param PostComment $subject
+ */
+ public function create($subject, array $tags, bool $context = false): array
{
if ($context) {
$note['@context'] = $this->contextProvider->referencedContexts();
}
- if ('random' !== $comment->magazine->name && !$comment->magazine->apId) { // @todo
- $tags[] = $comment->magazine->name;
+ if ('random' !== $subject->magazine->name && !$subject->magazine->apId) { // @todo
+ $tags[] = $subject->magazine->name;
}
- $cc = [$this->groupFactory->getActivityPubId($comment->magazine)];
- if ($followersUrl = $comment->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $comment->apId)) {
+ $cc = [$this->groupFactory->getActivityPubId($subject->magazine)];
+ if ($followersUrl = $subject->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $subject->apId)) {
$cc[] = $followersUrl;
}
$note = array_merge($note ?? [], [
- 'id' => $this->getActivityPubId($comment),
+ 'id' => $this->getActivityPubId($subject),
'type' => 'Note',
- 'attributedTo' => $this->activityPubManager->getActorProfileId($comment->user),
- 'inReplyTo' => $this->getReplyTo($comment),
+ 'attributedTo' => $this->activityPubManager->getActorProfileId($subject->user),
+ 'inReplyTo' => $this->getReplyTo($subject),
'to' => [
ActivityPubActivityInterface::PUBLIC_URL,
],
'cc' => $cc,
- 'audience' => $this->groupFactory->getActivityPubId($comment->magazine),
- 'sensitive' => $comment->post->isAdult(),
+ 'audience' => $this->groupFactory->getActivityPubId($subject->magazine),
+ 'sensitive' => $subject->post->isAdult(),
'content' => $this->markdownConverter->convertToHtml(
- $comment->body,
+ $subject->body,
context: [MarkdownConverter::RENDER_TARGET => RenderTarget::ActivityPub],
),
'mediaType' => 'text/html',
- 'source' => $comment->body ? [
- 'content' => $comment->body,
+ 'source' => $subject->body ? [
+ 'content' => $subject->body,
'mediaType' => 'text/markdown',
] : null,
- 'url' => $this->getActivityPubId($comment),
+ 'url' => $this->getActivityPubId($subject),
'tag' => array_merge(
$this->tagsWrapper->build($tags),
- $this->mentionsWrapper->build($comment->mentions ?? [], $comment->body)
+ $this->mentionsWrapper->build($subject->mentions ?? [], $subject->body)
),
- 'published' => $comment->createdAt->format(DATE_ATOM),
+ 'published' => $subject->createdAt->format(DATE_ATOM),
]);
$note['contentMap'] = [
- $comment->lang => $note['content'],
+ $subject->lang => $note['content'],
];
- if ($comment->image) {
- $note = $this->imageWrapper->build($note, $comment->image, $comment->getShortTitle());
+ if ($subject->image) {
+ $note = $this->imageWrapper->build($note, $subject->image, $subject->getShortTitle());
}
$mentions = [];
- foreach ($comment->mentions ?? [] as $mention) {
+ foreach ($subject->mentions ?? [] as $mention) {
try {
$profileId = $this->activityPubManager->findActorOrCreate($mention)?->apProfileId;
if ($profileId) {
@@ -102,8 +118,8 @@ public function create(PostComment $comment, array $tags, bool $context = false)
array_merge(
$note['to'],
$mentions,
- $this->activityPubManager->createCcFromBody($comment->body),
- [$this->getReplyToAuthor($comment)],
+ $this->activityPubManager->createCcFromBody($subject->body),
+ [$this->getReplyToAuthor($subject)],
)
)
);
@@ -113,19 +129,7 @@ public function create(PostComment $comment, array $tags, bool $context = false)
public function getActivityPubId(PostComment $comment): string
{
- if ($comment->apId) {
- return $comment->apId;
- }
-
- return $this->urlGenerator->generate(
- 'ap_post_comment',
- [
- 'magazine_name' => $comment->magazine->name,
- 'post_id' => $comment->post->getId(),
- 'comment_id' => $comment->getId(),
- ],
- UrlGeneratorInterface::ABSOLUTE_URL
- );
+ return $this->postCommentUrlFactory->getActivityPubId($comment);
}
private function getReplyTo(PostComment $comment): string
diff --git a/src/Factory/ActivityPub/PostNoteFactory.php b/src/Factory/ActivityPub/PostNoteFactory.php
index c71abf1c6c..79241d268f 100644
--- a/src/Factory/ActivityPub/PostNoteFactory.php
+++ b/src/Factory/ActivityPub/PostNoteFactory.php
@@ -6,6 +6,9 @@
use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Post;
+use App\Entity\PostComment;
+use App\Factory\Contract\ActivityFactoryInterface;
+use App\Factory\Post\PostUrlFactory;
use App\Markdown\MarkdownConverter;
use App\Markdown\RenderTarget;
use App\Service\ActivityPub\ApHttpClientInterface;
@@ -14,11 +17,16 @@
use App\Service\ActivityPub\Wrapper\MentionsWrapper;
use App\Service\ActivityPub\Wrapper\TagsWrapper;
use App\Service\ActivityPubManager;
+use App\Service\Contracts\SwitchableService;
use App\Service\MentionManager;
use App\Service\TagExtractor;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-class PostNoteFactory
+/**
+ * @implements SwitchableService
+ * @implements ActivityPubActivityInterface
+ */
+class PostNoteFactory implements SwitchableService, ActivityFactoryInterface
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
@@ -29,86 +37,87 @@ public function __construct(
private readonly MentionsWrapper $mentionsWrapper,
private readonly ApHttpClientInterface $client,
private readonly ActivityPubManager $activityPubManager,
- private readonly MentionManager $mentionManager,
private readonly TagExtractor $tagExtractor,
private readonly MarkdownConverter $markdownConverter,
+ private readonly PostUrlFactory $urlFactory,
) {
}
- public function create(Post $post, array $tags, bool $context = false): array
+ public function getSupportedTypes(): array
+ {
+ return [Post::class];
+ }
+
+ /**
+ * @inheritDoc
+ * @param Post $subject
+ */
+ public function create($subject, array $tags, bool $context = false): array
{
if ($context) {
$note['@context'] = $this->contextProvider->referencedContexts();
}
- if ('random' !== $post->magazine->name && !$post->magazine->apId) { // @todo
- $tags[] = $post->magazine->name;
+ if ('random' !== $subject->magazine->name && !$subject->magazine->apId) { // @todo
+ $tags[] = $subject->magazine->name;
}
$body = $this->tagExtractor->joinTagsToBody(
- $post->body,
+ $subject->body,
$tags
);
$cc = [];
- if ($followersUrl = $post->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $post->apId)) {
+ if ($followersUrl = $subject->user->getFollowerUrl($this->client, $this->urlGenerator, null !== $subject->apId)) {
$cc[] = $followersUrl;
}
$note = array_merge($note ?? [], [
- 'id' => $this->getActivityPubId($post),
+ 'id' => $this->getActivityPubId($subject),
'type' => 'Note',
- 'attributedTo' => $this->activityPubManager->getActorProfileId($post->user),
+ 'attributedTo' => $this->activityPubManager->getActorProfileId($subject->user),
'inReplyTo' => null,
'to' => [
- $this->groupFactory->getActivityPubId($post->magazine),
+ $this->groupFactory->getActivityPubId($subject->magazine),
ActivityPubActivityInterface::PUBLIC_URL,
],
'cc' => $cc,
- 'audience' => $this->groupFactory->getActivityPubId($post->magazine),
- 'sensitive' => $post->isAdult(),
- 'stickied' => $post->sticky,
+ 'audience' => $this->groupFactory->getActivityPubId($subject->magazine),
+ 'sensitive' => $subject->isAdult(),
+ 'stickied' => $subject->sticky,
'content' => $this->markdownConverter->convertToHtml(
$body,
context: [MarkdownConverter::RENDER_TARGET => RenderTarget::ActivityPub],
),
'mediaType' => 'text/html',
- 'source' => $post->body ? [
+ 'source' => $subject->body ? [
'content' => $body,
'mediaType' => 'text/markdown',
] : null,
- 'url' => $this->getActivityPubId($post),
+ 'url' => $this->getActivityPubId($subject),
'tag' => array_merge(
$this->tagsWrapper->build($tags),
- $this->mentionsWrapper->build($post->mentions ?? [], $post->body)
+ $this->mentionsWrapper->build($subject->mentions ?? [], $subject->body)
),
- 'commentsEnabled' => !$post->isLocked,
- 'published' => $post->createdAt->format(DATE_ATOM),
+ 'commentsEnabled' => !$subject->isLocked,
+ 'published' => $subject->createdAt->format(DATE_ATOM),
]);
$note['contentMap'] = [
- $post->lang => $note['content'],
+ $subject->lang => $note['content'],
];
- if ($post->image) {
- $note = $this->imageWrapper->build($note, $post->image, $post->getShortTitle());
+ if ($subject->image) {
+ $note = $this->imageWrapper->build($note, $subject->image, $subject->getShortTitle());
}
- $note['to'] = array_unique(array_merge($note['to'], $this->activityPubManager->createCcFromBody($post->body)));
+ $note['to'] = array_unique(array_merge($note['to'], $this->activityPubManager->createCcFromBody($subject->body)));
return $note;
}
public function getActivityPubId(Post $post): string
{
- if ($post->apId) {
- return $post->apId;
- }
-
- return $this->urlGenerator->generate(
- 'ap_post',
- ['magazine_name' => $post->magazine->name, 'post_id' => $post->getId()],
- UrlGeneratorInterface::ABSOLUTE_URL
- );
+ return $this->urlFactory->getActivityPubId($post);
}
}
diff --git a/src/Factory/ContentManagerFactory.php b/src/Factory/ContentManagerFactory.php
deleted file mode 100644
index 0165cf3fcf..0000000000
--- a/src/Factory/ContentManagerFactory.php
+++ /dev/null
@@ -1,41 +0,0 @@
-entryManager;
- } elseif ($subject instanceof EntryComment) {
- return $this->entryCommentManager;
- } elseif ($subject instanceof Post) {
- return $this->postManager;
- } elseif ($subject instanceof PostComment) {
- return $this->postCommentManager;
- }
- throw new \LogicException("Unsupported subject type: '".\get_class($subject)."'");
- }
-}
diff --git a/src/Factory/Contract/ActivityFactoryInterface.php b/src/Factory/Contract/ActivityFactoryInterface.php
new file mode 100644
index 0000000000..932a86ae16
--- /dev/null
+++ b/src/Factory/Contract/ActivityFactoryInterface.php
@@ -0,0 +1,19 @@
+
+ */
+readonly class EntryCommentDtoFactory implements SwitchableService, ContentDtoFactory
+{
+
+ public function __construct(
+ private EntryCommentFactory $commentFactory,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [EntryComment::class];
+ }
+
+ public function createResponseDto($subject, array $hashtags): EntryCommentResponseDto
+ {
+ return $this->commentFactory->createResponseDto($subject, $hashtags);
+ }
+}
diff --git a/src/Factory/Entry/EntryCommentUrlFactory.php b/src/Factory/Entry/EntryCommentUrlFactory.php
new file mode 100644
index 0000000000..f788015049
--- /dev/null
+++ b/src/Factory/Entry/EntryCommentUrlFactory.php
@@ -0,0 +1,48 @@
+
+ */
+readonly class EntryCommentUrlFactory implements SwitchableService, ContentUrlFactory
+{
+
+ public function __construct(
+ private UrlGeneratorInterface $urlGenerator,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [EntryComment::class];
+ }
+
+ function getActivityPubId($subject): string
+ {
+ if ($subject->apId) {
+ return $subject->apId;
+ }
+
+ return $this->urlGenerator->generate('ap_entry_comment', [
+ 'magazine_name' => $subject->magazine->name,
+ 'entry_id' => $subject->entry->getId(),
+ 'comment_id' => $subject->getId(),
+ ], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
+ function getLocalUrl($subject): string
+ {
+ return $this->urlGenerator->generate('entry_comment_view', [
+ 'magazine_name' => $subject->magazine->name,
+ 'entry_id' => $subject->entry->getId(),
+ 'slug' => $subject->entry->slug ?? '-',
+ 'comment_id' => $subject->getId(),
+ ], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+}
diff --git a/src/Factory/Entry/EntryDtoFactory.php b/src/Factory/Entry/EntryDtoFactory.php
new file mode 100644
index 0000000000..1933a8bc38
--- /dev/null
+++ b/src/Factory/Entry/EntryDtoFactory.php
@@ -0,0 +1,31 @@
+
+ */
+readonly class EntryDtoFactory implements SwitchableService, ContentDtoFactory
+{
+
+ public function __construct(
+ private EntryFactory $entryFactory,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [Entry::class];
+ }
+
+ public function createResponseDto($subject, array $hashtags): EntryResponseDto
+ {
+ return $this->entryFactory->createResponseDto($subject, $hashtags);
+ }
+}
diff --git a/src/Factory/Entry/EntryUrlFactory.php b/src/Factory/Entry/EntryUrlFactory.php
new file mode 100644
index 0000000000..ba32b5e5fb
--- /dev/null
+++ b/src/Factory/Entry/EntryUrlFactory.php
@@ -0,0 +1,47 @@
+
+ */
+readonly class EntryUrlFactory implements SwitchableService, ContentUrlFactory
+{
+
+ public function __construct(
+ private UrlGeneratorInterface $urlGenerator,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [Entry::class];
+ }
+
+ function getActivityPubId($subject): string
+ {
+ if ($subject->apId) {
+ return $subject->apId;
+ }
+
+ return $this->urlGenerator->generate(
+ 'ap_entry',
+ ['magazine_name' => $subject->magazine->name, 'entry_id' => $subject->getId()],
+ UrlGeneratorInterface::ABSOLUTE_URL
+ );
+ }
+
+ function getLocalUrl($subject): string
+ {
+ return $this->urlGenerator->generate('entry_single', [
+ 'magazine_name' => $subject->magazine->name,
+ 'entry_id' => $subject->getId(),
+ 'slug' => $subject->slug ?? '-'
+ ], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+}
diff --git a/src/Factory/InMagazineReportUrlFactory.php b/src/Factory/InMagazineReportUrlFactory.php
new file mode 100644
index 0000000000..99af700e2c
--- /dev/null
+++ b/src/Factory/InMagazineReportUrlFactory.php
@@ -0,0 +1,32 @@
+
+ */
+class InMagazineReportUrlFactory implements SwitchableService, ReportUrlFactory
+{
+
+ public function __construct(
+ private readonly UrlGeneratorInterface $urlGenerator,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [EntryReport::class, EntryCommentReport::class, PostReport::class, PostCommentReport::class];
+ }
+
+ public function getReportUrl($report, string $status): string {
+ return $this->urlGenerator->generate('magazine_panel_reports', ['name' => $report->magazine->name, 'status' => $status]).'#report-id-'.$report->getId();
+ }
+}
diff --git a/src/Factory/Magazine/MagazineUrlFactory.php b/src/Factory/Magazine/MagazineUrlFactory.php
new file mode 100644
index 0000000000..b45a95c5df
--- /dev/null
+++ b/src/Factory/Magazine/MagazineUrlFactory.php
@@ -0,0 +1,44 @@
+
+ */
+readonly class MagazineUrlFactory implements SwitchableService, ActorUrlFactory
+{
+
+ public function __construct(
+ private GroupFactory $groupFactory,
+ private UrlGeneratorInterface $urlGenerator,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [Magazine::class];
+ }
+
+ public function getActivityPubId($actor): string
+ {
+ return $this->groupFactory->getActivityPubId($actor);
+ }
+
+ public function getLocalUrl($actor): string
+ {
+ return $this->urlGenerator->generate('front_magazine', ['name' => $actor->name], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
+ public function getAvatarUrl($actor): ?string
+ {
+ $slash = $actor->icon && !str_starts_with('/', $actor->icon->filePath) ? '/' : '';
+ return $actor->icon ? '/media/cache/resolve/avatar_thumb'.$slash.$actor->icon->filePath : null;
+ }
+}
diff --git a/src/Factory/Message/MessageDtoFactory.php b/src/Factory/Message/MessageDtoFactory.php
new file mode 100644
index 0000000000..5e6ea3356a
--- /dev/null
+++ b/src/Factory/Message/MessageDtoFactory.php
@@ -0,0 +1,43 @@
+
+ */
+readonly class MessageDtoFactory implements SwitchableService, ContentDtoFactory
+{
+
+ public function __construct(
+ private MessageFactory $messageFactory,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [Message::class];
+ }
+
+ public function createResponseDto($subject, array $hashtags): MessageResponseDto
+ {
+ return $this->messageFactory->createResponseDto($subject);
+ }
+}
diff --git a/src/Factory/Message/MessageUrlFactory.php b/src/Factory/Message/MessageUrlFactory.php
new file mode 100644
index 0000000000..1d58c3bbde
--- /dev/null
+++ b/src/Factory/Message/MessageUrlFactory.php
@@ -0,0 +1,43 @@
+
+ * @implements ReportUrlFactory
+ */
+readonly class MessageUrlFactory implements SwitchableService, ContentUrlFactory, ReportUrlFactory
+{
+
+ public function __construct(
+ private UrlGeneratorInterface $urlGenerator,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [Message::class];
+ }
+
+ public function getActivityPubId($subject): string
+ {
+ return $this->urlGenerator->generate('ap_message', ['uuid' => $subject->uuid], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
+ public function getLocalUrl($subject): string
+ {
+ return $this->urlGenerator->generate('messages_single', ['id' => $subject->thread->getId()], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
+ public function getReportUrl($report, string $status): string
+ {
+ return $this->urlGenerator->generate('message_reports', ['status' => $status]).'#report-id-'.$report->getId();
+ }
+}
diff --git a/src/Factory/Post/PostCommentDtoFactory.php b/src/Factory/Post/PostCommentDtoFactory.php
new file mode 100644
index 0000000000..aa08300141
--- /dev/null
+++ b/src/Factory/Post/PostCommentDtoFactory.php
@@ -0,0 +1,37 @@
+
+ */
+readonly class PostCommentDtoFactory implements SwitchableService, ContentDtoFactory
+{
+
+ public function __construct(
+ private PostCommentFactory $commentFactory,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [PostComment::class];
+ }
+
+ public function createResponseDto($subject, array $hashtags): PostCommentResponseDto
+ {
+ return $this->commentFactory->createResponseDto($subject, $hashtags);
+ }
+}
diff --git a/src/Factory/Post/PostCommentUrlFactory.php b/src/Factory/Post/PostCommentUrlFactory.php
new file mode 100644
index 0000000000..e8a41ba3b6
--- /dev/null
+++ b/src/Factory/Post/PostCommentUrlFactory.php
@@ -0,0 +1,44 @@
+
+ */
+readonly class PostCommentUrlFactory implements SwitchableService, ContentUrlFactory
+{
+
+ public function __construct(
+ private UrlGeneratorInterface $urlGenerator,
+ private PostUrlFactory $postUrlFactory,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [PostComment::class];
+ }
+
+ function getActivityPubId($subject): string
+ {
+ if ($subject->apId) {
+ return $subject->apId;
+ }
+
+ return $this->urlGenerator->generate('ap_post_comment', [
+ 'magazine_name' => $subject->magazine->name,
+ 'post_id' => $subject->post->getId(),
+ 'comment_id' => $subject->getId(),
+ ], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
+ function getLocalUrl($subject): string
+ {
+ return $this->postUrlFactory->getLocalUrl($subject).'#post-comment-'.$subject->getId();
+ }
+}
diff --git a/src/Factory/Post/PostDtoFactory.php b/src/Factory/Post/PostDtoFactory.php
new file mode 100644
index 0000000000..aef1ce2fd5
--- /dev/null
+++ b/src/Factory/Post/PostDtoFactory.php
@@ -0,0 +1,40 @@
+
+ */
+readonly class PostDtoFactory implements SwitchableService, ContentDtoFactory
+{
+
+ public function __construct(
+ private PostFactory $postFactory,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [Post::class];
+ }
+
+ public function createResponseDto($subject, array $hashtags): PostResponseDto
+ {
+ return $this->postFactory->createResponseDto($subject, $hashtags);
+ }
+}
diff --git a/src/Factory/Post/PostUrlFactory.php b/src/Factory/Post/PostUrlFactory.php
new file mode 100644
index 0000000000..9c041f825a
--- /dev/null
+++ b/src/Factory/Post/PostUrlFactory.php
@@ -0,0 +1,48 @@
+
+ */
+readonly class PostUrlFactory implements SwitchableService, ContentUrlFactory
+{
+
+ public function __construct(
+ private UrlGeneratorInterface $urlGenerator,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [Post::class];
+ }
+
+ function getActivityPubId($subject): string
+ {
+ if ($subject->apId) {
+ return $subject->apId;
+ }
+
+ return $this->urlGenerator->generate(
+ 'ap_post',
+ ['magazine_name' => $subject->magazine->name, 'post_id' => $subject->getId()],
+ UrlGeneratorInterface::ABSOLUTE_URL
+ );
+ }
+
+ function getLocalUrl($subject): string
+ {
+ return $this->urlGenerator->generate('post_single', [
+ 'magazine_name' => $subject->magazine->name,
+ 'post_id' => $subject->getId(),
+ 'slug' => empty($subject->slug) ? '-' : $subject->slug,
+ ], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+}
diff --git a/src/Factory/ReportFactory.php b/src/Factory/ReportFactory.php
index 24d96f25f4..f2d6289283 100644
--- a/src/Factory/ReportFactory.php
+++ b/src/Factory/ReportFactory.php
@@ -6,16 +6,22 @@
use App\DTO\ReportDto;
use App\DTO\ReportResponseDto;
+use App\Entity\Contracts\HashtagableInterface;
use App\Entity\Entry;
use App\Entity\EntryComment;
use App\Entity\EntryCommentReport;
use App\Entity\EntryReport;
+use App\Entity\Message;
+use App\Entity\MessageReport;
use App\Entity\Post;
use App\Entity\PostComment;
use App\Entity\PostCommentReport;
use App\Entity\PostReport;
use App\Entity\Report;
+use App\Factory\Contract\ContentDtoFactory;
use App\Repository\TagLinkRepository;
+use App\Service\SwitchingServiceRegistry;
+use App\Utils\SqlHelpers;
use Doctrine\ORM\EntityManagerInterface;
class ReportFactory
@@ -24,11 +30,8 @@ public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserFactory $userFactory,
private readonly MagazineFactory $magazineFactory,
- private readonly EntryFactory $entryFactory,
- private readonly PostFactory $postFactory,
- private readonly EntryCommentFactory $entryCommentFactory,
- private readonly PostCommentFactory $postCommentFactory,
private readonly TagLinkRepository $tagLinkRepository,
+ private readonly SwitchingServiceRegistry $serviceRegistry,
) {
}
@@ -36,14 +39,15 @@ public function createFromDto(ReportDto $dto): Report
{
$className = $this->entityManager->getClassMetadata(\get_class($dto->getSubject()))->name.'Report';
- return new $className($dto->getSubject()->user, $dto->getSubject(), $dto->reason);
+ return new $className($dto->getSubject()->getUser(), $dto->getSubject(), $dto->reason);
}
public function createResponseDto(Report $report): ReportResponseDto
{
+ $magazine = $report->magazine !== null ? $this->magazineFactory->createSmallDto($report->magazine) : null;
$toReturn = ReportResponseDto::create(
$report->getId(),
- $this->magazineFactory->createSmallDto($report->magazine),
+ $magazine,
$this->userFactory->createSmallDto($report->reported),
$this->userFactory->createSmallDto($report->reporting),
$report->reason,
@@ -55,26 +59,9 @@ public function createResponseDto(Report $report): ReportResponseDto
);
$subject = $report->getSubject();
- switch (\get_class($report)) {
- case EntryReport::class:
- \assert($subject instanceof Entry);
- $toReturn->subject = $this->entryFactory->createResponseDto($subject, tags: $this->tagLinkRepository->getTagsOfContent($subject));
- break;
- case EntryCommentReport::class:
- \assert($subject instanceof EntryComment);
- $toReturn->subject = $this->entryCommentFactory->createResponseDto($subject, tags: $this->tagLinkRepository->getTagsOfContent($subject));
- break;
- case PostReport::class:
- \assert($subject instanceof Post);
- $toReturn->subject = $this->postFactory->createResponseDto($subject, tags: $this->tagLinkRepository->getTagsOfContent($subject));
- break;
- case PostCommentReport::class:
- \assert($subject instanceof PostComment);
- $toReturn->subject = $this->postCommentFactory->createResponseDto($subject, tags: $this->tagLinkRepository->getTagsOfContent($subject));
- break;
- default:
- throw new \LogicException();
- }
+ $hashtags = $subject instanceof HashtagableInterface ? $this->tagLinkRepository->getTagsOfContent($subject) : [];
+ $factory = $this->serviceRegistry->getService($subject, ContentDtoFactory::class);
+ $toReturn->subject = $factory->createResponseDto($subject, $hashtags);
return $toReturn;
}
diff --git a/src/Factory/User/UserUrlFactory.php b/src/Factory/User/UserUrlFactory.php
new file mode 100644
index 0000000000..220211b1f9
--- /dev/null
+++ b/src/Factory/User/UserUrlFactory.php
@@ -0,0 +1,46 @@
+
+ */
+readonly class UserUrlFactory implements SwitchableService, ActorUrlFactory
+{
+
+ public function __construct(
+ private PersonFactory $personFactory,
+ private UrlGeneratorInterface $urlGenerator,
+ ){}
+
+ public function getSupportedTypes(): array
+ {
+ return [User::class];
+ }
+
+ public function getActivityPubId($actor): string
+ {
+ return $this->personFactory->getActivityPubId($actor);
+ }
+
+ public function getLocalUrl($actor): string
+ {
+ return $this->urlGenerator->generate('user_overview', ['username' => $actor->username], UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
+ public function getAvatarUrl($actor): ?string
+ {
+ $slash = $actor->avatar && !str_starts_with('/', $actor->avatar->filePath) ? '/' : '';
+ return $actor->avatar ? '/media/cache/resolve/avatar_thumb'.$slash.$actor->avatar->filePath : null;
+ }
+}
diff --git a/src/MessageHandler/ActivityPub/Inbox/FlagHandler.php b/src/MessageHandler/ActivityPub/Inbox/FlagHandler.php
index 73213af630..59ffb08a65 100644
--- a/src/MessageHandler/ActivityPub/Inbox/FlagHandler.php
+++ b/src/MessageHandler/ActivityPub/Inbox/FlagHandler.php
@@ -13,6 +13,7 @@
use App\MessageHandler\MbinMessageHandler;
use App\Repository\EntryCommentRepository;
use App\Repository\EntryRepository;
+use App\Repository\MessageRepository;
use App\Repository\PostCommentRepository;
use App\Repository\PostRepository;
use App\Service\ActivityPubManager;
@@ -35,6 +36,7 @@ public function __construct(
private readonly EntryCommentRepository $entryCommentRepository,
private readonly PostRepository $postRepository,
private readonly PostCommentRepository $postCommentRepository,
+ private readonly MessageRepository $messageRepository,
private readonly SettingsManager $settingsManager,
private readonly LoggerInterface $logger,
) {
@@ -85,21 +87,11 @@ public function doWork(MessageInterface $message): void
private function findRemoteSubject(string $apUrl): ?ReportInterface
{
- $entry = $this->entryRepository->findOneBy(['apId' => $apUrl]);
- $entryComment = null;
- $post = null;
- $postComment = null;
- if (!$entry) {
- $entryComment = $this->entryCommentRepository->findOneBy(['apId' => $apUrl]);
- }
- if (!$entry and !$entryComment) {
- $post = $this->postRepository->findOneBy(['apId' => $apUrl]);
- }
- if (!$entry and !$entryComment and !$post) {
- $postComment = $this->postCommentRepository->findOneBy(['apId' => $apUrl]);
- }
-
- return $entry ?? $entryComment ?? $post ?? $postComment;
+ return $this->entryRepository->findOneBy(['apId' => $apUrl])
+ ?? $this->entryCommentRepository->findOneBy(['apId' => $apUrl])
+ ?? $this->postRepository->findOneBy(['apId' => $apUrl])
+ ?? $this->postCommentRepository->findOneBy(['apId' => $apUrl])
+ ?? $this->messageRepository->findOneBy(['apId' => $apUrl]);
}
private function findLocalSubject(string $apUrl): ?ReportInterface
@@ -117,6 +109,9 @@ private function findLocalSubject(string $apUrl): ?ReportInterface
if (preg_match_all("/\/m\/([a-zA-Z0-9\-_:@.]+)\/p\/([1-9][0-9]*)/", $apUrl, $matches)) {
return $this->postRepository->findOneBy(['id' => $matches[2][0]]);
}
+ if (preg_match_all("/\/message\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/", $apUrl, $matches)) {
+ return $this->messageRepository->findOneBy(['uuid' => $matches[1][0]]);
+ }
return null;
}
diff --git a/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php b/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php
index 517fff892a..4a3850f537 100644
--- a/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php
+++ b/src/MessageHandler/ActivityPub/Outbox/FlagHandler.php
@@ -75,14 +75,16 @@ private function getInboxUrls(Report $report): array
{
$urls = [];
- if (null === $report->magazine->apId) {
- foreach ($report->magazine->moderators as /* @var Moderator $moderator */ $moderator) {
- if ($moderator->user->apId and !\in_array($moderator->user->apInboxUrl, $urls)) {
- $urls[] = $moderator->user->apInboxUrl;
+ if (null !== $report->magazine) {
+ if (null === $report->magazine->apId) {
+ foreach ($report->magazine->moderators as /* @var Moderator $moderator */ $moderator) {
+ if ($moderator->user->apId and !\in_array($moderator->user->apInboxUrl, $urls)) {
+ $urls[] = $moderator->user->apInboxUrl;
+ }
}
+ } else {
+ $urls[] = $report->magazine->apInboxUrl;
}
- } else {
- $urls[] = $report->magazine->apInboxUrl;
}
if ($report->reported->apId and !\in_array($report->reported->apInboxUrl, $urls)) {
diff --git a/src/Repository/MagazineRepository.php b/src/Repository/MagazineRepository.php
index 4109411284..279b130a09 100644
--- a/src/Repository/MagazineRepository.php
+++ b/src/Repository/MagazineRepository.php
@@ -279,7 +279,7 @@ public function findReports(
);
try {
- $pagerfanta->setMaxPerPage(self::PER_PAGE);
+ $pagerfanta->setMaxPerPage($perPage);
$pagerfanta->setCurrentPage($page);
} catch (NotValidCurrentPageException $e) {
throw new NotFoundHttpException();
diff --git a/src/Repository/MessageRepository.php b/src/Repository/MessageRepository.php
index e21b4896e5..174e01aef2 100644
--- a/src/Repository/MessageRepository.php
+++ b/src/Repository/MessageRepository.php
@@ -5,9 +5,13 @@
namespace App\Repository;
use App\Entity\Message;
+use App\Entity\MessageReport;
+use App\Entity\Report;
use App\PageView\MessageThreadPageView;
+use App\Pagination\NativeQueryAdapter;
use App\Service\SettingsManager;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\DBAL\ParameterType;
use Doctrine\Persistence\ManagerRegistry;
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Exception\NotValidCurrentPageException;
@@ -97,4 +101,37 @@ public function findByApId(string $apId): ?Message
return null;
}
+
+ public function findReports(
+ ?int $page = 1,
+ int $perPage = self::PER_PAGE,
+ string $status = Report::STATUS_PENDING,
+ ): PagerfantaInterface {
+ $dql = 'SELECT r FROM '.MessageReport::class.' r';
+
+ if (Report::STATUS_ANY !== $status) {
+ $dql .= ' WHERE r.status = :status';
+ }
+
+ $dql .= " ORDER BY CASE WHEN r.status = 'pending' THEN 1 ELSE 2 END, r.weight DESC, r.createdAt DESC";
+
+ $query = $this->getEntityManager()->createQuery($dql);
+
+ if (Report::STATUS_ANY !== $status) {
+ $query->setParameter('status', $status);
+ }
+
+ $pagerfanta = new Pagerfanta(
+ new QueryAdapter($query)
+ );
+
+ try {
+ $pagerfanta->setMaxPerPage($perPage);
+ $pagerfanta->setCurrentPage($page);
+ } catch (NotValidCurrentPageException $e) {
+ throw new NotFoundHttpException();
+ }
+
+ return $pagerfanta;
+ }
}
diff --git a/src/Repository/MessageThreadRepository.php b/src/Repository/MessageThreadRepository.php
index 8d76fe69e2..27a06f9419 100644
--- a/src/Repository/MessageThreadRepository.php
+++ b/src/Repository/MessageThreadRepository.php
@@ -6,6 +6,7 @@
use App\Entity\Message;
use App\Entity\MessageThread;
+use App\Entity\Report;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\DBAL\Exception;
@@ -92,4 +93,13 @@ public function findByParticipants(array $participants): array
return [];
}
+
+ public function threadContainsReportedMessage(MessageThread $thread): bool {
+ $sql = 'SELECT EXISTS( SELECT 1 FROM message m INNER JOIN report r ON m.id = r.message_id WHERE m.thread_id = :tId AND (r.status = :statusPending OR r.status = :statusAppeal) );';
+ $query = $this->getEntityManager()->getConnection()->prepare($sql);
+ $query->bindValue('tId', $thread->getId(), ParameterType::INTEGER);
+ $query->bindValue('statusPending', Report::STATUS_PENDING);
+ $query->bindValue('statusAppeal', Report::STATUS_APPEAL);
+ return $query->executeQuery()->fetchFirstColumn()[0];
+ }
}
diff --git a/src/Repository/NotificationRepository.php b/src/Repository/NotificationRepository.php
index 0b471b2b78..c5aa472d4f 100644
--- a/src/Repository/NotificationRepository.php
+++ b/src/Repository/NotificationRepository.php
@@ -7,16 +7,21 @@
use App\Entity\Entry;
use App\Entity\EntryComment;
use App\Entity\Magazine;
+use App\Entity\Message;
+use App\Entity\MessageThread;
use App\Entity\Notification;
use App\Entity\Post;
use App\Entity\PostComment;
use App\Entity\User;
+use App\Utils\SqlHelpers;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\DBAL\ParameterType;
use Doctrine\Persistence\ManagerRegistry;
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Exception\NotValidCurrentPageException;
use Pagerfanta\Pagerfanta;
use Pagerfanta\PagerfantaInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
@@ -158,6 +163,34 @@ public function removePostCommentNotifications(PostComment $comment): void
$stmt->executeQuery();
}
+ public function removeMessageNotifications(Message $message): void
+ {
+ $conn = $this->getEntityManager()->getConnection();
+ $sql = 'DELETE FROM notification AS n WHERE n.message_id = :messageId';
+
+ $stmt = $conn->prepare($sql);
+ $stmt->bindValue('messageId', $message->getId());
+
+ $stmt->executeQuery();
+ }
+
+ public function markMessageNotificationsAsRead(User $user, MessageThread $thread): void {
+ $conn = $this->getEntityManager()->getConnection();
+ $sql = 'UPDATE notification n SET status = :s
+ WHERE n.user_id = :uId
+ AND EXISTS(
+ SELECT 1 FROM report r
+ INNER JOIN message m ON r.message_id = m.id
+ INNER JOIN message_thread t ON m.thread_id = t.id
+ WHERE t.id = :tId AND n.report_id = r.id
+ )';
+ $stmt = $conn->prepare($sql);
+ $stmt->bindValue('s', Notification::STATUS_READ);
+ $stmt->bindValue('uId', $user->getId(), ParameterType::INTEGER);
+ $stmt->bindValue('tId', $thread->getId(), ParameterType::INTEGER);
+ $stmt->executeQuery();
+ }
+
public function markReportNotificationsAsRead(User $user): void
{
$conn = $this->getEntityManager()->getConnection();
@@ -184,6 +217,26 @@ public function markReportNotificationsInMagazineAsRead(User $user, Magazine $ma
$stmt->executeQuery();
}
+ public function markReportNotificationsOfMessagesAsRead(User $user, array $reportIds): void
+ {
+ $sql = 'UPDATE notification n SET status = :s
+ WHERE n.user_id = :uId
+ AND n.report_id IN (:reports)';
+ $params = [
+ 'uId' => $user->getId(),
+ 'reports' => $reportIds,
+ 's' => Notification::STATUS_READ,
+ ];
+ $rewritten = SqlHelpers::rewriteArrayParameters($params, $sql);
+ $conn = $this->getEntityManager()->getConnection();
+ $stmt = $conn->prepare($rewritten['sql']);
+
+ foreach ($rewritten['parameters'] as $param => $value) {
+ $stmt->bindValue($param, $value, SqlHelpers::getSqlType($value));
+ }
+ $stmt->executeQuery();
+ }
+
public function markOwnReportNotificationsAsRead(User $user): void
{
$conn = $this->getEntityManager()->getConnection();
diff --git a/src/Repository/ReportRepository.php b/src/Repository/ReportRepository.php
index 74c7e297d9..e05845f2cd 100644
--- a/src/Repository/ReportRepository.php
+++ b/src/Repository/ReportRepository.php
@@ -9,6 +9,8 @@
use App\Entity\EntryComment;
use App\Entity\EntryCommentReport;
use App\Entity\EntryReport;
+use App\Entity\Message;
+use App\Entity\MessageReport;
use App\Entity\Post;
use App\Entity\PostComment;
use App\Entity\PostCommentReport;
@@ -45,6 +47,7 @@ public function findBySubject(ReportInterface $subject): ?Report
$subject instanceof EntryComment => $this->findByEntryComment($subject),
$subject instanceof Post => $this->findByPost($subject),
$subject instanceof PostComment => $this->findByPostComment($subject),
+ $subject instanceof Message => $this->findByMessage($subject),
default => throw new \LogicException(),
};
}
@@ -85,6 +88,15 @@ private function findByPostComment(PostComment $comment): ?PostCommentReport
->getOneOrNullResult();
}
+ private function findByMessage(Message $message): ?MessageReport
+ {
+ $dql = 'SELECT r FROM '.MessageReport::class.' r WHERE r.message = :message';
+
+ return $this->getEntityManager()->createQuery($dql)
+ ->setParameter('message', $message)
+ ->getOneOrNullResult();
+ }
+
public function findPendingBySubject(ReportInterface $subject): ?Report
{
return match (true) {
@@ -92,6 +104,7 @@ public function findPendingBySubject(ReportInterface $subject): ?Report
$subject instanceof EntryComment => $this->findPendingByEntryComment($subject),
$subject instanceof Post => $this->findPendingByPost($subject),
$subject instanceof PostComment => $this->findPendingByPostComment($subject),
+ $subject instanceof Message => $this->findPendingByMessage($subject),
default => throw new \LogicException(),
};
}
@@ -136,6 +149,16 @@ private function findPendingByPostComment(PostComment $comment): ?PostCommentRep
->getOneOrNullResult();
}
+ private function findPendingByMessage(Message $message): ?PostCommentReport
+ {
+ $dql = 'SELECT r FROM '.MessageReport::class.' r WHERE r.message = :comment AND r.status = :status';
+
+ return $this->getEntityManager()->createQuery($dql)
+ ->setParameter('message', $message)
+ ->setParameter('status', Report::STATUS_PENDING)
+ ->getOneOrNullResult();
+ }
+
public function findAllPaginated(int $page = 1, string $status = Report::STATUS_PENDING): PagerfantaInterface
{
$dql = 'SELECT r FROM '.Report::class.' r';
diff --git a/src/Repository/TagLinkRepository.php b/src/Repository/TagLinkRepository.php
index cc9e0035e1..7416404e33 100644
--- a/src/Repository/TagLinkRepository.php
+++ b/src/Repository/TagLinkRepository.php
@@ -4,6 +4,7 @@
namespace App\Repository;
+use App\Entity\Contracts\HashtagableInterface;
use App\Entity\Entry;
use App\Entity\EntryComment;
use App\Entity\Hashtag;
@@ -32,7 +33,7 @@ public function __construct(
/**
* @return string[]
*/
- public function getTagsOfContent(Entry|EntryComment|Post|PostComment $content): array
+ public function getTagsOfContent(HashtagableInterface $content): array
{
if ($content instanceof Entry) {
return $this->getTagsOfEntry($content);
@@ -43,7 +44,6 @@ public function getTagsOfContent(Entry|EntryComment|Post|PostComment $content):
} elseif ($content instanceof PostComment) {
return $this->getTagsOfPostComment($content);
} else {
- // this is unreachable because of the strict types
throw new \LogicException('Cannot handle content of type '.\get_class($content));
}
}
diff --git a/src/Security/Voter/MessageThreadVoter.php b/src/Security/Voter/MessageThreadVoter.php
index f7ae4bbe72..f562a7f9c6 100644
--- a/src/Security/Voter/MessageThreadVoter.php
+++ b/src/Security/Voter/MessageThreadVoter.php
@@ -6,6 +6,7 @@
use App\Entity\MessageThread;
use App\Entity\User;
+use App\Repository\MessageThreadRepository;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@@ -14,6 +15,13 @@ class MessageThreadVoter extends Voter
public const SHOW = 'show';
public const REPLY = 'reply';
+ public function __construct(
+ private readonly MessageThreadRepository $messageThreadRepository,
+ )
+ {
+
+ }
+
protected function supports(string $attribute, $subject): bool
{
return $subject instanceof MessageThread
@@ -45,11 +53,17 @@ private function canShow(MessageThread $thread, User $user): bool
return false;
}
- if (!$thread->userIsParticipant($user)) {
- return false;
+ if ($thread->userIsParticipant($user)) {
+ return true;
}
- return true;
+ if ($user->isModerator() || $user->isAdmin()) {
+ if($this->messageThreadRepository->threadContainsReportedMessage($thread)) {
+ return true;
+ }
+ }
+
+ return false;
}
private function canReply(MessageThread $thread, User $user): bool
diff --git a/src/Service/ActivityPub/ActivityJsonBuilder.php b/src/Service/ActivityPub/ActivityJsonBuilder.php
index 71b7ec682b..d2a18cc5dd 100644
--- a/src/Service/ActivityPub/ActivityJsonBuilder.php
+++ b/src/Service/ActivityPub/ActivityJsonBuilder.php
@@ -7,6 +7,7 @@
use App\Entity\Activity;
use App\Entity\Contracts\ActivityPubActivityInterface;
use App\Entity\Contracts\ActivityPubActorInterface;
+use App\Entity\Contracts\ContentInterface;
use App\Entity\Contracts\ReportInterface;
use App\Entity\Entry;
use App\Entity\EntryComment;
@@ -24,6 +25,8 @@
use App\Factory\ActivityPub\PersonFactory;
use App\Factory\ActivityPub\PostCommentNoteFactory;
use App\Factory\ActivityPub\PostNoteFactory;
+use App\Factory\Contract\ContentUrlFactory;
+use App\Service\SwitchingServiceRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -31,19 +34,18 @@
class ActivityJsonBuilder
{
public function __construct(
- private readonly UrlGeneratorInterface $urlGenerator,
- private readonly InstanceFactory $instanceFactory,
- private readonly PersonFactory $personFactory,
- private readonly GroupFactory $groupFactory,
- private readonly ActivityFactory $activityFactory,
- private readonly ContextsProvider $contextsProvider,
- private readonly EntryPageFactory $entryPageFactory,
- private readonly EntryCommentNoteFactory $entryCommentNoteFactory,
- private readonly PostNoteFactory $postNoteFactory,
- private readonly PostCommentNoteFactory $postCommentNoteFactory,
- private readonly LoggerInterface $logger,
- private readonly ApHttpClientInterface $apHttpClient,
- private readonly KernelInterface $kernel,
+ private readonly UrlGeneratorInterface $urlGenerator,
+ private readonly InstanceFactory $instanceFactory,
+ private readonly PersonFactory $personFactory,
+ private readonly GroupFactory $groupFactory,
+ private readonly ActivityFactory $activityFactory,
+ private readonly ContextsProvider $contextsProvider,
+ private readonly EntryPageFactory $entryPageFactory,
+ private readonly PostNoteFactory $postNoteFactory,
+ private readonly SwitchingServiceRegistry $serviceRegistry,
+ private readonly LoggerInterface $logger,
+ private readonly ApHttpClientInterface $apHttpClient,
+ private readonly KernelInterface $kernel,
) {
}
@@ -304,7 +306,11 @@ public function buildFlagFromActivity(Activity $activity): array
// 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) {
+ if (null === $activity->audience) {
+ // likely a message and so can either be from Lemmy or Mastodon or something else; defaulting to Lemmy
+ $audience = $this->personFactory->getActivityPubId($activity->objectUser);
+ $object = $lemmyObject;
+ } elseif ('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.
@@ -326,7 +332,7 @@ public function buildFlagFromActivity(Activity $activity): array
'content' => $activity->contentString,
];
- if ('random' !== $activity->audience->name || $activity->audience->apId) {
+ if ($activity->audience !== null && ('random' !== $activity->audience->name || $activity->audience->apId)) {
$result['to'] = [$this->groupFactory->getActivityPubId($activity->audience)];
}
@@ -517,20 +523,8 @@ private function buildLockFromActivity(Activity $activity): array
];
}
- public function getPublicUrl(ReportInterface|ActivityPubActivityInterface $subject): string
+ public function getPublicUrl(ContentInterface $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));
+ return $this->serviceRegistry->getService($subject, ContentUrlFactory::class)->getActivityPubId($subject);
}
}
diff --git a/src/Service/Contracts/ContentManagerInterface.php b/src/Service/Contracts/ContentManagerInterface.php
index f3e9539f70..0e0d725331 100644
--- a/src/Service/Contracts/ContentManagerInterface.php
+++ b/src/Service/Contracts/ContentManagerInterface.php
@@ -4,6 +4,14 @@
namespace App\Service\Contracts;
+use App\Entity\Contracts\ContentInterface;
+use App\Entity\Contracts\ContentVisibilityInterface;
+use App\Entity\User;
+
interface ContentManagerInterface extends ManagerInterface
{
+
+ function restore(User $moderator, ContentVisibilityInterface $content): void;
+
+ function delete(User $moderator, ContentInterface $subject): void;
}
diff --git a/src/Service/Contracts/ContentNotificationManagerInterface.php b/src/Service/Contracts/ContentNotificationManagerInterface.php
index 9cc9ac5fb0..22e4563bae 100644
--- a/src/Service/Contracts/ContentNotificationManagerInterface.php
+++ b/src/Service/Contracts/ContentNotificationManagerInterface.php
@@ -13,4 +13,8 @@ public function sendCreated(ContentInterface $subject): void;
public function sendEdited(ContentInterface $subject): void;
public function sendDeleted(ContentInterface $subject): void;
+
+ public function purgeNotifications(ContentInterface $subject): void;
+
+ public function purgeMagazineLog(ContentInterface $subject): void;
}
diff --git a/src/Service/Contracts/SwitchableService.php b/src/Service/Contracts/SwitchableService.php
new file mode 100644
index 0000000000..dcfd95ca82
--- /dev/null
+++ b/src/Service/Contracts/SwitchableService.php
@@ -0,0 +1,14 @@
+apDomain && $user->apDomain !== parse_url($comment->apId ?? '', PHP_URL_HOST) && !$comment->magazine->userIsModerator($user)) {
- $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m]', ['u' => $user->apId, 'm' => $comment->magazine->apId ?? $comment->magazine->name]);
+ $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m}', ['u' => $user->apId, 'm' => $comment->magazine->apId ?? $comment->magazine->name]);
return;
}
@@ -225,7 +238,12 @@ private function isTrashed(User $user, EntryComment $comment): bool
return !$comment->isAuthor($user);
}
- public function restore(User $user, EntryComment $comment): void
+ /**
+ * @param User $user
+ * @param EntryComment $comment
+ * @return void
+ */
+ public function restore(User $user, ContentVisibilityInterface $comment): void
{
if (VisibilityInterface::VISIBILITY_TRASHED !== $comment->visibility) {
throw new \Exception('Invalid visibility');
diff --git a/src/Service/EntryManager.php b/src/Service/EntryManager.php
index 1f0dc731c8..d73d321c7c 100644
--- a/src/Service/EntryManager.php
+++ b/src/Service/EntryManager.php
@@ -5,6 +5,8 @@
namespace App\Service;
use App\DTO\EntryDto;
+use App\Entity\Contracts\ContentInterface;
+use App\Entity\Contracts\ContentVisibilityInterface;
use App\Entity\Contracts\VisibilityInterface;
use App\Entity\Entry;
use App\Entity\Magazine;
@@ -32,6 +34,7 @@
use App\Repository\ImageRepository;
use App\Service\ActivityPub\ApHttpClientInterface;
use App\Service\Contracts\ContentManagerInterface;
+use App\Service\Contracts\SwitchableService;
use App\Utils\Slugger;
use App\Utils\UrlCleaner;
use Doctrine\Common\Collections\Criteria;
@@ -46,7 +49,7 @@
use Symfony\Contracts\Translation\TranslatorInterface;
use Webmozart\Assert\Assert;
-class EntryManager implements ContentManagerInterface
+class EntryManager implements SwitchableService, ContentManagerInterface
{
public function __construct(
private readonly LoggerInterface $logger,
@@ -71,6 +74,11 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [Entry::class];
+ }
+
/**
* @throws TagBannedException
* @throws UserBannedException
@@ -242,10 +250,15 @@ public function edit(Entry $entry, EntryDto $dto, User $editedBy): Entry
return $entry;
}
- public function delete(User $user, Entry $entry): void
+ /**
+ * @param User $user
+ * @param Entry $entry
+ * @return void
+ */
+ public function delete(User $user, ContentInterface $entry): void
{
if ($user->apDomain && $user->apDomain !== parse_url($entry->apId ?? '', PHP_URL_HOST) && !$entry->magazine->userIsModerator($user)) {
- $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m]', ['u' => $user->apId, 'm' => $entry->magazine->apId ?? $entry->magazine->name]);
+ $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m}', ['u' => $user->apId, 'm' => $entry->magazine->apId ?? $entry->magazine->name]);
return;
}
@@ -295,7 +308,12 @@ public function purge(User $user, Entry $entry): void
}
}
- public function restore(User $user, Entry $entry): void
+ /**
+ * @param User $user
+ * @param Entry $entry
+ * @return void
+ */
+ public function restore(User $user, ContentVisibilityInterface $entry): void
{
if (VisibilityInterface::VISIBILITY_TRASHED !== $entry->visibility) {
throw new \Exception('Invalid visibility');
diff --git a/src/Service/FeedManager.php b/src/Service/FeedManager.php
index c7bd9b2446..f53637407a 100644
--- a/src/Service/FeedManager.php
+++ b/src/Service/FeedManager.php
@@ -6,6 +6,8 @@
use App\Entity\Entry;
use App\Entity\Post;
+use App\Factory\Entry\EntryUrlFactory;
+use App\Factory\Post\PostUrlFactory;
use App\Markdown\MarkdownConverter;
use App\PageView\ContentPageView;
use App\Repository\ContentRepository;
@@ -32,6 +34,8 @@ public function __construct(
private readonly MagazineRepository $magazineRepository,
private readonly UserRepository $userRepository,
private readonly TagLinkRepository $tagLinkRepository,
+ private readonly EntryUrlFactory $entryUrlFactory,
+ private readonly PostUrlFactory $postUrlFactory,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly Security $security,
private readonly MediaExtensionRuntime $mediaExtensionRuntime,
@@ -98,11 +102,7 @@ public function getItems(iterable $content): \Generator
}
if ($subject instanceof Entry) {
- $link = $this->urlGenerator->generate('entry_single', [
- 'magazine_name' => $subject->magazine->name,
- 'entry_id' => $subject->getId(),
- 'slug' => $subject->slug,
- ], UrlGeneratorInterface::ABSOLUTE_URL);
+ $link = $this->entryUrlFactory->getLocalUrl($subject);
$item->setContent($this->markdownConverter->convertToHtml($subject->body ?? '', 'entry'));
$item->setSummary($subject->getShortDesc());
@@ -110,11 +110,7 @@ public function getItems(iterable $content): \Generator
$item->setLink($link);
$item->set('comments', $link.'#comments');
} elseif ($subject instanceof Post) {
- $link = $this->urlGenerator->generate('post_single', [
- 'magazine_name' => $subject->magazine->name,
- 'post_id' => $subject->getId(),
- 'slug' => $subject->slug,
- ], UrlGeneratorInterface::ABSOLUTE_URL);
+ $link = $this->postUrlFactory->getLocalUrl($subject);
$item->setContent($this->markdownConverter->convertToHtml($subject->body ?? '', 'post'));
$item->setSummary($subject->getShortTitle());
diff --git a/src/Service/MessageManager.php b/src/Service/MessageManager.php
index 89560dc7c6..af9e075f27 100644
--- a/src/Service/MessageManager.php
+++ b/src/Service/MessageManager.php
@@ -5,6 +5,8 @@
namespace App\Service;
use App\DTO\MessageDto;
+use App\Entity\Contracts\ContentInterface;
+use App\Entity\Contracts\ContentVisibilityInterface;
use App\Entity\Message;
use App\Entity\MessageThread;
use App\Entity\User;
@@ -15,13 +17,15 @@
use App\Exception\UserDeletedException;
use App\Message\ActivityPub\Outbox\CreateMessage;
use App\Repository\MessageThreadRepository;
+use App\Service\Contracts\ContentManagerInterface;
+use App\Service\Contracts\SwitchableService;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
-class MessageManager
+class MessageManager implements SwitchableService, ContentManagerInterface
{
public function __construct(
private readonly MessageThreadRepository $messageThreadRepository,
@@ -33,6 +37,31 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [Message::class];
+ }
+
+ public function delete(User $moderator, ContentInterface $subject): void
+ {
+ \assert($subject instanceof Message);
+
+ if ($moderator->apDomain && $moderator->apDomain !== parse_url($subject->apId ?? '', PHP_URL_HOST)) {
+ $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted message', ['u' => $moderator->apId]);
+
+ return;
+ }
+
+ // we currently have no way to soft-delete messages, so just replace the content
+ $subject->body = '[deleted]';
+ $this->entityManager->flush();
+ }
+
+ public function restore(User $moderator, ContentVisibilityInterface $content): void
+ {
+ // not implemented, as we currently have no way to soft-delete messages
+ }
+
public function toThread(MessageDto $dto, User $sender, User ...$receivers): MessageThread
{
$thread = new MessageThread($sender, ...$receivers);
@@ -164,6 +193,19 @@ public function editMessage(Message $message, array $object): void
}
}
+ public function removeUserFromThread(MessageThread $thread, User $user): void
+ {
+ if (!$thread->userIsParticipant($user)) {
+ throw new \InvalidArgumentException('user is not a participant of this message thread');
+ }
+ if ($thread->participants->count() > 1) {
+ $thread->participants->removeElement($user);
+ } else {
+ $this->entityManager->remove($thread);
+ }
+ $this->entityManager->flush();
+ }
+
/** @return string[] */
public function findAudience(MessageThread $thread): array
{
diff --git a/src/Service/Notification/EntryCommentNotificationManager.php b/src/Service/Notification/EntryCommentNotificationManager.php
index 0381ec8672..f242713593 100644
--- a/src/Service/Notification/EntryCommentNotificationManager.php
+++ b/src/Service/Notification/EntryCommentNotificationManager.php
@@ -21,6 +21,7 @@
use App\Repository\NotificationSettingsRepository;
use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
+use App\Service\Contracts\SwitchableService;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
use App\Service\ImageManagerInterface;
@@ -34,7 +35,7 @@
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
-class EntryCommentNotificationManager implements ContentNotificationManagerInterface
+class EntryCommentNotificationManager implements SwitchableService, ContentNotificationManagerInterface
{
use NotificationTrait;
@@ -58,6 +59,11 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [EntryComment::class];
+ }
+
public function sendCreated(ContentInterface $subject): void
{
if ($subject->user->isBanned || $subject->user->isDeleted || $subject->user->isTrashed() || $subject->user->isSoftDeleted()) {
@@ -198,16 +204,22 @@ public function sendDeleted(ContentInterface $subject): void
if (!$subject instanceof EntryComment) {
throw new \LogicException();
}
- $this->notifyMagazine($notification = new EntryCommentDeletedNotification($subject->user, $subject));
+ $this->notifyMagazine(new EntryCommentDeletedNotification($subject->user, $subject));
}
- public function purgeNotifications(EntryComment $comment): void
+ public function purgeNotifications(ContentInterface $subject): void
{
- $this->notificationRepository->removeEntryCommentNotifications($comment);
+ if (!$subject instanceof EntryComment) {
+ throw new \LogicException();
+ }
+ $this->notificationRepository->removeEntryCommentNotifications($subject);
}
- public function purgeMagazineLog(EntryComment $comment): void
+ public function purgeMagazineLog(ContentInterface $subject): void
{
- $this->magazineLogRepository->removeEntryCommentLogs($comment);
+ if (!$subject instanceof EntryComment) {
+ throw new \LogicException();
+ }
+ $this->magazineLogRepository->removeEntryCommentLogs($subject);
}
}
diff --git a/src/Service/Notification/EntryNotificationManager.php b/src/Service/Notification/EntryNotificationManager.php
index 044914dcb7..51182cfeff 100644
--- a/src/Service/Notification/EntryNotificationManager.php
+++ b/src/Service/Notification/EntryNotificationManager.php
@@ -20,6 +20,7 @@
use App\Repository\NotificationSettingsRepository;
use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
+use App\Service\Contracts\SwitchableService;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
use App\Service\ImageManagerInterface;
@@ -34,7 +35,7 @@
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
-class EntryNotificationManager implements ContentNotificationManagerInterface
+class EntryNotificationManager implements SwitchableService, ContentNotificationManagerInterface
{
use NotificationTrait;
@@ -58,6 +59,11 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [Entry::class];
+ }
+
public function sendCreated(ContentInterface $subject): void
{
if ($subject->user->isBanned || $subject->user->isDeleted || $subject->user->isTrashed() || $subject->user->isSoftDeleted()) {
@@ -165,16 +171,22 @@ public function sendDeleted(ContentInterface $subject): void
if (!$subject instanceof Entry) {
throw new \LogicException();
}
- $this->notifyMagazine($notification = new EntryDeletedNotification($subject->user, $subject));
+ $this->notifyMagazine(new EntryDeletedNotification($subject->user, $subject));
}
- public function purgeNotifications(Entry $entry): void
+ public function purgeNotifications(ContentInterface $subject): void
{
- $this->notificationRepository->removeEntryNotifications($entry);
+ if (!$subject instanceof Entry) {
+ throw new \LogicException();
+ }
+ $this->notificationRepository->removeEntryNotifications($subject);
}
- public function purgeMagazineLog(Entry $entry): void
+ public function purgeMagazineLog(ContentInterface $subject): void
{
- $this->magazineLogRepository->removeEntryLogs($entry);
+ if (!$subject instanceof Entry) {
+ throw new \LogicException();
+ }
+ $this->magazineLogRepository->removeEntryLogs($subject);
}
}
diff --git a/src/Service/Notification/MessageNotificationManager.php b/src/Service/Notification/MessageNotificationManager.php
index d8d00d27d5..f1652fca38 100644
--- a/src/Service/Notification/MessageNotificationManager.php
+++ b/src/Service/Notification/MessageNotificationManager.php
@@ -4,29 +4,37 @@
namespace App\Service\Notification;
+use App\Entity\Contracts\ContentInterface;
use App\Entity\Message;
use App\Entity\MessageNotification;
+use App\Entity\Notification;
use App\Entity\User;
use App\Event\NotificationCreatedEvent;
use App\Factory\MagazineFactory;
use App\Repository\MagazineSubscriptionRepository;
+use App\Repository\NotificationRepository;
+use App\Service\Contracts\ContentNotificationManagerInterface;
+use App\Service\Contracts\SwitchableService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mercure\HubInterface;
-class MessageNotificationManager
+class MessageNotificationManager implements SwitchableService, ContentNotificationManagerInterface
{
use NotificationTrait;
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
- private readonly MagazineSubscriptionRepository $repository,
- private readonly MagazineFactory $magazineFactory,
- private readonly HubInterface $publisher,
+ private readonly NotificationRepository $notificationRepository,
private readonly EntityManagerInterface $entityManager,
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [Message::class];
+ }
+
public function send(Message $message, User $sender): void
{
$thread = $message->thread;
@@ -40,4 +48,37 @@ public function send(Message $message, User $sender): void
$this->entityManager->flush();
}
+
+ public function sendCreated(ContentInterface $subject): void
+ {
+ // not supported
+ }
+
+ private function notifyMagazine(Notification $notification): void
+ {
+ // not supported
+ }
+
+ public function sendEdited(ContentInterface $subject): void
+ {
+ // not supported
+ }
+
+ public function sendDeleted(ContentInterface $subject): void
+ {
+ // not supported
+ }
+
+ public function purgeNotifications(ContentInterface $subject): void
+ {
+ if (!$subject instanceof Message) {
+ throw new \LogicException();
+ }
+ $this->notificationRepository->removeMessageNotifications($subject);
+ }
+
+ public function purgeMagazineLog(ContentInterface $subject): void
+ {
+ // not supported
+ }
}
diff --git a/src/Service/Notification/PostCommentNotificationManager.php b/src/Service/Notification/PostCommentNotificationManager.php
index be18e2578a..b6ae8c3414 100644
--- a/src/Service/Notification/PostCommentNotificationManager.php
+++ b/src/Service/Notification/PostCommentNotificationManager.php
@@ -21,6 +21,7 @@
use App\Repository\NotificationSettingsRepository;
use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
+use App\Service\Contracts\SwitchableService;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
use App\Service\ImageManagerInterface;
@@ -34,7 +35,7 @@
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
-class PostCommentNotificationManager implements ContentNotificationManagerInterface
+class PostCommentNotificationManager implements SwitchableService, ContentNotificationManagerInterface
{
use NotificationTrait;
@@ -58,6 +59,11 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [PostComment::class];
+ }
+
public function sendCreated(ContentInterface $subject): void
{
if ($subject->user->isBanned || $subject->user->isDeleted || $subject->user->isTrashed() || $subject->user->isSoftDeleted()) {
@@ -196,16 +202,22 @@ public function sendDeleted(ContentInterface $subject): void
if (!$subject instanceof PostComment) {
throw new \LogicException();
}
- $this->notifyMagazine($notification = new PostCommentDeletedNotification($subject->user, $subject));
+ $this->notifyMagazine(new PostCommentDeletedNotification($subject->user, $subject));
}
- public function purgeNotifications(PostComment $comment): void
+ public function purgeNotifications(ContentInterface $subject): void
{
- $this->notificationRepository->removePostCommentNotifications($comment);
+ if (!$subject instanceof PostComment) {
+ throw new \LogicException();
+ }
+ $this->notificationRepository->removePostCommentNotifications($subject);
}
- public function purgeMagazineLog(PostComment $comment): void
+ public function purgeMagazineLog(ContentInterface $subject): void
{
- $this->magazineLogRepository->removePostCommentLogs($comment);
+ if (!$subject instanceof PostComment) {
+ throw new \LogicException();
+ }
+ $this->magazineLogRepository->removePostCommentLogs($subject);
}
}
diff --git a/src/Service/Notification/PostNotificationManager.php b/src/Service/Notification/PostNotificationManager.php
index 7f71daed13..d2fa355969 100644
--- a/src/Service/Notification/PostNotificationManager.php
+++ b/src/Service/Notification/PostNotificationManager.php
@@ -19,6 +19,7 @@
use App\Repository\NotificationSettingsRepository;
use App\Repository\UserRepository;
use App\Service\Contracts\ContentNotificationManagerInterface;
+use App\Service\Contracts\SwitchableService;
use App\Service\GenerateHtmlClassService;
use App\Service\ImageManager;
use App\Service\ImageManagerInterface;
@@ -32,7 +33,7 @@
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
-class PostNotificationManager implements ContentNotificationManagerInterface
+class PostNotificationManager implements SwitchableService, ContentNotificationManagerInterface
{
use NotificationTrait;
@@ -55,6 +56,11 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [Post::class];
+ }
+
public function sendCreated(ContentInterface $subject): void
{
if ($subject->user->isBanned || $subject->user->isDeleted || $subject->user->isTrashed() || $subject->user->isSoftDeleted()) {
@@ -156,16 +162,22 @@ public function sendDeleted(ContentInterface $subject): void
if (!$subject instanceof Post) {
throw new \LogicException();
}
- $this->notifyMagazine($notification = new PostDeletedNotification($subject->user, $subject));
+ $this->notifyMagazine(new PostDeletedNotification($subject->user, $subject));
}
- public function purgeNotifications(Post $post): void
+ public function purgeNotifications(ContentInterface $subject): void
{
- $this->notificationRepository->removePostNotifications($post);
+ if (!$subject instanceof Post) {
+ throw new \LogicException();
+ }
+ $this->notificationRepository->removePostNotifications($subject);
}
- public function purgeMagazineLog(Post $post): void
+ public function purgeMagazineLog(ContentInterface $subject): void
{
- $this->magazineLogRepository->removePostLogs($post);
+ if (!$subject instanceof Post) {
+ throw new \LogicException();
+ }
+ $this->magazineLogRepository->removePostLogs($subject);
}
}
diff --git a/src/Service/Notification/ReportNotificationManager.php b/src/Service/Notification/ReportNotificationManager.php
index 6a0ec25524..28f23d852f 100644
--- a/src/Service/Notification/ReportNotificationManager.php
+++ b/src/Service/Notification/ReportNotificationManager.php
@@ -11,6 +11,7 @@
use App\Event\NotificationCreatedEvent;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ReportNotificationManager
@@ -25,9 +26,12 @@ public function __construct(
public function sendReportCreatedNotification(Report $report): void
{
$receivers = [];
- foreach ($report->magazine->moderators as /* @var Moderator $moderator */ $moderator) {
- if (null === $moderator->user->apId) {
- $receivers[] = $moderator->user;
+
+ if($report->magazine !== null) {
+ foreach ($report->magazine->moderators as /* @var Moderator $moderator */ $moderator) {
+ if (null === $moderator->user->apId) {
+ $receivers[] = $moderator->user;
+ }
}
}
diff --git a/src/Service/Notification/UserPushSubscriptionManager.php b/src/Service/Notification/UserPushSubscriptionManager.php
index 7db160b7fa..877ce3db09 100644
--- a/src/Service/Notification/UserPushSubscriptionManager.php
+++ b/src/Service/Notification/UserPushSubscriptionManager.php
@@ -6,6 +6,7 @@
use App\Entity\Notification;
use App\Entity\User;
+use App\Kernel;
use App\Payloads\PushNotification;
use App\Repository\SiteRepository;
use App\Repository\UserPushSubscriptionRepository;
@@ -16,20 +17,27 @@
use Minishlink\WebPush\Subscription;
use Minishlink\WebPush\WebPush;
use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserPushSubscriptionManager
{
+
+ private readonly ContainerInterface $serviceContainer;
+
public function __construct(
private readonly SettingsManager $settingsManager,
private readonly SiteRepository $siteRepository,
private readonly UserPushSubscriptionRepository $pushSubscriptionRepository,
private readonly TranslatorInterface $translator,
- private readonly UrlGeneratorInterface $urlGenerator,
private readonly LoggerInterface $logger,
private readonly EntityManagerInterface $entityManager,
+ Kernel $kernel,
) {
+ $this->serviceContainer = $kernel->getContainer();
}
/**
@@ -48,7 +56,7 @@ public function sendTextToUser(User $user, PushNotification|Notification $pushNo
$subs = $this->pushSubscriptionRepository->findBy($criteria);
foreach ($subs as $sub) {
if ($pushNotification instanceof Notification) {
- $toSend = $pushNotification->getMessage($this->translator, $sub->locale ?? $this->settingsManager->get('KBIN_DEFAULT_LANG'), $this->urlGenerator);
+ $toSend = $pushNotification->getMessage($this->translator, $sub->locale ?? $this->settingsManager->get('KBIN_DEFAULT_LANG'), $this->serviceContainer);
} elseif ($pushNotification instanceof PushNotification) {
$toSend = $pushNotification;
} else {
diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php
index 4901b6e8b9..6cf931b672 100644
--- a/src/Service/NotificationManager.php
+++ b/src/Service/NotificationManager.php
@@ -10,6 +10,7 @@
use App\Entity\MessageNotification;
use App\Entity\Notification;
use App\Entity\User;
+use App\Service\Contracts\ContentNotificationManagerInterface;
use App\Service\Notification\MagazineBanNotificationManager;
use App\Service\Notification\MessageNotificationManager;
use Doctrine\ORM\EntityManagerInterface;
@@ -17,7 +18,7 @@
class NotificationManager
{
public function __construct(
- private readonly NotificationManagerTypeResolver $resolver,
+ private readonly SwitchingServiceRegistry $serviceRegistry,
private readonly MessageNotificationManager $messageNotificationManager,
private readonly EntityManagerInterface $entityManager,
private readonly MagazineBanNotificationManager $magazineBanNotificationManager,
@@ -26,17 +27,17 @@ public function __construct(
public function sendCreated(ContentInterface $subject): void
{
- $this->resolver->resolve($subject)->sendCreated($subject);
+ $this->serviceRegistry->getService($subject, ContentNotificationManagerInterface::class)->sendCreated($subject);
}
public function sendEdited(ContentInterface $subject): void
{
- $this->resolver->resolve($subject)->sendEdited($subject);
+ $this->serviceRegistry->getService($subject, ContentNotificationManagerInterface::class)->sendEdited($subject);
}
public function sendDeleted(ContentInterface $subject): void
{
- $this->resolver->resolve($subject)->sendDeleted($subject);
+ $this->serviceRegistry->getService($subject, ContentNotificationManagerInterface::class)->sendDeleted($subject);
}
public function sendMessageNotification(Message $message, User $sender): void
diff --git a/src/Service/NotificationManagerTypeResolver.php b/src/Service/NotificationManagerTypeResolver.php
deleted file mode 100644
index 951544255d..0000000000
--- a/src/Service/NotificationManagerTypeResolver.php
+++ /dev/null
@@ -1,38 +0,0 @@
- $this->entryNotificationManager,
- $subject instanceof EntryComment => $this->entryCommentNotificationManager,
- $subject instanceof Post => $this->postNotificationManager,
- $subject instanceof PostComment => $this->postCommentNotificationManager,
- default => throw new \LogicException(),
- };
- }
-}
diff --git a/src/Service/PostCommentManager.php b/src/Service/PostCommentManager.php
index 8e567dec70..4503bc4238 100644
--- a/src/Service/PostCommentManager.php
+++ b/src/Service/PostCommentManager.php
@@ -5,6 +5,8 @@
namespace App\Service;
use App\DTO\PostCommentDto;
+use App\Entity\Contracts\ContentInterface;
+use App\Entity\Contracts\ContentVisibilityInterface;
use App\Entity\Contracts\VisibilityInterface;
use App\Entity\PostComment;
use App\Entity\User;
@@ -23,6 +25,7 @@
use App\Message\DeleteImageMessage;
use App\Repository\ImageRepository;
use App\Service\Contracts\ContentManagerInterface;
+use App\Service\Contracts\SwitchableService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
@@ -31,7 +34,7 @@
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
use Webmozart\Assert\Assert;
-class PostCommentManager implements ContentManagerInterface
+class PostCommentManager implements SwitchableService, ContentManagerInterface
{
public function __construct(
private readonly LoggerInterface $logger,
@@ -48,6 +51,11 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [PostComment::class];
+ }
+
/**
* @throws TagBannedException
* @throws UserBannedException
@@ -170,10 +178,15 @@ public function edit(PostComment $comment, PostCommentDto $dto, ?User $editedBy
return $comment;
}
- public function delete(User $user, PostComment $comment): void
+ /**
+ * @param User $user
+ * @param PostComment $comment
+ * @return void
+ */
+ public function delete(User $user, ContentInterface $comment): void
{
if ($user->apDomain && $user->apDomain !== parse_url($comment->apId ?? '', PHP_URL_HOST) && !$comment->magazine->userIsModerator($user)) {
- $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m]', ['u' => $user->apId, 'm' => $comment->magazine->apId ?? $comment->magazine->name]);
+ $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m}', ['u' => $user->apId, 'm' => $comment->magazine->apId ?? $comment->magazine->name]);
return;
}
@@ -227,9 +240,12 @@ private function isTrashed(User $user, PostComment $comment): bool
}
/**
+ * @param User $user
+ * @param PostComment $comment
+ * @return void
* @throws \Exception
*/
- public function restore(User $user, PostComment $comment): void
+ public function restore(User $user, ContentVisibilityInterface $comment): void
{
if (VisibilityInterface::VISIBILITY_TRASHED !== $comment->visibility) {
throw new \Exception('Invalid visibility');
diff --git a/src/Service/PostManager.php b/src/Service/PostManager.php
index e40cb99449..3e6618df80 100644
--- a/src/Service/PostManager.php
+++ b/src/Service/PostManager.php
@@ -5,6 +5,8 @@
namespace App\Service;
use App\DTO\PostDto;
+use App\Entity\Contracts\ContentInterface;
+use App\Entity\Contracts\ContentVisibilityInterface;
use App\Entity\Contracts\VisibilityInterface;
use App\Entity\Magazine;
use App\Entity\MagazineLogPostLocked;
@@ -27,6 +29,7 @@
use App\Repository\PostRepository;
use App\Service\ActivityPub\ApHttpClientInterface;
use App\Service\Contracts\ContentManagerInterface;
+use App\Service\Contracts\SwitchableService;
use App\Utils\Slugger;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
@@ -40,7 +43,7 @@
use Symfony\Contracts\Translation\TranslatorInterface;
use Webmozart\Assert\Assert;
-class PostManager implements ContentManagerInterface
+class PostManager implements SwitchableService, ContentManagerInterface
{
public function __construct(
private readonly LoggerInterface $logger,
@@ -63,6 +66,11 @@ public function __construct(
) {
}
+ public function getSupportedTypes(): array
+ {
+ return [Post::class];
+ }
+
/**
* @throws TagBannedException
* @throws UserBannedException
@@ -179,10 +187,15 @@ public function edit(Post $post, PostDto $dto, ?User $editedBy = null): Post
return $post;
}
- public function delete(User $user, Post $post): void
+ /**
+ * @param User $user
+ * @param Post $post
+ * @return void
+ */
+ public function delete(User $user, ContentInterface $post): void
{
if ($user->apDomain && $user->apDomain !== parse_url($post->apId ?? '', PHP_URL_HOST) && !$post->magazine->userIsModerator($user)) {
- $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m]', ['u' => $user->apId, 'm' => $post->magazine->apId ?? $post->magazine->name]);
+ $this->logger->info('Got a delete activity from user {u}, but they are not from the same instance as the deleted post and they are not a moderator on {m}', ['u' => $user->apId, 'm' => $post->magazine->apId ?? $post->magazine->name]);
return;
}
@@ -237,7 +250,12 @@ private function isTrashed(User $user, Post $post): bool
return !$post->isAuthor($user);
}
- public function restore(User $user, Post $post): void
+ /**
+ * @param User $user
+ * @param Post $post
+ * @return void
+ */
+ public function restore(User $user, ContentVisibilityInterface $post): void
{
if (VisibilityInterface::VISIBILITY_TRASHED !== $post->visibility) {
throw new \Exception('Invalid visibility');
diff --git a/src/Service/ReportManager.php b/src/Service/ReportManager.php
index 499ea5436d..23d623acec 100644
--- a/src/Service/ReportManager.php
+++ b/src/Service/ReportManager.php
@@ -5,15 +5,17 @@
namespace App\Service;
use App\DTO\ReportDto;
+use App\Entity\Contracts\ContentInterface;
+use App\Entity\Contracts\ContentVisibilityInterface;
use App\Entity\Report;
use App\Entity\User;
use App\Event\Report\ReportApprovedEvent;
use App\Event\Report\ReportRejectedEvent;
use App\Event\Report\SubjectReportedEvent;
use App\Exception\SubjectHasBeenReportedException;
-use App\Factory\ContentManagerFactory;
use App\Factory\ReportFactory;
use App\Repository\ReportRepository;
+use App\Service\Contracts\ContentManagerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
@@ -24,7 +26,7 @@ public function __construct(
private readonly ReportRepository $repository,
private readonly EventDispatcherInterface $dispatcher,
private readonly EntityManagerInterface $entityManager,
- private readonly ContentManagerFactory $managerFactory,
+ private readonly SwitchingServiceRegistry $serviceRegistry,
) {
}
@@ -51,14 +53,14 @@ public function report(ReportDto $dto, User $reporting): Report
public function reject(Report $report, User $moderator): void
{
- $manager = $this->managerFactory->createManager($report->getSubject());
-
$report->status = Report::STATUS_REJECTED;
$report->consideredBy = $moderator;
$report->consideredAt = new \DateTimeImmutable();
- if ($report->getSubject()->isTrashed()) {
- $manager->restore($moderator, $report->getSubject());
+ $subject = $report->getSubject();
+ if ($subject instanceof ContentVisibilityInterface && $subject->isTrashed()) {
+ $manager = $this->serviceRegistry->getService($subject, ContentManagerInterface::class);
+ $manager->restore($moderator, $subject);
}
$this->entityManager->flush();
@@ -68,13 +70,15 @@ public function reject(Report $report, User $moderator): void
public function accept(Report $report, User $moderator): void
{
- $manager = $this->managerFactory->createManager($report->getSubject());
-
$report->status = Report::STATUS_APPROVED;
$report->consideredBy = $moderator;
$report->consideredAt = new \DateTimeImmutable();
- $manager->delete($moderator, $report->getSubject());
+ $subject = $report->getSubject();
+ if($subject instanceof ContentInterface) {
+ $manager = $this->serviceRegistry->getService($subject, ContentManagerInterface::class);
+ $manager->delete($moderator, $subject);
+ }
$this->entityManager->flush();
diff --git a/src/Service/SwitchingServiceRegistry.php b/src/Service/SwitchingServiceRegistry.php
new file mode 100644
index 0000000000..00722d5189
--- /dev/null
+++ b/src/Service/SwitchingServiceRegistry.php
@@ -0,0 +1,71 @@
+services = [...$services];
+ }
+
+ /**
+ * @template S of SwitchableService
+ * @template O of object
+ * @param O $object
+ * @param class-string $interface
+ * @return S
+ */
+ public function getService(object $object, string $interface): object {
+ $objectClass = \get_class($object);
+ if(isset($this->cache[$objectClass][$interface])) {
+ return $this->cache[$objectClass][$interface];
+ }
+
+ $closestImpl = null;
+ foreach ($this->services as $impl) {
+ if(!\is_subclass_of($impl, $interface)) continue;
+ foreach ($impl->getSupportedTypes() as $supportedClass) {
+ if ($objectClass === $supportedClass || \is_subclass_of($object, $supportedClass)) {
+ $dist = $this->parentDistance($objectClass, $supportedClass);
+ if ($closestImpl === null || $closestImpl[0] > $dist) {
+ $closestImpl = [$dist, $impl];
+ }
+ }
+ }
+ }
+
+ if($closestImpl === null){
+ throw new \LogicException('service '.$interface.' was requested for '.\get_class($object).' but no implementation is available');
+ }
+
+ $this->cache[$objectClass][$interface] = $closestImpl[1];
+ return $closestImpl[1];
+ }
+
+ private function parentDistance(string $class, string $target): int {
+ $cur = $class;
+ $distance = 0;
+ while($cur !== $target){
+ $cur = \get_parent_class($cur);
+ if($cur === false) {
+ throw new \LogicException($class.' is not a subclass of '.$target);
+ }
+ }
+ return $distance;
+ }
+}
diff --git a/src/Twig/Extension/SubjectExtension.php b/src/Twig/Extension/SubjectExtension.php
index d3ddf165b5..dd0a85ac2e 100644
--- a/src/Twig/Extension/SubjectExtension.php
+++ b/src/Twig/Extension/SubjectExtension.php
@@ -7,6 +7,7 @@
use App\Entity\Entry;
use App\Entity\EntryComment;
use App\Entity\Magazine;
+use App\Entity\Message;
use App\Entity\Post;
use App\Entity\PostComment;
use App\Entity\User;
@@ -48,6 +49,11 @@ public function getTests(): array
return $subject instanceof User;
}
),
+ new TwigTest(
+ 'message', function ($subject) {
+ return $subject instanceof Message;
+ }
+ ),
];
}
}
diff --git a/src/Twig/Extension/UrlExtension.php b/src/Twig/Extension/UrlExtension.php
index db38bc44e6..cac17c2dc0 100644
--- a/src/Twig/Extension/UrlExtension.php
+++ b/src/Twig/Extension/UrlExtension.php
@@ -38,6 +38,7 @@ public function getFunctions(): array
new TwigFunction('post_comment_voters_url', [UrlExtensionRuntime::class, 'postCommentVotersUrl']),
new TwigFunction('post_comment_favourites_url', [UrlExtensionRuntime::class, 'postCommentFavouritesUrl']),
new TwigFunction('post_comment_delete_url', [UrlExtensionRuntime::class, 'postCommentDeleteUrl']),
+ new TwigFunction('message_url', [UrlExtensionRuntime::class, 'messageUrl']),
new TwigFunction('options_url', [UrlExtensionRuntime::class, 'optionsUrl']),
new TwigFunction('mention_url', [UrlExtensionRuntime::class, 'mentionUrl']),
];
diff --git a/src/Twig/Runtime/UrlExtensionRuntime.php b/src/Twig/Runtime/UrlExtensionRuntime.php
index d8ee916ee3..7f49329b04 100644
--- a/src/Twig/Runtime/UrlExtensionRuntime.php
+++ b/src/Twig/Runtime/UrlExtensionRuntime.php
@@ -6,9 +6,12 @@
use App\Entity\Entry;
use App\Entity\EntryComment;
+use App\Entity\Message;
use App\Entity\Post;
use App\Entity\PostComment;
+use App\Factory\Contract\ContentUrlFactory;
use App\Service\MentionManager;
+use App\Service\SwitchingServiceRegistry;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Extension\RuntimeExtensionInterface;
@@ -17,6 +20,7 @@ class UrlExtensionRuntime implements RuntimeExtensionInterface
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
+ private readonly SwitchingServiceRegistry $serviceRegistry,
private readonly RequestStack $requestStack,
private readonly MentionManager $mentionManager,
) {
@@ -24,11 +28,7 @@ public function __construct(
public function entryUrl(Entry $entry): string
{
- return $this->urlGenerator->generate('entry_single', [
- 'magazine_name' => $entry->magazine->name,
- 'entry_id' => $entry->getId(),
- 'slug' => empty($entry->slug) ? '-' : $entry->slug,
- ]);
+ return $this->serviceRegistry->getService($entry, ContentUrlFactory::class)->getLocalUrl($entry);
}
public function entryFavouritesUrl(Entry $entry): string
@@ -89,12 +89,7 @@ public function entryCommentCreateUrl(EntryComment $comment): string
public function entryCommentViewUrl(EntryComment $comment): string
{
- return $this->urlGenerator->generate('entry_comment_view', [
- 'magazine_name' => $comment->magazine->name,
- 'entry_id' => $comment->entry->getId(),
- 'slug' => empty($comment->entry->slug) ? '-' : $comment->entry->slug,
- 'comment_id' => $comment->getId(),
- ]);
+ return $this->serviceRegistry->getService($comment, ContentUrlFactory::class)->getLocalUrl($comment);
}
public function entryCommentEditUrl(EntryComment $comment): string
@@ -150,11 +145,7 @@ public function entryCommentModerateUrl(EntryComment $comment): string
public function postUrl(Post $post): string
{
- return $this->urlGenerator->generate('post_single', [
- 'magazine_name' => $post->magazine->name,
- 'post_id' => $post->getId(),
- 'slug' => empty($post->slug) ? '-' : $post->slug,
- ]);
+ return $this->serviceRegistry->getService($post, ContentUrlFactory::class)->getLocalUrl($post);
}
public function postEditUrl(Post $post): string
@@ -263,6 +254,10 @@ public function postCommentDeleteUrl(PostComment $comment): string
]);
}
+ public function messageUrl(Message $message): string {
+ return $this->serviceRegistry->getService($message, ContentUrlFactory::class)->getLocalUrl($message);
+ }
+
// $additionalParams indicates extra parameters to set in addition to [$name] = $value
// Set $value to null to indicate deleting a parameter
// TODO: It'd be better to have just a single $params which is an associative array
diff --git a/templates/components/report_list.html.twig b/templates/components/report_list.html.twig
index 77874f958b..58d0324d38 100644
--- a/templates/components/report_list.html.twig
+++ b/templates/components/report_list.html.twig
@@ -34,6 +34,12 @@
{% for report in reports %}
+ {% if report.subject is message %}
+ {% set routePrefix = 'message_report' %}
+ {% else %}
+ {% set routePrefix = 'magazine_panel_report' %}
+ {% endif %}
+
{{ component('user_inline', {user: report.reporting, showNewIcon: true}) }},
@@ -52,7 +58,7 @@
{% if report.status is not same as REPORT_CLOSED %}
{% if report.status is not same as REPORT_REJECTED %}
{% endif %}
{% endif %}
+ {% if report.subject.magazine is not same as null %}
{{ 'ban'|trans }} ({{ report.reported.username|username(true) }})
+ {% endif %}
{% endfor %}
diff --git a/templates/layout/_subject_link.html.twig b/templates/layout/_subject_link.html.twig
index 5ca9781994..5dc1690aac 100644
--- a/templates/layout/_subject_link.html.twig
+++ b/templates/layout/_subject_link.html.twig
@@ -6,4 +6,6 @@
{{ subject.shortTitle }}
{%- elseif subject is post_comment -%}
{{ subject.shortTitle }}
+{%- elseif subject is message -%}
+ {{ subject.shortTitle }}
{%- endif -%}
diff --git a/templates/messages/front.html.twig b/templates/messages/front.html.twig
index ff2c1ef0f9..4220b851a1 100644
--- a/templates/messages/front.html.twig
+++ b/templates/messages/front.html.twig
@@ -41,7 +41,15 @@
+
{{ component('date', {date: thread.updatedAt}) }}
+
+
{% endif %}
{% endfor %}
diff --git a/templates/messages/reports.html.twig b/templates/messages/reports.html.twig
new file mode 100644
index 0000000000..50e04eb3e7
--- /dev/null
+++ b/templates/messages/reports.html.twig
@@ -0,0 +1,18 @@
+{% extends 'base.html.twig' %}
+
+{%- block title -%}
+ {{- 'reports'|trans }} - {{ parent() -}}
+{%- endblock -%}
+
+{% block mainClass %}page-magazine-reports{% endblock %}
+
+{% block header_nav %}
+{% endblock %}
+
+{% block sidebar_top %}
+{% endblock %}
+
+{% block body %}
+ {{ 'reports'|trans }}
+ {{ component('report_list', {reports: reports, routeName: 'message_reports'}) }}
+{% endblock %}
diff --git a/templates/messages/single.html.twig b/templates/messages/single.html.twig
index 354ad13fc3..7e8908e729 100644
--- a/templates/messages/single.html.twig
+++ b/templates/messages/single.html.twig
@@ -57,6 +57,13 @@
({{ 'edited'|trans }} {{ component('date', {date: message.editedAt}) }})
{% endif %}
+
+
{% endfor %}
diff --git a/templates/notifications/_blocks.html.twig b/templates/notifications/_blocks.html.twig
index 907ac64bf7..c2c2c53773 100644
--- a/templates/notifications/_blocks.html.twig
+++ b/templates/notifications/_blocks.html.twig
@@ -140,14 +140,21 @@
{{ postComment.getShortTitle() }}
+ {% elseif notification.report.message is defined and notification.report.message is not same as null %}
+ {% set message = notification.report.message %}
+ {{ message.shortTitle }}
{% endif %}
{% endblock %}
{% block report_created_notification %}
{{ component('user_inline', {user: notification.report.reporting, showNewIcon: true}) }} {{ 'reported'|trans|lower }} {{ component('user_inline', {user: notification.report.reported, showNewIcon: true}) }}
{{ 'report_subject'|trans }}: {{ block('reportlink') }}
- {% if app.user.admin or app.user.moderator or notification.report.magazine.userIsModerator(app.user) %}
- {{ 'open_report'|trans }}
+ {% if app.user.admin or app.user.moderator or (notification.report.magazine is not same as null and notification.report.magazine.userIsModerator(app.user)) %}
+ {% if notification.report.subject is message %}
+ {{ 'open_report'|trans }}
+ {% else %}
+ {{ 'open_report'|trans }}
+ {% endif %}
{% endif %}
{% endblock report_created_notification %}
@@ -155,8 +162,12 @@
{{ 'own_report_rejected'|trans }}
{{ 'reported_user'|trans }}: {{ component('user_inline', {user: notification.report.reported, showNewIcon: true}) }}
{{ 'report_subject'|trans }}: {{ block('reportlink') }}
- {% if app.user.admin or app.user.moderator or notification.report.magazine.userIsModerator(app.user) %}
- {{ 'open_report'|trans }}
+ {% if app.user.admin or app.user.moderator or (notification.report.magazine is not same as null and notification.report.magazine.userIsModerator(app.user)) %}
+ {% if notification.report.subject is message %}
+ {{ 'open_report'|trans }}
+ {% else %}
+ {{ 'open_report'|trans }}
+ {% endif %}
{% endif %}
{% endblock report_rejected_notification %}
@@ -171,8 +182,12 @@
{{ 'reporting_user'|trans }}: {{ component('user_inline', {user: notification.report.reported, showNewIcon: true}) }}
{% endif %}
{{ 'report_subject'|trans }}: {{ block('reportlink') }}
- {% if app.user.admin or app.user.moderator or notification.report.magazine.userIsModerator(app.user) %}
- {{ 'open_report'|trans }}
+ {% if app.user.admin or app.user.moderator or (notification.report.magazine is not same as null and notification.report.magazine.userIsModerator(app.user)) %}
+ {% if notification.report.subject is message %}
+ {{ 'open_report'|trans }}
+ {% else %}
+ {{ 'open_report'|trans }}
+ {% endif %}
{% endif %}
{% endblock report_approved_notification %}
diff --git a/tests/Functional/Controller/Api/Message/MessageRemoveApiTest.php b/tests/Functional/Controller/Api/Message/MessageRemoveApiTest.php
new file mode 100644
index 0000000000..be662dcd35
--- /dev/null
+++ b/tests/Functional/Controller/Api/Message/MessageRemoveApiTest.php
@@ -0,0 +1,81 @@
+createMessage($this->getUserByUsername('JohnDoe'), $this->getUserByUsername('JaneDoe'), 'test message');
+ $this->client->request('DELETE', "/api/messages/thread/{$message->thread->getId()}");
+ self::assertResponseStatusCodeSame(401);
+ }
+
+ public function testApiCannotRemoveMessagesWithoutScope(): void
+ {
+ self::createOAuth2AuthCodeClient();
+ $user = $this->getUserByUsername('JohnDoe');
+ $this->client->loginUser($user);
+
+ $codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'read');
+ $token = $codes['token_type'].' '.$codes['access_token'];
+
+ $from = $this->getUserByUsername('JaneDoe');
+ $user = $this->entityManager->getRepository(User::class)->find($user->getId());
+ $message = $this->createMessage($user, $from, 'test message');
+
+ $this->client->request('DELETE', "/api/messages/thread/{$message->thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
+ self::assertResponseStatusCodeSame(403);
+ }
+
+ public function testApiCanRemoveThread(): void
+ {
+ self::createOAuth2AuthCodeClient();
+ $user = $this->getUserByUsername('JohnDoe');
+ $this->client->loginUser($user);
+
+ $codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'user:message:delete user:message:read');
+ $token = $codes['token_type'].' '.$codes['access_token'];
+
+ $from = $this->getUserByUsername('JaneDoe');
+ $user = $this->entityManager->getRepository(User::class)->find($user->getId());
+ $message = $this->createMessage($user, $from, 'test message');
+
+ $this->client->request('DELETE', "/api/messages/thread/{$message->thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
+ self::assertResponseStatusCodeSame(204);
+
+ $this->client->request('GET', "/api/messages/thread/{$message->thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
+ self::assertResponseStatusCodeSame(403);
+ }
+
+ public function testRemovedThreadIgnoresNewMessages(): void
+ {
+ self::createOAuth2AuthCodeClient();
+ $user = $this->getUserByUsername('JohnDoe');
+ $this->client->loginUser($user);
+
+ $codes = self::getAuthorizationCodeTokenResponse($this->client, scopes: 'user:message:delete user:message:read');
+ $token = $codes['token_type'].' '.$codes['access_token'];
+
+ $from = $this->getUserByUsername('JaneDoe');
+ $user = $this->entityManager->getRepository(User::class)->find($user->getId());
+ $message = $this->createMessage($user, $from, 'test message');
+
+ $this->client->request('DELETE', "/api/messages/thread/{$message->thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
+ self::assertResponseStatusCodeSame(204);
+
+ $messageDto = new MessageDto();
+ $messageDto->body = 'test message';
+ $message2 = $this->messageManager->toMessage($messageDto, $message->thread, $from);
+ self::assertSame($message->thread->getId(), $message2->thread->getId());
+
+ $this->client->request('GET', "/api/messages/thread/{$message->thread->getId()}", server: ['HTTP_AUTHORIZATION' => $token]);
+ self::assertResponseStatusCodeSame(403);
+ }
+}