A Telegram bot serving HTML games
  • Rust 54.8%
  • JavaScript 23.7%
  • HTML 11.2%
  • CSS 10.3%
Find a file
2026-05-29 09:43:09 -03:00
migration feat: self-handle highscores 2026-05-28 20:44:04 -03:00
src chore: fix some issues 2026-05-29 09:43:09 -03:00
.env-sample feat: add games directory 2026-05-27 20:17:11 -03:00
.gitignore Initial commit 2026-05-27 19:02:50 -03:00
Cargo.lock feat: self-handle highscores 2026-05-28 20:44:04 -03:00
Cargo.toml feat: self-handle highscores 2026-05-28 20:44:04 -03:00
docker-compose.yaml Initial commit 2026-05-27 19:02:50 -03:00
README.md chore: fix some issues 2026-05-29 09:43:09 -03:00

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 scores
  • GET /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 set WEBHOOK_URL to 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) use CorsLayer::permissive() so the game frontend can reach them from any origin. The Telegram webhook path is not affected. To restrict by origin, update src/api/mod.rs to use CorsLayer::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.