feat: moderation
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user