surfaces/docs/superpowers/specs/2026-03-31-forum-topics-design.md

180 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`