Initial commit: delta-chat-bot
This commit is contained in:
commit
8f47610133
10 changed files with 3603 additions and 0 deletions
587
AGENTS.MD
Normal file
587
AGENTS.MD
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
# Delta Chat Bot: deltabot
|
||||
|
||||
## ⚠️ Важно: как мы работаем
|
||||
|
||||
- Я работаю на **локальном Mac** (`/Users/alexabudaev/Documents/Zed/`), не на сервере.
|
||||
- Не пытаюсь башить команды на макбуке — все изменения в `deltabot.py` только локально редактируются.
|
||||
- Загрузка на сервер — ручная:
|
||||
- Бот: `scp deltabot.py SERVER:~/delta-bot/deltabot.py && ssh SERVER sudo systemctl restart deltabot`
|
||||
- Прокси (PHP — без перезапуска): `scp php-proxy/*.php php-proxy/.htaccess php-proxy/channels.json SERVER:~/delta-bot/php-proxy/`
|
||||
- Сервер: Ubuntu 24.04, бот под `alexabudaev`, Postfix/Dovecot под `bot`.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
deltabot.py
|
||||
└── deltachat_rpc_client (Bot, DeltaChat, Rpc)
|
||||
└── deltachat-rpc-server (Rust Core)
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
|
||||
- **`deltabot.py`** — основной скрипт бота (JSON-RPC).
|
||||
- **`AGENTS.MD`** — документация проекта.
|
||||
- **Конфиги и БД:** `~/.config/deltabot/` (subscribers, channels, pending, bridges)
|
||||
- **Аккаунты DC:** `~/delta-bot/accounts/`
|
||||
|
||||
## Развёртывание
|
||||
|
||||
```bash
|
||||
# После изменений в deltabot.py:
|
||||
scp /Users/alexabudaev/Documents/Zed/delta-bot/deltabot.py alexabudaev@10.0.0.1:/home/alexabudaev/delta-bot/deltabot.py
|
||||
ssh alexabudaev@10.0.0.1 sudo systemctl restart deltabot
|
||||
|
||||
# После изменений в php-proxy/ (перезагрузка не нужна — PHP):
|
||||
scp /Users/alexabudaev/Documents/Zed/delta-bot/php-proxy/*.php alexabudaev@10.0.0.1:/home/alexabudaev/delta-bot/php-proxy/
|
||||
scp /Users/alexabudaev/Documents/Zed/delta-bot/php-proxy/.htaccess alexabudaev@10.0.0.1:/home/alexabudaev/delta-bot/php-proxy/
|
||||
scp /Users/alexabudaev/Documents/Zed/delta-bot/php-proxy/channels.json alexabudaev@10.0.0.1:/home/alexabudaev/delta-bot/php-proxy/
|
||||
|
||||
# Если менялся только .htaccess — только scp .htaccess
|
||||
|
||||
# Логи:
|
||||
sudo journalctl -u deltabot -f | grep "TG Broadcast"
|
||||
```
|
||||
|
||||
### Natural Language → Commands
|
||||
|
||||
Любое сообщение, не начинающееся с `/`, проходит через `parse_natural_language()` (строка 101):
|
||||
|
||||
| Фраза | Команда |
|
||||
|---|---|
|
||||
| `погода/weather/прогноз [город] [на N дней/неделю]` | `/weather ...` |
|
||||
| `дай/мосты/bridge/tor [obfs4/vanilla/ipv6]` | `/bridges ...` |
|
||||
| `статус/status/состояние` | `/status` |
|
||||
| `помощь/help/команды/что умеешь` | `/help` |
|
||||
| `подпишись/подписаться/subscribe` | `/subscribe` |
|
||||
| `отпишись/отписаться/unsubscribe` | `/unsubscribe` |
|
||||
| `qr/qrcode/кьюар` | `/qr` |
|
||||
|
||||
### Переменные окружения (`.env`)
|
||||
|
||||
Секреты вынесены в `~/.config/deltabot/.env`:
|
||||
|
||||
```
|
||||
GMAIL_EMAIL=dtorbot@gmail.com
|
||||
GMAIL_APP_PASSWORD=eciu llvw sjjz ygbb
|
||||
CLOUDFLARE_ACCOUNT_ID=7fb91c91800780ae7dcdf5d47cdf9a1d
|
||||
CLOUDFLARE_API_TOKEN=<token>
|
||||
OPENROUTER_API_KEY=sk-or-...
|
||||
CALDAV_URL=https://baikal.budaev.org/dav.php/calendars/alex@budaev.org/ai-notifications/
|
||||
CALDAV_USER=alex@budaev.org
|
||||
CALDAV_PASSWORD=<пароль>
|
||||
```
|
||||
|
||||
Права: `chmod 600 ~/.config/deltabot/.env`. systemd подхватывает через `EnvironmentFile=` в `deltabot.service`.
|
||||
|
||||
**Обязательные** (без них не работают мосты): `GMAIL_EMAIL`, `GMAIL_APP_PASSWORD`. **Для AI:** `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_API_TOKEN`. `OPENROUTER_API_KEY` — опционально (fallback). **Для CalDAV:** `CALDAV_USER`, `CALDAV_PASSWORD` (URL зашит как дефолт).
|
||||
|
||||
## Команды бота
|
||||
|
||||
- **`/status`** — состояние сервера
|
||||
- **`/weather [город] [дни]`** — погода (3/7/10 дней)
|
||||
- **`/bridges [obfs4|vanilla|ipv6]`** — Tor мосты
|
||||
- **`/save [путь]`** — сохранить прикреплённый файл
|
||||
- **`/subscribe` / `/unsubscribe`** — ежедневный отчёт (09:00 IRKT)
|
||||
- **`/channels list|add|remove|invite|description|image`** — управление broadcast каналами
|
||||
- **`/telegram <username> [N]`** — посты из TG канала
|
||||
- **`/cal today|week|list|add|delete`** — управление CalDAV-календарём
|
||||
- **`/note add|list|delete`** — заметки (per-chat)
|
||||
- **`/rate [валюта...]`** — курс валют ЦБ РФ
|
||||
- **`/ip [адрес]`** — внешний IP сервера или информация об адресе
|
||||
- **`/dns <домен>`** — DNS-запрос
|
||||
- **`/monitor add|list|remove|check`** — мониторинг сайтов
|
||||
- **`/ai summary`** — краткое резюме истории AI-чата
|
||||
- Голосовые сообщения → автоматическая транскрипция (Whisper via Cloudflare)
|
||||
- **`/qr`** — QR код для добавления бота
|
||||
- **`/join https://i.delta.chat/#...`** — secure join
|
||||
- **`/addcontact <email|*@domain>`** — добавить email или домен в белый список (только для верифицированных)
|
||||
- **`/contacts list|remove`** — просмотр и управление белым списком
|
||||
- **`/help`** — справка
|
||||
|
||||
> ⚠️ **/help и echo (ответ на не-команду) — один HELP_TEXT.** Определён как константа в начале файла. При добавлении/изменении команд править только HELP_TEXT. Echo не отстанет от help.
|
||||
|
||||
## PHP Proxy (кэширующий прокси для tg.i-c-a.su)
|
||||
|
||||
Архитектура:
|
||||
```
|
||||
tg.i-c-a.su (10-17s) → proxy.budaev.org (rss.php + media.php + кэш) → бот (<0.1s)
|
||||
```
|
||||
|
||||
Исходники: `php-proxy/` — заливаются на `proxy.budaev.org` (хостинг с ISPManager).
|
||||
|
||||
**Два PHP-скрипта (без cron — кэш наполняется лениво):**
|
||||
|
||||
| Файл | Роль |
|
||||
|------|------|
|
||||
| `rss.php` | GET `?channel=CHANNEL&limit=N`: отдаёт RSS из кэша (TTL 600с + stagger), при промахе проксирует с tg.i-c-a.su |
|
||||
| `media.php` | GET `?url=...`: отдаёт картинку из кэша (TTL 24ч), при промахе скачивает и кэширует |
|
||||
|
||||
**`rss.php`** (оптимизирован 02.06.2026):
|
||||
- **Pre-rewritten cache:** XML сохраняется в кэш уже с переписанными `<enclosure url>` (на media.php). При cache hit — `readfile()` без DOMDocument.
|
||||
- **Staggered TTL:** `600 + crc32(channel) % 300` секунд, чтобы каналы не истекали одновременно.
|
||||
- **Combined DOMDocument:** fetch + rewrite в одном проходе (раньше было два DOMDocument на miss).
|
||||
- **Empty feed вместо 502:** при ошибке tg.i-c-a.su и отсутствии кэша возвращается пустой RSS, чтобы бот не падал на fallback и не получал сырые enclosure URL.
|
||||
|
||||
**`media.php`** (оптимизирован 02.06.2026):
|
||||
- **Probabilistic cleanup:** `maybeCleanOldCache()` — при ~2% запросов удаляет файлы старше 24ч. Без cron.
|
||||
- **Cache-Control унифицирован:** и HIT и MISS отдают `max-age=86400` (было 604800 на MISS — неконсистентно с TTL).
|
||||
|
||||
**`channels.json`** — список каналов (для информации, работа не зависит от него).
|
||||
|
||||
**Новый канал:** первый poll медленный (10-30с, rss.php проксирует), со второго — мгновенно.
|
||||
|
||||
Телеграм с хостинга недоступен (блокировка), поэтому `rss.php` ходит через `tg.i-c-a.su`.
|
||||
|
||||
### IP restriction
|
||||
|
||||
Доступ к `proxy.budaev.org` разрешён только с IP бота (90.188.48.201). Реализовано через `.htaccess`:
|
||||
```apache
|
||||
RewriteCond %{REMOTE_ADDR} !^90\.188\.48\.201$
|
||||
RewriteRule ^ - [F,L]
|
||||
```
|
||||
Остальные IP получают 403 Forbidden на уровне Apache (до PHP не доходит).
|
||||
|
||||
### Empty feed вместо 502
|
||||
|
||||
При ошибке tg.i-c-a.su, если нет даже старого кэша, rss.php возвращает пустой RSS `<rss><channel><title>...</title></channel></rss>` с HTTP 200, вместо 502. Это не даёт боту упасть на fallback (tg.i-c-a.su напрямую), который возвращает сырые enclosure URL без перезаписи на media.php.
|
||||
|
||||
### Upstream timeout
|
||||
|
||||
`rss.php` ждёт ответ от tg.i-c-a.su до 30 секунд (было 20, увеличено 24.05.2026 из-за тяжёлых каналов типа gremtelegram, которые не влезали в 20с).
|
||||
|
||||
## Telegram Channels (Broadcast)
|
||||
|
||||
- **Прокси (основной):** `https://proxy.budaev.org/rss` (`TG_RSS_PROXY`)
|
||||
- **Прокси (fallback):** `https://tg.i-c-a.su/rss` (`TG_RSS_FALLBACK`) — если proxy.budaev.org недоступен
|
||||
- Бот пробует сначала основной, при ошибках — fallback (`get_telegram_feed()`, строки 189-238)
|
||||
- Polling каждые 90 секунд, `limit=10`, обработка всех новых постов (batch)
|
||||
- Кэш RSS: 600с TTL. Из ~7 poll-ов 1 медленный (10-30с для тяжёлых каналов), остальные — мгновенные (<0.1с)
|
||||
- При пропуске канала (ошибка RSS) — `logger.warning` с причиной и временем запроса
|
||||
- Для каждого канала логируется какой прокси сработал и сколько времени занял запрос: `TG Broadcast: Fetched gremtelegram (20.6s, 4 posts)`
|
||||
- Неудачные скачивания картинок — `logger.debug` (раньше был warning, засорял логи)
|
||||
- Описание канала устанавливается из RSS при создании. Картинка — вручную через `/channels image`
|
||||
|
||||
### Enclosure-картинки
|
||||
|
||||
Прокси (`tg.i-c-a.su`) добавляет `<enclosure url="..." type="image/jpeg" length="..."/>` в RSS для постов с медиа. Бот:
|
||||
1. Читает первый `<enclosure>` из RSS
|
||||
2. Если `type` начинается с `image/` — скачивает эту картинку
|
||||
3. Прикрепляет к тексту поста (поле `file` в `send_msg`)
|
||||
4. Удаляет временный файл после отправки
|
||||
5. Если скачать не удалось — пост уходит без картинки
|
||||
|
||||
### HTML → plain text
|
||||
|
||||
Delta Chat не рендерит Markdown. Из HTML вырезаются все теги:
|
||||
|
||||
| Тег в RSS | Результат |
|
||||
|-----------|-----------|
|
||||
| `<br>` | перенос строки |
|
||||
| `<a href="url">text</a>` | сохраняется только `text`, URL → сноска |
|
||||
| `<a href="*max.ru*">...</a>` | URL не попадает в сноску, текст сохраняется |
|
||||
| Сноска | `---\nurl1\nurl2` |
|
||||
| `<b>`, `<i>`, `<blockquote>` и т.д. | удаляются |
|
||||
| `<tg-emoji>` / `<a><img>` | удаляются |
|
||||
| HTML entities | `html.unescape()` |
|
||||
| `<title>` | fallback, если `<description>` пустой |
|
||||
|
||||
Код: `telegram_poll_worker()`. Ссылки через callback `collect_url()` — max.ru исключаются.
|
||||
|
||||
## CalDAV — Управление календарём
|
||||
|
||||
**Статус:** работает
|
||||
**Сервер:** Baikal на `baikal.budaev.org`
|
||||
**Календарь:** `https://baikal.budaev.org/dav.php/calendars/alex@budaev.org/ai-notifications/`
|
||||
**Зависимости:** только `requests` (уже в зависимостях), `uuid`, `xml.etree.ElementTree` (stdlib)
|
||||
|
||||
### Команды
|
||||
|
||||
| Команда | Действие |
|
||||
|---------|----------|
|
||||
| `/cal today` | События на сегодня (UTC → Иркутск) |
|
||||
| `/cal week` | События на 7 дней |
|
||||
| `/cal list [N]` | События на N дней (по умолч. 30), с короткими ID |
|
||||
| `/cal add <дата> <время> <название> [описание]` | Добавить событие |
|
||||
| `/cal delete <ID>` | Удалить событие по коротому ID (первые 8 символов UID) |
|
||||
|
||||
**Форматы даты** в `/cal add`: `сегодня`, `завтра`, `ДД.ММ`, `ДД.ММ.ГГГГ`
|
||||
**Примеры:**
|
||||
```
|
||||
/cal add сегодня 15:00 Встреча с командой
|
||||
/cal add завтра 10:30 Визит врача онлайн
|
||||
/cal add 20.06 09:00 Конференция описание здесь
|
||||
```
|
||||
|
||||
### Архитектура
|
||||
|
||||
```
|
||||
handle_message() → /cal subcmd
|
||||
├── today/week → cal_list_events(days) → REPORT (CalDAV) → форматирование
|
||||
├── list [N] → cal_list_events(N) → REPORT (CalDAV) → форматирование + short ID
|
||||
├── add → _parse_cal_datetime() → cal_add_event() → PUT (CalDAV)
|
||||
└── delete <ID> → cal_list_events(365) → поиск по UID-префиксу → cal_delete_event() → DELETE
|
||||
```
|
||||
|
||||
### HTTP-методы CalDAV
|
||||
|
||||
| Операция | Метод | Тело |
|
||||
|----------|-------|------|
|
||||
| Получение событий | `REPORT` + `Depth: 1` | XML calendar-query с time-range |
|
||||
| Создание события | `PUT` на `<uid>.ics` | iCalendar (VCALENDAR/VEVENT) |
|
||||
| Удаление события | `DELETE` на `<uid>.ics` | — |
|
||||
|
||||
Часовой пояс событий: `Asia/Irkutsk` (UTC+8). При создании пишется `TZID=Asia/Irkutsk` в DTSTART/DTEND.
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
```
|
||||
CALDAV_URL=https://baikal.budaev.org/dav.php/calendars/alex@budaev.org/ai-notifications/
|
||||
CALDAV_USER=alex@budaev.org
|
||||
CALDAV_PASSWORD=<пароль_baikal>
|
||||
```
|
||||
|
||||
`CALDAV_URL` имеет дефолтное значение в коде — можно не задавать, если URL не меняется.
|
||||
|
||||
---
|
||||
|
||||
## Secure join
|
||||
|
||||
Единственный способ добавить контакт (`create_contact` RPC сломан на core 2.49.0). Бот обрабатывает ссылку `https://i.delta.chat/#...` в фоновом потоке через `account._rpc.secure_join()`. Чтобы handshake завершился, получатель должен ответить (принять приглашение). Пока ответа нет — `key is missing` при отправке.
|
||||
|
||||
## Mail сервер (dc.budaev.org)
|
||||
|
||||
### Системный пользователь bot
|
||||
|
||||
- **Postfix:** приём на 25 (postscreen), submission на 587 (STARTTLS + SASL), доставка напрямую на MX (relayhost выключен).
|
||||
- **Dovecot:** SASL через PAM (пользователь `bot`), maildir в `~/Maildir`.
|
||||
- **Сертификаты:** Let's Encrypt, `/etc/letsencrypt/live/dc.budaev.org/`.
|
||||
- **sasl_passwd:** `[dc.budaev.org]:587 bot:ПАРОЛЬ` (username `bot`, не `bot@...`).
|
||||
- **Важно:** `relayhost` не ставить — loop. `smtp_tls_wrappermode` / `smtp_use_tls` не использовать.
|
||||
|
||||
### Виртуальные ящики (Dovecot LMTP + passwd-file)
|
||||
|
||||
Для дополнительных адресов (например, `macky@dc.budaev.org`) используется Dovecot LMTP + `passwd-file`.
|
||||
|
||||
**Postfix (main.cf):**
|
||||
```
|
||||
transport_maps = hash:/etc/postfix/transport
|
||||
```
|
||||
**Transport** (`/etc/postfix/transport`):
|
||||
```
|
||||
macky@dc.budaev.org lmtp:unix:private/dovecot-lmtp
|
||||
```
|
||||
|
||||
**Dovecot LMTP** (`/etc/dovecot/conf.d/20-lmtp.conf`):
|
||||
```
|
||||
service lmtp {
|
||||
unix_listener /var/spool/postfix/private/dovecot-lmtp {
|
||||
group = postfix
|
||||
mode = 0600
|
||||
user = postfix
|
||||
}
|
||||
}
|
||||
```
|
||||
⚠️ На Ubuntu пользователь `postfix`, не `_postfix` (как на macOS).
|
||||
|
||||
**passwd-file auth** (`/etc/dovecot/conf.d/auth-passwdfile.conf.ext`):
|
||||
```
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
args = scheme=crypt username_format=%u /etc/dovecot/users
|
||||
}
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = username_format=%u /etc/dovecot/users
|
||||
default_fields = uid=5000 gid=5000 home=/var/mail/vhosts/dc.budaev.org/%n
|
||||
}
|
||||
```
|
||||
|
||||
**Users** (`/etc/dovecot/users`, формат passwd-file):
|
||||
```
|
||||
macky@dc.budaev.org:{SHA512-CRYPT}$6$...:5000:5000::/var/mail/vhosts/dc.budaev.org/macky:/usr/sbin/nologin
|
||||
```
|
||||
|
||||
Порядок настройки для нового виртуального ящика:
|
||||
1. Установить `dovecot-lmtpd` (отдельный пакет на Ubuntu)
|
||||
2. Создать конфиги LMTP и auth-passwdfile
|
||||
3. Раскомментировать `!include auth-passwdfile.conf.ext` в `/etc/dovecot/conf.d/10-auth.conf`
|
||||
4. Создать запись в `/etc/dovecot/users` (хэш через `doveadm pw -s SHA512-CRYPT`)
|
||||
5. Создать Maildir с uid/gid из userdb: `mkdir -p /var/mail/vhosts/dc.budaev.org/macky/Maildir/{new,cur,tmp}; chown -R 5000:5000`
|
||||
6. Добавить transport в Postfix: `echo 'macky@dc.budaev.org lmtp:unix:private/dovecot-lmtp' >> /etc/postfix/transport && postmap /etc/postfix/transport`
|
||||
7. Перезапустить dovecot + postfix
|
||||
|
||||
### Известные грабли mail
|
||||
|
||||
1. **relayhost** — НЕ СТАВИТЬ. Бот аутентифицирован на submission:587, Postfix сам доставляет на внешние MX.
|
||||
2. **smtp_tls_wrappermode / smtp_use_tls** — ломают TLS. Не использовать.
|
||||
3. **`bot@dc.budaev.org` vs `bot`** — в sasl_passwd username должен быть `bot`, не `bot@dc.budaev.org`.
|
||||
4. **key_id в БД** — после DELETE keypairs.id autoincrement не переиспользуется. Убедись что key_id в config совпадает с реальным id в keypairs.
|
||||
5. **Spamhaus PBL** — IP 90.188.48.201 в списке, iCloud/Gmail могут отклонять почту пока PTR не заработает.
|
||||
6. **dovecot-lmtpd** — обязательный пакет на Ubuntu, без него LMTP сокет не создаётся.
|
||||
7. **Group `postfix` vs `_postfix`** — на Ubuntu пользователь `postfix`, на macOS `_postfix`.
|
||||
8. **Permission denied autocreate Maildir** — виртуальный ящик не может создать Maildir сам, нужно создать вручную с uid/gid, совпадающим с `default_fields` в userdb.
|
||||
9. **`local_recipient_maps =`** — обязательно очистить (Postfix иначе rejected для виртуальных), доставка через transport_maps.
|
||||
|
||||
## DKIM
|
||||
|
||||
OpenDKIM milter (`Mode sv`) верифицирует входящие, но не вызывается для исходящих (submission). Решение не deployed. Документировано в `AGENTS.MD.bak` — dkimpy pipe after-queue.
|
||||
|
||||
## Bridges (Tor мосты)
|
||||
|
||||
Запрос через Gmail-посредник. Кэш 6ч, лимит 5 мостов на тип. Баг дубликатов (16.05.2026) починен: добавлен `_tor_lock`, сохранение `delivered` в файл, проверка в цикле.
|
||||
|
||||
## Известные проблемы
|
||||
|
||||
1. **"key is missing" при отправке в новый чат** — secure join handshake не завершён.
|
||||
2. **Описание канала** — `set_chat_description` требует core v2.50.0 (не released).
|
||||
3. **Аватарка канала** — `set_image()` может не работать на core v2.49.0.
|
||||
4. **Greylisting на mail.budaev.org** — `451-Greylisted`, потом `421 Too many connections`.
|
||||
5. **Spamhaus PBL** — PTR для 90.188.48.201 ещё не разрезолвился.
|
||||
6. **tg.i-c-a.su ненадёжен** — частые таймауты (17s+), per-channel баны, 15 RPM. PHP-прокси на budaev.org смягчает (кэш + fallback).
|
||||
7. **Unbound на сервере** — медленный резолвинг (122ms), рекомендуется форвардинг на 1.1.1.1.
|
||||
|
||||
## Планы на оптимизацию
|
||||
|
||||
1. Поддержка аватарок каналов (`chat.set_image()`).
|
||||
2. Очистка старых аккаунтов (`~/.local/share/deltachat/accounts/`).
|
||||
3. Автообновление LE сертификата (certbot renew + хуки).
|
||||
4. Уменьшить MDN (уведомления о прочтении).
|
||||
|
||||
## Выполненные оптимизации
|
||||
|
||||
### 02.06.2026 — Proxy + bot
|
||||
|
||||
- **`rss.php`:** pre-rewritten cache (cache hit = `readfile()`, без DOMDocument), staggered TTL (600 + crc32%300), combined DOMDocument.
|
||||
- **`rss.php`:** пустой RSS вместо 502 при ошибке tg.i-c-a.su — бот не падает на fallback.
|
||||
- **`.htaccess`:** убрано правило прямого кэша (мешало: нет проверки TTL, отдавало устаревшие файлы).
|
||||
- **`media.php`:** `maybeCleanOldCache()` — вероятностная очистка (~2% запросов) файлов старше 24ч.
|
||||
- **`deltabot.py`:** `TG_POLL_INTERVAL` 90→180с.
|
||||
- **`channels.json`:** синхронизирован с реальным списком (6 каналов: markettwits, raiznews, droidergram, gremtelegram, postnauka, kartiny2).
|
||||
|
||||
### 13.06.2026 — Security fixes
|
||||
|
||||
- **`deltabot.py` `smtp_send()`:** убраны `context.check_hostname = False` и `context.verify_mode = ssl.CERT_NONE` — TLS к Gmail теперь с полной проверкой сертификата. Ранее Gmail app password передавался через непроверенное соединение.
|
||||
- **`deltabot.py` `/save`:** ограничение размера файла до 50 МБ (`os.path.getsize`) до base64-кодирования — защита от DoS через pending_save.json.
|
||||
- **`deltabot.py` `/join`:** проверка URL заменена с `"i.delta.chat" in value` на `re.match(r'https://i\.delta\.chat/#', value)` — устраняет обход подстрокой.
|
||||
- **`deltabot.py` `/save` и `/channels image`:** `file_blob` валидируется через `os.path.realpath` + проверка что результирующий путь остаётся внутри `ACCOUNTS_DIR` — защита от path traversal.
|
||||
|
||||
### 13.06.2026 — Изоляция бота
|
||||
|
||||
- **`deltabot.py`:** добавлена проверка отправителя в начале `handle_message`. Сообщения от неизвестных email игнорируются без ответа.
|
||||
- **`deltabot.py`:** `is_contact_allowed()` — проверяет `isVerified` (JSON-RPC), затем `TRUSTED_DOMAINS` (zашиты в коде: `budaev.org`, `dc.budaev.org`), затем `allowed_contacts.json`.
|
||||
- **`deltabot.py`:** `_matches_allowlist()` — поддержка масок `*@domain`.
|
||||
- **`deltabot.py`:** команды `/addcontact` и `/contacts list|remove` — управление белым списком; изменения доступны только верифицированным контактам.
|
||||
|
||||
### 04.06.2026 — Poll interval decreased
|
||||
|
||||
- **`deltabot.py`:** `TG_POLL_INTERVAL` 180→90с — интервал уменьшен для устранения пропусков в часто обновляемых каналах.
|
||||
- **`deltabot.py`:** Мульти-енклозур откачен (один пост — одна картинка, как раньше).
|
||||
|
||||
### 26.05.2026 — strip_markdown
|
||||
|
||||
- **`deltabot.py`:** добавлена `strip_markdown()` — удаляет `**`, `*`, `` ` ``, `[]()` (→ сноски), `#`, списки, цитаты, `---` из AI-ответов перед отправкой.
|
||||
|
||||
## Изоляция бота (безопасность)
|
||||
|
||||
**Статус:** реализовано (13.06.2026)
|
||||
|
||||
Бот отвечает **только** контактам из белого списка. Все остальные сообщения молча игнорируются (`logger.info`) — бот не отвечает и не подтверждает получение.
|
||||
|
||||
### Логика проверки (`is_contact_allowed`)
|
||||
|
||||
Для каждого входящего сообщения в самом начале `handle_message`:
|
||||
|
||||
```
|
||||
is_contact_allowed(account, from_id)
|
||||
1. account._rpc.get_contact(account.id, from_id)
|
||||
2. contact['isVerified'] == True → разрешить (SecureJoin/QR)
|
||||
3. contact['address'] in TRUSTED_DOMAINS → разрешить (домен в коде)
|
||||
4. contact['address'] in allowed_contacts.json → разрешить (явный список)
|
||||
5. иначе → игнорировать
|
||||
```
|
||||
|
||||
### Доверенные домены (зашиты в коде)
|
||||
|
||||
```python
|
||||
TRUSTED_DOMAINS = {"budaev.org", "dc.budaev.org"}
|
||||
```
|
||||
|
||||
Любой адрес `*@budaev.org` или `*@dc.budaev.org` проходит без SecureJoin.
|
||||
|
||||
### Белый список (`allowed_contacts.json`)
|
||||
|
||||
**Файл:** `~/.config/deltabot/allowed_contacts.json`
|
||||
**Формат:** `["user@example.com", "*@otherdomain.org"]`
|
||||
|
||||
Поддерживаются:
|
||||
- Точный email: `user@example.com`
|
||||
- Доменная маска: `*@example.com`
|
||||
|
||||
### Управление белым списком
|
||||
|
||||
| Команда | Кто может | Действие |
|
||||
|---------|-----------|----------|
|
||||
| `/addcontact user@example.com` | верифицированный контакт | добавить email |
|
||||
| `/addcontact *@domain.org` | верифицированный контакт | добавить домен-маску |
|
||||
| `/contacts list` | любой разрешённый | показать список |
|
||||
| `/contacts remove user@example.com` | верифицированный контакт | удалить из списка |
|
||||
|
||||
Право на изменение белого списка — только у контактов с `isVerified = True` (добавленных через SecureJoin/QR).
|
||||
|
||||
### Известные ограничения
|
||||
|
||||
- `isVerified` в JSON-RPC требует **двусторонней** верификации. После `/join` бот верифицирует пользователя, но сам `isVerified` может остаться `False` до завершения handshake. В таком случае пользователь должен находиться в `TRUSTED_DOMAINS` или `allowed_contacts.json`.
|
||||
- При `/join` может создаваться новый неверифицированный контакт — старый верифицированный остаётся отдельно. Workaround: использовать `TRUSTED_DOMAINS` для своих доменов.
|
||||
|
||||
---
|
||||
|
||||
## Заметки (/note)
|
||||
|
||||
**Файл:** `~/.config/deltabot/notes.json`
|
||||
**Формат:** `{chat_id: [{id, text, created}]}`
|
||||
**ID** — автоинкремент в рамках каждого чата.
|
||||
|
||||
| Команда | Действие |
|
||||
|---------|----------|
|
||||
| `/note add <текст>` | Добавить заметку |
|
||||
| `/note list` | Список заметок чата |
|
||||
| `/note delete <N>` | Удалить по ID |
|
||||
|
||||
---
|
||||
|
||||
## Курс валют (/rate)
|
||||
|
||||
**API:** ЦБ РФ `https://www.cbr.ru/scripts/XML_daily.asp` (XML, без ключа)
|
||||
**Кэш:** в памяти, TTL 1 час (`_rates_cache` dict).
|
||||
|
||||
| Команда | Действие |
|
||||
|---------|----------|
|
||||
| `/rate` | USD, EUR, CNY, GBP |
|
||||
| `/rate USD EUR` | Указанные валюты |
|
||||
|
||||
---
|
||||
|
||||
## Сетевые утилиты (/ip, /dns)
|
||||
|
||||
| Команда | Действие |
|
||||
|---------|----------|
|
||||
| `/ip` | Внешний IP сервера (api.ipify.org) |
|
||||
| `/ip 8.8.8.8` | Страна, город, org (ipinfo.io, 50k/мес free) |
|
||||
| `/dns budaev.org` | DNS A/AAAA (socket.getaddrinfo) |
|
||||
|
||||
---
|
||||
|
||||
## Мониторинг сайтов (/monitor)
|
||||
|
||||
**Файл:** `~/.config/deltabot/monitors.json`
|
||||
**Воркер:** `monitor_worker()` — проверяет каждые 5 минут, шлёт алерт всем подписчикам при смене статуса.
|
||||
|
||||
| Команда | Действие |
|
||||
|---------|----------|
|
||||
| `/monitor add <url>` | Добавить (мгновенная проверка при добавлении) |
|
||||
| `/monitor list` | Список с последним статусом |
|
||||
| `/monitor remove <N\|url>` | Удалить по номеру или URL |
|
||||
| `/monitor check` | Немедленная проверка всех |
|
||||
|
||||
Алерт при падении отправляется **всем подписчикам** (`subscribers.json`).
|
||||
|
||||
---
|
||||
|
||||
## CalDAV напоминания
|
||||
|
||||
**Воркер:** `cal_reminder_worker()` — каждые 5 минут проверяет события на ближайшие 15 минут.
|
||||
**Файл отправленных:** `~/.config/deltabot/cal_reminders_sent.json` — `{uid: timestamp}`, очищается автоматически (старше 25ч).
|
||||
Уведомления получают все подписчики (как утренний отчёт).
|
||||
|
||||
---
|
||||
|
||||
## CalDAV — события дня в утреннем отчёте
|
||||
|
||||
Функция `send_daily_status_worker()` вызывает `cal_list_events(days_ahead=1)` и добавляет список событий к отчёту. При ошибке CalDAV — отчёт уходит без раздела событий (warning в лог).
|
||||
|
||||
---
|
||||
|
||||
## Whisper — транскрипция голосовых сообщений
|
||||
|
||||
**API:** Cloudflare Workers AI `@cf/openai/whisper`
|
||||
**Триггер:** любое входящее сообщение с `file_mime_type` начинающимся на `audio/` и без текста.
|
||||
**Функция:** `transcribe_audio(file_path)` → POST бинарного аудио на Cloudflare.
|
||||
Использует уже имеющиеся `CLOUDFLARE_ACCOUNT_ID` и `CLOUDFLARE_API_TOKEN` из `.env`.
|
||||
|
||||
---
|
||||
|
||||
## /ai summary
|
||||
|
||||
Субкоманда `/ai summary` — разовый вызов AI без изменения истории. Реализована в `ai_agent.summarize_session(chat_id)`. Берёт последние 20 сообщений, просит модель кратко резюмировать диалог.
|
||||
|
||||
---
|
||||
|
||||
## AI Chat (Cloudflare Workers AI + OpenRouter fallback)
|
||||
|
||||
**Статус:** работает
|
||||
**Файлы:** `ai_agent.py`, `~/.config/deltabot/ai_sessions.json`
|
||||
**Провайдеры:**
|
||||
- **Cloudflare Workers AI** (основной) — 10,000 нейронов/день, регистрация без карты, работает из России
|
||||
- **OpenRouter** (fallback) — 50 RPD, при ошибке Cloudflare
|
||||
|
||||
Один запрос = один ответ.
|
||||
|
||||
### Переменные окружения (.env)
|
||||
|
||||
```
|
||||
CLOUDFLARE_ACCOUNT_ID=<account_id>
|
||||
CLOUDFLARE_API_TOKEN=<api_token>
|
||||
OPENROUTER_API_KEY=sk-or-... # только для fallback
|
||||
```
|
||||
|
||||
### Команды
|
||||
|
||||
| Команда | Действие |
|
||||
|---------|----------|
|
||||
| `/ai on` | Включить AI (любое сообщение → ответ модели) |
|
||||
| `/ai off` | Выключить AI |
|
||||
| `/ai status` | Модель, сообщений, ключ |
|
||||
| `/model` | Список моделей / выбор модели |
|
||||
| `/apikey [ключ]` | Установить ключ OpenRouter (для fallback) |
|
||||
|
||||
### Модели
|
||||
|
||||
#### Cloudflare Workers AI
|
||||
|
||||
| ID | Нейронов/ответ | Ответов/день |
|
||||
|----|---------------|--------------|
|
||||
| `@cf/qwen/qwen3-30b-a3b-fp8` (по умолчанию) | ~20 | ~500 |
|
||||
| `@cf/meta/llama-3.3-70b-instruct-fp8-fast` | ~116 | ~86 |
|
||||
| `@cf/meta/llama-3.1-8b-instruct-fast` | ~20 | ~500 |
|
||||
| `@cf/meta/llama-4-scout-17b-16e-instruct` | ~40 | ~250 |
|
||||
| `@cf/deepseek-ai/deepseek-r1-distill-qwen-32b` | ~50 | ~200 |
|
||||
| `@cf/moonshotai/kimi-k2-instruct` | ~40 | ~250 |
|
||||
| `@cf/aisingapore/gemma-sea-lion-v4-27b-it` | ~40 | ~250 |
|
||||
|
||||
#### OpenRouter (fallback)
|
||||
|
||||
| ID | Контекст |
|
||||
|----|----------|
|
||||
| `deepseek/deepseek-v4-flash:free` | 1M |
|
||||
| `moonshotai/kimi-k2.6:free` | 262K |
|
||||
| `minimax/minimax-m2.5:free` | 262K |
|
||||
| `openrouter/free` | auto-route |
|
||||
|
||||
### Лимиты
|
||||
|
||||
- **Cloudflare:** 10,000 нейронов/день (см. таблицу выше)
|
||||
- **OpenRouter:** 50 запросов/день, 20 RPM (только при падении Cloudflare)
|
||||
|
||||
### Архитектура
|
||||
|
||||
```
|
||||
process_message()
|
||||
├── Cloudflare Workers AI (основной)
|
||||
│ └── при ошибке → OpenRouter (fallback)
|
||||
└── OpenRouter (если модель начинается не с @cf/)
|
||||
```
|
||||
|
||||
### Хранение
|
||||
|
||||
**ai_sessions.json:** `{chat_id: {model, enabled, messages[], api_key}}`
|
||||
Контекст — последние 20 сообщений. API ключ OpenRouter хранится per-session или в `OPENROUTER_API_KEY` env.
|
||||
Loading…
Add table
Add a link
Reference in a new issue