chore: add changelog and documentation updates
All checks were successful
CI / lint-and-test (push) Successful in 17s
All checks were successful
CI / lint-and-test (push) Successful in 17s
- Created a new `CHANGELOG.md` file to document all notable changes to the project, adhering to the Keep a Changelog format. - Updated `CONTRIBUTING.md` to include instructions for building and previewing documentation using MkDocs. - Added `mkdocs.yml` configuration for documentation generation, including navigation structure and theme settings. - Enhanced various documentation files, including API reference, architecture overview, configuration reference, and runbook, to provide comprehensive guidance for users and developers. - Included new sections in the README for changelog and documentation links, improving accessibility to project information.
This commit is contained in:
43
docs/api-reference.md
Normal file
43
docs/api-reference.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# API Reference
|
||||
|
||||
Generated from the `duty_teller` package. The following subpackages and modules are included.
|
||||
|
||||
## Configuration
|
||||
|
||||
::: duty_teller.config
|
||||
|
||||
## API (FastAPI and auth)
|
||||
|
||||
::: duty_teller.api
|
||||
::: duty_teller.api.app
|
||||
::: duty_teller.api.dependencies
|
||||
::: duty_teller.api.telegram_auth
|
||||
::: duty_teller.api.calendar_ics
|
||||
::: duty_teller.api.personal_calendar_ics
|
||||
|
||||
## Database
|
||||
|
||||
::: duty_teller.db
|
||||
::: duty_teller.db.models
|
||||
::: duty_teller.db.schemas
|
||||
::: duty_teller.db.session
|
||||
::: duty_teller.db.repository
|
||||
|
||||
## Services
|
||||
|
||||
::: duty_teller.services
|
||||
::: duty_teller.services.import_service
|
||||
::: duty_teller.services.group_duty_pin_service
|
||||
|
||||
## Handlers
|
||||
|
||||
::: duty_teller.handlers
|
||||
::: duty_teller.handlers.commands
|
||||
::: duty_teller.handlers.import_duty_schedule
|
||||
::: duty_teller.handlers.group_duty_pin
|
||||
::: duty_teller.handlers.errors
|
||||
|
||||
## Importers
|
||||
|
||||
::: duty_teller.importers
|
||||
::: duty_teller.importers.duty_schedule
|
||||
67
docs/architecture.md
Normal file
67
docs/architecture.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Architecture
|
||||
|
||||
High-level architecture of Duty Teller: components, data flow, and package relationships.
|
||||
|
||||
## Components
|
||||
|
||||
- **Bot** — [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) v22 (Application API). Handles commands and group messages; runs in polling mode.
|
||||
- **FastAPI** — HTTP server: REST API (`/api/duties`, `/api/calendar-events`, `/api/calendar/ical/{token}.ics`) and static miniapp at `/app`. Runs in a separate thread alongside the bot.
|
||||
- **Database** — SQLAlchemy ORM with Alembic migrations. Default backend: SQLite (`data/duty_teller.db`). Stores users, duties (with event types: duty, unavailable, vacation), group duty pins, calendar subscription tokens.
|
||||
- **Duty-schedule import** — Two-step admin flow: handover time (timezone → UTC), then JSON file. Parser produces per-person date lists; import service deletes existing duties in range and inserts new ones.
|
||||
- **Group duty pin** — In groups, the bot can pin the current duty message; time/timezone for the pinned text come from `DUTY_DISPLAY_TZ`. Pin state is restored on startup from the database.
|
||||
|
||||
## Data flow
|
||||
|
||||
- **Telegram → bot**
|
||||
User/group messages → handlers → services or DB. Handlers use `duty_teller.services` (e.g. import, group duty pin) and `duty_teller.db` (repository, session). Messages use `duty_teller.i18n` for Russian/English.
|
||||
|
||||
- **Miniapp → API**
|
||||
Browser opens `/app`; frontend calls `GET /api/duties` and `GET /api/calendar-events` with date range. FastAPI dependencies: DB session, Telegram initData validation (`require_miniapp_username`), date validation. Data is read via `duty_teller.db.repository`.
|
||||
|
||||
- **Import**
|
||||
Admin sends JSON file via `/import_duty_schedule`. Handler reads file → `duty_teller.importers.duty_schedule.parse_duty_schedule()` → `DutyScheduleResult` → `duty_teller.services.import_service.run_import()` → repository (`get_or_create_user_by_full_name`, `delete_duties_in_range`, `insert_duty`).
|
||||
|
||||
- **Personal calendar ICS**
|
||||
`GET /api/calendar/ical/{token}.ics` uses the secret token only (no Telegram auth); repository resolves user by token and returns duties; `personal_calendar_ics.build_personal_ics()` produces ICS bytes.
|
||||
|
||||
## Package layout
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph entry
|
||||
main[main.py / duty-teller]
|
||||
end
|
||||
subgraph duty_teller
|
||||
run[run.py]
|
||||
config[config.py]
|
||||
handlers[handlers]
|
||||
api[api]
|
||||
db[db]
|
||||
services[services]
|
||||
importers[importers]
|
||||
i18n[i18n]
|
||||
utils[utils]
|
||||
end
|
||||
main --> run
|
||||
run --> config
|
||||
run --> handlers
|
||||
run --> api
|
||||
handlers --> services
|
||||
handlers --> db
|
||||
handlers --> i18n
|
||||
api --> db
|
||||
api --> config
|
||||
services --> db
|
||||
services --> importers
|
||||
importers --> .
|
||||
```
|
||||
|
||||
- **handlers** — Telegram command and message handlers; call `services` and `db`, use `i18n` for user-facing text.
|
||||
- **api** — FastAPI app, dependencies (auth, DB session, date validation), calendar ICS builders; uses `db.repository` and `config`.
|
||||
- **db** — Models, session (`session_scope`), repository (CRUD for users, duties, pins, calendar tokens), schemas for API.
|
||||
- **services** — Business logic (import, group duty pin); receive DB session from caller, use `importers` for parsing.
|
||||
- **importers** — Duty-schedule JSON parser; no DB access, returns structured result.
|
||||
- **i18n** — Translations and language detection (ru/en) for bot and API.
|
||||
- **utils** — Shared helpers (dates, user, handover).
|
||||
|
||||
See [Project layout](../README.md#project-layout) in README for file-level details.
|
||||
28
docs/configuration.md
Normal file
28
docs/configuration.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Configuration reference
|
||||
|
||||
All configuration is read from the environment (e.g. `.env` via python-dotenv). Source of truth: `duty_teller/config.py` and `Settings.from_env()`.
|
||||
|
||||
| Variable | Type / format | Default | Description |
|
||||
|----------|----------------|---------|-------------|
|
||||
| **BOT_TOKEN** | string | *(empty)* | Telegram bot token from [@BotFather](https://t.me/BotFather). Required for the bot to run; if unset, the entry point exits with a clear message. The server that serves the Mini App API must use the **same** token as the bot; otherwise initData validation returns `hash_mismatch`. |
|
||||
| **DATABASE_URL** | string (SQLAlchemy URL) | `sqlite:///data/duty_teller.db` | Database connection URL. Example: `sqlite:///data/duty_teller.db`. |
|
||||
| **MINI_APP_BASE_URL** | string (URL, no trailing slash) | *(empty)* | Base URL of the miniapp (for documentation and CORS). Trailing slash is stripped. Example: `https://your-domain.com/app`. |
|
||||
| **HTTP_PORT** | integer | `8080` | Port for the HTTP server (FastAPI + static webapp). |
|
||||
| **ALLOWED_USERNAMES** | comma-separated list | *(empty)* | Telegram usernames allowed to open the calendar miniapp (without `@`; case-insensitive). If both this and `ADMIN_USERNAMES` are empty, no one can open the calendar. Example: `alice,bob`. |
|
||||
| **ADMIN_USERNAMES** | comma-separated list | *(empty)* | Telegram usernames with admin role (access to miniapp + `/import_duty_schedule` and future admin features). Example: `admin1,admin2`. |
|
||||
| **ALLOWED_PHONES** | comma-separated list | *(empty)* | Phone numbers allowed to access the miniapp (user sets via `/set_phone`). Comparison uses digits only (spaces, `+`, parentheses, dashes ignored). Example: `+7 999 123-45-67,89001234567`. |
|
||||
| **ADMIN_PHONES** | comma-separated list | *(empty)* | Phone numbers with admin role; same format as `ALLOWED_PHONES`. |
|
||||
| **MINI_APP_SKIP_AUTH** | `1`, `true`, or `yes` | *(unset)* | If set, `/api/duties` is allowed without Telegram initData (dev only; insecure). |
|
||||
| **INIT_DATA_MAX_AGE_SECONDS** | integer | `0` | Reject Telegram initData older than this many seconds. `0` = disabled. Example: `86400` for 24 hours. |
|
||||
| **CORS_ORIGINS** | comma-separated list | `*` | Allowed origins for CORS. Leave unset or set to `*` for allow-all. Example: `https://your-domain.com`. |
|
||||
| **EXTERNAL_CALENDAR_ICS_URL** | string (URL) | *(empty)* | URL of a public ICS calendar (e.g. holidays). If set, those days are highlighted on the duty grid; users can tap «i» on a cell to see the event summary. Empty = no external calendar. |
|
||||
| **DUTY_DISPLAY_TZ** | string (timezone name) | `Europe/Moscow` | Timezone for the pinned duty message in groups. Example: `Europe/Moscow`, `UTC`. |
|
||||
| **DEFAULT_LANGUAGE** | `en` or `ru` (normalized) | `en` | Default UI language when the user's Telegram language is unknown. Values starting with `ru` are normalized to `ru`, otherwise `en`. |
|
||||
|
||||
## Quick setup
|
||||
|
||||
1. Copy `.env.example` to `.env`.
|
||||
2. Set `BOT_TOKEN` to the token from BotFather.
|
||||
3. For miniapp access, set `ALLOWED_USERNAMES` and/or `ADMIN_USERNAMES` (and optionally `ALLOWED_PHONES` / `ADMIN_PHONES`).
|
||||
|
||||
For Mini App URL and production deployment notes (reverse proxy, initData), see the [README](../README.md) Setup and Docker sections.
|
||||
60
docs/import-format.md
Normal file
60
docs/import-format.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Duty-schedule import format
|
||||
|
||||
The **duty-schedule** format is used by the `/import_duty_schedule` command. Only users in `ADMIN_USERNAMES` or `ADMIN_PHONES` can import.
|
||||
|
||||
## Import flow
|
||||
|
||||
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 (see below). On re-import, duties in the same date range for each user are replaced by the new data.
|
||||
|
||||
## Format specification
|
||||
|
||||
- **meta** (required) — Object with:
|
||||
- **start_date** (required) — First day of the schedule, `YYYY-MM-DD`.
|
||||
- **weeks** (optional) — Not used to limit length; the number of days is derived from the longest `duty` string (see below).
|
||||
|
||||
- **schedule** (required) — Array of objects. Each object:
|
||||
- **name** (required) — Full name of the person (string).
|
||||
- **duty** (required) — String of cells separated by **`;`**. Each cell corresponds to one day starting from `meta.start_date` (first cell = start_date, second = start_date + 1 day, etc.). Empty or whitespace = no event for that day.
|
||||
|
||||
### Cell values (single character, case-sensitive where noted)
|
||||
|
||||
| Value | Meaning | Notes |
|
||||
|-------|----------------|--------------------------|
|
||||
| **в**, **В**, **б**, **Б** | Duty (дежурство) | Any of these four |
|
||||
| **Н** | Unavailable (недоступен) | Exactly `Н` |
|
||||
| **О** | Vacation (отпуск) | Exactly `О` |
|
||||
| (empty/space/other) | No event | Ignored for import |
|
||||
|
||||
The number of days in the schedule is the maximum length of any `duty` string when split by `;`. If `duty` is empty or missing, it is treated as an empty list of cells.
|
||||
|
||||
## Example JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"start_date": "2025-02-01",
|
||||
"weeks": 4
|
||||
},
|
||||
"schedule": [
|
||||
{
|
||||
"name": "Иванов Иван",
|
||||
"duty": ";;В;;;Н;;О;;В;;"
|
||||
},
|
||||
{
|
||||
"name": "Petrov Petr",
|
||||
"duty": ";;;В;;;;;;В;;;"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **start_date** is 2025-02-01; the longest `duty` has 14 cells (after splitting by `;`), so the schedule spans 14 days (2025-02-01 … 2025-02-14).
|
||||
- First person: duty on day index 2 (В), unavailable on 6 (Н), vacation on 8 (О), duty on 11 (В). Other cells are empty.
|
||||
- Second person: duty on day indices 3 and 10.
|
||||
|
||||
## Validation
|
||||
|
||||
- `meta` and `meta.start_date` must be present and valid; `start_date` must parse as `YYYY-MM-DD`.
|
||||
- `schedule` must be an array; each item must be an object with string `name` (non-empty after strip) and string `duty` (if missing, treated as `""`).
|
||||
- Invalid JSON or encoding raises an error; the parser reports missing or invalid fields (see `duty_teller.importers.duty_schedule.DutyScheduleParseError`).
|
||||
13
docs/index.md
Normal file
13
docs/index.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Duty Teller
|
||||
|
||||
Telegram bot for team duty shift calendar and group reminder. The bot and web UI support **Russian and English**.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Configuration](configuration.md) — Environment variables (types, defaults, examples).
|
||||
- [Architecture](architecture.md) — Components, data flow, package relationships.
|
||||
- [Import format](import-format.md) — Duty-schedule JSON format and example.
|
||||
- [Runbook](runbook.md) — Running the app, logs, common errors, DB and migrations.
|
||||
- [API Reference](api-reference.md) — Generated from code (api, db, services, handlers, importers, config).
|
||||
|
||||
For quick start, setup, and API overview see the main [README](../README.md).
|
||||
83
docs/runbook.md
Normal file
83
docs/runbook.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Runbook (operational guide)
|
||||
|
||||
This document covers running the application, checking health, logs, common errors, and database operations.
|
||||
|
||||
## Starting and stopping
|
||||
|
||||
### Local
|
||||
|
||||
- **Start:** From the repository root, with virtualenv activated:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
Or after `pip install -e .`: `duty-teller`
|
||||
- **Stop:** `Ctrl+C`
|
||||
|
||||
### Docker
|
||||
|
||||
- **Dev** (code mounted; no rebuild needed for code changes):
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
Stop: `Ctrl+C` or `docker compose -f docker-compose.dev.yml down`.
|
||||
|
||||
- **Prod** (built image; restarts on failure):
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
Stop: `docker compose -f docker-compose.prod.yml down`.
|
||||
|
||||
On container start, `entrypoint.sh` runs Alembic migrations then starts the app as user `botuser`. Ensure `.env` (or your orchestrator’s env) contains `BOT_TOKEN` and any required variables; see [configuration.md](configuration.md).
|
||||
|
||||
## Health check
|
||||
|
||||
- **HTTP:** The FastAPI app serves the API and static webapp. A simple way to verify it is up is to open the interactive API docs: **`GET /docs`** (e.g. `http://localhost:8080/docs`). If that page loads, the server is running.
|
||||
- There is no dedicated `/health` endpoint; use `/docs` or a lightweight API call (e.g. `GET /api/duties?from=...&to=...` with valid auth) as needed.
|
||||
|
||||
## Logs
|
||||
|
||||
- **Local:** Output goes to stdout/stderr; redirect or use your process manager’s logging (e.g. systemd, supervisord).
|
||||
- **Docker:** Use `docker compose logs -f` (with the appropriate compose file) to follow application logs. Adjust log level via Python `logging` if needed (e.g. environment or code).
|
||||
|
||||
## Common errors and what to check
|
||||
|
||||
### "hash_mismatch" (403 from `/api/duties` or Miniapp)
|
||||
|
||||
- **Cause:** The server that serves the Mini App (e.g. production host) uses a **different** `BOT_TOKEN` than the bot from which users open the Mini App (e.g. test vs production bot). Telegram signs initData with the bot token; if tokens differ, validation fails.
|
||||
- **Check:** Ensure the same `BOT_TOKEN` is set in `.env` (or equivalent) on the machine serving `/api/duties` as the one used by the bot instance whose menu button opens the Miniapp.
|
||||
|
||||
### Miniapp "Open in browser" or direct link — access denied
|
||||
|
||||
- **Cause:** When users open the calendar via “Open in browser” or a direct URL, Telegram may not send `tgWebAppData` (initData). The API requires initData (or `MINI_APP_SKIP_AUTH` / private IP in dev).
|
||||
- **Action:** Users should open the calendar **via the bot’s menu button** (e.g. ⋮ → «Календарь») or a **Web App inline button** so Telegram sends user data.
|
||||
|
||||
### 403 "Open from Telegram" / no initData
|
||||
|
||||
- **Cause:** Request to `/api/duties` (or calendar) without valid `X-Telegram-Init-Data` header. In production, only private IP clients can be allowed without initData (see `_is_private_client` in `api/dependencies.py`); behind a reverse proxy, `request.client.host` is often the proxy (e.g. 127.0.0.1), so the “private IP” bypass may not apply to the real user.
|
||||
- **Check:** Ensure the Mini App is opened from Telegram (menu or inline button). If behind a reverse proxy, see README “Production behind a reverse proxy” (forward real client IP or rely on initData).
|
||||
|
||||
### Mini App URL — redirect and broken auth
|
||||
|
||||
- **Cause:** If the Mini App URL is configured **without** a trailing slash (e.g. `https://your-domain.com/app`) and the server redirects `/app` → `/app/`, the browser can drop the fragment Telegram sends, breaking authorization.
|
||||
- **Action:** Configure the bot’s menu button / Web App URL **with a trailing slash**, e.g. `https://your-domain.com/app/`. See README “Mini App URL”.
|
||||
|
||||
### User not in allowlist (403)
|
||||
|
||||
- **Cause:** Telegram user’s username is not in `ALLOWED_USERNAMES` or `ADMIN_USERNAMES`, and (if using phone) their phone (set via `/set_phone`) is not in `ALLOWED_PHONES` or `ADMIN_PHONES`.
|
||||
- **Check:** [configuration.md](configuration.md) for `ALLOWED_USERNAMES`, `ADMIN_USERNAMES`, `ALLOWED_PHONES`, `ADMIN_PHONES`. Add the user or ask them to set phone and add it to the allowlist.
|
||||
|
||||
## Database and migrations
|
||||
|
||||
- **Default DB path (SQLite):** `data/duty_teller.db` (relative to working directory when using default `DATABASE_URL=sqlite:///data/duty_teller.db`). In Docker, the entrypoint creates `/app/data` and runs migrations there.
|
||||
- **Migrations (Alembic):** From the repository root:
|
||||
```bash
|
||||
alembic -c pyproject.toml upgrade head
|
||||
```
|
||||
Config: `pyproject.toml` → `[tool.alembic]`; script location `alembic/`; metadata and URL from `duty_teller.config` and `duty_teller.db.models.Base`.
|
||||
- **Rollback:** Use with care; test in a copy of the DB first. Example to go back one revision:
|
||||
```bash
|
||||
alembic -c pyproject.toml downgrade -1
|
||||
```
|
||||
Always backup the database before downgrading.
|
||||
|
||||
For full list of env vars (including `DATABASE_URL`), see [configuration.md](configuration.md). For reverse proxy and Mini App URL details, see the main [README](../README.md).
|
||||
Reference in New Issue
Block a user