*/ private array $blockedWords; public function __construct() { $this->openAiApiKey = config('services.openai.api_key', ''); $this->useOpenAI = $this->openAiApiKey !== ''; $this->sensitivity = (float) ($this->getSetting('ai_filter_sensitivity') ?? 0.8); $this->blockedWords = $this->loadBlockedWords(); } public function moderate(string $content, string $type = 'general'): array { $trimmedContent = trim($content); if ($trimmedContent === '') { return [ 'approved' => true, 'score' => 0, 'reason' => null, 'detected_words' => [], ]; } $localResult = $this->localModerate($trimmedContent); if ($localResult['blocked']) { return [ 'approved' => false, 'score' => 1.0, 'reason' => 'Blocked content detected', 'detected_words' => $localResult['words'], 'method' => 'local', ]; } if ($this->useOpenAI) { return $this->openAIModerate($trimmedContent, $type); } return [ 'approved' => true, 'score' => $localResult['score'], 'reason' => null, 'detected_words' => [], 'method' => 'local', ]; } private function localModerate(string $content): array { $text = strtolower(strip_tags($content)); $detectedWords = array_filter( $this->blockedWords, fn (string $word): bool => stripos($text, $word) !== false, ); if ($detectedWords !== []) { return [ 'blocked' => true, 'words' => array_values($detectedWords), 'score' => 1.0, ]; } $suspiciousPatterns = [ '/(.)\1{4,}/i', '/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/', '/\b(\w*?[\x27\`]+\w*?)+\b/i', ]; $patternScore = array_reduce( $suspiciousPatterns, fn (float $score, string $pattern): float => preg_match($pattern, $text) ? $score + 0.3 : $score, 0.0, ); return [ 'blocked' => $patternScore >= $this->sensitivity, 'words' => [], 'score' => min($patternScore, 1.0), ]; } private function openAIModerate(string $content, string $type): array { $cacheKey = 'moderation_' . md5($content . $type); return Cache::remember($cacheKey, now()->addMinutes(self::MODERATION_CACHE_DURATION_MINUTES), function () use ($content): array { try { $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . $this->openAiApiKey, 'Content-Type' => 'application/json', ])->post('https://api.openai.com/v1/moderations', [ 'input' => $content, ]); if ($response->successful()) { $result = $response->json(); $moderationResult = $result['results'][0]; $categories = $moderationResult['categories']; $categoryScores = $moderationResult['category_scores']; $maxScore = 0.0; $triggeredCategories = []; foreach ($categories as $category => $flagged) { $categoryScore = $categoryScores[$category] ?? 0; if ($flagged || $categoryScore > $this->sensitivity) { $triggeredCategories[] = $category; $maxScore = max($maxScore, $categoryScore); } } $approved = $triggeredCategories === [] || $maxScore < $this->sensitivity; return [ 'approved' => $approved, 'score' => $maxScore, 'reason' => $approved ? null : implode(', ', $triggeredCategories), 'detected_words' => $moderationResult['flagged_tokens'] ?? [], 'method' => 'openai', 'categories' => $triggeredCategories, ]; } } catch (\Exception $e) { report($e); } return [ 'approved' => true, 'score' => 0, 'reason' => null, 'detected_words' => [], 'method' => 'fallback', ]; }); } public function addBlockedWord(string $word): bool { $cleanWord = strtolower(trim($word)); $key = 'filter_word_' . $cleanWord; WebsiteSetting::updateOrCreate( ['key' => $key], ['value' => '1', 'comment' => 'Blocked word: ' . $word], ); Cache::forget(self::BLOCKED_WORDS_CACHE_KEY); return true; } public function removeBlockedWord(string $word): bool { $cleanWord = strtolower(trim($word)); $key = 'filter_word_' . $cleanWord; WebsiteSetting::where('key', $key)->delete(); Cache::forget(self::BLOCKED_WORDS_CACHE_KEY); return true; } /** * @return list */ public function getBlockedWords(): array { return $this->blockedWords; } public function setSensitivity(float $sensitivity): void { $newSensitivity = max(0, min(1, $sensitivity)); WebsiteSetting::updateOrCreate( ['key' => 'ai_filter_sensitivity'], ['value' => (string) $newSensitivity, 'comment' => 'AI filter sensitivity (0-1)'], ); Cache::forget(self::SETTINGS_CACHE_KEY); CacheService::clearContentModeration(); } public function getSensitivity(): float { return $this->sensitivity; } public function isUsingOpenAI(): bool { return $this->useOpenAI; } public function isEnabled(): bool { return (bool) $this->getSetting('ai_filter_enabled'); } public function shouldAutoReject(): bool { return (bool) $this->getSetting('ai_filter_auto_reject'); } public function getStats(): array { return [ 'enabled' => $this->isEnabled(), 'auto_reject' => $this->shouldAutoReject(), 'method' => $this->useOpenAI ? 'openai' : 'local', 'sensitivity' => $this->sensitivity, 'blocked_words_count' => count($this->blockedWords), ]; } /** * @return list */ private function loadBlockedWords(): array { return Cache::remember(self::BLOCKED_WORDS_CACHE_KEY, self::SETTINGS_CACHE_DURATION, fn (): array => WebsiteSetting::where('key', 'like', 'filter_word_%') ->where('value', '1') ->pluck('key') ->map(fn (string $key): string => str_replace('filter_word_', '', $key)) ->values() ->toArray()); } private function getSetting(string $key): ?string { return $this->getSettings()[$key] ?? null; } /** * @return array */ private function getSettings(): array { return Cache::remember(self::SETTINGS_CACHE_KEY, self::SETTINGS_CACHE_DURATION, fn (): array => WebsiteSetting::whereIn('key', [ 'ai_filter_sensitivity', 'ai_filter_enabled', 'ai_filter_auto_reject', ])->pluck('value', 'key')->toArray()); } }