230 lines
18 KiB
Markdown
230 lines
18 KiB
Markdown
# Social Post Writer Bot — development
|
||
|
||
Telegram-бот для генерации постов для соцсетей. Готовит текст по явной просьбе пользователя, адаптируя под его Tone of Voice, и работает по модели подписки (триал 3 дня, затем 100 звёзд в месяц).
|
||
|
||
---
|
||
|
||
## Содержание
|
||
- [Как работает бот (UX)](#как-работает-бот-ux)
|
||
- [Архитектура кода](#архитектура-кода)
|
||
- [Подписка и монетизация](#подписка-и-монетизация)
|
||
- [Данные и хранилище](#данные-и-хранилище)
|
||
- [Команды](#команды)
|
||
- [Деплой и конфигурация](#деплой-и-конфигурация)
|
||
- [Тесты](#тесты)
|
||
|
||
---
|
||
|
||
## Синхронизация документации
|
||
|
||
`README.md` и `IDEA.MD` — синхронизируемые документы проекта. Если меняются сведения о функциях,
|
||
текущем поведении, статусе, версии, решениях или процессе разработки в одном файле, внеси те же
|
||
смысловые изменения во второй. Перед завершением задачи проверь оба файла и убедись, что общие
|
||
сведения совпадают; различаться могут только детали, специфичные для самого документа.
|
||
|
||
## Как работает бот (UX)
|
||
|
||
### Dev-подтверждение подписки
|
||
|
||
Только при точном `DEPLOY_ENV=development` после обычного сообщения о счёте отдельным сообщением
|
||
показывается кнопка «🧪 Оплатил (dev)». Она включает доступ на 30 дней без реального платежа и
|
||
доступна только пользователю, которому выставлен счёт. В production, при пустом или неизвестном
|
||
значении `DEPLOY_ENV` dev-кнопка и callback не выдают доступ.
|
||
|
||
### Reply-клавиатура
|
||
|
||
После `/start` отображается одно welcome-сообщение с нижним меню; справка автоматически не вызывается.
|
||
В главном меню доступны только пользовательские режимы:
|
||
|
||
```
|
||
🎓 Обучение ✍️ Генерация
|
||
💳 Подписка 📖 Помощь
|
||
```
|
||
|
||
«Главное меню» — внутренняя точка входа после `/start` и выхода из режима, а не отдельный режим.
|
||
Внутри любого режима reply-клавиатура содержит только «🏠 Главное меню». При входе показывается короткий
|
||
onboarding. В «Подписке» сразу отображаются тип активного периода и оставшиеся дни; inline-кнопка
|
||
«Продлить подписку» сразу открывает Stars-инвойс на оплату.
|
||
|
||
Команды перехода: `/start`, `/training`, `/generate`, `/subscription`, `/help`; `/start` возвращает
|
||
в главное меню. Команды `/style` и `/clear` доступны только в обучении; `/post` и `/idea` — в генерации;
|
||
`/balance` и `/subscribe` — в подписке. Все команды публикуются через `set_my_commands` при старте.
|
||
|
||
### Онбординг (`/start`)
|
||
`/start` создаёт запись триала, очищает старое состояние FSM и возвращает пользователя в главное меню:
|
||
|
||
1. Отправляет ровно одно `WELCOME_RICH_HTML` с примерами и описанием режимов.
|
||
2. Показывает клавиатуру главного меню; справка открывается отдельно кнопкой или командой `/help`.
|
||
|
||
### Основные сценарии
|
||
|
||
| Что хочет пользователь | Что делает | Что видит |
|
||
|---|---|---|
|
||
| Написать пост | В режиме «Генерация» пишет «напиши пост про запуск продукта» | Индикатор «печатает», затем **3 отдельных сообщения** |
|
||
| Изменить пост | В режиме «Генерация» отвечает реплаем на вариант бота | Три новых варианта с учётом исходного текста |
|
||
| Придумать идею | В режиме «Генерация» пишет «предложи тему» или `/idea` | Одна идея одним сообщением |
|
||
| Настроить стиль | В режиме «Обучение» присылает скриншоты постов | Бот распознаёт тексты, накапливает их и обновляет ToV |
|
||
| Голосом | В режиме «Генерация» отправляет голосовое | STT расшифровывает → обычный текстовый запрос |
|
||
| Узнать статус / продлить | Входит в режим «Подписка» и нажимает «Продлить подписку» | Текущий тип периода, остаток дней и Stars-инвойс на 100 звёзд |
|
||
| Очистить данные | В режиме «Обучение» вызывает `/clear` | Очищает стиль и историю, не меняя подписку или триал |
|
||
|
||
### Индикаторы и обратная связь
|
||
- Пока **идёт обработка** (классификация, расшифровка, чтение скриншота, генерация) — бот показывает «печатает» (`typing_status`), обновляя каждые 4 секунды.
|
||
- При **ошибке** пользователь получает общий безопасный ответ. Только в development отдельным сообщением дополнительно отправляется диагностика; production не раскрывает текст исключения.
|
||
- Служебные тексты — **всегда на русском**. Язык самого поста определяется языком запроса.
|
||
|
||
### Блокировка по подписке
|
||
Если подписка не активна (триал/подписка истекли или `is_active = 0`), генерация блокируется. Переключатели режимов доступны всегда; для оплаты пользователь переходит в режим «Подписка».
|
||
|
||
---
|
||
|
||
## Архитектура кода
|
||
|
||
```
|
||
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 — клавиатуры главного меню и режимов; маппинг кнопок на команды
|
||
help_message.py — Rich Message: `WELCOME_RICH_HTML` (онбординг) + `HELP_SECTIONS`/`help_view()` (справка с кнопками), plain fallback
|
||
VERSION — версия релиза
|
||
```
|
||
|
||
### Основные потоки
|
||
|
||
**Текстовый запрос:**
|
||
```
|
||
handle_text (только в режиме генерации) → _classify_and_respond
|
||
├─ _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 (пока всё идёт)
|
||
|
||
handle_command_text → переключатели режимов доступны всегда; действия проходят режимную проверку
|
||
```
|
||
|
||
**Голосовое:**
|
||
```
|
||
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, state)` | Одно welcome-сообщение, главное меню и создание триала |
|
||
| `handle_command_text` | Роутинг глобальных переключателей и профильных команд по режиму |
|
||
| `_classify_and_respond` | Ядро: проверка подписки → классификация → генерация |
|
||
| `_check_subscription` | Проверяет активную подписку; если нет — показывает статус и блокирует |
|
||
| `_show_subscription_info` | Показывает тип периода, остаток дней и inline-кнопку продления в режиме подписки |
|
||
| `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:<section>` → `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` очищает данные обучения и историю; подписка и триал не изменяются.
|
||
|
||
**Оплата реальная:** `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` | Возврат в главное меню | Любой |
|
||
| `/training`, `/generate`, `/subscription`, `/help` | Переключение режима | Любой |
|
||
| `/style`, `/clear` | Профиль стиля; сброс данных обучения | Обучение |
|
||
| `/post`, `/idea` | Создать пост или идею | Генерация; для генерации нужна подписка |
|
||
| `/balance`, `/subscribe` | Статус или оплата Stars | Подписка |
|
||
|
||
В режиме «Генерация» русские фразы («придумай идею», «о чём написать?») работают как `/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=...`).
|
||
- **Окружение:** `DEPLOY_ENV` включает dev-функции только при точном значении `development`; production, пустое и неизвестное значение отключают их. `BOT_ENV` отдельно выбирает Telegram Bot API и не используется для включения dev-поведения.
|
||
- **VERSION:** файл `VERSION` (`0.1.0+30`). При `DEPLOY_ENV=development` и изменении версии бот шлёт уведомление о версии в `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-логи, **режимные клавиатуры и роутинг** (переключатели, возврат в главное меню), **платежи** (Stars, защита от повторов, dev-подтверждение), **ошибки**, **справка**, **onboarding**, **подписки** (статус при входе, продление, оплата, баланс, блокировка, сохранение подписки при `/clear`).
|