Social Post Writer Bot — development
Telegram-бот для генерации постов для соцсетей. Готовит текст по явной просьбе пользователя, адаптируя под его Tone of Voice, и работает по модели подписки (триал 3 дня, затем 100 звёзд в месяц).
Содержание
- Как работает бот (UX)
- Архитектура кода
- Подписка и монетизация
- Данные и хранилище
- Команды
- Деплой и конфигурация
- Тесты
Версия разработки: 0.1.0+40.
Синхронизация документации
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 и возвращает пользователя в главное меню:
- Отправляет ровно одно
WELCOME_RICH_HTMLс примерами и описанием режимов. - Показывает клавиатуру главного меню; справка открывается отдельно кнопкой или командой
/help.
Основные сценарии
| Что хочет пользователь | Что делает | Что видит |
|---|---|---|
| Написать пост | В режиме «Генерация» пишет «напиши пост про запуск продукта» | Индикатор «печатает», затем 3 отдельных сообщения |
| Изменить пост | В режиме «Генерация» отвечает реплаем на вариант бота | Три новых варианта с учётом исходного текста |
| Придумать идею | В режиме «Генерация» пишет «предложи тему» или /idea |
Одна идея одним сообщением |
| Настроить стиль | В режиме «Обучение» присылает скриншоты постов | Бот распознаёт тексты, накапливает их, обновляет профиль и показывает только статус завершения |
| Голосом | В режиме «Генерация» отправляет голосовое | 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 и кнопка «Сбросить данные» показывают inline-подтверждение; сброс только по confirm в режиме обучения |
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):
TRIAL_DAYS = 3
SUBSCRIPTION_MONTH_DAYS = 30
SUBSCRIPTION_PRICE_STARS = 100
Таблица subscriptions:
telegram_user_id(PK)trial_started_at— момент создания (SQLiteCURRENT_TIMESTAMP, UTC-нейтральный)last_paid_at— момент/конец оплаченного периода;NULL= ещё триалis_active— 1/0monthly_days— длительность периода (30)updated_at
Жизненный цикл:
/start→ensure_subscription→ триал 3 дня,is_active = 1.- Генерация доступна, пока
_days_until > 0. - Триал кончился →
_check_subscriptionблокирует, предлагает/subscribe. /subscribe→sendInvoice(XTR, 100 звёзд) →pre_checkout_query(проверка суммы/валюты/payload) →successful_payment→activate_subscription(months=1)→ +30 дней./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, текстовые обучающие заметки, образцы постов со скриншотов, история сгенерированных постов и идеи. В «Обучении» текст/голос/скриншоты накапливают и обновляют профиль, а не запускают генерацию. История диалогов и исходные изображения не сохраняются.
- Пустые ответы APInex: при пустом
contentбот делает до двух повторов; третий запрос добавляетreasoning_effort: low. Если ответ всё ещё пустой, операция завершается ошибкой и каждая попытка попадает в API-журнал. Ошибки HTTP автоматически не повторяются. APInex документируетlow, но не режим отключения reasoning; поддержка параметра зависит от модели. - Починка формата JSON: при синтаксически невалидном JSON выполняется не более одной отдельной попытки модели
APINEX_RESPONSE_REPAIR_MODEL; затем исходные проверки структуры/схемы запускаются заново. Ошибки API, пустые ответы и JSON с неверной схемой автоматически не исправляются. В Coolify development заданаAPINEX_RESPONSE_REPAIR_MODEL. - API-логи: JSON запросов/ответов APInex, тело ответа провайдера при ошибке и ошибки разбора хранятся в SQLite 30 дней. Записи связаны
operation_idи содержат этап, модель, HTTP-статус и длительность. Системные промпты не дублируются — только имя и SHA-256; секреты и base64 изображений маскируются, бинарное аудио не пишется. Пользовательские тексты в запросах/ответах — приватные данные. - Логи контейнера: ошибки операций содержат
operation_id, Telegram user id, этап и полный traceback; ошибки APInex дополнительно показывают модель, статус, длительность и диагностическую структуру ответа без вывода текста пользовательского запроса. Ищи все связанные записи по одномуoperation_id. /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,APINEX_RESPONSE_REPAIR_MODEL(optional; default uses the standard 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"].
Тесты
python -m unittest discover -s tests -v
Покрытие: STT, LLM, vision, ToV, reply-поля, изоляция пользователей, API-логи, режимные клавиатуры и роутинг (переключатели, возврат в главное меню), платежи (Stars, защита от повторов, dev-подтверждение), ошибки, справка, onboarding, подписки (статус при входе, продление, оплата, баланс, блокировка, сохранение подписки при /clear).