api/app/Services/ModerationReportService.php

139 lines
4.6 KiB
PHP

<?php
namespace App\Services;
use App\Enums\ModerationCaseStatus;
use App\Enums\ReportReason;
use App\Enums\ReportStatus;
use App\Enums\UserRole;
use App\Models\MealPosts;
use App\Models\ModerationCase;
use App\Models\Report;
use App\Models\User;
use App\Notifications\ModerationThresholdReached;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
class ModerationReportService
{
public function report(Model $reportable, User $reporter, ReportReason $reason, ?string $details): Report
{
$this->abortIfInvalidTarget($reportable, $reporter);
$shouldNotifyModerators = false;
try {
$report = DB::transaction(function () use ($reportable, $reporter, $reason, $details, &$shouldNotifyModerators): Report {
$moderationCase = $this->findOrCreateModerationCase($reportable);
abort_if(
Report::query()
->where('reporter_id', $reporter->getKey())
->where('reportable_type', $reportable::class)
->where('reportable_id', $reportable->getKey())
->exists(),
409,
__('api.reports.already_exists'),
);
$report = Report::create([
'moderation_case_id' => $moderationCase->getKey(),
'reporter_id' => $reporter->getKey(),
'reportable_type' => $reportable::class,
'reportable_id' => $reportable->getKey(),
'reason' => $reason,
'details' => $details,
'status' => ReportStatus::OPEN,
]);
$reportsCount = $moderationCase->reports()->count();
$updates = ['reports_count' => $reportsCount];
if ($reportsCount >= $moderationCase->threshold && $moderationCase->threshold_notified_at === null) {
$updates['threshold_notified_at'] = now();
$shouldNotifyModerators = true;
}
$moderationCase->forceFill($updates)->save();
return $report->load('moderationCase.caseable');
});
} catch (QueryException $exception) {
if ($this->isDuplicateReportException($exception)) {
abort(409, __('api.reports.already_exists'));
}
throw $exception;
}
if ($shouldNotifyModerators) {
$this->notifyModerators($report->moderationCase);
}
return $report;
}
private function findOrCreateModerationCase(Model $reportable): ModerationCase
{
$moderationCase = ModerationCase::query()
->where('caseable_type', $reportable::class)
->where('caseable_id', $reportable->getKey())
->lockForUpdate()
->first();
if ($moderationCase) {
return $moderationCase;
}
return ModerationCase::create([
'caseable_type' => $reportable::class,
'caseable_id' => $reportable->getKey(),
'status' => ModerationCaseStatus::OPEN,
'threshold' => $this->thresholdFor($reportable),
]);
}
private function thresholdFor(Model $reportable): int
{
$thresholds = config('moderation.report_thresholds', []);
return (int) ($thresholds[$reportable::class] ?? config('moderation.default_report_threshold', 3));
}
private function notifyModerators(ModerationCase $moderationCase): void
{
$freshCase = $moderationCase->fresh('caseable');
if (! $freshCase) {
return;
}
User::query()
->whereIn('role', [UserRole::ADMIN->value, UserRole::MODERATOR->value])
->get()
->each(fn (User $moderator): mixed => $moderator->notify(new ModerationThresholdReached($freshCase)));
}
private function abortIfInvalidTarget(Model $reportable, User $reporter): void
{
abort_if(
$reportable instanceof User && $reportable->is($reporter),
422,
__('api.reports.cannot_report_self'),
);
abort_if(
$reportable instanceof MealPosts && $reportable->user_id === $reporter->getKey(),
422,
__('api.reports.cannot_report_own_meal'),
);
}
private function isDuplicateReportException(QueryException $exception): bool
{
return $exception->getCode() === '23000'
&& str_contains($exception->getMessage(), 'reports_reporter_reportable_unique');
}
}