docs: Forum Topics mode design spec
This commit is contained in:
parent
41660fe84a
commit
a8885aeaa1
1 changed files with 180 additions and 0 deletions
180
docs/superpowers/specs/2026-03-31-forum-topics-design.md
Normal file
180
docs/superpowers/specs/2026-03-31-forum-topics-design.md
Normal file
|
|
@ -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`
|
||||||
Loading…
Add table
Add a link
Reference in a new issue