A self-hosted Telegram antispam bot (Python 3.12+ / python-telegram-bot v22). It classifies messages from new members with a pluggable classifier backend (LLM via any OpenAI-compatible API, native Anthropic, local Ollama, or a zero-cost rule-based heuristic) and, on a spam verdict, deletes the message and applies a ban / mute / report action. Management, statistics and false-positive recovery happen in the bot's DM.
Languages: bot messages are available in English (default) and Russian, switchable at runtime with
/set language ru.
The moderation logic lives in a messenger-agnostic core that depends only on a
BotPlatform port; Telegram is one adapter. Supporting another messenger means
writing a single adapter (convert its updates to the event types in platform.py
and implement BotPlatform) — the core, classifiers and storage are reused as-is.
Message in a group
│
▼
Chat allowed? (ALLOWED_CHAT_IDS) ── no ─► ignore (bot leaves foreign chats)
│ yes
▼
Author trusted / admin / whitelisted? ── yes ─► skip
│ no
▼
Pre-filter (short & no links?) ── yes ─► skip (saves quota)
│ no
▼
Daily budget left? (llm_daily_limit) ── no ─► skip + alert (fail-safe)
│ yes
▼
Classifier backend ─► {is_spam, confidence, reason}
│
├─ spam, confidence ≥ threshold ─► delete + action (ban/mute/report) + report
├─ not spam ─► +1 clean; after N messages the author becomes trusted
└─ error ─► fail-safe: do nothing; alert admin on repeated failures
- Trust model. A newcomer is checked until they accumulate
trust_after_clean_msgsclean messages or staytrust_after_hoursin the group. Admins and whitelisted users are trusted immediately. - Fail-safe. If the classifier is unavailable, no sanction is applied.
- Prompt-injection guard. Message text is passed to the model as untrusted data inside markers; instructions inside the text are ignored.
- Abuse hardening. Chat allowlist with auto-leave, per-day classifier budget, callback-button authorization.
Select with CLASSIFIER_BACKEND:
| Backend | CLASSIFIER_BACKEND |
Notes |
|---|---|---|
| OpenAI-compatible | openai_compat (default) |
OpenRouter, OpenAI, Together, Groq, vLLM, LM Studio, … — set LLM_BASE_URL |
| Anthropic | anthropic |
Native Messages API; set LLM_BASE_URL=https://api.anthropic.com |
| Ollama | ollama |
Local models; set LLM_BASE_URL=http://localhost:11434 |
| Heuristic | heuristic |
Offline rule-based, no API key, zero cost |
Adding a backend = implement Classifier in antispam_bot/classifiers/ and register
it in classifiers/__init__.py.
- Create a bot via @BotFather, get
BOT_TOKEN. - Disable Privacy Mode: BotFather →
/mybots→ bot → Bot Settings → Group Privacy → Turn off (otherwise the bot can't see normal messages). - Add the bot to the group as an administrator with delete messages and ban users rights.
- Pick a backend and provide its credentials (for OpenRouter: a key from https://openrouter.ai/keys).
- Put your group's id in
ALLOWED_CHAT_IDSand your@username/id inADMIN_USERNAMES/ADMIN_USER_IDS.
cp .env.example .env # then fill it in
docker compose up -d --build
docker compose logs -fThe SQLite DB is stored in ./data (volume) and survives restarts. A container
healthcheck (heartbeat file) reports status in docker ps.
pip install -e . # or: pip install .
# set DB_PATH=./data/bot.db in .env for local runs
antispam-bot # or: python -m antispam_botThe bot uses long polling, so the server only needs outbound HTTPS to the Telegram API and your LLM provider — no inbound ports, public IP, or domain. That makes it easy to run behind NAT/firewalls. Requirements: Docker + Docker Compose, plus the BotFather steps from Setup (token, Privacy Mode off, bot is a group admin with delete/ban rights).
git clone https://github.com/kiselas/next_antibot.git
cd next_antibot
cp .env.example .env # then edit itMinimum .env for production:
BOT_TOKEN=123456:ABC...
ADMIN_USER_IDS=<your_telegram_id> # ask @userinfobot; more reliable than @username
ADMIN_CHAT_ID=<where to send action reports; your id works>
ALLOWED_CHAT_IDS=-100xxxxxxxxxx # your group id (see below)
CLASSIFIER_BACKEND=openai_compat # or `heuristic` for a key-less first run
LLM_API_KEY=sk-or-v1-...
LLM_MODEL=google/gemini-2.0-flash-lite-001Finding the group id: leave ALLOWED_CHAT_IDS empty for the first start, add the
bot to the group, and read Bot added to chat <id> from the logs (or use
@getidsbot). Then set ALLOWED_CHAT_IDS and restart.
docker compose up -d --build
docker compose logs -fOn first start, Alembic migrations create ./data/bot.db and the log shows
Bot @<name> started. Backend: ....
- Container health:
docker compose ps→STATUSbecomeshealthy(~30 s; driven by a heartbeat file). - Bot/admin works: DM the bot
/start,/stats,/config. Run/test купи крипту, пиши в личку t.me/scam— it returnsDecision: spam …. This exercises the whole classifier path with no side effects (best smoke test). - Live moderation: from a second, non-admin account post spam in the group —
the message is deleted and the user is banned/muted, with a report (and inline
buttons) sent to
ADMIN_CHAT_ID. If nothing happens, it's almost always Privacy Mode still on or the bot missing admin rights. - State on disk:
ls -la data/showsbot.dbandheartbeat.
docker compose logs -f --tail=100 # logs
docker compose restart # restart (e.g. after editing .env)
git pull && docker compose up -d --build # update (migrations auto-apply on start)- Backups: save the
./datadirectory (it holds the whole SQLite DB). - Tuning without restart:
/set,/setchat,/configin the bot's DM.
| Symptom | Likely cause / fix |
|---|---|
| Bot ignores group messages | Privacy Mode on, or bot not an admin with delete/ban rights |
| Log says it will work in ANY chat | ALLOWED_CHAT_IDS empty — set your group id |
| Spam not actioned; LLM errors (402/429) | Out of provider credits / rate limit — top up or switch LLM_MODEL; on failure the bot is fail-safe (no bans) |
| No action reports | ADMIN_CHAT_ID not set |
| Won't start, complains about variables | BOT_TOKEN missing (or LLM_API_KEY for non-heuristic backends) |
| Can't reach Telegram from the host | Needs outbound HTTPS to api.telegram.org. Where it's blocked, set TELEGRAM_PROXY (e.g. socks5h://user:pass@host:1080 or http://host:8080) to route Bot API + polling through a proxy |
| Can't reach OpenRouter/LLM from the host | Needs outbound HTTPS; behind restrictions use a host-level outbound proxy |
| Command | Action |
|---|---|
/stats |
Moderation statistics |
/recent [N] |
Last N actions with reason and text |
/test <text> |
Run the classifier on text with no side effects |
/config [chat_id] |
Current parameters (effective for a chat) |
/set <key> <value> |
Change a global parameter |
/setchat <chat_id> <key> <value> |
Override a parameter for one chat |
/unban <user_id> [chat_id] |
Lift a ban/mute and mark trusted |
/allow /unallow /whitelist |
Manage the whitelist |
/resetstats |
Reset statistics |
If ADMIN_CHAT_ID is set, each action is reported there with inline buttons
(Unban/Unmute, OK, or Ban in report mode) for one-tap correction.
Runtime parameters: enabled, language, action_mode (ban/mute/report),
spam_confidence_threshold, trust_after_clean_msgs, trust_after_hours,
min_chars_for_llm, max_chars_to_llm, llm_daily_limit, group_topic,
allowed_domains, model, use_json_format.
Per-chat settings. /set changes the global default; /setchat overrides a key
for a single chat (e.g. a stricter spam_confidence_threshold in one group). A chat
falls back to the global default for any key it hasn't overridden. language is
resolved globally.
State lives in SQLite (DB_PATH), with the schema managed by Alembic. Migrations
are applied automatically at startup, so upgrades are seamless. To run them by hand:
DB_PATH=./data/bot.db alembic upgrade headpip install -e ".[dev]"
ruff check antispam_bot tests
mypy antispam_bot
pytest --cov # 100% coverage
pre-commit install # optional: run ruff+mypy on commit- Classification uses text/captions only; image-only messages aren't analyzed.
- On OpenRouter's strictly-free tier the cap is 50 requests/day (1000/day after a one-time $10 top-up).
trust_after_hourscounts from when the bot first saw the user.
antispam_bot/
main.py entry point, application builder, polling
config.py env config (pydantic-settings)
i18n.py message catalogs (en/ru)
storage.py SQLite data access (trust, action log, stats, settings, whitelist)
db.py Alembic migration runner (applied at startup)
migrations/ Alembic environment + versioned schema migrations
runtime_settings.py dynamic per-chat parameters (/set, /setchat)
pipeline.py pre-filter -> classifier -> decision
core.py messenger-agnostic moderation logic + admin commands
platform.py BotPlatform port + normalized event types
telegram_adapter.py Telegram adapter: implements BotPlatform, converts updates
classifiers/ pluggable backends (base, openai_compat, anthropic, ollama, heuristic)
tests/ unit + integration tests (100% coverage)
MIT © 2026 Aleksandr