# Duty Teller (Telegram Bot) A minimal Telegram bot boilerplate using [python-telegram-bot](https://github.com/python-telegram-bot/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](CHANGELOG.md). ## Get a bot token 1. Open Telegram and search for [@BotFather](https://t.me/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** ```bash cd duty-teller ``` 2. **Create a virtual environment (recommended)** ```bash python -m venv .venv source .venv/bin/activate # Linux/macOS # or: .venv\Scripts\activate # Windows ``` 3. **Install dependencies** ```bash pip install -r requirements.txt ``` 4. **Configure the bot** ```bash 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](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](docs/configuration.md)**. ## Run ```bash python main.py ``` Or after `pip install -e .`: ```bash 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 user’s 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 bot’s 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): ```bash 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): ```bash docker compose -f docker-compose.prod.yml up -d --build ``` For production deployments you may use Docker secrets or your orchestrator’s 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](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`. - `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/` – Miniapp UI (calendar, duty list); 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](docs/import-format.md). ## Tests Run from the repository root (no `src/` directory; package is `duty_teller` at the root). Use `PYTHONPATH=.` if needed: ```bash 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](https://www.conventionalcommits.org/): `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, etc. - **Branches:** Follow [Gitea Flow](https://docs.gitea.io/en-us/workflow-branching/): 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](CONTRIBUTING.md) for details. ## Logs and rotation To meet the 7-day log retention policy, configure log rotation at deploy time: e.g. [logrotate](https://manpages.ubuntu.com/logrotate), systemd logging settings, or Docker (size/time retention limits). Keep application logs for no more than 7 days.