Plugin: rate limiter
Un plugin completo que limita cuántos mensajes puede enviar un usuario por ventana de tiempo.
El plugin
ts
import { Bot, definePlugin } from '@botmesh/core'
import { BaileysProvider } from '@botmesh/baileys'
interface RateLimitConfig {
max?: number
windowMs?: number
}
const rateLimitPlugin = definePlugin<RateLimitConfig>({
name: 'rate-limit',
version: '1.0.0',
description: 'Limit messages per user — max N messages per window',
install(ctx, config = {}) {
const max = config.max ?? 10
const windowMs = config.windowMs ?? 60000
const counts = new Map<string, { count: number; resetAt: number }>()
ctx.bot.before('message', async (msgCtx, next) => {
const now = Date.now()
let entry = counts.get(msgCtx.from)
if (!entry || entry.resetAt < now) {
entry = { count: 0, resetAt: now + windowMs }
counts.set(msgCtx.from, entry)
}
entry.count++
if (entry.count > max) {
const wait = Math.ceil((entry.resetAt - now) / 1000)
await msgCtx.reply(`⏳ Too many messages. Wait ${wait}s.`)
msgCtx.abort()
return
}
await next()
})
// El hook before('message') de arriba se desregistra automáticamente
// cuando se desconecta el plugin. Usa onDispose() para cualquier otra
// limpieza — aquí, un barrido periódico de contadores expirados.
const gc = setInterval(() => {
const now = Date.now()
for (const [chatId, entry] of counts) {
if (entry.resetAt < now) counts.delete(chatId)
}
}, windowMs)
ctx.onDispose(() => clearInterval(gc))
ctx.logger.info(`Rate limit active: ${max} msg / ${windowMs / 1000}s`)
},
})
const bot = new Bot({
provider: new BaileysProvider({ session: 'demo-bot', qr: { terminal: true, small: true } }),
})
// Configuración por defecto (10 msg / 60s)
const unplug = bot.plug(rateLimitPlugin)
// Configuración personalizada:
// bot.plug(rateLimitPlugin, { max: 20, windowMs: 60000 })
// Más adelante, para quitarlo por completo (desregistra el hook y limpia el interval):
// unplug()
await bot.start()Cómo funciona
- Cada remitente tiene un contador con una hora de reinicio (
resetAt) - Cada mensaje entrante incrementa el contador de
msgCtx.from - Si el contador supera
maxdentro dewindowMs:- el bot responde
⏳ Too many messages. Wait Xs. msgCtx.abort()detiene el pipeline — no corre ningún otro handler para este mensaje
- el bot responde
- Cuando pasa
resetAt, el siguiente mensaje arranca una ventana nueva bot.plug()devuelve una funciónunplug()que revierte todo lo que el plugin registró a través dectx.bot(el hook), más todo lo encolado conctx.onDispose()(el interval de limpieza)
Variantes
Solo en grupos
ts
ctx.bot.before('message', async (msgCtx, next) => {
if (!msgCtx.isGroup) { await next(); return }
// ... lógica de rate limit ...
})Con un ban temporal
ts
const bans = new Map<string, { until: number }>()
ctx.bot.before('message', async (msgCtx, next) => {
const now = Date.now()
const ban = bans.get(msgCtx.from)
if (ban && ban.until > now) {
await msgCtx.reply(`🚫 Blocked for ${Math.ceil((ban.until - now) / 1000)}s.`)
msgCtx.abort()
return
}
// ... incrementar el contador por remitente como en "El plugin" arriba ...
const entry = counts.get(msgCtx.from)!
if (entry.count > max) {
bans.set(msgCtx.from, { until: now + 5 * 60 * 1000 })
await msgCtx.reply('🚫 Too many messages. Blocked for 5 minutes.')
msgCtx.abort()
return
}
await next()
})Con whitelist
ts
const whitelist = new Set(['573001112233@s.whatsapp.net'])
ctx.bot.before('message', async (msgCtx, next) => {
if (whitelist.has(msgCtx.from)) { await next(); return }
// ... lógica de rate limit ...
})Limitar los envíos salientes del propio bot
before('send') se dispara en cada mensaje saliente (bot.send, ctx.reply, ctx.photo, ...). Su payload es { chatId, content, options } (donde content es el OutboundContent neutral de transporte) y se puede abortar de la misma forma, saltándose el envío por completo:
ts
ctx.bot.before('send', async (payload, next) => {
const now = Date.now()
let entry = counts.get(payload.chatId)
if (!entry || entry.resetAt < now) {
entry = { count: 0, resetAt: now + windowMs }
counts.set(payload.chatId, entry)
}
entry.count++
if (entry.count > max) {
payload.abort() // no se transmite nada para este envío
return
}
await next()
})