# 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-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](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.
