- Rust 54.8%
- JavaScript 23.7%
- HTML 11.2%
- CSS 10.3%
| migration | ||
| src | ||
| .env-sample | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| docker-compose.yaml | ||
| README.md | ||
Maxwell Games Bot
Telegram bot for hosting games (DumbGame, Vector) with score tracking. Migrated from Python/Flask to Rust.
Built with teloxide + Axum + PostgreSQL via SeaORM.
Features
/start,/play— welcome message with inline button to browse games/about— bot description and links- Inline query — list available games in any chat via
@botname - Callback query — handle "Play" button clicks, redirect to game frontend with encoded session data
POST /setScore— REST endpoint called by game frontend to submit scoresGET /getScoreBoard— REST endpoint to fetch high scores for a game session- Usage metrics tracked in PostgreSQL and emitted as structured logs via
tracing
Prerequisites
- Rust (stable, 1.75+)
- PostgreSQL 14+ (or Docker)
- A Telegram bot token from @BotFather
- The bot must have Inline Mode enabled (via @BotFather) and the games registered
Local Development
1. Clone and configure
cp .env-sample .env
# Edit .env with your values
2. Start the database
docker compose up -d
3. Run the bot
cargo run
Migrations run automatically at startup. Logs go to stdout; set RUST_LOG=debug for verbose output.
Webhook note: Telegram cannot reach
localhost. For local development use ngrok or localtunnel to expose your port, then setWEBHOOK_URLto the tunnel's HTTPS URL.ngrok http 8080 # Set WEBHOOK_URL=https://<random>.ngrok.io/<path> in .env
Configuration
All configuration is via environment variables (.env file in development).
| Variable | Required | Description |
|---|---|---|
TELOXIDE_TOKEN |
Yes | Bot token from @BotFather |
DATABASE_URL |
Yes | PostgreSQL connection string, e.g. postgres://postgres:pass@localhost:5432/games_bot |
WEBHOOK_PORT |
Yes | Local port the process listens on (e.g. 8080) |
WEBHOOK_URL |
Yes | Public HTTPS URL Telegram posts updates to (must be reachable by Telegram) |
POSTGRES_PASSWORD |
For Docker | Password used by docker-compose.yaml |
POSTGRES_DB |
For Docker | Database name (default: games_bot) |
TELEGRAM_BOT_API_URL |
Yes | URL of the Telegram Bot API server (local or official, e.g. http://localhost:8081) |
RUST_LOG |
No | Log level filter, e.g. info, debug, maxwell_games_bot=debug |
Deploy (systemd + nginx)
1. Build the release binary
cargo build --release
# Binary at: target/release/maxwell-games-bot
2. Copy binary and env file to server
scp target/release/maxwell-games-bot user@server:/opt/maxwell-games-bot/
scp .env user@server:/opt/maxwell-games-bot/
3. Create systemd service
/etc/systemd/system/maxwell-games-bot.service:
[Unit]
Description=Maxwell Games Telegram Bot
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/maxwell-games-bot
EnvironmentFile=/opt/maxwell-games-bot/.env
ExecStart=/opt/maxwell-games-bot/maxwell-games-bot
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now maxwell-games-bot
sudo journalctl -u maxwell-games-bot -f # follow logs
4. Configure nginx reverse proxy
The bot and REST API run on the same port. Expose them via a single nginx location block:
server {
listen 443 ssl;
server_name yourdomain.com;
# SSL config here (certbot/Let's Encrypt recommended)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Set WEBHOOK_URL=https://yourdomain.com/<chosen-path> and WEBHOOK_PORT=8080 in your .env. The path is arbitrary — teloxide registers it with Telegram on startup.
CORS: The API endpoints (
/setScore,/getScoreBoard,/games) useCorsLayer::permissive()so the game frontend can reach them from any origin. The Telegram webhook path is not affected. To restrict by origin, updatesrc/api/mod.rsto useCorsLayer::new().allow_origin(...).
Usage Metrics
Every interaction is logged in two ways:
1. Structured logs (stdout, feed to any log aggregator):
{"timestamp":"...","level":"INFO","fields":{"user_id":123,"game":"dumbgame","message":"game_session_started"}}
Set RUST_LOG=info for production; RUST_LOG=debug for verbose output including DB operations.
2. PostgreSQL event table (queryable analytics):
-- Most played games
SELECT game_name, COUNT(*) AS sessions
FROM event
WHERE event_type = 'GameStarted'
GROUP BY game_name
ORDER BY sessions DESC;
-- Daily active users
SELECT DATE(date) AS day, COUNT(DISTINCT user_id) AS dau
FROM event
GROUP BY day
ORDER BY day DESC;
-- Score update rate
SELECT
COUNT(*) FILTER (WHERE event_type = 'SetScore') AS total_submissions,
COUNT(*) FILTER (WHERE event_type = 'ScoreUpdated') AS actual_updates
FROM event;
-- New users per week
SELECT DATE_TRUNC('week', created_at) AS week, COUNT(*) AS new_users
FROM game_user
GROUP BY week
ORDER BY week DESC;
Event types logged:
| Event | Trigger |
|---|---|
InlineQuery |
User searches for games via @botname |
GameStarted |
User clicks "Play" on a game |
SetScore |
Game frontend submits a score |
ScoreUpdated |
Score was actually a new high score |
GetScoreboard |
Game frontend fetches the leaderboard |
CommandStart |
User sends /start |
CommandPlay |
User sends /play |
CommandAbout |
User sends /about |
Architecture
maxwell-games-bot/
├── src/
│ ├── main.rs — entry point: tracing setup, DB init, webhook + API server, dispatcher
│ ├── models.rs — GameData (base64+JSON encoding), Telegram API types, ScoreEntry
│ ├── bot/
│ │ ├── commands.rs — /start, /play, /about handlers
│ │ ├── inline.rs — inline query handler (game list)
│ │ └── callbacks.rs — callback query handler (game launch URL)
│ ├── api/
│ │ ├── mod.rs — Axum router
│ │ └── handlers.rs — set_score, get_scoreboard, health_check
│ └── db/
│ ├── mod.rs — DatabaseHandler (upsert_user, log_event, run_migrations)
│ └── schema/ — SeaORM entities: game_user, event
└── migration/ — DB migration crate (runs via DatabaseHandler::run_migrations at startup)
The Telegram webhook and the REST API share a single Axum server (via teloxide's axum_to_router). On startup, the process registers the webhook URL with Telegram, applies pending migrations, and starts serving.
Migration from Python
The original Python bot used eval(bytes.fromhex(data)) to decode game session data — a remote code execution vulnerability. The Rust bot replaces this with base64-encoded JSON (models::GameData).
Impact: URLs generated by the old Python bot (stored in Telegram chat history) will not decode correctly with the new bot. New sessions created by the Rust bot work correctly end-to-end. No action needed — old URLs simply expire as users start new game sessions.