From de638bb4d0af5e56518c82173b55757d2bc32900 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 29 Oct 2026 22:19:40 +0200 Subject: [PATCH 1/7] docs: describe analytics storage and production raw update log --- IDEA.MD | 11 +++++++---- README.md | 25 +++++++++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/IDEA.MD b/IDEA.MD index f4e8b82..237d0a1 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -35,6 +35,7 @@ Telegram-бот, который по запросу пользователя г ## Технические заметки - Формат: Telegram-бот. - Стек: Python 3.12, aiogram 3.x, aiohttp, SQLite. +- Аналитика: отдельная SQLite `/app/data/analytics.sqlite3`, события на пользователя; команда `/stats` в админском топике выводит таблицы по периодам «Сегодня», «7 дней», «30 дней», «Всё время». - APInex Voice STT расшифровывает голосовые; Gemini 3.8 Flash через APInex классифицирует намерения, пишет/правит посты и извлекает ToV из скриншотов. - Coolify env с отдельными модельными настройками: `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_INJECTION_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `APINEX_RESPONSE_REPAIR_MODEL`; общий секрет `APINEX_API_KEY`. При невалидном JSON допускается одна попытка исправления форматирования до валидации схемы. Обрезанный на середине строки ответ не требует доотправки: `_extract_complete_post_strings` достаёт целые строки и возвращает три полных варианта. Лимит `max_tokens` у вызовов LLM снят. - В режиме «Обучение» текстовые заметки, голосовые/аудио и примеры постов дополняют единый накопительный профиль аккаунта; сброс обучения удаляет и заметки, и образцы. Флаг `users.is_trained` включает самостоятельную генерацию поста. @@ -65,13 +66,13 @@ Telegram-бот, который по запросу пользователя г - Gitea: `admin/social-post-writer-bot`, ветка `dev`. - Coolify проект: `Social Post Writer Bot`, окружение `development`. - Coolify приложение: `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) на `spb-easy-dale`. -- SQLite: `/app/data/social_writer.sqlite3`; persistent volume `d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data` монтируется в `/app/data`. +- SQLite: `/app/data/social_writer.sqlite3`; analytics: `/app/data/analytics.sqlite3` на том же persistent volume `d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data` монтируется в `/app/data`. - Важно при поиске логов: приложение запущено на сервере `spb-easy-dale` (`31.129.103.242`), который может отличаться от хоста управления Coolify. Ориентируйся на поле Server у приложения; `docker ps` и `docker logs --since 24h ` выполняй на сервере приложения, имя контейнера начинается с `d32u5zcq2g4odmdpq9hw91yn-`. Не делай вывод об отсутствии контейнера по проверке только хоста Coolify. - После деплоя файл приложения `/app/data/bot.log` и его ротационные копии хранятся на persistent volume выше и переживают пересоздание контейнера. Пока logging-изменение не задеплоено, используй `docker logs` и API-журнал `api_call_logs` в SQLite. API-логи сохраняются в `/app/data/social_writer.sqlite3` на том же volume. -- Dev-only raw update log: отдельная SQLite `/app/data/bot_updates.sqlite3`, таблица `raw_updates`; поиск по Telegram `update_id` (`SELECT payload FROM raw_updates WHERE update_id = ?`). Middleware подключается только при `DEPLOY_ENV=development`; записи старше 30 дней удаляются при запуске и записи новых событий. Секретные поля/токены маскируются. JSON включает приватный текст и metadata апдейта; production его не пишет. +- Raw update log (все окружения, включая production): отдельная SQLite `/app/data/bot_updates.sqlite3`, таблица `raw_updates`; поиск по Telegram `update_id` (`SELECT payload FROM raw_updates WHERE update_id = ?`). Middleware подключается всегда — в проде это единственный способ увидеть события из групп администрирования и поддержки; записи старше 30 дней удаляются при запуске и записи новых событий. Секретные поля/токены маскируются. JSON включает приватный текст и metadata апдейта. - Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории. - Поддержка: `SUPPORT_GROUP_ID` — id группы операторов (`-1008000863273`), `SUPPORT_NOTE_PREFIX` (по умолчанию `//`), `SUPPORT_GREETINGS` (пул приветствий, разделитель `|`), `SUPPORT_OPERATOR_SIGNATURE` (по умолчанию `Оператор поддержки`), `SUPPORT_TOPIC_EMOJI_ID` (опционально). Без `SUPPORT_GROUP_ID` поддержка выключена. Подробнее — раздел «Поддержка». -- `DEPLOY_ENV` включает dev-поведение только при точном значении `development`; production, пустое и неизвестное значение отключают его. `BOT_ENV` отдельно выбирает Telegram Bot API. `DEV_CHAT_ID` используется для уведомления о версии; версия в файле `VERSION` (сейчас `0.1.0+64`). Ротационный файл лога — `BOT_LOG_PATH` (по умолчанию `/app/data/bot.log`) на том же volume; сырые апдейты — `RAW_UPDATES_DB_PATH` (по умолчанию `/app/data/bot_updates.sqlite3`). +- `DEPLOY_ENV` включает dev-поведение только при точном значении `development`; production, пустое и неизвестное значение отключают его. `BOT_ENV` отдельно выбирает Telegram Bot API. `DEV_CHAT_ID` используется для уведомления о версии; версия в файле `VERSION` (сейчас `0.1.0+67`). Ротационный файл лога — `BOT_LOG_PATH` (по умолчанию `/app/data/bot.log`) на том же volume; сырые апдейты — `RAW_UPDATES_DB_PATH` (по умолчанию `/app/data/bot_updates.sqlite3`). - Вики Gitea: страницы `Home`, `User-Guide`, `Developer-Guide`, `Subscription`, `Deployment`. ## Статус @@ -80,6 +81,7 @@ Telegram-бот, который по запросу пользователя г - [x] Классификатор намерений и Tone of Voice из скриншота - [x] SQLite-настройки пользователей и reply-правки без хранения диалога - [x] Development-развёртывание в Coolify: `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) +- [x] Аналитика пользователей: отдельная SQLite, отчёт `/stats` с таблицами и периодами - [x] Подключён persistent Coolify volume `/app/data` для SQLite - [x] Проверены на моках STT, LLM, vision, ToV, reply-поля и изоляция пользователей - [x] Выполнен live smoke test: классификатор вернул `write_post`, генератор — 3 разных варианта; vision распознал тестовый скриншот @@ -104,7 +106,8 @@ Telegram-бот, который по запросу пользователя г - [x] Исправлен баг: `_send_idea` была потеряна в коммите про подписку — `/idea` падал с `NameError` - [x] Реальная оплата Stars: `sendInvoice` (XTR) → `pre_checkout_query` → `successful_payment`; подписка активируется только после оплаты, `telegram_payment_charge_id` защищает от двойного начисления - [x] В `DEPLOY_ENV=development` после обычного сообщения об инвойсе отдельным сообщением доступна кнопка «🧪 Оплатил (dev)»; callback ограничен пользователем инвойса, а все dev-ветви fail-closed выключены вне development -- Версия разработки: `0.1.0+64` +- [x] Статистика активности по пользователям хранится в отдельной SQLite `/app/data/analytics.sqlite3`; `/stats` в админском топике показывает таблицы с колонками за сегодня, 7 дней, 30 дней и всё время. Генерация считает посты, идеи, правки и типы сообщений; обучение — только типы сообщений. +- Версия разработки: `0.1.0+67` - [x] Статус долгой операции стримится живым драфтом `sendMessageDraft` в приватном чате и каждые 3–8 сек меняет случайную фразу из пула операции (`activity_phrases.py`); в группах и топиках работает прежний `edit_text` - [x] Справка `/help` отправляется как Rich Message с обычной inline-клавиатурой; каждый callback `help:
` открывает раздел, кнопки доступны на всех страницах. - [x] Проект Gitea «Social Post Writer Bot Roadmap» с задачами (выполненные — в «Готово», будущие — в Backlog) diff --git a/README.md b/README.md index ed91613..098b121 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,8 @@ onboarding. В «Подписке» сразу отображаются тип --- +- **Статистика бота:** команда `/stats` доступна в топике «Статистика» группы «Мастер постинга: Админка» (`chat_id=-1008001427913`, `message_thread_id=5`). Отчёт отправляется Rich Message таблицами с колонками «Сегодня», «7 дней», «30 дней», «Всё время». События хранятся по Telegram user id в отдельной SQLite `/app/data/analytics.sqlite3` на persistent volume; текст/медиа пользователей не сохраняются. В режиме генерации учитываются генерации постов, идеи, правки и типы сообщений; в обучении — только типы сообщений. Групповой роутер ограничен аналитическим топиком; доступ к отчёту ограничивается `OWNER_USER_ID`, если переменная задана. + ## Архитектура кода ``` @@ -192,7 +194,8 @@ bot.py — роутинг, команды, обработка со payments.py — оплата Stars: sendInvoice, pre_checkout_query, successful_payment, защита от двойного списания apinex_client.py — клиент APInex (LLM, STT, vision), промпты, логирование вызовов user_store.py — SQLite: пользователи, ToV, посты, идеи, API-логи, подписки -raw_updates.py — отдельная SQLite-БД сырых входящих Telegram Update для development +analytics.py — отдельная SQLite-аналитика по Telegram user id; события активности и отчёты +raw_updates.py — отдельная SQLite-БД сырых входящих Telegram Update (работает и в production) support.py — поддержка: личка ↔ персональный топик в группе операторов keyboards.py — клавиатуры главного меню и режимов; маппинг кнопок на команды help_message.py — Rich Message: `WELCOME_RICH_HTML` (онбординг) + `HELP_SECTIONS`/`help_view()` (справка с кнопками), plain fallback @@ -316,11 +319,13 @@ SUBSCRIPTION_PRICE_STARS = 100 ## Данные и хранилище -- **SQLite:** `/app/data/social_writer.sqlite3` (persistent Coolify volume). +- SQLite `/app/data/social_writer.sqlite3` (persistent Coolify volume) — бизнес-данные пользователей, подписки и платежи. +- SQLite `/app/data/analytics.sqlite3` (тот же persistent volume) — append-only события по `telegram_user_id`, режиму, действию и типу входящего сообщения; без пользовательского текста и медиа. Общий отчёт агрегируется запросами, отдельные дублирующие сводки не хранятся. +- Команда `/stats` работает в форуме группы «Мастер постинга: Админка», топик `5`, и показывает таблицы за сегодня, последние 7 дней, последние 30 дней и всё время. Счётчики генерации: посты, идеи, правки, сообщения и типы сообщений; обучение — типы входящих сообщений. - **Постоянное хранилище dev:** volume `d32u5zcq2g4odmdpq9hw91yn-social-post-writer-dev-data` смонтирован в `/app/data` контейнера на сервере Coolify `spb-easy-dale` (`31.129.103.242`). Сервер приложения и сервер управления Coolify могут быть разными: сначала смотри назначенный приложению Server в записи Coolify, затем проверяй контейнер именно на нём. Не ищи контейнер только на хосте Coolify. - **Где смотреть ошибки:** подключись к `spb-easy-dale`, найди контейнер по имени `d32u5zcq2g4odmdpq9hw91yn-*` командой `docker ps`, затем смотри его stdout/stderr через `docker logs --since 24h `. После деплоя ротационный файл приложения будет доступен как `/app/data/bot.log` (до 5 MiB и 3 архива); он сохраняется в указанном persistent volume при пересоздании контейнера. До деплоя изменения логирования ещё не действуют. - **API-журнал ошибки:** открой `/app/data/social_writer.sqlite3`, таблицу `api_call_logs`; записи связываются по `operation_id` и времени. Файл/БД находятся в volume на сервере приложения, не в локальном checkout и не на сервере Coolify, если это другой хост. -- **Сырые Telegram Update (только dev):** отдельная БД `/app/data/bot_updates.sqlite3`, таблица `raw_updates`; хранится JSON входящего Update, `update_id`, тип события и время получения. Поиск по `update_id`: `SELECT payload FROM raw_updates WHERE update_id = ?`. Записи старше 30 дней удаляются при старте dev-бота и при сохранении новых апдейтов. Чувствительные значения маскируются; Update могут содержать тексты и персональные данные пользователей. В production middleware не подключается и БД не создаётся. +- **Сырые Telegram Update (все окружения, включая production):** отдельная БД `/app/data/bot_updates.sqlite3`, таблица `raw_updates`; хранится JSON входящего Update, `update_id`, тип события и время получения. Поиск по `update_id`: `SELECT payload FROM raw_updates WHERE update_id = ?`. Записи старше 30 дней удаляются при старте бота и при сохранении новых апдейтов. Чувствительные значения маскируются; Update могут содержать тексты и персональные данные пользователей. Middleware подключается всегда: в production это единственный способ посмотреть события из групп администрирования и поддержки. - **Обращения в поддержку:** таблица `support_topics` в той же рабочей БД — `telegram_user_id` (PK), `thread_id` топика в группе операторов, `is_active`, время создания и обновления. Один пользователь — один топик; при завершении запись не удаляется, только снимается `is_active`, поэтому повторное обращение продолжается с сохранённой историей. Если топик удалили вручную в Telegram, при следующем сообщении он создаётся заново. - **Per-user данные:** профиль аккаунта/ToV, текстовые обучающие заметки, образцы постов со скриншотов, история сгенерированных постов и идеи. В «Обучении» текст/голос/скриншоты накапливают и обновляют профиль, а не запускают генерацию. История диалогов и исходные изображения **не сохраняются**. - **Пустые ответы APInex:** при пустом `content` бот делает до двух повторов; третий запрос добавляет `reasoning_effort: low`. Если ответ всё ещё пустой, операция завершается ошибкой и каждая попытка попадает в API-журнал. Ошибки HTTP автоматически не повторяются. APInex документирует `low`, но не режим отключения reasoning; поддержка параметра зависит от модели. @@ -332,6 +337,17 @@ SUBSCRIPTION_PRICE_STARS = 100 --- +## Статистика и аналитика + +- `/stats` доступна в админском форуме, топик «Статистика» (`chat_id=-1008001427913`, `message_thread_id=5`). Отчёт уходит в этот же топик Rich Message-таблицами. +- По умолчанию `/stats` показывает данные за всё время; отчёт содержит сопоставимые колонки «Сегодня», «7 дней», «30 дней», «Всё время». +- Таблицы: активные пользователи; генерации постов, идеи, правки и сообщения; распределение типов сообщений в генерации; сообщения и типы сообщений в обучении. +- Периоды рассчитываются как текущие 24 часа, последние 7×24 часа, последние 30×24 часа и весь срок хранения соответственно. +- События хранятся в `/app/data/analytics.sqlite3`, отдельной SQLite на persistent volume. Запись привязана к `telegram_user_id`; общие значения агрегируются запросом без дублирующих таблиц. +- Сохраняются только режим, тип действия, тип входящего сообщения и время. Содержимое сообщений, изображений и аудио аналитикой не сохраняется. +- В генерации учитываются действие генерации поста, идеи, правки и входящие сообщения по типам. В обучении — только входящие сообщения по типам. +- Вход в отчёт проверяет точные `chat_id` и `message_thread_id`; при заданном `OWNER_USER_ID` команда доступна только этому Telegram-пользователю. + ## Команды | Команда | Что делает | Режим | @@ -342,6 +358,7 @@ SUBSCRIPTION_PRICE_STARS = 100 | `/post`, `/idea` | Самостоятельный пост по обученному профилю; идея | Генерация; для генерации нужна подписка | | `/balance`, `/subscribe` | Статус или оплата Stars | Подписка | | `/support` | Написать в поддержку | Любой | +| `/stats` | Отчёт аналитики в админском топике | Топик «Статистика» | В режиме «Генерация» русские фразы («придумай идею», «о чём написать?») работают как `/idea`. @@ -354,7 +371,7 @@ SUBSCRIPTION_PRICE_STARS = 100 - **Деплой:** `bash /root/.hermes/scripts/gitea_deploy.sh dev` (POST `/api/v1/deploy?uuid=...&force=false`). - **Окружение:** `DEPLOY_ENV` включает dev-функции только при точном значении `development`; production, пустое и неизвестное значение отключают их. `BOT_ENV` отдельно выбирает Telegram Bot API и не используется для включения dev-поведения. - **VERSION:** файл `VERSION` (`0.1.0+64`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление о версии в `DEV_CHAT_ID`. -- **Переменные Coolify:** `BOT_TOKEN`, `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_INJECTION_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `APINEX_RESPONSE_REPAIR_MODEL` (optional; default uses the standard model), `BOT_DB_PATH`, `RAW_UPDATES_DB_PATH` (optional; default `/app/data/bot_updates.sqlite3`), `DEPLOY_ENV`, `DEV_CHAT_ID`, `BOT_LOG_PATH` (optional; default `/app/data/bot.log`), `ACTIVITY_MIN_SECONDS` / `ACTIVITY_MAX_SECONDS` (optional; default 3 / 8 — границы паузы между сменами фраз в драфте), `ACTIVITY_MAX_ROTATIONS` (optional; default 12 — предохранитель от бесконечной ротации), +- **Переменные Coolify:** `BOT_TOKEN`, `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_INJECTION_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `APINEX_RESPONSE_REPAIR_MODEL` (optional; default uses the standard model), `BOT_DB_PATH`, `ANALYTICS_DB_PATH` (optional; default `/app/data/analytics.sqlite3`), `ANALYTICS_CHAT_ID` (default `-1008001427913`), `ANALYTICS_TOPIC_ID` (default `5`), `OWNER_USER_ID` (optional; when set, `/stats` is restricted to this Telegram user), `RAW_UPDATES_DB_PATH` (optional; default `/app/data/bot_updates.sqlite3`), `DEPLOY_ENV`, `DEV_CHAT_ID`, `BOT_LOG_PATH` (optional; default `/app/data/bot.log`), `ACTIVITY_MIN_SECONDS` / `ACTIVITY_MAX_SECONDS` (optional; default 3 / 8 — границы паузы между сменами фраз в драфте), `ACTIVITY_MAX_ROTATIONS` (optional; default 12 — предохранитель от бесконечной ротации), `SUPPORT_GROUP_ID` (id супергруппы с темами для обращений; без неё поддержка выключена), `SUPPORT_NOTE_PREFIX` (optional; default `//` — префикс заметки, которая не уходит пользователю), `SUPPORT_GREETINGS` (optional; пул приветствий оператора, разделитель `|`), From 089c59bc68fb46f1154f4dd74ff0770d6bd48841 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 29 Oct 2026 22:19:40 +0200 Subject: [PATCH 2/7] fix(logging): write raw updates in production too --- VERSION | 2 +- bot.py | 12 ++++++++---- tests/test_raw_updates.py | 22 ++++++++++------------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/VERSION b/VERSION index 3fa4ee1..4070802 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+76 +0.1.0+77 diff --git a/bot.py b/bot.py index d5bede1..72dbf44 100644 --- a/bot.py +++ b/bot.py @@ -255,8 +255,11 @@ def is_development_environment() -> bool: def configure_raw_update_logging(dispatcher: Dispatcher) -> bool: - if not is_development_environment(): - return False + """Persist incoming updates for diagnostics in every deploy environment. + + Групповые события (администрирование и поддержка) видно только через + мидлваре — без него прод-бот остаётся «чёрным ящиком». + """ dispatcher.update.outer_middleware(RawUpdateMiddleware()) return True @@ -1660,8 +1663,9 @@ async def main() -> None: analytics.init_db() payments.init_payments_table() await asyncio.to_thread(support.init_support_table) - if is_development_environment(): - await asyncio.to_thread(init_raw_updates_db) + # Инициализируем БД сырых апдейтов в любом окружении: в проде она нужна, + # чтобы видеть события из групп администрирования и поддержки. + await asyncio.to_thread(init_raw_updates_db) token = os.environ.get("BOT_TOKEN", "").strip() if not token: raise RuntimeError("BOT_TOKEN is required") diff --git a/tests/test_raw_updates.py b/tests/test_raw_updates.py index 25aa08f..a8a4c67 100644 --- a/tests/test_raw_updates.py +++ b/tests/test_raw_updates.py @@ -92,15 +92,13 @@ class RawUpdateMiddlewareTests(unittest.IsolatedAsyncioTestCase): class RawUpdateModeTests(unittest.TestCase): - def test_middleware_is_registered_only_for_development(self): - dispatcher = Dispatcher() - with patch.object(bot, "DEPLOY_ENV", "production"), patch.object( - dispatcher.update, "outer_middleware" - ) as register: - self.assertFalse(bot.configure_raw_update_logging(dispatcher)) - register.assert_not_called() - with patch.object(bot, "DEPLOY_ENV", "development"), patch.object( - dispatcher.update, "outer_middleware" - ) as register: - self.assertTrue(bot.configure_raw_update_logging(dispatcher)) - register.assert_called_once() + def test_middleware_is_registered_in_every_environment(self): + """Прод-бот тоже пишет сырые апдейты: групповые события видны только так.""" + for deploy_env in ("production", "development"): + with self.subTest(deploy_env=deploy_env): + dispatcher = Dispatcher() + with patch.object(bot, "DEPLOY_ENV", deploy_env), patch.object( + dispatcher.update, "outer_middleware" + ) as register: + self.assertTrue(bot.configure_raw_update_logging(dispatcher)) + register.assert_called_once() From f1712d6db0d25c6e41f3324d3916462b0ac32903 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 29 Oct 2026 22:54:48 +0200 Subject: [PATCH 3/7] docs(groups): pin admin group and topic ids --- IDEA.MD | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/IDEA.MD b/IDEA.MD index 237d0a1..e64daeb 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -71,6 +71,7 @@ Telegram-бот, который по запросу пользователя г - После деплоя файл приложения `/app/data/bot.log` и его ротационные копии хранятся на persistent volume выше и переживают пересоздание контейнера. Пока logging-изменение не задеплоено, используй `docker logs` и API-журнал `api_call_logs` в SQLite. API-логи сохраняются в `/app/data/social_writer.sqlite3` на том же volume. - Raw update log (все окружения, включая production): отдельная SQLite `/app/data/bot_updates.sqlite3`, таблица `raw_updates`; поиск по Telegram `update_id` (`SELECT payload FROM raw_updates WHERE update_id = ?`). Middleware подключается всегда — в проде это единственный способ увидеть события из групп администрирования и поддержки; записи старше 30 дней удаляются при запуске и записи новых событий. Секретные поля/токены маскируются. JSON включает приватный текст и metadata апдейта. - Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории. +- Админская группа «Писатель постов: Админка»: `chat_id = -1004459624166` (supergroup, forum). Топики: `4` — Статистика, `5` — Расходы, `6` — Рассылка, `7` — Ошибки. В production эти значения заданы env (`ADMIN_GROUP_ID`, `ANALYTICS_CHAT_ID`, `ANALYTICS_TOPIC_ID`, `TOPIC_STATS_ID`, `TOPIC_EXPENSES_ID`, `TOPIC_BROADCAST_ID`, `TOPIC_ERRORS_ID`); в коде оставлены дефолты для dev. Реализован только топик «Статистика» (`/stats`), остальные зарезервированы. - Поддержка: `SUPPORT_GROUP_ID` — id группы операторов (`-1008000863273`), `SUPPORT_NOTE_PREFIX` (по умолчанию `//`), `SUPPORT_GREETINGS` (пул приветствий, разделитель `|`), `SUPPORT_OPERATOR_SIGNATURE` (по умолчанию `Оператор поддержки`), `SUPPORT_TOPIC_EMOJI_ID` (опционально). Без `SUPPORT_GROUP_ID` поддержка выключена. Подробнее — раздел «Поддержка». - `DEPLOY_ENV` включает dev-поведение только при точном значении `development`; production, пустое и неизвестное значение отключают его. `BOT_ENV` отдельно выбирает Telegram Bot API. `DEV_CHAT_ID` используется для уведомления о версии; версия в файле `VERSION` (сейчас `0.1.0+67`). Ротационный файл лога — `BOT_LOG_PATH` (по умолчанию `/app/data/bot.log`) на том же volume; сырые апдейты — `RAW_UPDATES_DB_PATH` (по умолчанию `/app/data/bot_updates.sqlite3`). - Вики Gitea: страницы `Home`, `User-Guide`, `Developer-Guide`, `Subscription`, `Deployment`. diff --git a/README.md b/README.md index 098b121..895a0fe 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,7 @@ SUBSCRIPTION_PRICE_STARS = 100 - **Окружение:** `DEPLOY_ENV` включает dev-функции только при точном значении `development`; production, пустое и неизвестное значение отключают их. `BOT_ENV` отдельно выбирает Telegram Bot API и не используется для включения dev-поведения. - **VERSION:** файл `VERSION` (`0.1.0+64`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление о версии в `DEV_CHAT_ID`. - **Переменные Coolify:** `BOT_TOKEN`, `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_INJECTION_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `APINEX_RESPONSE_REPAIR_MODEL` (optional; default uses the standard model), `BOT_DB_PATH`, `ANALYTICS_DB_PATH` (optional; default `/app/data/analytics.sqlite3`), `ANALYTICS_CHAT_ID` (default `-1008001427913`), `ANALYTICS_TOPIC_ID` (default `5`), `OWNER_USER_ID` (optional; when set, `/stats` is restricted to this Telegram user), `RAW_UPDATES_DB_PATH` (optional; default `/app/data/bot_updates.sqlite3`), `DEPLOY_ENV`, `DEV_CHAT_ID`, `BOT_LOG_PATH` (optional; default `/app/data/bot.log`), `ACTIVITY_MIN_SECONDS` / `ACTIVITY_MAX_SECONDS` (optional; default 3 / 8 — границы паузы между сменами фраз в драфте), `ACTIVITY_MAX_ROTATIONS` (optional; default 12 — предохранитель от бесконечной ротации), +`ADMIN_GROUP_ID` (id админской супергруппы с темами; production: `-1004459624166` — «Писатель постов: Админка»), `SUPPORT_GROUP_ID` (id супергруппы с темами для обращений; без неё поддержка выключена), `SUPPORT_NOTE_PREFIX` (optional; default `//` — префикс заметки, которая не уходит пользователю), `SUPPORT_GREETINGS` (optional; пул приветствий оператора, разделитель `|`), From 741a27eb276ff3aa13d721365dfc21d5e0d9d035 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 29 Oct 2026 23:10:15 +0200 Subject: [PATCH 4/7] fix(admin): accept /stats with bot mention --- VERSION | 2 +- bot.py | 5 ++++- tests/test_admin_stats.py | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 tests/test_admin_stats.py diff --git a/VERSION b/VERSION index 4070802..7b96df7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0+77 +0.1.0+78 diff --git a/bot.py b/bot.py index 72dbf44..1417be9 100644 --- a/bot.py +++ b/bot.py @@ -1621,7 +1621,10 @@ async def send_owner_stats(bot: Bot, period: str = "day") -> None: await bot.send_rich_message(chat_id=ANALYTICS_CHAT_ID, message_thread_id=ANALYTICS_TOPIC_ID, rich_message=InputRichMessage(html=report)) -@admin_router.message(F.text.regexp(r"^/stats(?:\s+(day|week|month|all))?$") ) +STATS_COMMAND_PATTERN = r"^/stats(?:@\w+)?(?:\s+(day|week|month|all))?$" + + +@admin_router.message(F.text.regexp(STATS_COMMAND_PATTERN)) async def stats_command(message: Message, state: FSMContext) -> None: if message.chat.id != ANALYTICS_CHAT_ID or message.message_thread_id != ANALYTICS_TOPIC_ID: return diff --git a/tests/test_admin_stats.py b/tests/test_admin_stats.py new file mode 100644 index 0000000..8304de9 --- /dev/null +++ b/tests/test_admin_stats.py @@ -0,0 +1,27 @@ +import re +import unittest + +from bot import STATS_COMMAND_PATTERN + + +class StatsCommandPatternTests(unittest.TestCase): + def test_matches_plain_and_mentioned_forms(self): + # В группе команду часто отправляют с явным обращением к боту. + for text in ("/stats", "/stats@pisatel_posts_bot", "/stats all", "/stats@pisatel_posts_bot week"): + with self.subTest(text=text): + self.assertIsNotNone(re.match(STATS_COMMAND_PATTERN, text)) + + def test_rejects_unknown_arguments(self): + for text in ("/stats yearly", "/stats@bot unknown", "stats", "/statsical"): + with self.subTest(text=text): + self.assertIsNone(re.match(STATS_COMMAND_PATTERN, text)) + + def test_period_argument_is_extractable(self): + match = re.match(STATS_COMMAND_PATTERN, "/stats@pisatel_posts_bot month") + self.assertIsNotNone(match) + assert match is not None + self.assertEqual(match.group(1), "month") + + +if __name__ == "__main__": + unittest.main() From 48a82bd0b3cf1bc2fa016fc98e1328782b1070b1 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 29 Oct 2026 23:22:44 +0200 Subject: [PATCH 5/7] docs(groups): pin support group id --- IDEA.MD | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/IDEA.MD b/IDEA.MD index e64daeb..85b8bde 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -72,7 +72,7 @@ Telegram-бот, который по запросу пользователя г - Raw update log (все окружения, включая production): отдельная SQLite `/app/data/bot_updates.sqlite3`, таблица `raw_updates`; поиск по Telegram `update_id` (`SELECT payload FROM raw_updates WHERE update_id = ?`). Middleware подключается всегда — в проде это единственный способ увидеть события из групп администрирования и поддержки; записи старше 30 дней удаляются при запуске и записи новых событий. Секретные поля/токены маскируются. JSON включает приватный текст и metadata апдейта. - Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории. - Админская группа «Писатель постов: Админка»: `chat_id = -1004459624166` (supergroup, forum). Топики: `4` — Статистика, `5` — Расходы, `6` — Рассылка, `7` — Ошибки. В production эти значения заданы env (`ADMIN_GROUP_ID`, `ANALYTICS_CHAT_ID`, `ANALYTICS_TOPIC_ID`, `TOPIC_STATS_ID`, `TOPIC_EXPENSES_ID`, `TOPIC_BROADCAST_ID`, `TOPIC_ERRORS_ID`); в коде оставлены дефолты для dev. Реализован только топик «Статистика» (`/stats`), остальные зарезервированы. -- Поддержка: `SUPPORT_GROUP_ID` — id группы операторов (`-1008000863273`), `SUPPORT_NOTE_PREFIX` (по умолчанию `//`), `SUPPORT_GREETINGS` (пул приветствий, разделитель `|`), `SUPPORT_OPERATOR_SIGNATURE` (по умолчанию `Оператор поддержки`), `SUPPORT_TOPIC_EMOJI_ID` (опционально). Без `SUPPORT_GROUP_ID` поддержка выключена. Подробнее — раздел «Поддержка». +- Поддержка: `SUPPORT_GROUP_ID` — id группы операторов (`-1003957663233`, «Писатель постов: Поддержка», forum; production), `SUPPORT_NOTE_PREFIX` (по умолчанию `//`), `SUPPORT_GREETINGS` (пул приветствий, разделитель `|`), `SUPPORT_OPERATOR_SIGNATURE` (по умолчанию `Оператор поддержки`), `SUPPORT_TOPIC_EMOJI_ID` (опционально). Без `SUPPORT_GROUP_ID` поддержка выключена. Подробнее — раздел «Поддержка». - `DEPLOY_ENV` включает dev-поведение только при точном значении `development`; production, пустое и неизвестное значение отключают его. `BOT_ENV` отдельно выбирает Telegram Bot API. `DEV_CHAT_ID` используется для уведомления о версии; версия в файле `VERSION` (сейчас `0.1.0+67`). Ротационный файл лога — `BOT_LOG_PATH` (по умолчанию `/app/data/bot.log`) на том же volume; сырые апдейты — `RAW_UPDATES_DB_PATH` (по умолчанию `/app/data/bot_updates.sqlite3`). - Вики Gitea: страницы `Home`, `User-Guide`, `Developer-Guide`, `Subscription`, `Deployment`. diff --git a/README.md b/README.md index 895a0fe..af33436 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ SUBSCRIPTION_PRICE_STARS = 100 - **VERSION:** файл `VERSION` (`0.1.0+64`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление о версии в `DEV_CHAT_ID`. - **Переменные Coolify:** `BOT_TOKEN`, `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_INJECTION_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `APINEX_RESPONSE_REPAIR_MODEL` (optional; default uses the standard model), `BOT_DB_PATH`, `ANALYTICS_DB_PATH` (optional; default `/app/data/analytics.sqlite3`), `ANALYTICS_CHAT_ID` (default `-1008001427913`), `ANALYTICS_TOPIC_ID` (default `5`), `OWNER_USER_ID` (optional; when set, `/stats` is restricted to this Telegram user), `RAW_UPDATES_DB_PATH` (optional; default `/app/data/bot_updates.sqlite3`), `DEPLOY_ENV`, `DEV_CHAT_ID`, `BOT_LOG_PATH` (optional; default `/app/data/bot.log`), `ACTIVITY_MIN_SECONDS` / `ACTIVITY_MAX_SECONDS` (optional; default 3 / 8 — границы паузы между сменами фраз в драфте), `ACTIVITY_MAX_ROTATIONS` (optional; default 12 — предохранитель от бесконечной ротации), `ADMIN_GROUP_ID` (id админской супергруппы с темами; production: `-1004459624166` — «Писатель постов: Админка»), -`SUPPORT_GROUP_ID` (id супергруппы с темами для обращений; без неё поддержка выключена), +`SUPPORT_GROUP_ID` (id супергруппы с темами для обращений; production: `-1003957663233` — «Писатель постов: Поддержка»; без неё поддержка выключена), `SUPPORT_NOTE_PREFIX` (optional; default `//` — префикс заметки, которая не уходит пользователю), `SUPPORT_GREETINGS` (optional; пул приветствий оператора, разделитель `|`), `SUPPORT_TOPIC_EMOJI_ID` (optional; custom emoji id на топике обращения). From 2b783517075998891769aca172e8ff9ecdf06c93 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 29 Oct 2026 23:57:49 +0200 Subject: [PATCH 6/7] docs(support): require Manage Topics right for the support group --- IDEA.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IDEA.MD b/IDEA.MD index 85b8bde..6ee355c 100644 --- a/IDEA.MD +++ b/IDEA.MD @@ -72,7 +72,7 @@ Telegram-бот, который по запросу пользователя г - Raw update log (все окружения, включая production): отдельная SQLite `/app/data/bot_updates.sqlite3`, таблица `raw_updates`; поиск по Telegram `update_id` (`SELECT payload FROM raw_updates WHERE update_id = ?`). Middleware подключается всегда — в проде это единственный способ увидеть события из групп администрирования и поддержки; записи старше 30 дней удаляются при запуске и записи новых событий. Секретные поля/токены маскируются. JSON включает приватный текст и metadata апдейта. - Секреты хранятся в переменных окружения приложения Coolify; не сохраняются в репозитории. - Админская группа «Писатель постов: Админка»: `chat_id = -1004459624166` (supergroup, forum). Топики: `4` — Статистика, `5` — Расходы, `6` — Рассылка, `7` — Ошибки. В production эти значения заданы env (`ADMIN_GROUP_ID`, `ANALYTICS_CHAT_ID`, `ANALYTICS_TOPIC_ID`, `TOPIC_STATS_ID`, `TOPIC_EXPENSES_ID`, `TOPIC_BROADCAST_ID`, `TOPIC_ERRORS_ID`); в коде оставлены дефолты для dev. Реализован только топик «Статистика» (`/stats`), остальные зарезервированы. -- Поддержка: `SUPPORT_GROUP_ID` — id группы операторов (`-1003957663233`, «Писатель постов: Поддержка», forum; production), `SUPPORT_NOTE_PREFIX` (по умолчанию `//`), `SUPPORT_GREETINGS` (пул приветствий, разделитель `|`), `SUPPORT_OPERATOR_SIGNATURE` (по умолчанию `Оператор поддержки`), `SUPPORT_TOPIC_EMOJI_ID` (опционально). Без `SUPPORT_GROUP_ID` поддержка выключена. Подробнее — раздел «Поддержка». +- Поддержка: `SUPPORT_GROUP_ID` — id группы операторов (`-1003957663233`, «Писатель постов: Поддержка», forum; production), `SUPPORT_NOTE_PREFIX` (по умолчанию `//`), `SUPPORT_GREETINGS` (пул приветствий, разделитель `|`), `SUPPORT_OPERATOR_SIGNATURE` (по умолчанию `Оператор поддержки`), `SUPPORT_TOPIC_EMOJI_ID` (опционально). Без `SUPPORT_GROUP_ID` поддержка выключена. **Требование к правам:** бот в этой группе должен быть админом с включённым «Manage Topics» (`can_manage_topics=true`) — иначе создание топика падает с `not enough rights to create a topic`, а пользователь видит «Поддержка временно недоступна». Проверка: `get_chat_member(SUPPORT_GROUP_ID, bot_id)`. - `DEPLOY_ENV` включает dev-поведение только при точном значении `development`; production, пустое и неизвестное значение отключают его. `BOT_ENV` отдельно выбирает Telegram Bot API. `DEV_CHAT_ID` используется для уведомления о версии; версия в файле `VERSION` (сейчас `0.1.0+67`). Ротационный файл лога — `BOT_LOG_PATH` (по умолчанию `/app/data/bot.log`) на том же volume; сырые апдейты — `RAW_UPDATES_DB_PATH` (по умолчанию `/app/data/bot_updates.sqlite3`). - Вики Gitea: страницы `Home`, `User-Guide`, `Developer-Guide`, `Subscription`, `Deployment`. From 376f26b2ebe27a53003f8d4a87fcb58a13a790d7 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 30 Oct 2026 00:12:01 +0200 Subject: [PATCH 7/7] fix(llm): default model slug free/gemini-3.8-flash (billing) --- apinex_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apinex_client.py b/apinex_client.py index a74353b..666ebce 100644 --- a/apinex_client.py +++ b/apinex_client.py @@ -96,7 +96,7 @@ TONE_PROFILE_SYSTEM_PROMPT = """Составь или обнови единый """ -def _setting(name: str, default: str = "gemini-3.8-flash") -> str: +def _setting(name: str, default: str = "free/gemini-3.8-flash") -> str: return os.getenv(name, "").strip() or default