Compare commits

...
5 Commits
6 changed files with 57 additions and 23 deletions

No files matched your search

+4 -3
View File
@@ -68,10 +68,11 @@ Telegram-бот, который по запросу пользователя г
- SQLite: `/app/data/social_writer.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 <container>` выполняй на сервере приложения, имя контейнера начинается с `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`).
- Админская группа «Писатель постов: Админка»: `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` поддержка выключена. **Требование к правам:** бот в этой группе должен быть админом с включённым «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+78`). Ротационный файл лога — `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`.
## Статус
+3 -2
View File
@@ -354,8 +354,9 @@ 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 — предохранитель от бесконечной ротации),
`SUPPORT_GROUP_ID` (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 супергруппы с темами для обращений; production: `-1003957663233` — «Писатель постов: Поддержка»; без неё поддержка выключена),
`SUPPORT_NOTE_PREFIX` (optional; default `//` — префикс заметки, которая не уходит пользователю),
`SUPPORT_GREETINGS` (optional; пул приветствий оператора, разделитель `|`),
`SUPPORT_TOPIC_EMOJI_ID` (optional; custom emoji id на топике обращения).
+1 -1
View File
@@ -1 +1 @@
0.1.0+76
0.1.0+78
+12 -5
View File
@@ -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
@@ -1618,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
@@ -1660,8 +1666,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")
+27
View File
@@ -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()
+10 -12
View File
@@ -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()