From d11f697dec0b9b0025664389a0a21e4c50338334 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 5 Dec 2020 12:26:38 +0200 Subject: [PATCH] effective rate limiting --- simplecommands/plugin.ts | 73 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/simplecommands/plugin.ts b/simplecommands/plugin.ts index a89bd6b..db0f847 100644 --- a/simplecommands/plugin.ts +++ b/simplecommands/plugin.ts @@ -29,16 +29,71 @@ interface CommandSpec { execute(msg: IMessage, command: CommandSpec, prefix: string, ...args: any[]): Promise; } +interface RateLimit { + rooms: string[]; + perSecond: number; + cooldown: number; +} + +interface Rate { + lastMessage: number; + messages: number; +} + +const rates: {[key: string]: Rate} = {}; + @Configurable({ prefix: { '*': '!' }, - keywords: ['squeebot'] + keywords: ['squeebot'], + rateLimits: [], }) class SqueebotCommandsAPIPlugin extends Plugin { private commands: CommandSpec[] = []; private permissions: any = null; + public getRateLimit(room: string): RateLimit | null { + for (const rm of this.config.get('rateLimits', [])) { + if (rm.rooms && (rm.rooms.indexOf(room) !== -1 || rm.rooms.indexOf('*') !== -1)) { + return rm; + } + } + return null; + } + + public doLimiting(room: string, sender: string): boolean { + const rl = this.getRateLimit(room); + if (!rl) { + return false; + } + + // Hasn't been limited yet + if (!rates[sender]) { + rates[sender] = { + lastMessage: Date.now(), + messages: 1, + }; + return false; + } + + const r = rates[sender]; + if (r.lastMessage > Date.now() - rl.cooldown) { + if (r.messages >= rl.perSecond) { + // Rate limited + return true; + } + + r.lastMessage = Date.now(); + r.messages++; + return false; + } + + r.lastMessage = Date.now(); + r.messages = 1; + return false; + } + private roomMatcher(msg: IMessage, specList: CommandSpec[]): CommandSpec[] { const roomMatches = []; @@ -143,6 +198,14 @@ class SqueebotCommandsAPIPlugin extends Plugin { // Iteration 4: Match permissions for user const permitted = this.permissionMatcher(msg, sorted); + // Rate limit check + if (permitted.length && + this.config.get('rateLimits', []).length && + this.doLimiting(msg.fullRoomID!, msg.fullSenderID!)) { + logger.warn('[%s] User %s rate limited', this.name, msg.fullSenderID); + return; + } + // Start executing for (const spec of permitted) { const success = await spec.execute(msg, spec, prefix, ...separate.slice(1)); @@ -199,6 +262,14 @@ class SqueebotCommandsAPIPlugin extends Plugin { // Iteration 3: Match permissions for user const permitted = this.permissionMatcher(msg, sorted); + // Rate limit check + if (permitted.length && + this.config.get('rateLimits', []).length && + this.doLimiting(msg.fullRoomID!, msg.fullSenderID!)) { + logger.warn('[%s] User %s rate limited', this.name, msg.fullSenderID); + return; + } + // Start executing for (const spec of permitted) { const success = await spec.execute(msg, spec, keyword);