diff options
| author | Fuwn <[email protected]> | 2025-10-03 14:35:51 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-10-03 14:35:51 -0700 |
| commit | 21b17ed9ad0c67a0a8effd84603c21bd1c968375 (patch) | |
| tree | 66f8ab40344864f7f81c44cf85e0b3c1d7d69388 /packages/gateway | |
| parent | feat(gateway:commands): Add role command (diff) | |
| download | umabotdiscord-21b17ed9ad0c67a0a8effd84603c21bd1c968375.tar.xz umabotdiscord-21b17ed9ad0c67a0a8effd84603c21bd1c968375.zip | |
feat(gateway:moderationAgent): Delete agent
Diffstat (limited to 'packages/gateway')
4 files changed, 0 insertions, 758 deletions
diff --git a/packages/gateway/src/listeners/messageCreate/index.ts b/packages/gateway/src/listeners/messageCreate/index.ts index c099142..b294a30 100644 --- a/packages/gateway/src/listeners/messageCreate/index.ts +++ b/packages/gateway/src/listeners/messageCreate/index.ts @@ -2,7 +2,6 @@ import { Client, Events, Message } from "discord.js"; import { handleIqdbModeration } from "./iqdbModeration"; import { handleRoleplayUmagram } from "./roleplayUmagram"; // import { handleArtMediaModeration } from "./artMediaModeration"; -import { handleAIModeration } from "./moderationAgent"; import { handleAnnouncementReaction } from "./announcementReaction"; import { handleRoleMentionCooldown } from "./roleMentionCooldown"; import { handleAICommand } from "./aiCommandHandler"; @@ -13,7 +12,6 @@ export const handleMessageCreate = (client: Client) => { handleIqdbModeration(message), handleRoleplayUmagram(message), // handleArtMediaModeration(message), - handleAIModeration(message), handleAnnouncementReaction(message), handleRoleMentionCooldown(message), handleAICommand(message), diff --git a/packages/gateway/src/listeners/messageCreate/moderationAgent/constants.ts b/packages/gateway/src/listeners/messageCreate/moderationAgent/constants.ts deleted file mode 100644 index 43468a2..0000000 --- a/packages/gateway/src/listeners/messageCreate/moderationAgent/constants.ts +++ /dev/null @@ -1,263 +0,0 @@ -export const SKIP_PRIMARY_NOTIFICATION = false; -export const SKIP_ACTION = true; -export const EXCLUDED_CATEGORIES = [ - "1406422619934167103", // Staff - "1420604833286852608", // Staff Automation -]; -export const MODERATION_LOG_CHANNEL_ID = "1406422619934167106"; -export const MIN_MESSAGE_LENGTH = 15; -export const MAX_SYMBOL_DENSITY = 0.6; -export const MAX_COMPLETION_TOKENS = 4000; -export const MESSAGE_HISTORY_SIZE = 3; -export const MODEL = "cognitivecomputations/dolphin3.0-r1-mistral-24b"; -export const SAFE_WORDS = new Set([ - "hello", - "hi", - "hey", - "bye", - "goodbye", - "thanks", - "thank", - "welcome", - "please", - "sorry", - "yes", - "no", - "maybe", - "ok", - "okay", - "sure", - "alright", - "fine", - "good", - "bad", - "nice", - "cool", - "awesome", - "great", - "amazing", - "wow", - "omg", - "lol", - "haha", - "hehe", - "lmao", - "what", - "why", - "how", - "when", - "where", - "who", - "which", - "this", - "that", - "these", - "those", - "here", - "there", - "everywhere", - "nowhere", - "somewhere", - "anywhere", - "up", - "down", - "left", - "right", - "forward", - "backward", - "start", - "stop", - "begin", - "end", - "finish", - "first", - "last", - "next", - "previous", - "same", - "different", - "similar", - "opposite", - "more", - "less", - "most", - "least", - "many", - "few", - "big", - "small", - "large", - "tiny", - "huge", - "mini", - "fast", - "slow", - "quick", - "rapid", - "hot", - "cold", - "warm", - "cool", - "new", - "old", - "young", - "fresh", - "easy", - "hard", - "simple", - "complex", - "happy", - "sad", - "angry", - "excited", - "bored", - "always", - "never", - "sometimes", - "often", - "rarely", - "today", - "yesterday", - "tomorrow", - "tonight", - "morning", - "afternoon", - "evening", - "night", - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday", - "january", - "february", - "march", - "april", - "may", - "june", - "july", - "august", - "september", - "october", - "november", - "december", - "true", - "false", - "maybe", - "probably", - "definitely", - "certainly", - "absolutely", - "exactly", - "precisely", - "correct", - "wrong", - "right", - "wrong", - "mistake", - "error", - "success", - "failure", - "win", - "lose", - "victory", - "defeat", - "champion", - "winner", - "loser", - "player", - "game", - "play", - "fun", - "boring", - "interesting", - "exciting", - "calm", - "peaceful", - "quiet", - "loud", - "silent", - "bright", - "dark", - "light", - "heavy", - "empty", - "full", - "open", - "closed", - "free", - "busy", - "available", - "unavailable", - "online", - "offline", - "active", - "inactive", - "ready", - "not ready", -]); -export const LOW_RISK_PATTERNS = [ - /^(good|bad|nice|great|awesome|terrible|amazing|wow|omg|wtf|haha|hehe|lmao|rofl)$/i, - /^(what|why|how|when|where|who)$/i, - /^(this|that|these|those)$/i, - /^(here|there|everywhere|nowhere)$/i, - /^(up|down|left|right|forward|backward)$/i, - /^(start|stop|begin|end|finish)$/i, - /^(first|last|next|previous)$/i, - /^(same|different|similar|opposite)$/i, - /^(more|less|most|least|many|few)$/i, - /^(big|small|large|tiny|huge|mini)$/i, - /^(fast|slow|quick|rapid|slowly)$/i, - /^(hot|cold|warm|cool|freezing)$/i, - /^(new|old|young|fresh|stale)$/i, - /^(easy|hard|simple|complex|difficult)$/i, - /^(happy|sad|angry|excited|bored)$/i, - /^(sure|maybe|perhaps|probably|definitely)$/i, - /^(always|never|sometimes|often|rarely)$/i, - /^(today|yesterday|tomorrow|tonight|morning)$/i, - /^(monday|tuesday|wednesday|thursday|friday|saturday|sunday)$/i, - /^(january|february|march|april|may|june|july|august|september|october|november|december)$/i, - /^(ok|okay|yes|no|hi|hey|yo|lol|thanks|thx|np|cool)$/i, - /^(true|false|maybe|probably|definitely|certainly|absolutely)$/i, - /^(exactly|precisely|correct|wrong|right|mistake|error)$/i, - /^(success|failure|win|lose|victory|defeat|champion|winner|loser)$/i, - /^(player|game|play|fun|boring|interesting|exciting|calm|peaceful)$/i, - /^(quiet|loud|silent|bright|dark|light|heavy|empty|full)$/i, - /^(open|closed|free|busy|available|unavailable|online|offline)$/i, - /^(active|inactive|ready|not ready|working|broken|fixed|repair)$/i, -]; -export const SERVER_RULES = { - "3": { - name: "Spoiler Tagging", - description: "Media spoilers must use ||spoiler|| tags (except in #uma-musume-anime)", - severity: "medium", - criteria: "Reveals plot details of movies/TV/anime/manga without spoiler tags" - }, - "6": { - name: "Underage Sexualization", - description: "No sexualizing characters explicitly stated as underage", - severity: "critical", - criteria: "Explicitly states character is underage AND involves sexual content" - }, - "8": { - name: "NSFW Content", - description: "Graphic sexual content only in NSFW channels", - severity: "high", - criteria: "Detailed sexual descriptions in non-NSFW channels" - }, - "14": { - name: "Test String", - description: "Never use 'test_string_1337'", - severity: "low", - criteria: "Contains exact string 'test_string_1337'" - } -}; - -export const RULE_DECISION_TREE = ` -RULE 3 (Spoilers): Flag if media plot details revealed without ||spoiler|| tags -RULE 6 (Underage): Flag ONLY if explicitly states character is underage + sexual content -RULE 8 (NSFW): Flag ONLY if detailed sexual descriptions in SFW channels -RULE 14 (Test): Flag if contains exact string "test_string_1337" -`; diff --git a/packages/gateway/src/listeners/messageCreate/moderationAgent/index.ts b/packages/gateway/src/listeners/messageCreate/moderationAgent/index.ts deleted file mode 100644 index 2787971..0000000 --- a/packages/gateway/src/listeners/messageCreate/moderationAgent/index.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Message, TextChannel, ThreadChannel } from "discord.js"; -import { sendAuditLog } from "../../../commands/utilities"; -import { - EXCLUDED_CATEGORIES, - LOW_RISK_PATTERNS, - MAX_SYMBOL_DENSITY, - MIN_MESSAGE_LENGTH, - MODERATION_LOG_CHANNEL_ID, - SAFE_WORDS, - SKIP_ACTION, - SKIP_PRIMARY_NOTIFICATION, -} from "./constants"; -import { analyzeMessageWithAI, fetchMessageContext } from "./utilities"; - -export const handleAIModeration = async (message: Message) => { - if (message.author.bot) return; - - if (!message.content && message.attachments.size === 0) return; - - if (!message.content && message.attachments.size > 0) { - return; - } - - if (message.content) { - const content = message.content.trim(); - - if (!content) { - return; - } - - const shortWhitelist = - /^(ok|okay|yes|no|hi|hey|yo|lol|thanks|thx|np|cool)$/i; - - if (content.length < MIN_MESSAGE_LENGTH && !shortWhitelist.test(content)) { - return; - } - - const emojiRegExp = - /^(?:\p{Emoji_Presentation}|\p{Extended_Pictographic}|\s)+$/u; - - if (emojiRegExp.test(content)) { - return; - } - - const symbolRegExp = /^[^\p{L}\p{N}\s]+$/u; - - if (symbolRegExp.test(content)) { - return; - } - - const symbolDensity = - content.replace(/[\p{L}\p{N}\s]/gu, "").length / content.length; - - if (symbolDensity >= MAX_SYMBOL_DENSITY && content.length > 3) { - return; - } - - for (const pattern of LOW_RISK_PATTERNS) - if (pattern.test(content)) { - return; - } - - if (content.length <= 20 && /^[a-zA-Z]+$/.test(content)) - if (SAFE_WORDS.has(content.toLowerCase())) { - return; - } - } - - if (message.channel.isThread()) { - const parentChannel = message.channel.parent; - - if (parentChannel && "parentId" in parentChannel && parentChannel.parentId) - if (EXCLUDED_CATEGORIES.includes(parentChannel.parentId)) return; - } else if ("parentId" in message.channel && message.channel.parentId) { - if (EXCLUDED_CATEGORIES.includes(message.channel.parentId)) return; - } - - try { - const context = await fetchMessageContext( - message.channel as TextChannel | ThreadChannel, - message.id, - ); - const analysis = await analyzeMessageWithAI(message, context); - - if (!analysis) { - console.error("AI analysis failed, skipping moderation"); - return; - } - - if (!analysis.violation) { - return; - } - - console.warn( - `Rule violation detected: ${analysis.rule} (severity: ${analysis.severity}, confidence: ${analysis.confidence}%)`, - ); - - if (SKIP_ACTION) { - console.warn( - `SKIP_ACTION enabled - logging violation without taking action (severity: ${analysis.severity}, confidence: ${analysis.confidence}%)`, - ); - } else if ( - (analysis.severity === "critical" || analysis.severity === "high") && - analysis.confidence >= 85 - ) { - try { - await message.delete(); - console.warn(`Auto-deleted high severity violation`); - - try { - const notificationText = `${message.author}, your message was deleted: **${analysis.brief}**. This notification will be deleted in 10 seconds.\n\nIf you believe this was a mistake, you can ignore this notification or let <@217348698294714370> know.`; - const notificationMessage = await (message.channel as any).send( - notificationText, - ); - - setTimeout(async () => { - try { - await notificationMessage.delete(); - } catch (error) { - console.error("Failed to delete notification message:", error); - } - }, 10000); - } catch (error) { - console.error("Failed to send notification message:", error); - } - } catch (error) { - console.error("Failed to delete message:", error); - } - } else { - console.warn( - `Logging violation for human review (severity: ${analysis.severity}, confidence: ${analysis.confidence}%)`, - ); - } - - const { EmbedBuilder } = await import("discord.js"); - const wasDeleted = - !SKIP_ACTION && - (analysis.severity === "critical" || analysis.severity === "high") && - analysis.confidence >= 85; - const embed = new EmbedBuilder() - .setTitle( - wasDeleted - ? "🤖 Message Deleted - Rule Violation" - : "⚠️ Rule Violation Detected", - ) - .setColor( - analysis.severity === "critical" - ? "#ff0000" - : analysis.severity === "high" - ? "#ff6600" - : analysis.severity === "medium" - ? "#ffaa00" - : "#ffff00", - ) - .addFields( - { - name: "Channel", - value: `<#${message.channelId}>`, - inline: true, - }, - { - name: "Author", - value: `<@${message.author.id}>`, - inline: true, - }, - { - name: "Message ID", - value: `[${message.id}](https://discord.com/channels/${message.guildId}/${message.channelId}/${message.id})`, - inline: true, - }, - { - name: "Rule Violation", - value: analysis.rule, - inline: false, - }, - { - name: "Severity", - value: analysis.severity.toUpperCase(), - inline: true, - }, - { - name: "Confidence", - value: `${analysis.confidence}%`, - inline: true, - }, - { - name: "Brief", - value: analysis.brief, - inline: false, - }, - { - name: "Explanation", - value: analysis.explanation, - inline: false, - }, - ) - .setTimestamp() - .setFooter({ - text: `Guild: ${message.guild?.name || "Unknown"}`, - }); - - if (message.content && message.content.length <= 1000) - embed.addFields({ - name: wasDeleted ? "Deleted Message Content" : "Message Content", - value: message.content, - inline: false, - }); - - if (!SKIP_PRIMARY_NOTIFICATION) - await sendAuditLog( - message.client, - embed, - message.content && message.content.length > 1000 - ? message.content - : undefined, - MODERATION_LOG_CHANNEL_ID, - ); - - await sendAuditLog( - message.client, - embed, - message.content && message.content.length > 1000 - ? message.content - : undefined, - "1420931142600757280", - ); - } catch (error) { - console.error("Error in AI moderation:", error); - } -}; diff --git a/packages/gateway/src/listeners/messageCreate/moderationAgent/utilities.ts b/packages/gateway/src/listeners/messageCreate/moderationAgent/utilities.ts deleted file mode 100644 index 296d05f..0000000 --- a/packages/gateway/src/listeners/messageCreate/moderationAgent/utilities.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { Message, TextChannel, ThreadChannel } from "discord.js"; -import { - MESSAGE_HISTORY_SIZE, - MAX_COMPLETION_TOKENS, - MODEL, - SERVER_RULES, - RULE_DECISION_TREE, -} from "./constants"; - -export const fetchMessageContext = async ( - channel: TextChannel | ThreadChannel, - messageId: string, -): Promise<string> => { - if (MESSAGE_HISTORY_SIZE <= 0) return ""; - - try { - const messages = await channel.messages.fetch({ - limit: MESSAGE_HISTORY_SIZE, - before: messageId, - }); - const contextMessages = Array.from(messages.values()) - .reverse() - .filter(msg => msg.content && msg.content.length > 10) - .slice(0, 2) - .map((msg) => `${msg.author.username}: ${msg.content}`) - .join(" | "); - - return contextMessages || "No relevant context"; - } catch (error) { - console.error("Error fetching message context:", error); - - return "Context unavailable"; - } -}; - -export const analyzeMessageWithAI = async ( - message: Message, - context: string, -): Promise<{ - violation: boolean; - rule: string; - severity: "low" | "medium" | "high" | "critical"; - explanation: string; - brief: string; - confidence: number; -} | null> => { - try { - const channel = message.channel; - const channelName = "name" in channel ? channel.name : "Unknown"; - const isThread = channel.isThread(); - let isNSFW = false; - - if (isThread && channel.parent) { - isNSFW = "nsfw" in channel.parent ? channel.parent.nsfw : false; - } else { - isNSFW = "nsfw" in channel ? channel.nsfw : false; - } - - const fullContext = `Channel: #${channelName} | NSFW: ${isNSFW ? "Yes" : "No"} | Context: ${context || "None"} -Message: "${message.content || "[No content]"}" - -Rules: ${JSON.stringify(SERVER_RULES, null, 2)} -Decision Tree: ${RULE_DECISION_TREE}`; - - const prompt = `Analyze message for rule violations. Respond ONLY with valid JSON. - -RULES: -- Rule 3: Media spoilers need ||spoiler|| tags -- Rule 6: No sexualizing explicitly underage characters -- Rule 8: No graphic sexual content in SFW channels -- Rule 14: No "test_string_1337" - -SEVERITY: low/medium/high/critical -CONFIDENCE: 0-100% - -JSON FORMAT: -{ - "violation": boolean, - "rule": "rule number or empty string", - "severity": "severity level", - "explanation": "brief explanation", - "brief": "one sentence summary", - "confidence": number -}`; - const response = await fetch( - "https://openrouter.ai/api/v1/chat/completions", - { - method: "POST", - headers: { - Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: MODEL, - messages: [ - { - role: "system", - content: prompt, - }, - { - role: "user", - content: fullContext, - }, - ], - max_tokens: MAX_COMPLETION_TOKENS, - }), - }, - ); - - if (!response.ok) { - const errorText = await response.text(); - - console.error( - "OpenRouter API error:", - response.status, - response.statusText, - ); - console.error("Error response body:", errorText); - - return null; - } - - const data = await response.json(); - - const content = data.choices[0]?.message?.content; - - if (!content) { - console.error("No content in OpenRouter response"); - console.error("Finish reason:", data.choices[0]?.finish_reason); - - return null; - } - - try { - let jsonContent = content; - - if (content.startsWith("```json")) { - if (content.endsWith("```")) { - jsonContent = content.slice(7, -3).trim(); - } else { - jsonContent = content.slice(7).trim(); - } - } else if (content.startsWith("```")) { - if (content.endsWith("```")) { - jsonContent = content.slice(3, -3).trim(); - } else { - jsonContent = content.slice(3).trim(); - } - } - - if (!jsonContent.startsWith("{")) { - const jsonMatch = jsonContent.match(/\{[\s\S]*\}/); - - if (jsonMatch) jsonContent = jsonMatch[0]; - } - - if (!jsonContent.endsWith("}")) { - const openBraces = (jsonContent.match(/\{/g) || []).length; - const closeBraces = (jsonContent.match(/\}/g) || []).length; - - if (openBraces > closeBraces) { - let truncatedJson = jsonContent; - - if (truncatedJson.match(/"[^"]*$/)) - truncatedJson = truncatedJson.replace(/"[^"]*$/, '""'); - - const missingBraces = openBraces - closeBraces; - - truncatedJson += "}".repeat(missingBraces); - jsonContent = truncatedJson; - } - } - - jsonContent = jsonContent - .replace(/,\s*}/g, "}") - .replace(/,\s*]/g, "]") - .replace(/(\w+):/g, '"$1":'); - - if (jsonContent.includes("'") && !jsonContent.includes('"')) { - jsonContent = jsonContent.replace(/'/g, '"'); - } else if (jsonContent.includes("'") && jsonContent.includes('"')) { - jsonContent = jsonContent.replace(/\\'/g, "'"); - } - - try { - const fixedJson = jsonContent - .replace(/"([^"]*):([^"]*)":/g, '"$1:$2":') - .replace(/: "([^"]*):([^"]*)"/g, ': "$1:$2"') - .replace(/: "([^"]*)"([^",}])/g, ': "$1"$2') - .replace(/"([^"]*)"([^",}:])/g, '"$1"$2'); - - return JSON.parse(fixedJson); - } catch { - return JSON.parse(jsonContent); - } - } catch (parseError) { - console.error("Failed to parse OpenRouter response as JSON:", content); - console.error("Parse error:", parseError); - - try { - let fallbackContent = content; - - if (fallbackContent.includes("```json")) { - const match = fallbackContent.match(/```json\s*([\s\S]*?)\s*```/); - - if (match) fallbackContent = match[1].trim(); - } else if (fallbackContent.includes("```")) { - const match = fallbackContent.match(/```\s*([\s\S]*?)\s*```/); - - if (match) fallbackContent = match[1].trim(); - } - - const jsonMatch = fallbackContent.match(/\{[\s\S]*\}/); - - if (jsonMatch) fallbackContent = jsonMatch[0]; - - if (!fallbackContent.endsWith("}")) { - const openBraces = (fallbackContent.match(/\{/g) || []).length; - const closeBraces = (fallbackContent.match(/\}/g) || []).length; - - if (openBraces > closeBraces) { - let truncatedJson = fallbackContent; - - if (truncatedJson.match(/"[^"]*$/)) - truncatedJson = truncatedJson.replace(/"[^"]*$/, '""'); - - const missingBraces = openBraces - closeBraces; - - truncatedJson += "}".repeat(missingBraces); - fallbackContent = truncatedJson; - } - } - - fallbackContent = fallbackContent - .replace(/,\s*}/g, "}") - .replace(/,\s*]/g, "]") - .replace(/(\w+):/g, '"$1":') - .replace(/'/g, '"') - .replace(/\\"/g, '"'); - - try { - const fixedJson = fallbackContent - .replace(/"([^"]*):([^"]*)":/g, '"$1:$2":') - .replace(/: "([^"]*):([^"]*)"/g, ': "$1:$2"') - .replace(/: "([^"]*)"([^",}])/g, ': "$1"$2') - .replace(/"([^"]*)"([^",}:])/g, '"$1"$2'); - - return JSON.parse(fixedJson); - } catch { - return JSON.parse(fallbackContent); - } - } catch (fallbackError) { - console.error("Fallback parsing also failed:", fallbackError); - - return null; - } - } - } catch (error) { - console.error("Error in AI analysis:", error); - - return null; - } -}; |