- Refactored the configuration loading in `config.py` to utilize a single source of truth through the `Settings` class, improving maintainability and clarity. - Introduced the `is_admin_for_telegram_user` function in `repository.py` to centralize admin checks based on usernames and phone numbers. - Updated command handlers to use the new admin check function, ensuring consistent access control across the application. - Enhanced error handling in the `error_handler` to log exceptions when sending error replies to users, improving debugging capabilities. - Improved the handling of user phone updates in `repository.py` to ensure proper normalization and validation of phone numbers.
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
- Open Telegram and search for @BotFather.
- Send
/newbotand follow the prompts to create a bot. - Copy the token BotFather gives you.
Setup
-
Clone and enter the project
cd duty-teller -
Create a virtual environment (recommended)
python -m venv venv source venv/bin/activate # Linux/macOS # or: venv\Scripts\activate # Windows -
Install dependencies
pip install -r requirements.txt -
Configure the bot
cp .env.example .envEdit
.envand setBOT_TOKENto the token from BotFather. -
Miniapp access (calendar)
SetALLOWED_USERNAMES(and optionallyADMIN_USERNAMES) to allow access to the calendar miniapp; if both are empty, no one can open it. Users can also be allowed by phone viaALLOWED_PHONES/ADMIN_PHONESafter setting a phone with/set_phone.
Mini App URL: When configuring the bot's menu button or Web App URL (e.g. in @BotFather or viasetChatMenuButton), use the URL with a trailing slash, e.g.https://your-domain.com/app/. A redirect from/appto/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.envthe 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. -
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 viaALLOWED_PHONES/ADMIN_PHONES./import_duty_schedule— Import duty schedule (only forADMIN_USERNAMES/ADMIN_PHONES); see Duty schedule import below for the two-step flow./pin_duty— Pin the current duty message in a group (reply to the bot’s duty message); time/timezone for the pinned message come fromDUTY_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 --buildStop with
Ctrl+Cordocker 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 --buildFor production deployments you may use Docker secrets or your orchestrator’s env instead of a
.envfile.
The image is built from Dockerfile; on start, entrypoint.sh runs Alembic migrations then starts the app as user botuser.
Production behind a reverse proxy: When the app is behind nginx/Caddy etc., request.client.host is usually the proxy (e.g. 127.0.0.1). The "private IP" bypass (allowing requests without initData from localhost) then applies to the proxy, not the real client. Either ensure the Mini App always sends initData, or forward the real client IP (e.g. X-Forwarded-For) and use it for that check. See api/app.py _is_private_client for details.
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,MINI_APP_SKIP_AUTH/ private IP).GET /api/calendar-events— Calendar events (including external ICS whenEXTERNAL_CALENDAR_ICS_URLis set).GET /api/calendar/ical/{token}.ics— Personal ICS calendar (by secret token; no Telegram auth).
For production, initData validation is required; see the reverse-proxy paragraph above for proxy/headers.
Project layout
High-level architecture (components, data flow, package relationships) is described in docs/architecture.md.
main.py– Entry point: callsduty_teller.run:main. Alternatively, afterpip install -e ., run the console commandduty-teller(seepyproject.tomlandduty_teller/run.py). The runner builds theApplication, registers handlers, runs polling and FastAPI in a thread, and callsduty_teller.config.require_bot_token()so the app exits with a clear message ifBOT_TOKENis missing.duty_teller/– Main package (install withpip install -e .). Contains:config.py– LoadsBOT_TOKEN,DATABASE_URL,ALLOWED_USERNAMES, etc. from env; no exit on import; userequire_bot_token()in the entry point when running the bot. OptionalSettingsdataclass for tests.PROJECT_ROOTfor webapp path.api/– FastAPI app (/api/duties,/api/calendar-events),dependencies.py(DB session, auth, date validation), static webapp mounted fromPROJECT_ROOT/webapp.db/– SQLAlchemy models, session (session_scope), repository, schemas.handlers/– Telegram command and chat handlers; register viaregister_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 inpyproject.tomlunder[tool.alembic]; URL and metadata fromduty_teller.configandduty_teller.db.models.Base. Run:alembic -c pyproject.toml upgrade head.webapp/– Miniapp UI (calendar, duty list); served at/app.tests/– Tests;helpers.pyprovidesmake_init_datafor 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:
- Handover time — The bot asks for the shift handover time and optional timezone (e.g.
09:00 Europe/Moscowor06:00 UTC). This is converted to UTC and used as the boundary between duty periods when creating records. - 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). If the workflow uses PYTHONPATH: src or bandit -r src, update it to match the repo layout (no src/).
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.
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.