Skip to content

About

Self-hosted Telegram antispam bot with pluggable LLM classifiers (OpenRouter/OpenAI/Anthropic/Ollama) and a messenger-agnostic core.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Antispam Bot

CI CodeQL License: MIT

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.

How it works

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_msgs clean messages or stay trust_after_hours in 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.

Classifier backends

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.

Setup

  1. Create a bot via @BotFather, get BOT_TOKEN.
  2. Disable Privacy Mode: BotFather → /mybots → bot → Bot Settings → Group Privacy → Turn off (otherwise the bot can't see normal messages).
  3. Add the bot to the group as an administrator with delete messages and ban users rights.
  4. Pick a backend and provide its credentials (for OpenRouter: a key from https://openrouter.ai/keys).
  5. Put your group's id in ALLOWED_CHAT_IDS and your @username/id in ADMIN_USERNAMES / ADMIN_USER_IDS.

Run

cp .env.example .env   # then fill it in
docker compose up -d --build
docker compose logs -f

The SQLite DB is stored in ./data (volume) and survives restarts. A container healthcheck (heartbeat file) reports status in docker ps.

Local / without Docker

pip install -e .            # or: pip install .
# set DB_PATH=./data/bot.db in .env for local runs
antispam-bot               # or: python -m antispam_bot

Deployment

The 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).

1. Get the code and configure

git clone https://github.com/kiselas/next_antibot.git
cd next_antibot
cp .env.example .env        # then edit it

Minimum .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-001

Finding 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.

2. Start

docker compose up -d --build
docker compose logs -f

On first start, Alembic migrations create ./data/bot.db and the log shows Bot @<name> started. Backend: ....

3. Verify

  1. Container health: docker compose ps → STATUS becomes healthy (~30 s; driven by a heartbeat file).
  2. Bot/admin works: DM the bot /start, /stats, /config. Run /test купи крипту, пиши в личку t.me/scam — it returns Decision: spam …. This exercises the whole classifier path with no side effects (best smoke test).
  3. 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.
  4. State on disk: ls -la data/ shows bot.db and heartbeat.

4. Operate

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 ./data directory (it holds the whole SQLite DB).
  • Tuning without restart: /set, /setchat, /config in the bot's DM.

Troubleshooting

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

Management (in the bot's DM, admins only)

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.

Database & migrations

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 head

Development

pip install -e ".[dev]"
ruff check antispam_bot tests
mypy antispam_bot
pytest --cov            # 100% coverage
pre-commit install      # optional: run ruff+mypy on commit

Limitations

  • 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_hours counts from when the bot first saw the user.

Project layout

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)

Contributing & security

License

MIT © 2026 Aleksandr

About

Self-hosted Telegram antispam bot with pluggable LLM classifiers (OpenRouter/OpenAI/Anthropic/Ollama) and a messenger-agnostic core.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages