Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ TELEGRAM_RESENDER_WHITELIST_PATH=whitelist.csv
TELEGRAM_RESENDER_ROUTES_PATH=
TELEGRAM_RESENDER_LOCALE=ru
TELEGRAM_RESENDER_CONFIRM_BEFORE_FORWARD=false
TELEGRAM_RESENDER_PENDING_REQUEST_TTL_SECONDS=900
TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS=300
TELEGRAM_RESENDER_ADMIN_IDS=
TELEGRAM_RESENDER_STORAGE_PATH=telegram_resender.sqlite3
TELEGRAM_RESENDER_DELIVERY_MAX_ATTEMPTS=3
Expand Down
2 changes: 2 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ TELEGRAM_RESENDER_WHITELIST_PATH=/data/whitelist.csv
TELEGRAM_RESENDER_ROUTES_PATH=
TELEGRAM_RESENDER_LOCALE=ru
TELEGRAM_RESENDER_CONFIRM_BEFORE_FORWARD=true
TELEGRAM_RESENDER_PENDING_REQUEST_TTL_SECONDS=900
TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS=300
TELEGRAM_RESENDER_ADMIN_IDS=123456789
TELEGRAM_RESENDER_STORAGE_PATH=/data/telegram_resender.sqlite3
TELEGRAM_RESENDER_DELIVERY_MAX_ATTEMPTS=3
Expand Down
15 changes: 15 additions & 0 deletions .env.systemd.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
TELEGRAM_RESENDER_BOT_TOKEN=123456:replace-me
TELEGRAM_RESENDER_FORWARD_CHAT_ID=-1001234567890
TELEGRAM_RESENDER_WHITELIST_PATH=/opt/telegram-resender/data/whitelist.csv
TELEGRAM_RESENDER_ROUTES_PATH=
TELEGRAM_RESENDER_LOCALE=ru
TELEGRAM_RESENDER_CONFIRM_BEFORE_FORWARD=true
TELEGRAM_RESENDER_PENDING_REQUEST_TTL_SECONDS=900
TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS=300
TELEGRAM_RESENDER_ADMIN_IDS=123456789
TELEGRAM_RESENDER_STORAGE_PATH=/opt/telegram-resender/data/telegram_resender.sqlite3
TELEGRAM_RESENDER_DELIVERY_MAX_ATTEMPTS=3
TELEGRAM_RESENDER_DELIVERY_RETRY_BACKOFF=1.0
TELEGRAM_RESENDER_LOG_LEVEL=INFO
TELEGRAM_RESENDER_LOG_FORMAT=JSON
TELEGRAM_RESENDER_POLLING_TIMEOUT=30
7 changes: 1 addition & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,4 @@ jobs:
python -m pip install --require-hashes -r requirements-dev.lock
python -m pip install --require-hashes -r requirements-audit.lock
- name: Pip audit
run: |
# aiogram 3.28.2 pins aiohttp<3.14 while these CVEs are fixed in aiohttp 3.14.0.
# Remove these ignores once aiogram allows aiohttp>=3.14.
python -m pip_audit \
--ignore-vuln CVE-2026-34993 \
--ignore-vuln CVE-2026-47265
run: python -m pip_audit
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## 1.6.2
- Harden storage, message handling, dependency locks, Docker metadata, and CI/release checks after repository audit.
- Add atomic versioned SQLite delivery leases with stale-owner recovery.
- Persist confirmation previews and ownership versions in SQLite across process restarts.
- Add immutable `allowed_user_ids` route filters while retaining username filters for migration.
- Reject duplicate route JSON keys and normalized-empty username filters.
- Add bounded retry guidance after failed confirmed delivery.
- Split Docker and systemd environment templates so their data paths match each runtime.

## 1.6.1
- Apply `ruff format` to match the CI formatter gate after the `1.6.0` release.
Expand Down
46 changes: 40 additions & 6 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The project is intentionally small and practical:
- required-field validation and optional confirmation before forwarding
- admin commands for status checks and whitelist reloads without restart
- optional multi-route rules through `routes.json`
- SQLite delivery log, retry/backoff, and request-id idempotency
- SQLite delivery/pending state, retry/backoff, and request-id idempotency
- Docker, docker-compose, systemd, and health checks for production
- deterministic formatting of forwarded messages with request id and submitted time
- strict startup validation for secrets and a `doctor` diagnostics command
Expand Down Expand Up @@ -69,8 +69,10 @@ All settings are read from environment variables (or `.env`).
| `TELEGRAM_RESENDER_ROUTES_PATH` | no | JSON route config path |
| `TELEGRAM_RESENDER_LOCALE` | no | `ru` or `en`, default `ru` |
| `TELEGRAM_RESENDER_CONFIRM_BEFORE_FORWARD` | no | `true` to show a preview and wait for `/confirm` |
| `TELEGRAM_RESENDER_PENDING_REQUEST_TTL_SECONDS` | no | preview lifetime in seconds, default `900` |
| `TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS` | no | stale delivery/confirmation owner timeout, default `300` |
| `TELEGRAM_RESENDER_ADMIN_IDS` | no | comma-separated Telegram user IDs allowed to run admin commands |
| `TELEGRAM_RESENDER_STORAGE_PATH` | no | SQLite delivery log path, default `telegram_resender.sqlite3` |
| `TELEGRAM_RESENDER_STORAGE_PATH` | no | SQLite delivery and pending-state path, default `telegram_resender.sqlite3` |
| `TELEGRAM_RESENDER_DELIVERY_MAX_ATTEMPTS` | no | Telegram send attempts, default `3` |
| `TELEGRAM_RESENDER_DELIVERY_RETRY_BACKOFF` | no | base retry delay in seconds |
| `TELEGRAM_RESENDER_REQUEST_ACCEPTED_MESSAGE` | no | text returned on success |
Expand Down Expand Up @@ -100,10 +102,15 @@ Comment: meeting with facilities
```

The bot currently accepts text only. Media and documents are not forwarded.
If the formatted request or confirmation preview exceeds Telegram's message limit, the bot asks
the user to shorten the comment or other fields.

Required fields are building, arrival date/time, vehicle, and license plate. With
`TELEGRAM_RESENDER_CONFIRM_BEFORE_FORWARD=true`, the bot shows a preview and only
forwards after `/confirm`. `/cancel` discards the pending request.
forwards after `/confirm`. With multiple previews, target one with
`/confirm <request_id>` or `/cancel <request_id>`; commands without an ID apply to
the latest request. Pending previews are stored in SQLite, survive process restarts,
and expire after the configured TTL.

## Administration

Expand All @@ -128,7 +135,8 @@ If `TELEGRAM_RESENDER_ROUTES_PATH` is not set, the bot uses a single route from
{
"name": "tower-a",
"target_chat_id": -1002222222222,
"allowed_usernames": ["building_admin"],
"allowed_user_ids": [123456789],
"allowed_usernames": [],
"keywords_any": ["Tower A"],
"keywords_none": ["cancel"],
"template": "[{route}]\n{request}",
Expand All @@ -139,12 +147,22 @@ If `TELEGRAM_RESENDER_ROUTES_PATH` is not set, the bot uses a single route from
```

The bot forwards a request to every enabled route matching the user and keyword filters.
Use `allowed_user_ids` for authorization-sensitive routing. `allowed_usernames` remains
accepted for migration compatibility, but Telegram usernames are mutable and this legacy
filter must only be used as a non-security label in addition to the numeric-ID whitelist.
If both fields are present, both filters must match. Duplicate JSON keys and malformed or
empty filters are rejected at startup.
When several rules match the same target chat, the first matching rule in the file wins so
that one request produces one deterministic payload per destination.

## Delivery reliability

The bot keeps a SQLite delivery log. Each `request_id + target_chat_id` pair stores
`pending`, `delivered`, or `failed` status, sender, and the last error. Once a pair is
delivered, processing the same request id again will not send a duplicate message.
An atomic SQLite owner/version lease also blocks overlapping handlers from sending the same
pair concurrently. A stale lease can be reclaimed after
`TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS`.

```bash
telegram-resender doctor --storage-check
Expand All @@ -163,8 +181,14 @@ cp whitelist.example.csv data/whitelist.csv
docker compose up -d --build
```

The systemd example is in [deploy/telegram-resender.service](deploy/telegram-resender.service).
The production environment template is [.env.production.example](.env.production.example).
The Docker environment template is [.env.production.example](.env.production.example).
For systemd, use [deploy/telegram-resender.service](deploy/telegram-resender.service) together
with [.env.systemd.example](.env.systemd.example); both use
`/opt/telegram-resender/data` for mutable files.

```bash
cp .env.systemd.example /opt/telegram-resender/.env.production
```

For production logs, use:

Expand Down Expand Up @@ -192,6 +216,16 @@ gh attestation verify telegram_resender-*.whl --repo krotname/TelegramResenderBo
- This is bot-based intake/forwarding, not a userbot.
- The bot does not bypass protected or restricted Telegram chats.
- Media, documents, voice messages, and polls are not forwarded as payloads yet.
- Telegram delivery and the following SQLite commit cannot be one atomic operation. A process
crash after Telegram accepts a message but before the delivered state is committed can cause
one retry after the stale lease expires.
- Sending a confirmation preview and publishing its SQLite pending row are likewise not one
transaction. A crash in that narrow interval can leave a visible preview that must be sent
again. Publishing happens after a successful Telegram response so a failed preview never
replaces an older confirmable request.
- Known retry waits extend both ownership leases automatically. An individual Telegram API call
that hangs longer than `TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS` can still be reclaimed; set
the lease above the transport timeout used in the deployment.
- AI rewrite, translation, and digest features are not implemented.
- Hosted SaaS, mobile apps, and a web dashboard are outside the current self-hosted scope.

Expand Down
51 changes: 42 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
- Проверка обязательных полей заявки и опциональное подтверждение перед пересылкой.
- Админские команды для статуса и перезагрузки whitelist без рестарта.
- Опциональные multi-route правила через `routes.json`.
- SQLite delivery log, retry/backoff и идемпотентность по request id.
- SQLite delivery/pending state, retry/backoff и идемпотентность по request id.
- Docker, docker-compose, systemd и health-check для production.
- Детерминированный формат пересылки с request id и временем заявки.

Expand Down Expand Up @@ -60,8 +60,10 @@ telegram-resender
| `TELEGRAM_RESENDER_ROUTES_PATH` | нет | путь к JSON-файлу маршрутов |
| `TELEGRAM_RESENDER_LOCALE` | нет | `ru` или `en`, по умолчанию `ru` |
| `TELEGRAM_RESENDER_CONFIRM_BEFORE_FORWARD` | нет | `true`, чтобы показывать preview и ждать `/confirm` |
| `TELEGRAM_RESENDER_PENDING_REQUEST_TTL_SECONDS` | нет | срок жизни preview в секундах, по умолчанию `900` |
| `TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS` | нет | timeout stale-владельца доставки/подтверждения, по умолчанию `300` |
| `TELEGRAM_RESENDER_ADMIN_IDS` | нет | Telegram user id администраторов через запятую |
| `TELEGRAM_RESENDER_STORAGE_PATH` | нет | SQLite delivery log, по умолчанию `telegram_resender.sqlite3` |
| `TELEGRAM_RESENDER_STORAGE_PATH` | нет | SQLite delivery/pending state, по умолчанию `telegram_resender.sqlite3` |
| `TELEGRAM_RESENDER_DELIVERY_MAX_ATTEMPTS` | нет | попытки отправки в Telegram, по умолчанию `3` |
| `TELEGRAM_RESENDER_DELIVERY_RETRY_BACKOFF` | нет | базовая задержка retry в секундах |
| `TELEGRAM_RESENDER_LOG_LEVEL` | нет | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
Expand All @@ -73,8 +75,8 @@ telegram-resender
Формат whitelist (по одному неизменяемому числовому Telegram user ID в строке; ID можно узнать через `/whoami`):

```csv
alice
@bob
123456789
987654321
```

## Формат заявки
Expand All @@ -90,11 +92,15 @@ alice
```

Пока бот принимает только текст. Медиа и документы не пересылаются.
Если заявка со служебным заголовком или preview превышает лимит Telegram, бот попросит
сократить комментарий или другие поля.

Обязательные поля: объект/здание, дата и время прибытия, автомобиль, госномер.
Если включить `TELEGRAM_RESENDER_CONFIRM_BEFORE_FORWARD=true`, бот покажет preview
заявки и отправит ее администратору только после `/confirm`. Команда `/cancel`
отменяет ожидающую подтверждения заявку.
заявки и отправит ее администратору только после `/confirm`. Для нескольких preview
можно указать ID: `/confirm <request_id>` или `/cancel <request_id>`; команды без ID
относятся к последней заявке. Pending preview хранится в SQLite, переживает рестарт
процесса и истекает через настроенный TTL.

## Администрирование

Expand All @@ -119,7 +125,8 @@ alice
{
"name": "tower-a",
"target_chat_id": -1002222222222,
"allowed_usernames": ["building_admin"],
"allowed_user_ids": [123456789],
"allowed_usernames": [],
"keywords_any": ["Башня А"],
"keywords_none": ["отмена"],
"template": "[{route}]\n{request}",
Expand All @@ -130,13 +137,23 @@ alice
```

Заявка отправляется во все активные маршруты, где совпали пользователь и keyword-фильтры.
Для маршрутизации с контролем доступа используйте `allowed_user_ids`. Поле
`allowed_usernames` сохранено для миграционной совместимости, но Telegram username изменяем:
этот legacy-фильтр допустим только как незащитная метка поверх общего числового whitelist.
Если заданы оба поля, должны совпасть оба. Дубли JSON-ключей, пустые и некорректные
фильтры отклоняются при старте.
Если одному target chat соответствуют несколько правил, применяется первое совпавшее
правило из файла: одна заявка формирует один детерминированный payload на destination.

## Надежность доставки

Бот ведет SQLite-журнал доставок. Для каждой пары `request_id + target_chat_id`
хранится статус `pending`, `delivered` или `failed`, отправитель и последняя ошибка.
Если такая пара уже доставлена, повторная обработка того же request id не отправит
сообщение второй раз.
Атомарный SQLite lease с owner/version также не дает параллельным handler-задачам
одновременно отправить одну пару. Stale lease можно занять заново после
`TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS`.

```bash
telegram-resender doctor --storage-check
Expand All @@ -155,8 +172,14 @@ copy whitelist.example.csv data\whitelist.csv
docker compose up -d --build
```

Systemd пример лежит в [deploy/telegram-resender.service](deploy/telegram-resender.service).
Production env template: [.env.production.example](.env.production.example).
Docker env template: [.env.production.example](.env.production.example). Для systemd
используйте [deploy/telegram-resender.service](deploy/telegram-resender.service) вместе с
[.env.systemd.example](.env.systemd.example): оба используют
`/opt/telegram-resender/data` для изменяемых файлов.

```bash
cp .env.systemd.example /opt/telegram-resender/.env.production
```

Для production-логов включите:

Expand Down Expand Up @@ -184,6 +207,16 @@ gh attestation verify telegram_resender-*.whl --repo krotname/TelegramResenderBo
- Это bot-based intake/forwarding, не userbot.
- Бот не обходит protected/restricted Telegram-чаты.
- Медиа, документы, voice и polls пока не пересылаются как payload.
- Отправку в Telegram и следующий SQLite commit невозможно сделать одной атомарной операцией.
Если процесс завершится после приема сообщения Telegram, но до записи `delivered`, возможна
одна повторная отправка после истечения stale lease.
- Отправка confirmation preview и публикация pending-строки в SQLite тоже не являются одной
транзакцией. Сбой в узком промежутке может оставить видимый preview, который придется отправить
заново. Pending публикуется после успешного ответа Telegram, поэтому неотправленный preview не
заменяет более старую подтверждаемую заявку.
- Известные retry waits автоматически продлевают оба lease. Но отдельный Telegram API call,
зависший дольше `TELEGRAM_RESENDER_DELIVERY_LEASE_SECONDS`, может быть занят заново; задавайте
lease больше transport timeout текущего deployment.
- AI rewrite/translate/digest не реализованы.
- Hosted SaaS, mobile app и web dashboard не входят в текущую self-hosted область.

Expand Down
Loading