From a8885aeaa15acf45e02501a33e21d0455b11d2c2 Mon Sep 17 00:00:00 2001 From: Mikhail Putilovskij Date: Tue, 31 Mar 2026 22:54:44 +0300 Subject: [PATCH] docs: Forum Topics mode design spec --- .../specs/2026-03-31-forum-topics-design.md | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-31-forum-topics-design.md diff --git a/docs/superpowers/specs/2026-03-31-forum-topics-design.md b/docs/superpowers/specs/2026-03-31-forum-topics-design.md new file mode 100644 index 0000000..1e7cb29 --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-forum-topics-design.md @@ -0,0 +1,180 @@ +# Forum Topics Mode Design + +**Date:** 2026-03-31 +**Status:** Approved — ready for implementation +**Scope:** `adapter/telegram/` — расширение существующего адаптера + +--- + +## Контекст + +Forum Topics — опциональный advanced-режим поверх существующих виртуальных DM-чатов. +Пользователь подключает свою Telegram-супергруппу с Topics — и его чаты появляются +как нативные темы Telegram. DM и Forum работают **одновременно**: один контекст, +две поверхности. + +--- + +## Принцип работы + +Каждый чат (`chat_id` = UUID) получает опциональный `forum_thread_id`. + +- Пользователь пишет в DM → бот отвечает в DM с тегом `[Чат #N]` +- Пользователь пишет в Forum-тему → бот отвечает в ту же тему (без тега) +- Контекст (`chat_id`) один и тот же — платформа видит единый разговор + +--- + +## БД — изменения схемы + +```sql +ALTER TABLE tg_users ADD COLUMN forum_group_id INTEGER; +ALTER TABLE chats ADD COLUMN forum_thread_id INTEGER; +``` + +`forum_group_id` — ID супергруппы пользователя (NULL если группа не подключена). +`forum_thread_id` — ID темы в форуме (NULL если чат создан только в DM). + +Новые функции в `db.py`: +```python +def set_forum_group(tg_user_id: int, group_id: int) -> None +def get_forum_group(tg_user_id: int) -> int | None +def set_forum_thread(chat_id: str, thread_id: int) -> None +def get_chat_by_thread(tg_user_id: int, thread_id: int) -> dict | None +``` + +--- + +## Онбординг — `/forum` + +### FSM + +```python +class ForumSetupState(StatesGroup): + waiting_for_group = State() # ждём пересылку из группы +``` + +### Флоу + +``` +/forum +→ FSM: ForumSetupState.waiting_for_group +→ "Создай супергруппу, включи Topics, добавь меня администратором + с правом управления темами. Затем перешли мне любое сообщение из группы." + +[пользователь пересылает сообщение] +→ Проверить: forward_from_chat.type == "supergroup" +→ Проверить права бота (администратор + can_manage_topics) + ❌ нет прав → объяснить что именно не так, остаться в состоянии +→ Сохранить forum_group_id в БД +→ Создать Forum-тему для каждого существующего активного DM-чата +→ Записать forum_thread_id для каждого чата +→ Ответить в DM: "✅ Группа подключена! Твои чаты теперь доступны в Forum-темах." +→ FSM: clear +``` + +### Проверка прав + +```python +async def check_forum_admin(bot: Bot, group_id: int) -> bool: + member = await bot.get_chat_member(group_id, (await bot.get_me()).id) + return ( + member.status in ("administrator", "creator") + and getattr(member, "can_manage_topics", False) + ) +``` + +--- + +## Создание чатов — синхронизация + +### `/new` в DM (группа подключена) + +1. Создать UUID-запись в `chats` (как сейчас) +2. `create_forum_topic(bot, group_id, chat_name)` → получить `thread_id` +3. Записать `forum_thread_id` в БД +4. Переключить FSM на новый чат +5. Ответить в DM: `"✅ [chat_name] создан."` + +### `/new` в DM (группа НЕ подключена) + +Без изменений — только DM-чат. + +### `/new` в Forum-теме + +1. Определить `thread_id` из `message.message_thread_id` +2. Создать UUID-запись в `chats` с `forum_thread_id = thread_id` +3. Название: из аргумента `/new Название` или из названия темы (`message.chat.forum_topic_created.name` при создании — иначе запросить у Telegram) +4. Ответить в теме: `"✅ Чат зарегистрирован. Пиши здесь!"` + +--- + +## Маршрутизация сообщений + +### Определение источника + +```python +def is_forum_message(message: Message) -> bool: + return message.message_thread_id is not None + +def resolve_chat_id(message: Message, tg_user_id: int) -> str | None: + if is_forum_message(message): + chat = db.get_chat_by_thread(tg_user_id, message.message_thread_id) + return chat["chat_id"] if chat else None + else: + # DM — берём active_chat_id из FSM StateData (как сейчас) + return None # caller reads from FSM +``` + +### Ответ + +- Пришло из DM → `bot.send_message(tg_user_id, f"[{chat_name}] {text}")` +- Пришло из Forum-темы → `bot.send_message(group_id, text, message_thread_id=thread_id)` + +В Forum-теме тег `[Чат #N]` **не нужен** — тема сама является визуальным разделителем. + +--- + +## Обработчики — изменения + +### `handlers/forum.py` (новый файл) + +```python +router = Router(name="forum") + +@router.message(Command("forum")) +async def cmd_forum(message, state): ... # запускает онбординг + +@router.message(ForumSetupState.waiting_for_group, F.forward_from_chat) +async def handle_group_forward(message, state, dispatcher): ... # регистрирует группу +``` + +### `handlers/chat.py` — изменения + +- `handle_message`: если `is_forum_message` → брать `chat_id` из БД по `thread_id`, отвечать в тему +- `cmd_new_chat`: ветвление по источнику (DM vs Forum) и наличию `forum_group_id` + +### `states.py` — добавить + +```python +class ForumSetupState(StatesGroup): + waiting_for_group = State() +``` + +--- + +## Что НЕ реализуем + +- Отслеживание создания тем пользователем без `/new` — Telegram не присылает событие создания темы в боте +- Синхронизация удаления темы ↔ архивация DM-чата (только через команды) +- Поддержка нескольких групп на одного пользователя + +--- + +## Порядок реализации + +1. `db.py` — миграция + 4 новых функции +2. `states.py` — `ForumSetupState` +3. `handlers/forum.py` — `/forum` + onboarding +4. `handlers/chat.py` — `cmd_new_chat` с ветвлением, `handle_message` с Forum-маршрутизацией +5. `converter.py` — `is_forum_message`, `resolve_chat_id`