Nikolay Tatarinov 87e8417675 refactor: improve code readability and structure in various components
- Refactored the `mini_app_short_name` assignment in `config.py` for better clarity.
- Enhanced the `app_config_js` function in `app.py` to improve formatting of the JavaScript response body.
- Added per-chat locks in `group_duty_pin.py` to prevent concurrent refreshes, improving message handling.
- Updated `_schedule_next_update` to include optional jitter for scheduling, enhancing performance during high-load scenarios.
- Cleaned up test files by removing unused imports and improving test descriptions for clarity.
2026-03-03 17:52:23 +03:00

Duty Teller (Telegram Bot)

A minimal Telegram bot boilerplate using python-telegram-bot v22 with the Application API. The bot and web UI support Russian and English (language from Telegram or DEFAULT_LANGUAGE).

History of changes: CHANGELOG.md.

Get a bot token

  1. Open Telegram and search for @BotFather.
  2. Send /newbot and follow the prompts to create a bot.
  3. Copy the token BotFather gives you.

Setup

  1. Clone and enter the project

    cd duty-teller
    
  2. Create a virtual environment (recommended)

    python -m venv .venv
    source .venv/bin/activate   # Linux/macOS
    # or: .venv\Scripts\activate  # Windows
    
  3. Install dependencies

    pip install -r requirements.txt
    
  4. Configure the bot

    cp .env.example .env
    

    Edit .env and set BOT_TOKEN to the token from BotFather.

  5. Miniapp access (calendar)
    Access is controlled by roles in the DB (assigned by an admin with /set_role @username user|admin). Set ADMIN_USERNAMES (and optionally ADMIN_PHONES) so that at least one admin can use the bot and assign roles; these also act as a fallback for admin when a user has no role in the DB. See docs/configuration.md.
    Mini App URL: When configuring the bot's menu button or Web App URL (e.g. in @BotFather or via setChatMenuButton), use the URL with a trailing slash, e.g. https://your-domain.com/app/. A redirect from /app to /app/ can cause the browser to drop the fragment that Telegram sends, which breaks authorization.
    How to open: Users must open the calendar via the bot's menu button (⋮ → "Calendar" or the configured label) or a Web App inline button. If they use "Open in browser" or a direct link, Telegram may not send user data (tgWebAppData), and access will be denied.
    BOT_TOKEN: The server that serves /api/duties (e.g. your production host) must have in .env the same bot token as the bot from which users open the Mini App. If the token differs (e.g. test vs production bot), validation returns "hash_mismatch" and access is denied.

  6. Other options
    Full list of environment variables (types, defaults, examples): docs/configuration.md.

Run

python main.py

Or after pip install -e .:

duty-teller

The bot runs in polling mode. Send /start or /help to your bot in Telegram to test.

Bot commands

  • /start — Greeting and user registration in the database.
  • /help — Help on available commands.
  • /set_phone [number] — Set or clear phone number (private chat only); used for access via ALLOWED_PHONES / ADMIN_PHONES.
  • /import_duty_schedule — Import duty schedule (admin only); see Duty schedule import below for the two-step flow.
  • /set_role @username user|admin — Set a users role (admin only). Alternatively, reply to a message and send /set_role user|admin.
  • /pin_duty — Pin the current duty message in a group (reply to the bots duty message); time/timezone for the pinned message come from DUTY_DISPLAY_TZ.

Run with Docker

Ensure .env exists (e.g. cp .env.example .env) and contains BOT_TOKEN.

  • Dev (volume mount; code changes apply without rebuild):

    docker compose -f docker-compose.dev.yml up --build
    

    Stop with Ctrl+C or docker compose -f docker-compose.dev.yml down.

  • Prod (no volume; runs the built image; restarts on failure):

    docker compose -f docker-compose.prod.yml up -d --build
    

    For production deployments you may use Docker secrets or your orchestrators env instead of a .env file.

The image is built from Dockerfile; on start, entrypoint.sh runs Alembic migrations then starts the app as user botuser.

API auth: Without valid X-Telegram-Init-Data header, GET /api/duties and GET /api/calendar-events return 403. The only way to allow unauthenticated access is MINI_APP_SKIP_AUTH=1 (dev only; do not use in production). When behind a reverse proxy, ensure the Mini App is opened from Telegram so initData is sent.

API

The HTTP server is FastAPI; the miniapp is served at /app.

Interactive API documentation (Swagger UI) is available at /docs, e.g. http://localhost:8080/docs when running locally.

  • GET /api/duties — List of duties (date params; auth via Telegram initData or, in dev only, MINI_APP_SKIP_AUTH).
  • GET /api/calendar-events — Calendar events (including external ICS when EXTERNAL_CALENDAR_ICS_URL is set).
  • GET /api/calendar/ical/{token}.ics — Personal ICS calendar (by secret token; no Telegram auth).

Without initData the API returns 403; for local dev without Telegram use MINI_APP_SKIP_AUTH=1 (insecure, dev only).

Project layout

High-level architecture (components, data flow, package relationships) is described in docs/architecture.md.

  • main.py Entry point: calls duty_teller.run:main. Alternatively, after pip install -e ., run the console command duty-teller (see pyproject.toml and duty_teller/run.py). The runner builds the Application, registers handlers, runs polling and FastAPI in a thread, and calls duty_teller.config.require_bot_token() so the app exits with a clear message if BOT_TOKEN is missing.
  • duty_teller/ Main package (install with pip install -e .). Contains:
    • config.py Loads BOT_TOKEN, DATABASE_URL, ALLOWED_USERNAMES, etc. from env; no exit on import; use require_bot_token() in the entry point when running the bot. Optional Settings dataclass for tests. PROJECT_ROOT for webapp path.
    • api/ FastAPI app (/api/duties, /api/calendar-events), dependencies.py (DB session, auth, date validation), static webapp mounted from PROJECT_ROOT/webapp-next/out (built from webapp-next/).
    • db/ SQLAlchemy models, session (session_scope), repository, schemas.
    • handlers/ Telegram command and chat handlers; register via register_handlers(app).
    • i18n/ Translations and language detection (ru/en); used by handlers and API.
    • services/ Business logic (group duty pin, import); accept session from caller.
    • utils/ Shared date, user, and handover helpers.
    • importers/ Duty-schedule JSON parser.
  • alembic/ Migrations; config in pyproject.toml under [tool.alembic]; URL and metadata from duty_teller.config and duty_teller.db.models.Base. Run: alembic -c pyproject.toml upgrade head.
  • webapp-next/ Miniapp UI (Next.js, TypeScript, Tailwind, shadcn/ui); build output in webapp-next/out/, served at /app.
  • tests/ Tests; helpers.py provides make_init_data for auth tests.
  • pyproject.toml Installable package (pip install -e .).

Documentation: The docs/ folder contains configuration reference, architecture, import format, and runbook. API reference is generated from the code. Build: mkdocs build (requires pip install -e ".[docs]"). Preview: mkdocs serve.

To add commands, define async handlers in duty_teller/handlers/commands.py (or a new module) and register them in duty_teller/handlers/__init__.py.

Duty schedule import (duty-schedule)

The /import_duty_schedule command is available only to users in ADMIN_USERNAMES or ADMIN_PHONES. Import is done in two steps:

  1. Handover time — The bot asks for the shift handover time and optional timezone (e.g. 09:00 Europe/Moscow or 06:00 UTC). This is converted to UTC and used as the boundary between duty periods when creating records.
  2. JSON file — Send a file in duty-schedule format.

Format: at the root of the JSON — a meta object with start_date (YYYY-MM-DD) and a schedule array of objects with name (full name) and duty (string with separator ;; characters в/В/б = duty, Н = unavailable, О = vacation). The number of days is given by the length of the duty string. On re-import, duties in the same date range for each user are replaced by the new data.

Full format description and example JSON: docs/import-format.md.

Tests

Run from the repository root (no src/ directory; package is duty_teller at the root). Use PYTHONPATH=. if needed:

pip install -r requirements-dev.txt
pytest

Tests cover api/telegram_auth (validate_init_data, auth_date expiry), config (is_admin, can_access_miniapp), and the API (date validation, 403/200 with mocked auth, plus an E2E auth test without auth mocks).

CI (Gitea Actions): Lint (ruff), tests (pytest), security (bandit). Run from the repo root with PYTHONPATH=. if needed.

Contributing

  • Commits: Use Conventional Commits: feat:, fix:, docs:, refactor:, test:, chore:, etc.
  • Branches: Follow Gitea Flow: main branch main, features and fixes in separate branches.
  • Changes: Via Pull Request in Gitea; run linters and tests (ruff check ., pytest) before merge.
  • Documentation: Project documentation is in English; see CONTRIBUTING.md for details.

Logs and rotation

To meet the 7-day log retention policy, configure log rotation at deploy time: e.g. logrotate, systemd logging settings, or Docker (size/time retention limits). Keep application logs for no more than 7 days.

Description
No description provided
Readme 2.8 MiB
v2.1.3 Latest
2026-03-07 00:40:47 +03:00
Languages
HTML 52.1%
Python 25.8%
TypeScript 20.7%
CSS 1.2%
Dockerfile 0.1%