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