A multi-bot Telegram platform on a single domain. A bot-agnostic Core layer
hosts self-contained bot modules under src/Bot/<Name>/. The first module is
the AI sticker bot, which generates stickers with OpenAI's image generation.
Adding a bot is near-zero-ops: per-bot Telegram credentials live in the database (not env/secrets), so a new bot that reuses an existing module is just a row in the admin UI — no env changes, no redeploy.
src/
├── Core/ # bot-agnostic platform: webhook routing, command dispatch,
│ # per-bot API clients, DB-backed bot config, user scoping
└── Bot/Sticker/ # the sticker bot module (reference implementation)
- One webhook endpoint —
POST /webhook/{bot}. The{bot}segment selects the bot from thebotDB table; the row supplies the token and the webhook secret. - Per-bot users — a single
usertable scoped by abotcolumn; each module's repository always queries by bot, so users never leak across bots. - Telegram SDK —
irazasyed/telegram-bot-sdkwrapped in a small Symfony integration (no framework bundle).
In a chat with the bot:
/sticker 🐱 happy orange cat --pixel # generate a sticker (optional style flag)
/remix <photo caption> # turn a photo into a sticker
/pack # manage your sticker packs
- Framework: Symfony 7
- Database: MySQL
- Telegram: irazasyed/telegram-bot-sdk
- Image Processing: Intervention Image
- Runtime: Docker (php-fpm + nginx), reverse-proxied by Plesk
- Deployment: GitHub Actions → GHCR images → Docker Compose on the host
Reusing an existing module (e.g. a second sticker bot) — no deploy:
- Create the bot with @BotFather.
- In the admin UI (
/admin/bots) → Add bot (name, username, token). The webhook secret is generated automatically. - Click Register webhook. Done — it serves at
/webhook/<name>.
CLI equivalent: bin/console app:bot:add <name> <token> <username> then
bin/console app:telegram:webhook set --bot=<name>.
A new bot type (new commands) — deploy once, then the steps above:
- Create
src/Bot/<Name>/:<Name>Module.phpwithpublic const NAME = '<name>';Telegram/Abstract<Name>Command.phpextendingCore\Telegram\BotCommandwithpublic const BOT = <Name>Module::NAME;Entity/<Name>User.php extends Core\Entity\BotUser(can add its own fields)- command classes extending the module's abstract command
- If the module has entities, add a
doctrine.yamlmapping and runbin/console doctrine:migrations:diff/migrate. - Deploy, then add the bot row + register its webhook as above.
No new domain, no routing edits, no per-bot env vars or deploy secrets.
Only app-global values — not per-bot Telegram credentials (those live in the DB):
| Variable | Description |
|---|---|
APP_BASE_URL |
Public base URL, used to build webhook URLs (no trailing slash) |
OPENAI_API_KEY |
OpenAI API key (sticker module) |
DATABASE_URL |
MySQL connection string |
APP_SECRET |
Random 64-char hex string (openssl rand -hex 32) |
SENTRY_DSN |
Sentry DSN (optional) |
docker compose up --build # php (:8000) + mysql
docker compose exec php composer install
docker compose exec php bin/console doctrine:migrations:migrate
docker compose exec php bin/console app:bot:add sticker <token> <username>
docker compose exec php bin/console app:dev:webhook # ngrok + register webhooksOr run the app directly with symfony server:start against a local MySQL.
Pushes to master build the php/web images, push them to GHCR, and deploy via
Docker Compose on the Plesk host (reachable at the loopback port Plesk proxies).
Required repository secrets:
| Secret | Description |
|---|---|
SERVER_HOST |
Server IP / hostname |
SERVER_USER |
SSH username |
SERVER_SSH_KEY |
Private SSH key |
DEPLOY_PATH |
Directory on the host holding compose.prod.yaml + .env.prod.local |
TELEGRAM_BOT_TOKEN |
Sticker bot token — used once to seed the DB row on first deploy |
TELEGRAM_BOT_USERNAME |
Sticker bot username — same |
On the host, create .env.prod.local from .env.prod.example (app-global vars only).
After the sticker bot's row is seeded, the TELEGRAM_BOT_* secrets and the seed step
can be removed — bots are managed from /admin/bots.
MIT