Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/guardrails/agent.guardrails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion src/services/messageHandler.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);

Expand Down
12 changes: 12 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Types
*/

export interface protocolType {
name: string;
allowGroupReplies: boolean;
Expand All @@ -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;
}
71 changes: 71 additions & 0 deletions src/utils/moderation.ts
Original file line number Diff line number Diff line change
@@ -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;
};
6 changes: 6 additions & 0 deletions tests/messageHandlerService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
30 changes: 30 additions & 0 deletions tests/moderation.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
14 changes: 14 additions & 0 deletions tests/rateLimit.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading