168 lines
5.5 KiB
PHP
168 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Enums\ModerationContentType;
|
|
use App\Enums\ModerationStatus;
|
|
use App\Models\ContentModerationFinding;
|
|
use App\Services\Moderation\ContentModerationActionLogService;
|
|
use App\Services\Moderation\ContentModerationProcessingService;
|
|
use App\Services\Moderation\ContentModerationSourceService;
|
|
use Illuminate\Console\Command;
|
|
|
|
class RescanContentModerationCommand extends Command
|
|
{
|
|
protected $signature = 'skinbase:rescan-content-moderation
|
|
{--only= : comments, descriptions, titles, bios, profile-links, collections, stories, cards, or a comma-separated list}
|
|
{--status=pending : Filter findings by moderation status}
|
|
{--limit= : Maximum number of findings to rescan}
|
|
{--from-id= : Start rescanning at or after this finding ID}
|
|
{--force : Rescan all matching findings, including already resolved findings}';
|
|
|
|
protected $description = 'Rescan existing moderation findings using the latest rules and scanner version.';
|
|
|
|
public function __construct(
|
|
private readonly ContentModerationProcessingService $processing,
|
|
private readonly ContentModerationSourceService $sources,
|
|
private readonly ContentModerationActionLogService $actionLogs,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
public function handle(): int
|
|
{
|
|
$limit = max(0, (int) ($this->option('limit') ?? 0));
|
|
$fromId = max(0, (int) ($this->option('from-id') ?? 0));
|
|
$status = trim((string) ($this->option('status') ?? 'pending'));
|
|
$force = (bool) $this->option('force');
|
|
$types = $this->selectedTypes();
|
|
|
|
$counts = [
|
|
'rescanned' => 0,
|
|
'updated' => 0,
|
|
'auto_hidden' => 0,
|
|
'missing_source' => 0,
|
|
];
|
|
|
|
$query = ContentModerationFinding::query()->orderBy('id');
|
|
|
|
if ($status !== '') {
|
|
$query->where('status', $status);
|
|
}
|
|
|
|
if ($fromId > 0) {
|
|
$query->where('id', '>=', $fromId);
|
|
}
|
|
|
|
if ($types !== []) {
|
|
$query->whereIn('content_type', array_map(static fn (ModerationContentType $type): string => $type->value, $types));
|
|
}
|
|
|
|
if (! $force) {
|
|
$query->where('status', ModerationStatus::Pending->value);
|
|
}
|
|
|
|
if ($limit > 0) {
|
|
$query->limit($limit);
|
|
}
|
|
|
|
$query->chunkById(100, function ($findings) use (&$counts): bool {
|
|
foreach ($findings as $finding) {
|
|
$counts['rescanned']++;
|
|
$rescanned = $this->processing->rescanFinding($finding, $this->sources);
|
|
|
|
if ($rescanned === null) {
|
|
$counts['missing_source']++;
|
|
continue;
|
|
}
|
|
|
|
$counts['updated']++;
|
|
if ($rescanned->is_auto_hidden) {
|
|
$counts['auto_hidden']++;
|
|
}
|
|
|
|
$this->actionLogs->log(
|
|
$rescanned,
|
|
'finding',
|
|
$rescanned->id,
|
|
'rescan',
|
|
null,
|
|
null,
|
|
$rescanned->status->value,
|
|
null,
|
|
null,
|
|
'Finding rescanned with the latest moderation rules.',
|
|
['scanner_version' => $rescanned->scanner_version],
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}, 'id');
|
|
|
|
$this->table(['Metric', 'Count'], [
|
|
['Rescanned', $counts['rescanned']],
|
|
['Updated', $counts['updated']],
|
|
['Auto-hidden', $counts['auto_hidden']],
|
|
['Missing source', $counts['missing_source']],
|
|
]);
|
|
|
|
$this->info('Content moderation rescan complete.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, ModerationContentType>
|
|
*/
|
|
private function selectedTypes(): array
|
|
{
|
|
$raw = trim((string) ($this->option('only') ?? ''));
|
|
if ($raw === '') {
|
|
return [];
|
|
}
|
|
|
|
$selected = \collect(explode(',', $raw))
|
|
->map(static fn (string $value): string => trim(strtolower($value)))
|
|
->filter()
|
|
->values();
|
|
|
|
$types = [];
|
|
|
|
if ($selected->contains('comments')) {
|
|
$types[] = ModerationContentType::ArtworkComment;
|
|
}
|
|
|
|
if ($selected->contains('descriptions')) {
|
|
$types[] = ModerationContentType::ArtworkDescription;
|
|
}
|
|
|
|
if ($selected->contains('titles') || $selected->contains('artwork_titles')) {
|
|
$types[] = ModerationContentType::ArtworkTitle;
|
|
}
|
|
|
|
if ($selected->contains('bios') || $selected->contains('user_bios')) {
|
|
$types[] = ModerationContentType::UserBio;
|
|
}
|
|
|
|
if ($selected->contains('profile-links') || $selected->contains('profile_links')) {
|
|
$types[] = ModerationContentType::UserProfileLink;
|
|
}
|
|
|
|
if ($selected->contains('collections') || $selected->contains('collection_titles')) {
|
|
$types[] = ModerationContentType::CollectionTitle;
|
|
$types[] = ModerationContentType::CollectionDescription;
|
|
}
|
|
|
|
if ($selected->contains('stories') || $selected->contains('story_titles')) {
|
|
$types[] = ModerationContentType::StoryTitle;
|
|
$types[] = ModerationContentType::StoryContent;
|
|
}
|
|
|
|
if ($selected->contains('cards') || $selected->contains('card_titles')) {
|
|
$types[] = ModerationContentType::CardTitle;
|
|
$types[] = ModerationContentType::CardText;
|
|
}
|
|
|
|
return $types;
|
|
}
|
|
} |