Some checks failed
CI / lint-and-test (push) Failing after 23s
- Added *.egg-info/ to .gitignore to prevent egg metadata from being tracked. - Updated virtual environment instructions in CONTRIBUTING.md and README.md to use .venv for consistency. - Revised mkdocs.yml to include a placeholder for the repository URL when publishing. - Cleaned up pyproject.toml by removing unnecessary pylint configuration. - Enhanced import-format.md and runbook.md documentation for clarity on user roles and health check endpoints.
158 lines
9.3 KiB
Markdown
158 lines
9.3 KiB
Markdown
# 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.
|
||
|
||
## 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.
|