optimizations
This commit is contained in:
231
app/Services/NovaCards/NovaCardPublishModerationService.php
Normal file
231
app/Services/NovaCards/NovaCardPublishModerationService.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\NovaCards;
|
||||
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\User;
|
||||
|
||||
class NovaCardPublishModerationService
|
||||
{
|
||||
private const REASON_LABELS = [
|
||||
'duplicate_content' => 'Duplicate content',
|
||||
'self_remix_loop' => 'Self-remix loop',
|
||||
];
|
||||
|
||||
public const DISPOSITION_LABELS = [
|
||||
'cleared_after_review' => 'Cleared after review',
|
||||
'approved_with_watch' => 'Approved with watch',
|
||||
'escalated_for_review' => 'Escalated for review',
|
||||
'rights_review_required' => 'Rights review required',
|
||||
'rejected_after_review' => 'Rejected after review',
|
||||
'returned_to_pending' => 'Returned to pending',
|
||||
];
|
||||
|
||||
public function evaluate(NovaCard $card): array
|
||||
{
|
||||
$card->loadMissing(['originalCard.user', 'rootCard.user']);
|
||||
|
||||
$reasons = [];
|
||||
|
||||
if ($this->hasDuplicateContent($card)) {
|
||||
$reasons[] = 'duplicate_content';
|
||||
}
|
||||
|
||||
if ($this->hasSelfRemixLoop($card)) {
|
||||
$reasons[] = 'self_remix_loop';
|
||||
}
|
||||
|
||||
return [
|
||||
'flagged' => $reasons !== [],
|
||||
'reasons' => $reasons,
|
||||
];
|
||||
}
|
||||
|
||||
public function moderationStatus(NovaCard $card): string
|
||||
{
|
||||
return $this->evaluate($card)['flagged'] ? NovaCard::MOD_FLAGGED : NovaCard::MOD_APPROVED;
|
||||
}
|
||||
|
||||
public function applyPublishOutcome(NovaCard $card, array $evaluation): NovaCard
|
||||
{
|
||||
$project = (array) ($card->project_json ?? []);
|
||||
$project['moderation'] = [
|
||||
'source' => 'publish_heuristics',
|
||||
'flagged' => (bool) ($evaluation['flagged'] ?? false),
|
||||
'reasons' => $this->normalizeReasons($evaluation['reasons'] ?? []),
|
||||
'updated_at' => now()->toISOString(),
|
||||
];
|
||||
|
||||
$card->forceFill([
|
||||
'project_json' => $project,
|
||||
'status' => NovaCard::STATUS_PUBLISHED,
|
||||
'moderation_status' => (bool) ($evaluation['flagged'] ?? false) ? NovaCard::MOD_FLAGGED : NovaCard::MOD_APPROVED,
|
||||
])->save();
|
||||
|
||||
return $card->refresh();
|
||||
}
|
||||
|
||||
public function storedReasons(NovaCard $card): array
|
||||
{
|
||||
return $this->normalizeReasons(((array) (($card->project_json ?? [])['moderation'] ?? []))['reasons'] ?? []);
|
||||
}
|
||||
|
||||
public function storedReasonLabels(NovaCard $card): array
|
||||
{
|
||||
return $this->labelsFor($this->storedReasons($card));
|
||||
}
|
||||
|
||||
public function storedSource(NovaCard $card): ?string
|
||||
{
|
||||
$source = ((array) (($card->project_json ?? [])['moderation'] ?? []))['source'] ?? null;
|
||||
|
||||
return is_string($source) && $source !== '' ? $source : null;
|
||||
}
|
||||
|
||||
public function latestOverride(NovaCard $card): ?array
|
||||
{
|
||||
$override = ((array) (($card->project_json ?? [])['moderation'] ?? []))['override'] ?? null;
|
||||
|
||||
return is_array($override) && $override !== [] ? $override : null;
|
||||
}
|
||||
|
||||
public function dispositionOptions(?string $moderationStatus = null): array
|
||||
{
|
||||
$keys = match ($moderationStatus) {
|
||||
NovaCard::MOD_APPROVED => ['cleared_after_review', 'approved_with_watch'],
|
||||
NovaCard::MOD_FLAGGED => ['escalated_for_review', 'rights_review_required'],
|
||||
NovaCard::MOD_REJECTED => ['rejected_after_review'],
|
||||
NovaCard::MOD_PENDING => ['returned_to_pending'],
|
||||
default => array_keys(self::DISPOSITION_LABELS),
|
||||
};
|
||||
|
||||
return array_values(array_map(fn (string $key): array => [
|
||||
'value' => $key,
|
||||
'label' => self::DISPOSITION_LABELS[$key] ?? ucwords(str_replace('_', ' ', $key)),
|
||||
], $keys));
|
||||
}
|
||||
|
||||
public function overrideHistory(NovaCard $card): array
|
||||
{
|
||||
$history = ((array) (($card->project_json ?? [])['moderation'] ?? []))['override_history'] ?? [];
|
||||
|
||||
return array_values(array_filter($history, fn ($entry): bool => is_array($entry) && $entry !== []));
|
||||
}
|
||||
|
||||
public function recordStaffOverride(
|
||||
NovaCard $card,
|
||||
string $moderationStatus,
|
||||
?User $actor,
|
||||
string $source,
|
||||
array $context = [],
|
||||
): NovaCard {
|
||||
$project = (array) ($card->project_json ?? []);
|
||||
$moderation = (array) ($project['moderation'] ?? []);
|
||||
$disposition = $this->normalizeDisposition(
|
||||
$context['disposition'] ?? $this->defaultDispositionForStatus($moderationStatus),
|
||||
$moderationStatus,
|
||||
);
|
||||
$override = array_filter([
|
||||
'moderation_status' => $moderationStatus,
|
||||
'previous_status' => (string) $card->moderation_status,
|
||||
'disposition' => $disposition,
|
||||
'disposition_label' => self::DISPOSITION_LABELS[$disposition] ?? ucwords(str_replace('_', ' ', $disposition)),
|
||||
'source' => $source,
|
||||
'actor_user_id' => $actor?->id,
|
||||
'actor_username' => $actor?->username,
|
||||
'note' => isset($context['note']) && is_string($context['note']) && trim($context['note']) !== '' ? trim($context['note']) : null,
|
||||
'report_id' => isset($context['report_id']) ? (int) $context['report_id'] : null,
|
||||
'updated_at' => now()->toISOString(),
|
||||
], fn ($value): bool => $value !== null);
|
||||
|
||||
$history = $this->overrideHistory($card);
|
||||
array_unshift($history, $override);
|
||||
|
||||
$moderation['override'] = $override;
|
||||
$moderation['override_history'] = array_slice($history, 0, 10);
|
||||
$project['moderation'] = $moderation;
|
||||
|
||||
$card->forceFill([
|
||||
'moderation_status' => $moderationStatus,
|
||||
'project_json' => $project,
|
||||
])->save();
|
||||
|
||||
return $card->refresh();
|
||||
}
|
||||
|
||||
public function labelsFor(array $reasons): array
|
||||
{
|
||||
return array_values(array_map(
|
||||
fn (string $reason): string => self::REASON_LABELS[$reason] ?? ucwords(str_replace('_', ' ', $reason)),
|
||||
$this->normalizeReasons($reasons),
|
||||
));
|
||||
}
|
||||
|
||||
private function normalizeReasons(array $reasons): array
|
||||
{
|
||||
return array_values(array_unique(array_filter(array_map(
|
||||
fn ($reason): string => is_string($reason) ? trim($reason) : '',
|
||||
$reasons,
|
||||
))));
|
||||
}
|
||||
|
||||
private function normalizeDisposition(mixed $disposition, string $moderationStatus): string
|
||||
{
|
||||
$value = is_string($disposition) ? trim($disposition) : '';
|
||||
|
||||
return $value !== '' && array_key_exists($value, self::DISPOSITION_LABELS)
|
||||
? $value
|
||||
: $this->defaultDispositionForStatus($moderationStatus);
|
||||
}
|
||||
|
||||
private function defaultDispositionForStatus(string $moderationStatus): string
|
||||
{
|
||||
return match ($moderationStatus) {
|
||||
NovaCard::MOD_APPROVED => 'cleared_after_review',
|
||||
NovaCard::MOD_FLAGGED => 'escalated_for_review',
|
||||
NovaCard::MOD_REJECTED => 'rejected_after_review',
|
||||
default => 'returned_to_pending',
|
||||
};
|
||||
}
|
||||
|
||||
private function hasDuplicateContent(NovaCard $card): bool
|
||||
{
|
||||
$title = mb_strtolower(trim((string) $card->title));
|
||||
$quote = mb_strtolower(trim((string) $card->quote_text));
|
||||
|
||||
if ($title === '' || $quote === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return NovaCard::query()
|
||||
->where('id', '!=', $card->id)
|
||||
->where('status', NovaCard::STATUS_PUBLISHED)
|
||||
->whereNotIn('moderation_status', [NovaCard::MOD_FLAGGED, NovaCard::MOD_REJECTED])
|
||||
->whereRaw('LOWER(title) = ?', [$title])
|
||||
->whereRaw('LOWER(quote_text) = ?', [$quote])
|
||||
->exists();
|
||||
}
|
||||
|
||||
private function hasSelfRemixLoop(NovaCard $card): bool
|
||||
{
|
||||
if (! $card->originalCard || ! $card->rootCard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$depth = 0;
|
||||
$cursor = $card;
|
||||
$visited = [];
|
||||
|
||||
while ($cursor->originalCard && ! in_array($cursor->id, $visited, true)) {
|
||||
$visited[] = $cursor->id;
|
||||
$cursor = $cursor->originalCard;
|
||||
$depth++;
|
||||
}
|
||||
|
||||
return $depth >= 3
|
||||
&& (int) $card->user_id === (int) ($card->originalCard->user_id ?? 0)
|
||||
&& (int) $card->user_id === (int) ($card->rootCard->user_id ?? 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user