normalize($content); $campaignNormalized = app(DuplicateDetectionService::class)->campaignText($content); $linkRule = app(LinkPresenceRule::class); $extractedUrls = $linkRule->extractUrls($content); $extractedDomains = array_values(array_unique(array_filter(array_map( static fn (string $url): ?string => $linkRule->extractHost($url), $extractedUrls )))); $riskAssessment = app(UserRiskScoreService::class)->assess( isset($context['user_id']) ? (int) $context['user_id'] : null, $extractedDomains, ); $context['extracted_urls'] = $extractedUrls; $context['extracted_domains'] = $extractedDomains; $context['user_risk_assessment'] = $riskAssessment; $score = 0; $reasons = []; $matchedLinks = []; $matchedDomains = []; $matchedKeywords = []; $ruleHits = []; $scoreBreakdown = []; foreach ($this->rules() as $rule) { foreach ($rule->analyze($content, $normalized, $context) as $finding) { $ruleScore = (int) ($finding['score'] ?? 0); $score += $ruleScore; $reason = (string) ($finding['reason'] ?? 'Flagged by moderation rule'); $reasons[] = $reason; $matchedLinks = array_merge($matchedLinks, (array) ($finding['links'] ?? [])); $matchedDomains = array_merge($matchedDomains, array_filter((array) ($finding['domains'] ?? []))); $matchedKeywords = array_merge($matchedKeywords, array_filter((array) ($finding['keywords'] ?? []))); $ruleKey = (string) ($finding['rule'] ?? 'unknown'); $ruleHits[$ruleKey] = ($ruleHits[$ruleKey] ?? 0) + 1; $scoreBreakdown[] = [ 'rule' => $ruleKey, 'score' => $ruleScore, 'reason' => $reason, ]; } } $modifier = (int) ($riskAssessment['score_modifier'] ?? 0); if ($modifier !== 0) { $score += $modifier; $reasons[] = $modifier > 0 ? 'User risk profile increased moderation score by ' . $modifier : 'User trust profile reduced moderation score by ' . abs($modifier); $ruleHits['user_risk_modifier'] = 1; $scoreBreakdown[] = [ 'rule' => 'user_risk_modifier', 'score' => $modifier, 'reason' => $modifier > 0 ? 'User risk profile increased moderation score by ' . $modifier : 'User trust profile reduced moderation score by ' . abs($modifier), ]; } $score = max(0, $score); $severity = ModerationSeverity::fromScore($score); $policy = $this->policies->resolve($context, $riskAssessment); $autoHideRecommended = $this->shouldAutoHide($score, $ruleHits, $matchedDomains ?: $extractedDomains, $policy); $groupKey = app(DuplicateDetectionService::class)->buildGroupKey($content, $matchedDomains ?: $extractedDomains); $draft = new ModerationResultData( score: $score, severity: $severity, status: $score >= (int) ($policy['queue_threshold'] ?? app('config')->get('content_moderation.queue_threshold', 30)) ? ModerationStatus::Pending : ModerationStatus::ReviewedSafe, reasons: array_values(array_unique(array_filter($reasons))), matchedLinks: array_values(array_unique(array_filter($matchedLinks))), matchedDomains: array_values(array_unique(array_filter($matchedDomains))), matchedKeywords: array_values(array_unique(array_filter($matchedKeywords))), contentHash: hash('sha256', $normalized), scannerVersion: (string) app('config')->get('content_moderation.scanner_version', '1.0'), ruleHits: $ruleHits, contentHashNormalized: hash('sha256', $campaignNormalized), groupKey: $groupKey, userRiskScore: (int) ($riskAssessment['risk_score'] ?? 0), autoHideRecommended: $autoHideRecommended, contentTargetType: isset($context['content_target_type']) ? (string) $context['content_target_type'] : null, contentTargetId: isset($context['content_target_id']) ? (int) $context['content_target_id'] : null, policyName: (string) ($policy['name'] ?? 'default'), scoreBreakdown: $scoreBreakdown, ); $suggestion = $this->suggestions->suggest($content, $draft, $context); $cluster = $this->clusters->classify($content, $draft, $context, [ 'campaign_tags' => $suggestion->campaignTags, 'confidence' => $suggestion->confidence, ]); $priority = $this->priorities->score($draft, $context, $policy, [ 'confidence' => $suggestion->confidence, 'campaign_tags' => $suggestion->campaignTags, ]); return new ModerationResultData( score: $score, severity: $severity, status: $score >= (int) ($policy['queue_threshold'] ?? app('config')->get('content_moderation.queue_threshold', 30)) ? ModerationStatus::Pending : ModerationStatus::ReviewedSafe, reasons: array_values(array_unique(array_filter($reasons))), matchedLinks: array_values(array_unique(array_filter($matchedLinks))), matchedDomains: array_values(array_unique(array_filter($matchedDomains))), matchedKeywords: array_values(array_unique(array_filter($matchedKeywords))), contentHash: hash('sha256', $normalized), scannerVersion: (string) app('config')->get('content_moderation.scanner_version', '1.0'), ruleHits: $ruleHits, contentHashNormalized: hash('sha256', $campaignNormalized), groupKey: $groupKey, userRiskScore: (int) ($riskAssessment['risk_score'] ?? 0), autoHideRecommended: $autoHideRecommended, contentTargetType: isset($context['content_target_type']) ? (string) $context['content_target_type'] : null, contentTargetId: isset($context['content_target_id']) ? (int) $context['content_target_id'] : null, campaignKey: $cluster['campaign_key'], clusterScore: $cluster['cluster_score'], clusterReason: $cluster['cluster_reason'], policyName: (string) ($policy['name'] ?? 'default'), priorityScore: (int) ($priority['priority_score'] ?? $score), reviewBucket: (string) ($priority['review_bucket'] ?? ($policy['review_bucket'] ?? 'standard')), escalationStatus: (string) ($priority['escalation_status'] ?? 'none'), aiProvider: $suggestion->provider, aiLabel: $suggestion->suggestedLabel, aiSuggestedAction: $suggestion->suggestedAction, aiConfidence: $suggestion->confidence, aiExplanation: $suggestion->explanation, aiRawResponse: $suggestion->rawResponse, scoreBreakdown: $scoreBreakdown, ); } public function normalize(string $content): string { $normalized = preg_replace('/\s+/u', ' ', trim($content)); return mb_strtolower((string) $normalized); } /** * @return array */ private function rules(): array { $classes = app('config')->get('content_moderation.rules.enabled', []); return array_values(array_filter(array_map(function (string $class): ?ModerationRuleInterface { $rule = app($class); return $rule instanceof ModerationRuleInterface ? $rule : null; }, $classes))); } /** * @param array $ruleHits * @param array $matchedDomains */ private function shouldAutoHide(int $score, array $ruleHits, array $matchedDomains, array $policy = []): bool { if (! app('config')->get('content_moderation.auto_hide.enabled', true)) { return false; } $threshold = (int) ($policy['auto_hide_threshold'] ?? app('config')->get('content_moderation.auto_hide.threshold', 95)); if ($score >= $threshold) { return true; } $blockedHit = isset($ruleHits['blacklisted_domain']) || isset($ruleHits['blocked_domain']); $severeHitCount = collect($ruleHits) ->only(['blacklisted_domain', 'blocked_domain', 'high_risk_keyword', 'near_duplicate_campaign', 'duplicate_comment']) ->sum(); return $blockedHit && $severeHitCount >= 2 && count($matchedDomains) >= 1; } }