Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config/mbin_routes/message.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion config/mbin_routes/message_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
format: json
22 changes: 22 additions & 0 deletions config/mbin_routes/message_reports.yaml
Original file line number Diff line number Diff line change
@@ -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+
1 change: 1 addition & 0 deletions config/packages/league_oauth2_server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions config/packages/nelmio_api_doc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -259,3 +264,7 @@ services:
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
arguments:
- '%env(DATABASE_URL)%'

App\Service\SwitchingServiceRegistry:
arguments:
- !tagged 'switchable_service'
2 changes: 2 additions & 0 deletions docs/04-app_developers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 38 additions & 0 deletions migrations/Version20260311182316.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

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

final class Version20260311182316 extends AbstractMigration
{
public function getDescription(): string
{
return 'support reporting messages';
}

public function up(Schema $schema): void
{
$this->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');
}
}
2 changes: 1 addition & 1 deletion src/Controller/ActivityPub/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
1 change: 1 addition & 0 deletions src/Controller/Admin/AdminReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
78 changes: 78 additions & 0 deletions src/Controller/Api/Message/MessageRemoveApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace App\Controller\Api\Message;

use App\Controller\Traits\PrivateContentTrait;
use App\Entity\MessageThread;
use App\Service\MessageManager;
use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Attribute\Security;
use OpenApi\Attributes as OA;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class MessageRemoveApi extends MessageBaseApi
{
use PrivateContentTrait;

#[OA\Response(
response: 204,
description: 'The thread was deleted for the user',
headers: [
new OA\Header(header: 'X-RateLimit-Remaining', schema: new OA\Schema(type: 'integer'), description: 'Number of requests left until you will be rate limited'),
new OA\Header(header: 'X-RateLimit-Retry-After', schema: new OA\Schema(type: 'integer'), description: 'Unix timestamp to retry the request after'),
new OA\Header(header: 'X-RateLimit-Limit', schema: new OA\Schema(type: 'integer'), description: 'Number of requests available'),
]
)]
#[OA\Response(
response: 401,
description: 'Permission denied due to missing or expired token',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\UnauthorizedErrorSchema::class))
)]
#[OA\Response(
response: 403,
description: 'You are not allowed to view the messages in this thread',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\ForbiddenErrorSchema::class))
)]
#[OA\Response(
response: 404,
description: 'Page not found',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\NotFoundErrorSchema::class))
)]
#[OA\Response(
response: 429,
description: 'You are being rate limited',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\TooManyRequestsErrorSchema::class)),
headers: [
new OA\Header(header: 'X-RateLimit-Remaining', schema: new OA\Schema(type: 'integer'), description: 'Number of requests left until you will be rate limited'),
new OA\Header(header: 'X-RateLimit-Retry-After', schema: new OA\Schema(type: 'integer'), description: 'Unix timestamp to retry the request after'),
new OA\Header(header: 'X-RateLimit-Limit', schema: new OA\Schema(type: 'integer'), description: 'Number of requests available'),
]
)]
#[OA\Parameter(
name: 'thread_id',
description: 'Thread from which to retrieve messages',
in: 'path',
schema: new OA\Schema(type: 'integer')
)]
#[OA\Tag(name: 'message')]
#[Security(name: 'oauth2', scopes: ['user:message:read'])]
#[IsGranted('ROLE_OAUTH2_USER:MESSAGE:DELETE')]
#[IsGranted('show', subject: 'thread', statusCode: 403)]
public function removeThread(
#[MapEntity(id: 'thread_id')]
MessageThread $thread,
MessageManager $manager,
RateLimiterFactoryInterface $apiReadLimiter,
): Response {
$headers = $this->rateLimit($apiReadLimiter);

$manager->removeUserFromThread($thread, $this->getUserOrThrow());

return new Response(status: 204, headers: $headers);
}
}
83 changes: 83 additions & 0 deletions src/Controller/Api/Message/MessageReportApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace App\Controller\Api\Message;

use App\Controller\Api\Post\PostsBaseApi;
use App\Controller\Traits\PrivateContentTrait;
use App\DTO\ReportRequestDto;
use App\Entity\Message;
use App\Entity\Post;
use Nelmio\ApiDocBundle\Attribute\Model;
use Nelmio\ApiDocBundle\Attribute\Security;
use OpenApi\Attributes as OA;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class MessageReportApi extends PostsBaseApi
{
use PrivateContentTrait;

#[OA\Response(
response: 204,
description: 'Report created',
content: null,
headers: [
new OA\Header(header: 'X-RateLimit-Remaining', schema: new OA\Schema(type: 'integer'), description: 'Number of requests left until you will be rate limited'),
new OA\Header(header: 'X-RateLimit-Retry-After', schema: new OA\Schema(type: 'integer'), description: 'Unix timestamp to retry the request after'),
new OA\Header(header: 'X-RateLimit-Limit', schema: new OA\Schema(type: 'integer'), description: 'Number of requests available'),
]
)]
#[OA\Response(
response: 401,
description: 'Permission denied due to missing or expired token',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\UnauthorizedErrorSchema::class))
)]
#[OA\Response(
response: 403,
description: 'You have not been authorized to report this post',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\ForbiddenErrorSchema::class))
)]
#[OA\Response(
response: 404,
description: 'Post not found',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\NotFoundErrorSchema::class))
)]
#[OA\Response(
response: 429,
description: 'You are being rate limited',
content: new OA\JsonContent(ref: new Model(type: \App\Schema\Errors\TooManyRequestsErrorSchema::class)),
headers: [
new OA\Header(header: 'X-RateLimit-Remaining', schema: new OA\Schema(type: 'integer'), description: 'Number of requests left until you will be rate limited'),
new OA\Header(header: 'X-RateLimit-Retry-After', schema: new OA\Schema(type: 'integer'), description: 'Unix timestamp to retry the request after'),
new OA\Header(header: 'X-RateLimit-Limit', schema: new OA\Schema(type: 'integer'), description: 'Number of requests available'),
]
)]
#[OA\Parameter(
name: 'post_id',
in: 'path',
description: 'The post to report',
schema: new OA\Schema(type: 'integer')
)]
#[OA\RequestBody(content: new Model(type: ReportRequestDto::class))]
#[OA\Tag(name: 'post')]
#[Security(name: 'oauth2', scopes: ['post:report'])]
#[IsGranted('ROLE_OAUTH2_POST:REPORT')]
public function __invoke(
#[MapEntity(id: 'message_id')]
Message $message,
RateLimiterFactoryInterface $apiReportLimiter,
): JsonResponse {
$headers = $this->rateLimit($apiReportLimiter);

$this->reportContent($message);

return new JsonResponse(
status: 204,
headers: $headers
);
}
}
Loading
Loading