summaryrefslogtreecommitdiff
path: root/packages/gateway
diff options
context:
space:
mode:
Diffstat (limited to 'packages/gateway')
-rw-r--r--packages/gateway/src/listeners/messageCreate/index.ts2
-rw-r--r--packages/gateway/src/listeners/messageCreate/moderationAgent/constants.ts263
-rw-r--r--packages/gateway/src/listeners/messageCreate/moderationAgent/index.ts230
-rw-r--r--packages/gateway/src/listeners/messageCreate/moderationAgent/utilities.ts263
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;
- }
-};