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 $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 $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; } }