Implement creator studio and upload updates

This commit is contained in:
2026-04-04 10:12:02 +02:00
parent 1da7d3bf88
commit 0b216b7ecd
15107 changed files with 31206 additions and 626514 deletions

View File

@@ -0,0 +1,182 @@
<?php
namespace App\Services\Moderation;
use App\Data\Moderation\ModerationResultData;
use App\Enums\ModerationEscalationStatus;
use App\Enums\ModerationContentType;
use App\Enums\ModerationStatus;
use App\Models\ContentModerationAiSuggestion;
use App\Models\ContentModerationFinding;
class ContentModerationPersistenceService
{
public function __construct(
private readonly ContentModerationReviewService $review,
private readonly ContentModerationActionLogService $actionLogs,
) {
}
public function shouldQueue(ModerationResultData $result): bool
{
return $result->score >= (int) \app('config')->get('content_moderation.queue_threshold', 30);
}
public function hasCurrentFinding(string $contentType, int $contentId, string $contentHash, string $scannerVersion): bool
{
return ContentModerationFinding::query()
->where('content_type', $contentType)
->where('content_id', $contentId)
->where('content_hash', $contentHash)
->where('scanner_version', $scannerVersion)
->exists();
}
/**
* @param array<string, mixed> $context
* @return array{finding:?ContentModerationFinding, created:bool, updated:bool}
*/
public function persist(ModerationResultData $result, array $context): array
{
$contentType = (string) ($context['content_type'] ?? '');
$contentId = (int) ($context['content_id'] ?? 0);
if ($contentType === '' || $contentId <= 0) {
return ['finding' => null, 'created' => false, 'updated' => false];
}
$existing = ContentModerationFinding::query()
->where('content_type', $contentType)
->where('content_id', $contentId)
->where('content_hash', $result->contentHash)
->where('scanner_version', $result->scannerVersion)
->first();
if (! $this->shouldQueue($result) && $existing === null) {
return ['finding' => null, 'created' => false, 'updated' => false];
}
$finding = $existing ?? new ContentModerationFinding();
$isNew = ! $finding->exists;
$finding->fill([
'content_type' => $contentType,
'content_id' => $contentId,
'content_target_type' => $result->contentTargetType,
'content_target_id' => $result->contentTargetId,
'artwork_id' => $context['artwork_id'] ?? null,
'user_id' => $context['user_id'] ?? null,
'severity' => $result->severity->value,
'score' => $result->score,
'content_hash' => $result->contentHash,
'scanner_version' => $result->scannerVersion,
'reasons_json' => $result->reasons,
'matched_links_json' => $result->matchedLinks,
'matched_domains_json' => $result->matchedDomains,
'matched_keywords_json' => $result->matchedKeywords,
'rule_hits_json' => $result->ruleHits,
'score_breakdown_json' => $result->scoreBreakdown,
'content_hash_normalized' => $result->contentHashNormalized,
'group_key' => $result->groupKey,
'campaign_key' => $result->campaignKey,
'cluster_score' => $result->clusterScore,
'cluster_reason' => $result->clusterReason,
'priority_score' => $result->priorityScore,
'policy_name' => $result->policyName,
'review_bucket' => $result->reviewBucket,
'escalation_status' => $result->escalationStatus ?? ModerationEscalationStatus::None->value,
'ai_provider' => $result->aiProvider,
'ai_label' => $result->aiLabel,
'ai_suggested_action' => $result->aiSuggestedAction,
'ai_confidence' => $result->aiConfidence,
'ai_explanation' => $result->aiExplanation,
'user_risk_score' => $result->userRiskScore,
'content_snapshot' => (string) ($context['content_snapshot'] ?? ''),
'auto_action_taken' => $result->autoHideRecommended ? 'recommended' : null,
]);
if ($isNew) {
$finding->status = $result->status;
} elseif (! $this->shouldQueue($result) && $finding->isPending()) {
$finding->status = ModerationStatus::Resolved;
$finding->action_taken = 'rescanned_clean';
$finding->resolved_at = \now();
$finding->resolved_by = null;
}
$finding->save();
if ($result->aiProvider !== null && ($result->aiLabel !== null || $result->aiExplanation !== null || $result->aiConfidence !== null)) {
ContentModerationAiSuggestion::query()->create([
'finding_id' => $finding->id,
'provider' => $result->aiProvider,
'suggested_label' => $result->aiLabel,
'suggested_action' => $result->aiSuggestedAction,
'confidence' => $result->aiConfidence,
'explanation' => $result->aiExplanation,
'campaign_tags_json' => $result->campaignKey ? [$result->campaignKey] : [],
'raw_response_json' => $result->aiRawResponse,
'created_at' => now(),
]);
}
if (! $isNew && ! $this->shouldQueue($result) && $finding->action_taken === 'rescanned_clean') {
$this->actionLogs->log(
$finding,
'finding',
$finding->id,
'rescan',
null,
ModerationStatus::Pending->value,
ModerationStatus::Resolved->value,
null,
null,
'Finding resolved automatically after a clean rescan.',
['scanner_version' => $result->scannerVersion],
);
}
return [
'finding' => $finding,
'created' => $isNew,
'updated' => ! $isNew,
];
}
/**
* @param array<string, mixed> $context
*/
public function applyAutomatedActionIfNeeded(ContentModerationFinding $finding, ModerationResultData $result, array $context): bool
{
if (! $result->autoHideRecommended) {
return false;
}
if ($finding->is_auto_hidden) {
return false;
}
$supportedTypes = (array) \app('config')->get('content_moderation.auto_hide.supported_types', []);
if (! in_array($finding->content_type->value, $supportedTypes, true)) {
$finding->forceFill([
'auto_action_taken' => 'recommended_review',
])->save();
return false;
}
if (! in_array($finding->content_type, [ModerationContentType::ArtworkComment, ModerationContentType::ArtworkDescription], true)) {
if ($finding->content_type !== ModerationContentType::ArtworkTitle) {
return false;
}
}
if (! in_array($finding->content_type, [ModerationContentType::ArtworkComment, ModerationContentType::ArtworkDescription, ModerationContentType::ArtworkTitle], true)) {
return false;
}
$this->review->autoHideContent($finding, 'Triggered by automated moderation threshold.');
return true;
}
}