# Social Post Writer Bot — development Telegram-бот для генерации постов для соцсетей. Готовит текст по явной просьбе пользователя, адаптируя под его Tone of Voice, и работает по модели подписки (триал 3 дня, затем 100 звёзд в месяц). --- ## Содержание - [Как работает бот (UX)](#как-работает-бот-ux) - [Архитектура кода](#архитектура-кода) - [Подписка и монетизация](#подписка-и-монетизация) - [Данные и хранилище](#данные-и-хранилище) - [Команды](#команды) - [Деплой и конфигурация](#деплой-и-конфигурация) - [Тесты](#тесты) --- ## Как работает бот (UX) ### Reply-клавиатура Основное меню показывается в `/start`: ``` ✍️ Написать пост 💡 Идея 🎨 Мой стиль 📖 Помощь 💳 Подписка 👤 Баланс ``` Если подписки нет, `_show_subscription_info` ставит `LOCKED_MENU` — с кнопкой «💳 Оплатить подписку» на первом месте. Нажатие кнопки отправляет текст, который `resolve_button_text` превращает либо в slash-команду (`/help`, `/balance`, `/subscribe`, `/style`), либо в готовый запрос («Напиши пост», «предложи идею для нового поста») — то есть кнопки не обходят роутинг, а идут через него. Slash-команды публикуются в Telegram через `set_my_commands` при старте (`set_main_menu`). ### Онбординг (`/start`) Первое сообщение пользователя запускает онбординг — **два сообщения с паузой 0.8 сек** между ними: 1. **Welcome (Rich Message)** — `WELCOME_RICH_HTML`: приветствие, примеры запросов, упоминание скриншотов для Tone of Voice. 2. **Вызов `help_command`** — пауза 0.8 сек, затем меню справки с инлайн-кнопками (тот же хендлер, что и по команде `/help`). Одновременно `/start` создаёт запись триала на 3 дня. **Логика:** welcome содержит только golden path, всё остальное (тон, голоса, правки, `/clear`, `/idea`) — в `/help`. Онбординг не дублирует текст справки, а **вызывает `help_command`**, поэтому правки `HELP_RICH_HTML` автоматически меняют и онбординг. ### Основные сценарии | Что хочет пользователь | Что делает | Что видит | |---|---|---| | Написать пост | Пишет «напиши пост про запуск продукта» | Индикатор «печатает», затем **3 отдельных сообщения** — по одному варианту в каждом | | Изменить пост | Отвечает реплаем на вариант бота: «сделай короче» | Снова 3 новых варианта, с учётом исходного текста | | Придумать идею | «предложи тему» или `/idea` | Одна идея одним сообщением (заголовок, угол, зачем) | | Настроить стиль | Присылает скриншот(ы) своих постов | Бот распознаёт тексты, накапливает их и обновляет ToV; показывает итоговый профиль стиля | | Голосом | Отправляет голосовое | STT расшифровывает → как обычный текстовый запрос | | Узнать статус | `/balance` | Триал активен (сколько дней) / подписка активна / период истёк | | Оплатить | `/subscribe` | Просит 100 звёзд, подтверждает активацию на 30 дней | | Начать заново | `/clear` | Полный сброс данных + обнуление подписки | ### Индикаторы и обратная связь - Пока **идёт обработка** (классификация, расшифровка, чтение скриншота, генерация) — бот показывает «печатает» (`typing_status`), обновляя каждые 4 секунды. - При **ошибке** пользователю уходит понятное сообщение: «❌ Не получилось: {этап}. Причина: …» — секреты (ключи, токены) и URL вырезаются. - Служебные тексты — **всегда на русском**. Язык самого поста определяется языком запроса. ### Блокировка по подписке Если подписка не активна (триал/подписка истекли или `is_active = 0`): - Генерация поста и идеи **блокируется**, бот пишет, что период истёк, и предлагает `/subscribe`. - `/help`, `/start`, `/balance`, `/subscribe` **доступны всегда** — пользователь не остаётся в тупике. --- ## Архитектура кода ``` bot.py — роутинг, команды, обработка сообщений (aiogram 3.x) payments.py — оплата Stars: sendInvoice, pre_checkout_query, successful_payment, защита от двойного списания apinex_client.py — клиент APInex (LLM, STT, vision), промпты, логирование вызовов user_store.py — SQLite: пользователи, ToV, посты, идеи, API-логи, подписки keyboards.py — reply-клавиатура: `MAIN_MENU` (основная) и `LOCKED_MENU` (без подписки), маппинг кнопок на команды help_message.py — Rich Message: `WELCOME_RICH_HTML` (онбординг) + `HELP_SECTIONS`/`help_view()` (справка с кнопками), plain fallback VERSION — версия релиза (сейчас 0.1.0+26) ``` ### Основные потоки **Текстовый запрос:** ``` handle_text → _classify_and_respond ├─ /help? → help_command (без проверки подписки) ├─ _check_subscription (блокировка, если нет подписки) ├─ classify_intent (APInex LLM) → intent │ ├─ help → help_command │ ├─ generate_ideas → generate_ideas → _send_idea │ └─ write_post / revise_post → generate_posts → _send_three_posts └─ typing_status (пока всё идёт) ``` **Голосовое:** ``` transcribe_voice → _process_voice → transcribe_audio (APInex STT) → _classify_and_respond ``` **Скриншоты (ToV):** ``` update_tone_from_image → _handle_image_message ├─ _collect_media_group (собирает галерею, ждёт 1 сек) ├─ extract_posts_from_images (APInex vision, inline base64) ├─ add_post_samples (накапливает по пользователю) └─ create_tone_profile → save_tone_of_voice → показывает стиль ``` ### Ключевые функции (`bot.py`) | Функция | Что делает | |---|---| | `start(message)` | Онбординг (welcome Rich Message + вызов `help_command`) + создание триала | | `handle_command_text` | Роутинг команд: `/help`, `/start`, `/clear`, `/idea`, `/balance`, `/subscribe` | | `_classify_and_respond` | Ядро: проверка подписки → классификация → генерация | | `_check_subscription` | Проверяет активную подписку; если нет — показывает статус и блокирует | | `_show_subscription_info` | Формирует сообщение о статусе (триал / подписка / истёк) | | `balance_command` | `/balance` — статус подписки | | `subscribe_command` | `/subscribe` — отправляет Stars-инвойс на 30 дней (подписка активируется только после оплаты) | | `handle_pre_checkout` | Подтверждает только свой инвойс в XTR на 100 звёзд, всё остальное отклоняет | | `handle_successful_payment` | Активирует подписку после реальной оплаты; `telegram_payment_charge_id` пишется в `payments` для защиты от повторного начисления | | `clear_command` | `/clear` — сброс данных + обнуление подписки | | `help_command` | `/help` — меню справки (Rich Message с кнопками) + fallback на plain text | | `help_navigation` | Callback `help:
` → `edit_message_text(rich_message=...)` — переключение разделов | | `edit_rich_message` | Редактирование текущего rich-сообщения на новый раздел | | `typing_status` | Индикатор «печатает» во время обработки | | `report_error` | Понятная ошибка пользователю, секреты вырезаны | ### Ключевые функции (`user_store.py`) | Функция | Что делает | |---|---| | `init_db` | Создаёт таблицы: `users`, `user_post_samples`, `generated_posts`, `generated_ideas`, `api_call_logs`, `subscriptions` | | `ensure_subscription` | Создаёт запись подписки с триалом, если нет (`INSERT OR IGNORE`) | | `activate_subscription` | Активирует/продлевает подписку на 30 дней от момента оплаты | | `deactivate_subscription` | `is_active = 0` | | `is_subscribed` | Проверяет: запись есть, `is_active = 1`, срок не истёк | | `subscription_row` | Возвращает строку подписки | | `_days_until` | Сколько дней осталось (триал — `trial_started_at + 3 дня`; подписка — `last_paid_at`); `-1` если истёкло/отключено | --- ## Подписка и монетизация **Модель:** триал 3 дня → подписка 30 дней за **100 звёзд** (Telegram Stars). **Константы** (`user_store.py`): ```python TRIAL_DAYS = 3 SUBSCRIPTION_MONTH_DAYS = 30 SUBSCRIPTION_PRICE_STARS = 100 ``` **Таблица `subscriptions`:** - `telegram_user_id` (PK) - `trial_started_at` — момент создания (SQLite `CURRENT_TIMESTAMP`, UTC-нейтральный) - `last_paid_at` — момент/конец оплаченного периода; `NULL` = ещё триал - `is_active` — 1/0 - `monthly_days` — длительность периода (30) - `updated_at` **Жизненный цикл:** 1. `/start` → `ensure_subscription` → триал 3 дня, `is_active = 1`. 2. Генерация доступна, пока `_days_until > 0`. 3. Триал кончился → `_check_subscription` блокирует, предлагает `/subscribe`. 4. `/subscribe` → `sendInvoice` (XTR, 100 звёзд) → `pre_checkout_query` (проверка суммы/валюты/payload) → `successful_payment` → `activate_subscription(months=1)` → +30 дней. 5. `/clear` → `deactivate_subscription` → `is_active = 0`; нужен `/subscribe`. **Оплата реальная:** `payments.py` проводит настоящий платёж Telegram Stars. Подписка активируется **только** в `successful_payment` — сам `/subscribe` её не продлевает. `telegram_payment_charge_id` каждого платежа пишется в таблицу `payments`, поэтому один платёж не может активировать подписку дважды; по этому же id делается возврат через `refundStarPayment`. Платится валюта `XTR`, `provider_token` пустой (цифровой товар). **Часовые пояса:** SQLite `CURRENT_TIMESTAMP` возвращает UTC без tz. Код аккуратно приводит все значения к naive UTC перед сравнением, чтобы не было сдвигов. --- ## Данные и хранилище - SQLite: `/app/data/social_writer.sqlite3` (persistent Coolify volume). - **Per-user данные:** ToV, образцы постов со скриншотов, история сгенерированных постов, история идей. История диалогов и исходные изображения **не сохраняются**. - **API-логи:** JSON запросов/ответов APInex + ошибки разбора, 30 дней. Системные промпты не дублируются — только имя и SHA-256. base64 изображений и бинарное аудио **не пишутся**. Содержимое логов — приватные данные пользователя. - `/clear` удаляет пользовательские данные (ToV, скриншоты, посты, идеи, настройки) и обнуляет подписку; **API-логи сохраняются**. --- ## Команды | Команда | Что делает | Доступна без подписки | |---|---|---| | `/start` | Онбординг (welcome Rich Message + справка) + создание триала | ✅ | | `/help` | Меню справки: полноширинные кнопки разделов, переход в любой раздел + plain fallback | ✅ | | `/balance` | Статус подписки / триала | ✅ | | `/subscribe` | Счёт на оплату: 100 звёзд (XTR) за 30 дней; активация — после оплаты | ✅ | | `/idea` | Одна идея для поста | ❌ (нужна подписка) | | `/clear` | Полный сброс данных + обнуление подписки | ✅ | Русские фразы («придумай идею», «о чём написать?») работают как `/idea`. --- ## Деплой и конфигурация - **Gitea:** `admin/social-post-writer-bot`, ветка `dev`. Push → автодеплой. - **Coolify:** проект `Social Post Writer Bot`, окружение `development`, приложение `social-post-writer-bot-development` (`d32u5zcq2g4odmdpq9hw91yn`) на `spb-easy-dale`. - **Деплой:** `.hermes/scripts/deploy.sh` (POST `/api/v1/deploy?uuid=...`). - **VERSION:** файл `VERSION` (`0.1.0+19`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление «🚀 Бот обновлён / Версия: X» в `DEV_CHAT_ID`. - **Переменные Coolify:** `BOT_TOKEN`, `APINEX_API_KEY`, `APINEX_STT_MODEL`, `APINEX_INTENT_CLASSIFIER_MODEL`, `APINEX_POST_WRITER_MODEL`, `APINEX_IMAGE_TONE_MODEL`, `BOT_DB_PATH`, `DEPLOY_ENV`, `DEV_CHAT_ID`. - **Docker:** `python:3.12-slim`, копирует `bot.py`, `apinex_client.py`, `user_store.py`, `help_message.py`, `VERSION`; `CMD ["python", "bot.py"]`. --- ## Тесты ```bash python -m unittest discover -s tests -v ``` Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, **reply-клавиатура** (маппинг кнопок, роутинг, `/style`), **платежи** (инвойс, pre_checkout, successful_payment, защита от двойного списания), **справка с кнопками** (меню, разделы, callback-навигация, plain fallback), **онбординг** (welcome → вызов `help_command`), **подписки** (6 тестов: таблица, триал после `/start`, оплата, баланс, блокировка, обнуление через `/clear`).