Skip to content

Commit 0ced4d7

Browse files
committed
feat: improved rate limiter & added chrome to the lists of requirements
1 parent 3b7f2b3 commit 0ced4d7

3 files changed

Lines changed: 33 additions & 41 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
A scalable, modular WhatsApp chatbot built in TypeScript. It leverages modern best practices, lean architecture, Prisma ORM, Dockerization, and environment-based configuration to deliver a robust, flexible successor to Project Orion.
66

7-
> ⚠️ **Warning:**
7+
> ⚠️ **Warning:**
88
> This repository is for educational and entertainment purposes only.
99
> Canis and Orion are not affiliated with Meta (WhatsApp/Facebook).
1010
> Use at your own risk, your WhatsApp account may be subject to suspension or bans.
1111
12-
> ⚠️ **Warning:**
12+
> ⚠️ **Warning:**
1313
> Spaghetting code ahead
1414
1515
## Supported AI Providers
@@ -31,6 +31,7 @@ Canis supports multiple AI providers out of the box:
3131

3232
- Redis/Valkey
3333
- WhatsApp Account
34+
- Chrome browser
3435

3536
## Getting started
3637

src/components/events/message.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default async function (msg: Message) {
6969
* Block users from running commands.
7070
*/
7171
const isBlockedUser = await isBlocked(
72-
msg.author ? msg.author.split("@")[0] : senderId
72+
msg.author ? msg.author.split("@")[0] : senderId,
7373
);
7474
if (isBlockedUser) {
7575
return;
@@ -80,8 +80,8 @@ export default async function (msg: Message) {
8080
*/
8181
if (!msg.fromMe) {
8282
const rate = await rateLimiter(msg.from);
83-
if (rate === null) return;
84-
if (!rate) {
83+
if (rate) return;
84+
if (rate === null) {
8585
msg.reply("Please wait a minute or so.");
8686
return;
8787
}
@@ -103,7 +103,7 @@ export default async function (msg: Message) {
103103
msg.reply = async (
104104
content: MessageContent,
105105
chatId?: string,
106-
options?: MessageSendOptions
106+
options?: MessageSendOptions,
107107
): Promise<Message> => {
108108
let messageBody = typeof content === "string" ? Font(content) : content;
109109

@@ -115,13 +115,13 @@ export default async function (msg: Message) {
115115

116116
if (
117117
/^(--?help|\bhelp\b|-h)$/i.test(
118-
msg.body.trim().replace(handler.command, "").trim()
118+
msg.body.trim().replace(handler.command, "").trim(),
119119
)
120120
) {
121121
const response = `
122122
\`${handler.command}\`
123123
${handler.description || "No description"}
124-
124+
125125
*Usage:* ${handler.usage || "No usage"}
126126
*Example:* ${handler.example || "No example"}
127127
*Role:* ${handler.role || "user"}
@@ -191,10 +191,10 @@ export default async function (msg: Message) {
191191
log.error(
192192
key,
193193
"Unexpected error occurred while processing the request:",
194-
error
194+
error,
195195
);
196196
await msg.reply(
197-
`An unexpected error occurred while processing your request for "${key}". Please try again later.`
197+
`An unexpected error occurred while processing your request for "${key}". Please try again later.`,
198198
);
199199
}
200200
}

src/components/utils/rateLimiter.ts

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,56 @@ import log from "./log";
33

44
const LIMIT = 5;
55
const BASE_WINDOW_MS = 30 * 1000;
6-
const PENALTY_INCREMENT_MS = 5 * 1000;
6+
const PENALTY_INCREMENT_MS = 10 * 1000;
77

88
function getKey(number: string) {
99
return `rate:${number}`;
1010
}
1111

12-
export default async function (number: string): Promise<boolean | null> {
12+
export default async function rateLimiter(
13+
number: string,
14+
): Promise<boolean | null> {
1315
const now = Date.now();
1416
const key = getKey(number);
1517

16-
// Get user entry from Redis
1718
const entryRaw = await redis.get(key);
1819
let entry = entryRaw
1920
? JSON.parse(entryRaw)
2021
: {
21-
timestamps: [],
22-
notified: false,
22+
timestamps: [] as number[],
2323
penaltyCount: 0,
2424
penaltyUntil: 0,
2525
};
2626

27-
// Penalty check
28-
if (entry.penaltyUntil > now) {
29-
log.info(
30-
"RateLimiter",
31-
`User ${number} is under penalty until ${new Date(
32-
entry.penaltyUntil
33-
).toLocaleTimeString()}`
27+
const isStillBlocked = entry.penaltyUntil > now;
28+
29+
if (!isStillBlocked) {
30+
entry.timestamps = entry.timestamps.filter(
31+
(ts: number) => now - ts < BASE_WINDOW_MS,
3432
);
35-
if (!entry.notified) return null;
36-
entry.notified = true;
37-
await redis.set(key, JSON.stringify(entry));
38-
return false;
3933
}
4034

41-
// Remove old timestamps
42-
entry.timestamps = entry.timestamps.filter(
43-
(ts: number) => now - ts < BASE_WINDOW_MS
44-
);
45-
46-
if (entry.timestamps.length >= LIMIT) {
35+
if (isStillBlocked || entry.timestamps.length >= LIMIT) {
4736
entry.penaltyCount += 1;
37+
4838
entry.penaltyUntil =
49-
now + BASE_WINDOW_MS + (entry.penaltyCount - 1) * PENALTY_INCREMENT_MS;
50-
entry.notified = false;
39+
Math.max(entry.penaltyUntil, now) +
40+
entry.penaltyCount * PENALTY_INCREMENT_MS +
41+
BASE_WINDOW_MS;
42+
5143
entry.timestamps = [];
44+
5245
log.warn(
5346
"RateLimiter",
54-
`User ${number} exceeded limit. Penalty until ${new Date(
55-
entry.penaltyUntil
56-
).toLocaleTimeString()}`
47+
`User ${number} blocked until ${new Date(entry.penaltyUntil).toLocaleTimeString()}`,
5748
);
49+
5850
await redis.set(key, JSON.stringify(entry));
59-
return false;
51+
if (entry.penaltyCount === 1) return null;
52+
return true;
6053
}
6154

62-
// Allowed
6355
entry.timestamps.push(now);
64-
entry.notified = false;
6556
await redis.set(key, JSON.stringify(entry));
66-
return true;
57+
return false;
6758
}

0 commit comments

Comments
 (0)