Skip to content
Merged
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
51 changes: 0 additions & 51 deletions .github/workflows/deno.yml

This file was deleted.

29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Test and Build

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x

- name: Check formatting
run: deno fmt --check

- name: Run linter
run: deno lint

- name: Run Deno tests
run: deno test --allow-net
21 changes: 18 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
/.idea/
# Build output
dist/
storages.js
storages.d.ts

# Node.js
node_modules/
package-lock.json
*.tsbuildinfo
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Deno
deno.lock

# Editor
.vscode/
node-compatible/out/

# OS
.DS_Store
Thumbs.db
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022-2024 Amir Zouerami
Copyright (c) 2022-2025 Amir Zouerami

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
14 changes: 0 additions & 14 deletions RATELIMITER.svg

This file was deleted.

132 changes: 16 additions & 116 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,127 +1,27 @@
[![Test Deno](https://github.com/Amir-Zouerami/ratelimiter/actions/workflows/deno.yml/badge.svg)](https://github.com/Amir-Zouerami/ratelimiter/actions/workflows/deno.yml)
[![Test and Build](https://github.com/Amir-Zouerami/ratelimiter/actions/workflows/test.yml/badge.svg)](https://github.com/Amir-Zouerami/ratelimiter/actions/workflows/test.yml)

# ratelimiter
# Rate Limit Users (`ratelimiter`)

`ratelimiter` is an advanced and flexible middleware for the grammY framework, designed to protect
Telegram bots from spam and resource abuse.

<p align="center">
<a href="https://github.com/Amir-Zouerami/rateLimiter">
<img src="./RATELIMITER.svg" alt="ratelimiter Logo">
<a href="https://github.com/grammyjs/ratelimiter">
<img src="./grammy-ratelimiter-cover.png" alt="grammY rate limiter cover">
</a>
</p>

## ❓ What does it do?
****
🔌 [ratelimiter](https://github.com/grammyjs/ratelimiter) is a rate-limiting middleware for Telegram bots made with [grammY](https://grammy.dev/) or [Telegraf](https://github.com/telegraf/telegraf) bot frameworks. It rate limits users and stop them from spamming requests to your bot. You should note that this package **does not** rate limit the incoming requests from telegram servers, instead, it tracks the incoming requests by `from.id` and dismisses them on arrival so no further processing load is added to your servers.

Under normal circumstances, every request will be processed & answered by your bot which means spamming it will not be that difficult. Adding this middleware to your bot limits the number of requests a specific Telegram user can send during a certain time frame.

## 🔧 Customizability
This middleware exposes 5 customizable options:
- `timeFrame`: The time frame during which the requests will be monitored (defaults to `1000` ms).
- `limit`: The number of requests allowed within each `timeFrame` (defaults to `1`).
- `storageClient`: The type of storage to use for keeping track of users and their requests. It supports Redis as well. The default value is `MEMORY_STORE` which uses an in-memory Map, but you can also pass in a Redis client from [ioredis](https://github.com/luin/ioredis) or [redis](https://deno.land/x/redis) packages. Other redis drivers might work as well, but I have not tested them.
- `onLimitExceeded`: A function that describes what to do if the user exceeds the limit (ignores the extra requests by default).
- `keyGenerator`: A function that returns a unique key generated for each user (it uses `from.id` by default). This key is used to identify the user, therefore it should be unique and user specific.
- `keyPrefix`: The prefix to be added to your key (returned from `keyGenerator`). Will be "RATE_LIMITER" if no value passed.

> Note: You must have redis-server **2.6.0** and above on your server to use Redis storage client with ratelimiter. Older versions of Redis are not supported.

## 💻 Runtime Support
This plugin supports both [grammY](https://grammy.dev/) and [Telegraf](https://telegraf.js.org/) bot frameworks, therefore Deno and Node are both supported. The following examples use [express](https://github.com/expressjs/express) but you can use ratelimiter with any grammy/telegraf supported framework or with no frameworks at all.

## 💻 How to Use
There are two ways of using ratelimiter:
- Accepting the defaults (Default Configuration).
- Passing a custom object containing your settings (Manual Configuration).

### ✅ Default Configuration

The following example uses [express](https://github.com/expressjs/express) as the webserver and [webhooks](https://grammy.dev/guide/deployment-types.html) to rate-limit users. This snippet demonstrates the easiest way of using ratelimiter which is accepting the default behavior:

``` typescript
import express from "express";
import { Bot } from "grammy";
import { limit } from "@grammyjs/ratelimiter"

const app = express();
const bot = new Bot("YOUR BOT TOKEN HERE");

app.use(express.json());
bot.use(limit());

app.listen(3000, () => {
bot.api.setWebhook("YOUR DOMAIN HERRE", { drop_pending_updates: true });
console.log('The application is listening on port 3000!');
})
```

### ✅ Manual Configuration

As mentioned before, you can pass an `Options` object to the `limit()` function to alter ratelimiter's behaviors. In the following snippet, I have decided to use Redis as my storage option:

``` typescript
import express from "express";
import { Bot } from "grammy";
import { limit } from "@grammyjs/ratelimiter"
import Redis from "ioredis";
At its core, `ratelimiter` acts as a configurable gatekeeper for incoming updates. It allows
developers to define precise rules for how many messages a user or chat (or any arbitrary entity)
can send in a given period, ensuring the bot remains responsive and server resources are protected
from overload.

The plugin inspects each incoming message, identifies its source, and decides if it should be
processed or dismissed based on the rules you set.

const app = express();
const bot = new Bot("YOUR BOT TOKEN HERE");
const redis = new Redis();


app.use(express.json());
bot.use(limit({
timeFrame: 2000,

limit: 3,

// "MEMORY_STORAGE" is the default mode. Therefore if you want to use Redis, do not pass storageClient at all.
storageClient: redis,

onLimitExceeded: ctx => { ctx?.reply("Please refrain from sending too many requests!") },

// Note that the key should be a number in string format such as "123456789"
keyGenerator: ctx => { return ctx.from?.id.toString() }
}));

app.listen(3000, () => {
bot.api.setWebhook("YOUR DOMAIN HERRE", { drop_pending_updates: true });
console.log('The application is listening on port 3000!');
})
```
As you can see in the above example, each user is allowed to send 3 requests every 2 seconds. If said user sends more requests, the bot replies with _Please refrain from sending too many requests_. That request will not travel further and dies immediately as we do not call `next()`.

> Note: To avoid flooding Telegram servers, `onLimitExceeded` is only executed once in every `timeFrame`.

Another use case would be limiting the incoming requests from a chat instead of a specific user:
``` typescript
import express from "express";
import { Bot } from "grammy";
import { limit } from "@grammyjs/ratelimiter"

const app = express();
const bot = new Bot("YOUR BOT TOKEN HERE");

app.use(express.json());
bot.use(limit({
keyGenerator: (ctx) => {
if (ctx.chat?.type === "group" || ctx.chat?.type === "supergroup") {
// Note that the key should be a number in string format such as "123456789"
return ctx.chat.id.toString();
}
}
}));

app.listen(3000, () => {
bot.api.setWebhook("YOUR DOMAIN HERRE", { drop_pending_updates: true });
console.log('The application is listening on port 3000!');
})
```
In this example, I have used `chat.id` as the unique key for rate-limiting.

## Acknowledgements
This package was heavily inspired by [telegraf-ratelimit](https://github.com/telegraf/telegraf-ratelimit).
> **For more information and how-to instructions, please visit**
> [**the official grammY ratelimiter documentation.**](https://grammy.dev/plugins/ratelimiter)

## License

Distributed under the MIT License. See `LICENSE` for more information.
27 changes: 27 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@grammyjs/ratelimiter",
"version": "2.0.0",
"exports": "./mod.ts",
"tasks": {
"check": "deno check **/*.ts",
"test": "deno test --allow-all",
"dev": "deno run --watch --allow-all examples/basic.ts"
},
"fmt": {
"options": {
"useTabs": true,
"lineWidth": 100,
"indentWidth": 4,
"singleQuote": true
}
},
"compilerOptions": {
"strict": true,
"lib": ["deno.ns", "deno.window"]
},
"imports": {
"grammy": "npm:grammy@^1.24.0",
"grammy/types": "npm:grammy@^1.24.0/types",
"@std/assert": "https://deno.land/std@0.224.0/assert/mod.ts"
}
}
Binary file added grammy-ratelimiter-cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading