Skip to content

Commit 3c56dd7

Browse files
authored
Implement optional flood protection (#11)
* Implement optional flood protection * Resolve formatting error * Move to plugin architecture * Clean up old changes * Add test for antiflood * Add negative test case * Fix missing await on standard send * Make floodDelay check explicit and bound to positive values * Use const in antiflood_test.ts * Fix formatting error * Add guard on no or invalid floodDelay to skip antiflood plugin
1 parent 076980f commit 3c56dd7

File tree

6 files changed

+100
-0
lines changed

6 files changed

+100
-0
lines changed

API.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [option: bufferSize](#option-buffersize)
66
- [option: channels](#option-channels)
77
- [option: ctcpReplies](#option-ctcpreplies)
8+
- [option: floodDelay](#option-flooddelay)
89
- [option: joinOnInvite](#option-joinoninvite)
910
- [option: maxListeners](#option-maxlisteners)
1011
- [option: nick](#option-nick)
@@ -238,6 +239,18 @@ const client = new Client({
238239
});
239240
```
240241

242+
### option: floodDelay
243+
244+
Milliseconds to wait between dispatching private messages.
245+
246+
Defaults to 0 milliseconds (no delay).
247+
248+
```ts
249+
const client = new Client({
250+
floodDelay: 2000,
251+
});
252+
```
253+
241254
### option: joinOnInvite
242255

243256
Enables auto join on invite.

client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CoreClient, type CoreFeatures } from "./core/client.ts";
44
import { type CombinePluginFeatures } from "./core/plugins.ts";
55

66
import action from "./plugins/action.ts";
7+
import antiflood from "./plugins/antiflood.ts";
78
import away from "./plugins/away.ts";
89
import cap from "./plugins/cap.ts";
910
import chanmodes from "./plugins/chanmodes.ts";
@@ -48,6 +49,7 @@ import whois from "./plugins/whois.ts";
4849

4950
const plugins = [
5051
action,
52+
antiflood,
5153
away,
5254
cap,
5355
chanmodes,

deps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ export { assertExists } from "https://deno.land/[email protected]/assert/assert_exists
1212
export { assertMatch } from "https://deno.land/[email protected]/assert/assert_match.ts";
1313
export { assertRejects } from "https://deno.land/[email protected]/assert/assert_rejects.ts";
1414
export { assertThrows } from "https://deno.land/[email protected]/assert/assert_throws.ts";
15+
16+
export { Queue } from "https://deno.land/x/[email protected]/mod.ts";

plugins/antiflood.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { createPlugin } from "../core/plugins.ts";
2+
import { Queue } from "../deps.ts";
3+
4+
interface AntiFloodFeatures {
5+
options: {
6+
/** Milliseconds to wait between dispatching private messages.
7+
*
8+
* Defaults to 0 milliseconds (no delay) */
9+
floodDelay?: number;
10+
};
11+
}
12+
13+
export default createPlugin("antiflood", [])<AntiFloodFeatures>(
14+
(client, options) => {
15+
if (!options.floodDelay || options.floodDelay <= 0) return;
16+
// Queue object and delay structure for anti-flood protection
17+
const queue = new Queue();
18+
const delay = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
19+
20+
// Queues up limiter for outgoing messages with before and after send hooks
21+
22+
client.hooks.hookCall("send", async (send, command, ...params) => {
23+
if (command === "PRIVMSG") {
24+
return queue.push(async () => {
25+
const raw = await send(command, ...params);
26+
if (raw) {
27+
await delay(options.floodDelay);
28+
}
29+
return raw;
30+
});
31+
} else {
32+
return await send(command, ...params);
33+
}
34+
});
35+
},
36+
);

plugins/antiflood_test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { assertEquals } from "../deps.ts";
2+
import { delay, describe } from "../testing/helpers.ts";
3+
import { mock } from "../testing/mock.ts";
4+
5+
describe("plugins/antiflood", (test) => {
6+
test("send two PRIVMSG with delay", async () => {
7+
const { client, server } = await mock({ floodDelay: 250 });
8+
9+
client.privmsg("#channel", "Hello world");
10+
client.privmsg("#channel", "Hello world, again");
11+
let raw = server.receive();
12+
13+
// Should only get first message
14+
assertEquals(raw, [
15+
"PRIVMSG #channel :Hello world",
16+
]);
17+
18+
// Wait for second message to make it through
19+
await delay(1000);
20+
raw = server.receive();
21+
22+
// Second message now dispatched to server
23+
assertEquals(raw, [
24+
"PRIVMSG #channel :Hello world, again",
25+
]);
26+
});
27+
});
28+
29+
describe("plugins/antiflood", (test) => {
30+
test("disabled when delay not set", async () => {
31+
const { client, server } = await mock();
32+
33+
client.privmsg("#channel", "Hello world");
34+
client.privmsg("#channel", "Hello world, again");
35+
const raw = server.receive();
36+
37+
// Should get both messages
38+
assertEquals(raw, [
39+
"PRIVMSG #channel :Hello world",
40+
"PRIVMSG #channel :Hello world, again",
41+
]);
42+
});
43+
});

testing/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ export function describe(name: string, fn: (test: Test) => void): void {
99
function prettify(describeName: string, testName: string) {
1010
return bold(describeName) + dim(" > ") + testName;
1111
}
12+
13+
export function delay(ms = 0) {
14+
return new Promise((resolve) => setTimeout(resolve, ms));
15+
}

0 commit comments

Comments
 (0)