Skip to content
abu-sinanPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

NextGen Verification Bot

A production-ready Telegram bot built with Aiogram 3.x that requires users to join a configurable set of channels before they can use any bot functionality. Built with a clean, layered architecture on top of PostgreSQL + SQLAlchemy 2.x (async) + Alembic.

Features

  • Force-subscription gate for any number of channels, configured in one place
  • Continuous re-validation: an already-verified user's membership is re-checked with Telegram on a TTL cache (default 10 min), so someone who leaves a channel is caught without calling the Telegram API on every single interaction
  • Resilient to flaky connections: every outgoing Telegram API call -- not just verification checks -- automatically retries on a dropped connection or flood control before giving up
  • Verification protects the entire bot by default -- new commands are gated automatically, with no per-handler boilerplate required
  • Edits the verification message in place on failure/retry; deletes it and sends exactly one welcome message on success -- never spams the chat
  • Repository pattern over an async SQLAlchemy engine, connection pooling, race-safe upsert-on-/start
  • Structured logging, global error handler, per-user callback throttling
  • Owner-only /stats command

Requirements

  • Python 3.12+
  • PostgreSQL 14+
  • A Telegram bot token from @BotFather
  • The bot added as an administrator to every required channel (see Troubleshooting -- this is a hard Telegram API requirement, not a bug)

Installation

git clone <your-repo-url> nextgen-bot
cd nextgen-bot
python3.12 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env                # then edit .env, see below

Configuration

All configuration lives in .env and is loaded centrally by bot/config/settings.py -- nothing else in the codebase reads environment variables directly.

Environment Variables

Variable Required Description
BOT_TOKEN yes Token from @BotFather
OWNER_ID yes Your numeric Telegram user ID; unlocks /stats
DATABASE_URL yes postgresql+asyncpg://user:pass@host:5432/dbname
REQUIRED_CHANNELS no Comma-separated channel usernames (default: the four NextGen channels)
VERIFICATION_CACHE_TTL_SECONDS no How long a verified user's membership stays trusted before re-checking (default 600 = 10 min)
NETWORK_RETRY_MAX_ATTEMPTS no Retries per outgoing API call on connection failure (default 2)
NETWORK_RETRY_DELAY_SECONDS no Delay between those retries (default 1.0)
LOG_LEVEL no DEBUG / INFO / WARNING / ERROR (default INFO)
AUTO_CREATE_TABLES no true to create tables from ORM metadata on startup (dev only)

To change the required channels later, edit REQUIRED_CHANNELS and restart the bot -- no code changes needed.

Database Setup

Apply migrations (recommended for any real deployment):

alembic upgrade head

For quick local testing without Postgres migrations, set AUTO_CREATE_TABLES=true in .env instead and skip the step above -- the bot will create the schema itself on startup.

When you change bot/models/user.py, generate a new migration with:

alembic revision --autogenerate -m "describe the change"
alembic upgrade head

Running the Bot

python main.py

The bot polls Telegram for updates; no public URL or webhook is required.

Project Structure

nextgen-bot/
├── main.py                    # Entry point: wiring + polling
├── bot/
│   ├── config/                # Settings (env vars) + logging setup
│   ├── models/                # SQLAlchemy ORM models
│   ├── database/               # Engine/session factory + base
│   │   └── repositories/       # Repository pattern (DB access only)
│   ├── services/                # Business logic (user, verification)
│   ├── middlewares/             # DB session, access control, throttling
│   │                             # + network_retry.py (client session, not dispatch)
│   ├── filters/                 # e.g. owner-only filter
│   ├── keyboards/                # Inline keyboard builders
│   ├── handlers/                 # Telegram-facing routers
│   ├── states/                   # Reserved for future FSM flows
│   └── utils/                    # Constants, message templates
├── alembic/                    # Migrations
├── logs/                        # Rotating log files
├── requirements.txt
├── .env.example
├── Dockerfile
└── docker-compose.yml

Adding a real feature: create a new router in bot/handlers/, register it in bot/handlers/__init__.py (before menu.router, which is the catch-all and must stay last). It will be verification-gated automatically. To exempt a handler from the gate, add flags={"skip_verification": True} to its registration, the same way /start and the Verify button do.

Deployment

Ubuntu VPS (systemd)

sudo apt update && sudo apt install -y python3.12 python3.12-venv postgresql
# ... create DB/user, then follow Installation above inside e.g. /opt/nextgen-bot

Create /etc/systemd/system/nextgen-bot.service:

[Unit]
Description=NextGen Verification Bot
After=network.target postgresql.service

[Service]
Type=simple
User=nextgen
WorkingDirectory=/opt/nextgen-bot
EnvironmentFile=/opt/nextgen-bot/.env
ExecStart=/opt/nextgen-bot/.venv/bin/python main.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now nextgen-bot
journalctl -u nextgen-bot -f

Docker

cp .env.example .env   # edit it, and set DATABASE_URL to use host "postgres"
docker compose up -d --build
docker compose exec bot alembic upgrade head

Railway / Render

  1. Push the repo, create a new Web/Background Worker service pointing at it (Render: choose Background Worker, not Web Service, since this bot polls rather than listening on a port).
  2. Add a managed PostgreSQL instance on the same platform and copy its connection string into DATABASE_URL (adjust the scheme to postgresql+asyncpg://).
  3. Set BOT_TOKEN, OWNER_ID, and any other variables from the table above.
  4. Set the start command to alembic upgrade head && python main.py (or run the migration once from a shell/one-off job, then use python main.py as the persistent start command).

Troubleshooting

Verification always fails even though I joined every channel. The bot must be an administrator of each required channel -- Telegram's getChatMember does not return reliable data to bots that are only regular members. Add the bot as admin (no special permissions needed beyond default) in every channel listed in REQUIRED_CHANNELS.

Verification silently fails for one specific channel. Check the logs -- VerificationService logs an error per channel it can't read, distinguishing "bot isn't admin" from "bot was removed/blocked" from other API errors.

"message is not modified" errors. Already handled -- this happens when a user presses Verify twice with the same missing channels; the bot suppresses it silently rather than crashing.

I changed REQUIRED_CHANNELS but nothing changed. Restart the bot process; environment variables are read once at startup.

High memory/connection usage under load. Tune pool_size / max_overflow in bot/database/engine.py against your Postgres server's max_connections, and consider AUTO_CREATE_TABLES=false plus proper indexes (already included) for large user counts.

Notes & Trade-offs

  • Network retries happen once, centrally -- not per call site. bot/middlewares/network_retry.py is a client session middleware (bot.session.middleware(...)), not a dispatch middleware like the others in that folder: it wraps every single outgoing Bot API call (sendMessage, editMessageText, getChatMember, all of it) and retries on TelegramNetworkError (connection resets/timeouts) and TelegramRetryAfter (flood control) before the exception ever reaches calling code. This replaced an earlier version that retried only inside VerificationService._check_channel -- that only protected membership checks, and a plain message.answer() failing from the same connection reset showed the gap. Tune NETWORK_RETRY_MAX_ATTEMPTS / NETWORK_RETRY_DELAY_SECONDS for a flakier or steadier environment. This only reduces how often a transient blip surfaces as a failure; it doesn't change what a failure means to either caller (fail-closed for the Verify button, fail-open for re-validation) -- retries happen first, and only a request that fails even after retrying reaches that logic at all.
  • Re-validation fails open, not closed. If a periodic re-check can't reach Telegram (network blip, bot temporarily lost admin rights, etc.), VerificationService.revalidate() keeps the user verified and retries on their next interaction -- it only revokes on an explicit LEFT/KICKED/ RESTRICTED status. This is intentional: the alternative (revoking on any failure) would mass-deverify your user base during a Telegram or network outage. MembershipResult.missing_channels (used by the Verify button) keeps the opposite, fail-closed behavior, since an unconfirmed channel shouldn't let someone in for the first time.
  • Middleware ordering is load-bearing, not stylistic. DatabaseMiddleware is registered as outer; AccessControlMiddleware is registered as inner (bot/middlewares/access_control.py explains why). aiogram only resolves a handler's flags after filter-matching, which happens between the outer and inner stages -- if AccessControlMiddleware were outer, it would never see the skip_verification flag on /start or the Verify button, and no one could ever complete verification. Don't "simplify" this by moving it back to outer.
  • FSM storage is in-memory (MemoryStorage). Fine for a single bot instance; if you scale to multiple workers, switch to Redis storage and move ThrottlingMiddleware's cache to Redis too, so both are shared across processes.
  • Polling vs. webhooks: this bot uses long polling for simplicity and portability across all four deployment targets. For very high throughput, a webhook-based aiohttp server is a drop-in alternative.
  • Required channels are configured via env var with a sensible default, not a database table. That satisfies "changeable without code changes"; if you want channels editable without a restart (e.g. via an admin command), move ChannelConfig storage into Postgres and cache it with a short TTL.

Improvements (optional future work)

  • Admin commands to add/remove required channels at runtime (DB-backed config)
  • Webhook mode for high-throughput deployments
  • Redis-backed FSM storage and throttling for multi-instance scaling
  • Per-channel "last checked" caching to reduce getChatMember calls further
  • i18n for multi-language verification messages (language_code is already stored)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages