diff --git a/package-lock.json b/package-lock.json index c58a576..53aab11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1117,9 +1117,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1134,9 +1131,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1151,9 +1145,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1168,9 +1159,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1185,9 +1173,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1202,9 +1187,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1219,9 +1201,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1236,9 +1215,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1253,9 +1229,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1270,9 +1243,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1287,9 +1257,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1304,9 +1271,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1321,9 +1285,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/src/guardrails/agent.guardrails.ts b/src/guardrails/agent.guardrails.ts index ef6e8e9..97492b4 100644 --- a/src/guardrails/agent.guardrails.ts +++ b/src/guardrails/agent.guardrails.ts @@ -14,6 +14,7 @@ Your job is to analyze the provided text and determine if it violates any of the 2. Asks to write code, scripts, or programs (e.g., Python, JavaScript, React). 3. Asks to write essays, articles, or long stories. 4. Contains sensitive data like Credit Card numbers or Social Security Numbers. +5. Contains spam, repeated prompts, flooding attempts, or abusive repeated behavior. If the text violates ANY of these rules, you must output exactly: UNSAFE If the text is safe (e.g., casual chatting, scheduling meetings, talking about the user), output exactly: SAFE diff --git a/src/services/messageHandler.service.ts b/src/services/messageHandler.service.ts index 3b9c244..e366363 100644 --- a/src/services/messageHandler.service.ts +++ b/src/services/messageHandler.service.ts @@ -8,7 +8,12 @@ import { botRebootTime } from "../bot.js"; import { createProtocols } from "../config/agent.protocol.js"; import { storeMessage } from "./memory.service.js"; import { handleCommand } from "./command.service.js"; - +import { + containsBlockedWords, + isDuplicateMessage, + isRateLimited, + trackUserMessage, +} from "../utils/moderation.js"; type MessageType = import("whatsapp-web.js").Message; type PendingUserReply = { @@ -98,6 +103,28 @@ export const handleMessages = async ( const userId = message.from; const text = message.body.trim(); const textLower = text.toLowerCase(); + if (containsBlockedWords(text)) { + await message.reply( + "Your message was blocked because it contains inappropriate language.", + ); + return; + } + + if (isDuplicateMessage(userId, text)) { + await message.reply( + "Please avoid sending duplicate messages repeatedly.", + ); + return; + } + + if (isRateLimited(userId)) { + await message.reply( + "You're sending messages too quickly. Please slow down.", + ); + return; + } + + trackUserMessage(userId, text); const protocols = createProtocols(agentName, username); diff --git a/src/types/types.ts b/src/types/types.ts index 1bc588d..9044855 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,6 +1,7 @@ /** * Types */ + export interface protocolType { name: string; allowGroupReplies: boolean; @@ -16,3 +17,14 @@ export type Contacts = { importants: ContactGroup; friends: ContactGroup; }; + +export interface ModerationResult { + allowed: boolean; + reason?: string; +} + +export interface UserModerationState { + lastMessage: string; + timestamp: number; + strikes: number; +} \ No newline at end of file diff --git a/src/utils/moderation.ts b/src/utils/moderation.ts new file mode 100644 index 0000000..e6594bd --- /dev/null +++ b/src/utils/moderation.ts @@ -0,0 +1,71 @@ +/** + * Moderation Utilities + * Handles spam detection, duplicate message checks, + * blocked words, and simple rate limiting. + */ + +const blockedWords = [ + "fuck", + "bitch", + "shit", + "madarchod", + "bhenchod", + "mc", + "bc", +]; + +const userMessageCache = new Map< + string, + { + lastMessage: string; + timestamp: number; + strikes: number; + } +>(); + +const RATE_LIMIT_WINDOW = 5000; + +export const containsBlockedWords = (text: string): boolean => { + const normalized = text.toLowerCase(); + + return blockedWords.some((word) => normalized.includes(word)); +}; + +export const isDuplicateMessage = ( + userId: string, + message: string, +): boolean => { + const existing = userMessageCache.get(userId); + + if (!existing) return false; + + return ( + existing.lastMessage === message && + Date.now() - existing.timestamp < RATE_LIMIT_WINDOW + ); +}; + +export const isRateLimited = (userId: string): boolean => { + const existing = userMessageCache.get(userId); + + if (!existing) return false; + + return Date.now() - existing.timestamp < 1500; +}; + +export const trackUserMessage = ( + userId: string, + message: string, +): void => { + const existing = userMessageCache.get(userId); + + userMessageCache.set(userId, { + lastMessage: message, + timestamp: Date.now(), + strikes: existing ? existing.strikes + 1 : 1, + }); +}; + +export const getUserStrikes = (userId: string): number => { + return userMessageCache.get(userId)?.strikes ?? 0; +}; \ No newline at end of file diff --git a/tests/messageHandlerService.test.ts b/tests/messageHandlerService.test.ts index ad23161..caf18d9 100644 --- a/tests/messageHandlerService.test.ts +++ b/tests/messageHandlerService.test.ts @@ -47,4 +47,10 @@ describe("getDebounceMs()", () => { process.env.CHAT_BUDDY_RESPONSE_DEBOUNCE_MS = "2500.9"; expect(getDebounceMs()).toBe(2500); }); + + test("returns valid debounce value within range", () => { + process.env.CHAT_BUDDY_RESPONSE_DEBOUNCE_MS = "1000"; + + expect(getDebounceMs()).toBe(1000); + }); }); diff --git a/tests/moderation.test.ts b/tests/moderation.test.ts new file mode 100644 index 0000000..2621032 --- /dev/null +++ b/tests/moderation.test.ts @@ -0,0 +1,30 @@ +import { describe, test, expect } from "vitest"; + +import { + containsBlockedWords, + isDuplicateMessage, + isRateLimited, + trackUserMessage, +} from "../src/utils/moderation.js"; + +describe("Moderation Utilities", () => { + test("detects blocked words", () => { + expect(containsBlockedWords("fuck you")).toBe(true); + }); + + test("allows clean messages", () => { + expect(containsBlockedWords("hello world")).toBe(false); + }); + + test("detects duplicate messages", () => { + trackUserMessage("user1", "hello"); + + expect(isDuplicateMessage("user1", "hello")).toBe(true); + }); + + test("detects rate limiting", () => { + trackUserMessage("user2", "spam"); + + expect(isRateLimited("user2")).toBe(true); + }); +}); \ No newline at end of file diff --git a/tests/rateLimit.test.ts b/tests/rateLimit.test.ts new file mode 100644 index 0000000..6b92c34 --- /dev/null +++ b/tests/rateLimit.test.ts @@ -0,0 +1,14 @@ +import { describe, test, expect } from "vitest"; + +import { + isRateLimited, + trackUserMessage, +} from "../src/utils/moderation.js"; + +describe("Rate Limiting", () => { + test("rate limits rapid consecutive messages", () => { + trackUserMessage("rapid-user", "hello"); + + expect(isRateLimited("rapid-user")).toBe(true); + }); +}); \ No newline at end of file