Implement creator studio and upload updates
This commit is contained in:
200
app/Services/Moderation/DomainReputationService.php
Normal file
200
app/Services/Moderation/DomainReputationService.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Moderation;
|
||||
|
||||
use App\Enums\ModerationActionType;
|
||||
use App\Enums\ModerationDomainStatus;
|
||||
use App\Models\ContentModerationActionLog;
|
||||
use App\Models\ContentModerationDomain;
|
||||
use App\Models\ContentModerationFinding;
|
||||
use App\Models\User;
|
||||
|
||||
class DomainReputationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DomainIntelligenceService $intelligence,
|
||||
) {
|
||||
}
|
||||
|
||||
public function normalizeDomain(?string $domain): ?string
|
||||
{
|
||||
if (! is_string($domain)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = trim(mb_strtolower($domain));
|
||||
$normalized = preg_replace('/^www\./', '', $normalized);
|
||||
|
||||
return $normalized !== '' ? $normalized : null;
|
||||
}
|
||||
|
||||
public function statusForDomain(string $domain): ModerationDomainStatus
|
||||
{
|
||||
$normalized = $this->normalizeDomain($domain);
|
||||
if ($normalized === null) {
|
||||
return ModerationDomainStatus::Neutral;
|
||||
}
|
||||
|
||||
$record = ContentModerationDomain::query()->where('domain', $normalized)->first();
|
||||
if ($record !== null) {
|
||||
return $record->status;
|
||||
}
|
||||
|
||||
if ($this->matchesAnyPattern($normalized, (array) \app('config')->get('content_moderation.allowed_domains', []))) {
|
||||
return ModerationDomainStatus::Allowed;
|
||||
}
|
||||
|
||||
if ($this->matchesAnyPattern($normalized, (array) \app('config')->get('content_moderation.blacklisted_domains', []))) {
|
||||
return ModerationDomainStatus::Blocked;
|
||||
}
|
||||
|
||||
if ($this->matchesAnyPattern($normalized, (array) \app('config')->get('content_moderation.suspicious_domains', []))) {
|
||||
return ModerationDomainStatus::Suspicious;
|
||||
}
|
||||
|
||||
return ModerationDomainStatus::Neutral;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $domains
|
||||
* @return array<int, ContentModerationDomain>
|
||||
*/
|
||||
public function trackDomains(array $domains, bool $flagged = false, bool $confirmedSpam = false): array
|
||||
{
|
||||
$normalized = \collect($domains)
|
||||
->map(fn (?string $domain): ?string => $this->normalizeDomain($domain))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($normalized->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$existing = ContentModerationDomain::query()
|
||||
->whereIn('domain', $normalized->all())
|
||||
->get()
|
||||
->keyBy('domain');
|
||||
|
||||
$records = [];
|
||||
$now = \now();
|
||||
|
||||
foreach ($normalized as $domain) {
|
||||
$defaultStatus = $this->statusForDomain($domain);
|
||||
$record = $existing[$domain] ?? new ContentModerationDomain([
|
||||
'domain' => $domain,
|
||||
'status' => $defaultStatus,
|
||||
'first_seen_at' => $now,
|
||||
]);
|
||||
|
||||
$record->forceFill([
|
||||
'status' => $record->status ?? $defaultStatus,
|
||||
'times_seen' => ((int) $record->times_seen) + 1,
|
||||
'times_flagged' => ((int) $record->times_flagged) + ($flagged ? 1 : 0),
|
||||
'times_confirmed_spam' => ((int) $record->times_confirmed_spam) + ($confirmedSpam ? 1 : 0),
|
||||
'first_seen_at' => $record->first_seen_at ?? $now,
|
||||
'last_seen_at' => $now,
|
||||
])->save();
|
||||
|
||||
$records[] = $record->fresh();
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
public function updateStatus(string $domain, ModerationDomainStatus $status, ?User $actor = null, ?string $notes = null): ContentModerationDomain
|
||||
{
|
||||
$normalized = $this->normalizeDomain($domain);
|
||||
\abort_unless($normalized !== null, 422, 'Invalid domain.');
|
||||
|
||||
$record = ContentModerationDomain::query()->firstOrNew(['domain' => $normalized]);
|
||||
$previous = $record->status?->value;
|
||||
|
||||
$record->forceFill([
|
||||
'status' => $status,
|
||||
'first_seen_at' => $record->first_seen_at ?? \now(),
|
||||
'last_seen_at' => \now(),
|
||||
'notes' => $notes !== null && trim($notes) !== '' ? trim($notes) : $record->notes,
|
||||
])->save();
|
||||
|
||||
ContentModerationActionLog::query()->create([
|
||||
'target_type' => 'domain',
|
||||
'target_id' => $record->id,
|
||||
'action_type' => match ($status) {
|
||||
ModerationDomainStatus::Blocked => ModerationActionType::BlockDomain->value,
|
||||
ModerationDomainStatus::Suspicious => ModerationActionType::MarkDomainSuspicious->value,
|
||||
ModerationDomainStatus::Escalated => ModerationActionType::Escalate->value,
|
||||
ModerationDomainStatus::ReviewRequired => ModerationActionType::MarkDomainSuspicious->value,
|
||||
ModerationDomainStatus::Allowed, ModerationDomainStatus::Neutral => ModerationActionType::AllowDomain->value,
|
||||
},
|
||||
'actor_type' => $actor ? 'admin' : 'system',
|
||||
'actor_id' => $actor?->id,
|
||||
'notes' => $notes,
|
||||
'old_status' => $previous,
|
||||
'new_status' => $status->value,
|
||||
'meta_json' => ['domain' => $normalized],
|
||||
'created_at' => \now(),
|
||||
]);
|
||||
|
||||
$this->intelligence->refreshDomain($normalized);
|
||||
|
||||
return $record->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function shortenerDomains(): array
|
||||
{
|
||||
return \collect((array) \app('config')->get('content_moderation.shortener_domains', []))
|
||||
->map(fn (string $domain): ?string => $this->normalizeDomain($domain))
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function attachDomainIds(ContentModerationFinding $finding): void
|
||||
{
|
||||
$domains = \collect((array) $finding->matched_domains_json)
|
||||
->map(fn (?string $domain): ?string => $this->normalizeDomain($domain))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($domains->isEmpty()) {
|
||||
$finding->forceFill(['domain_ids_json' => []])->save();
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = ContentModerationDomain::query()
|
||||
->whereIn('domain', $domains->all())
|
||||
->pluck('id')
|
||||
->map(static fn (int $id): int => $id)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$finding->forceFill(['domain_ids_json' => $ids])->save();
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$this->intelligence->refreshDomain((string) $domain);
|
||||
}
|
||||
}
|
||||
|
||||
private function matchesAnyPattern(string $domain, array $patterns): bool
|
||||
{
|
||||
foreach ($patterns as $pattern) {
|
||||
$pattern = trim(mb_strtolower((string) $pattern));
|
||||
if ($pattern === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quoted = preg_quote($pattern, '/');
|
||||
$regex = '/^' . str_replace('\\*', '.*', $quoted) . '$/i';
|
||||
if (preg_match($regex, $domain) === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user