diff --git a/.gitignore b/.gitignore index b25004e..2fdfbf5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ .DS_Store *.bak *.bak.* +AGENTS.MD diff --git a/AGENTS.MD b/AGENTS.MD deleted file mode 100644 index e01b3ca..0000000 --- a/AGENTS.MD +++ /dev/null @@ -1,608 +0,0 @@ -# Delta Chat Bot: deltabot - -## ⚠️ Важно: как мы работаем - -- Я работаю на **локальном Mac** (`/Users/alexabudaev/Documents/Zed/`), не на сервере. -- Все изменения — только локально. После завершения работы — `git push origin main`. -- **Репозиторий:** `https://git.budaev.org/alexabudaev/delta-chat-bot` -- **Авторизация:** HTTPS + токен (credentials в `~/.config/git/forgejo-credentials`) -- **Сервер:** Ubuntu 24.04, бот под `alexabudaev`, Postfix/Dovecot под `bot`. - -### Авто-деплой на сервер - -На сервере настроен systemd timer — каждые 5 минут проверяет обновления в `main` и перезапускает бота: - -```bash -sudo systemctl status deltabot-auto-update.timer -``` - -Принудительный деплой (если надо прямо сейчас): -```bash -ssh alexabudaev@10.0.0.1 "cd ~/delta-bot && git pull origin main && sudo systemctl restart deltabot" -``` - -### Прокси (PHP) - -После изменений в `php-proxy/` — `scp` вручную (пока не в git на сервере): -```bash -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/ -``` - -## Архитектура - -``` -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 -# После изменений — просто пуш: -git push origin main - -# На сервере авто-деплой каждые 5 мин. Принудительно: -ssh alexabudaev@10.0.0.1 "cd ~/delta-bot && git pull origin main && sudo systemctl restart deltabot" - -# Логи: -ssh alexabudaev@10.0.0.1 "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= -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 [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 или домен в белый список (только для верифицированных) -- **`/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 сохраняется в кэш уже с переписанными `` (на 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 `...` с 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`) добавляет `` в RSS для постов с медиа. Бот: -1. Читает первый `` из RSS -2. Если `type` начинается с `image/` — скачивает эту картинку -3. Прикрепляет к тексту поста (поле `file` в `send_msg`) -4. Удаляет временный файл после отправки -5. Если скачать не удалось — пост уходит без картинки - -### HTML → plain text - -Delta Chat не рендерит Markdown. Из HTML вырезаются все теги: - -| Тег в RSS | Результат | -|-----------|-----------| -| `
` | перенос строки | -| `text` | сохраняется только `text`, URL → сноска | -| `...` | URL не попадает в сноску, текст сохраняется | -| Сноска | `---\nurl1\nurl2` | -| ``, ``, `
` и т.д. | удаляются | -| `` / `` | удаляются | -| HTML entities | `html.unescape()` | -| `` | 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. - -### 14.06.2026 — Мониторинг per-chat - -- **`deltabot.py` `monitor_worker()` и `/monitor`:** мониторы разграничены по чатам. `monitors.json` — `{chat_id: [...]}` (был глобальный список). `/monitor list` показывает только свои мониторы, алерты уходят только в чат-владелец. Добавлена автоматическая миграция старого формата. - -### 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` -**Формат:** `{chat_id: [{url, last_ok, last_check, last_detail}, ...]}` — каждый чат видит только свои мониторы. -**Воркер:** `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.