146 lines
5.0 KiB
PHP
146 lines
5.0 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 Filament\Notifications\Notification as FilamentNotification;
|
|
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()
|
|
->whenNotEmpty(fn ($moderators) => FilamentNotification::make()
|
|
->title(__('api.notifications.moderation_threshold_title'))
|
|
->body(__('api.notifications.moderation_threshold_body', [
|
|
'type' => $freshCase->targetLabel(),
|
|
'count' => $freshCase->reports_count,
|
|
]))
|
|
->warning()
|
|
->sendToDatabase($moderators, isEventDispatched: true));
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|