- Added support for fetching and parsing external ICS calendars, allowing events to be displayed on the duty grid. - Introduced a new API endpoint `/api/calendar-events` to retrieve calendar events within a specified date range. - Updated configuration to include `EXTERNAL_CALENDAR_ICS_URL` for specifying the ICS calendar URL. - Enhanced the web application to visually indicate days with events and provide event summaries on hover. - Improved documentation in the README to include details about the new calendar integration and configuration options. - Updated tests to cover the new calendar functionality and ensure proper integration.
99 lines
5.0 KiB
Markdown
99 lines
5.0 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.
|
||
|
||
## 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)**
|
||
To allow access to the calendar miniapp, set `ALLOWED_USERNAMES` to a comma-separated list of Telegram usernames (without `@`). Users in `ADMIN_USERNAMES` also have access; the admin role is reserved for future bot commands and API features. If both are empty, no one can open the calendar.
|
||
**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** (⋮ → «Календарь» 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. **Optional env**
|
||
- `DATABASE_URL` – DB connection (default: `sqlite:///data/duty_teller.db`).
|
||
- `MINI_APP_BASE_URL` – Base URL of the miniapp (for documentation / CORS).
|
||
- `MINI_APP_SKIP_AUTH` – Set to `1` to allow `/api/duties` without Telegram initData (dev only; insecure).
|
||
- `INIT_DATA_MAX_AGE_SECONDS` – Reject Telegram initData older than this (e.g. `86400` = 24h). `0` = disabled (default).
|
||
- `CORS_ORIGINS` – Comma-separated allowed origins for CORS, or leave unset for `*`.
|
||
- `EXTERNAL_CALENDAR_ICS_URL` – 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.
|
||
|
||
## Run
|
||
|
||
```bash
|
||
python main.py
|
||
```
|
||
|
||
The bot runs in polling mode. Send `/start` or `/help` to your bot in Telegram to test.
|
||
|
||
## 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.
|
||
|
||
**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.
|
||
|
||
## Project layout
|
||
|
||
- `main.py` – Builds the `Application`, registers handlers, runs polling and FastAPI in a thread.
|
||
- `config.py` – Loads `BOT_TOKEN`, `DATABASE_URL`, `ALLOWED_USERNAMES`, `ADMIN_USERNAMES`, `CORS_ORIGINS`, etc. from env; exits if `BOT_TOKEN` is missing.
|
||
- `api/` – FastAPI app (`/api/duties`), Telegram initData validation, static webapp mount.
|
||
- `db/` – SQLAlchemy models, session, repository, schemas.
|
||
- `alembic/` – Migrations (use `config.DATABASE_URL`).
|
||
- `handlers/` – Command and error handlers; add new handlers here.
|
||
- `webapp/` – Miniapp UI (calendar, duty list); served at `/app`.
|
||
- `requirements.txt` – Pinned dependencies (PTB, FastAPI, SQLAlchemy, Alembic, etc.).
|
||
|
||
To add commands, define async handlers in `handlers/commands.py` (or a new module) and register them in `handlers/__init__.py`.
|
||
|
||
## Tests
|
||
|
||
Install dev dependencies and run pytest:
|
||
|
||
```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).
|