docs: Forum Topics implementation plan
This commit is contained in:
parent
a8885aeaa1
commit
bcdaea5143
1 changed files with 704 additions and 0 deletions
704
docs/superpowers/plans/2026-03-31-forum-topics.md
Normal file
704
docs/superpowers/plans/2026-03-31-forum-topics.md
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
# Forum Topics Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Добавить опциональный Forum Topics режим — пользователь подключает Telegram-супергруппу, его DM-чаты синхронизируются с нативными темами форума.
|
||||
|
||||
**Architecture:** Каждый `chat` в БД получает опциональный `forum_thread_id`. Адаптер маршрутизирует: пришло из DM → отвечает в DM с тегом, пришло из Forum-темы → отвечает в ту же тему без тега. Core не меняется — `chat_id` (UUID) одинаковый для обеих поверхностей.
|
||||
|
||||
**Tech Stack:** aiogram 3.x, SQLite (sqlite3), Python 3.11+
|
||||
|
||||
**Working directory:** `/Users/a/MAI/sem2/lambda/surfaces-bot/.worktrees/telegram`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| Файл | Действие | Что меняется |
|
||||
|------|----------|--------------|
|
||||
| `adapter/telegram/db.py` | Modify | Миграция схемы + 4 новых функции |
|
||||
| `adapter/telegram/states.py` | Modify | Добавить `ForumSetupState` |
|
||||
| `adapter/telegram/converter.py` | Modify | Добавить `is_forum_message`, `resolve_chat_id` |
|
||||
| `adapter/telegram/handlers/forum.py` | Create | `/forum` команда + онбординг |
|
||||
| `adapter/telegram/handlers/chat.py` | Modify | `cmd_new_chat` + `handle_message` с Forum-маршрутизацией |
|
||||
| `adapter/telegram/bot.py` | Modify | Зарегистрировать `forum.router` |
|
||||
| `tests/adapter/test_forum_db.py` | Create | Тесты новых функций БД |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: DB migration + новые функции
|
||||
|
||||
**Files:**
|
||||
- Modify: `adapter/telegram/db.py`
|
||||
- Create: `tests/adapter/__init__.py`
|
||||
- Create: `tests/adapter/test_forum_db.py`
|
||||
|
||||
- [ ] **Step 1: Создать тест-файл и написать падающие тесты**
|
||||
|
||||
```python
|
||||
# tests/adapter/__init__.py
|
||||
# (пустой файл)
|
||||
```
|
||||
|
||||
```python
|
||||
# tests/adapter/test_forum_db.py
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
|
||||
os.environ["DB_PATH"] = ":memory:"
|
||||
|
||||
from adapter.telegram.db import (
|
||||
init_db,
|
||||
get_or_create_tg_user,
|
||||
create_chat,
|
||||
set_forum_group,
|
||||
get_forum_group,
|
||||
set_forum_thread,
|
||||
get_chat_by_thread,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fresh_db(tmp_path, monkeypatch):
|
||||
db_file = str(tmp_path / "test.db")
|
||||
monkeypatch.setenv("DB_PATH", db_file)
|
||||
# reload module so DB_PATH is picked up
|
||||
import importlib
|
||||
import adapter.telegram.db as db_mod
|
||||
importlib.reload(db_mod)
|
||||
db_mod.init_db()
|
||||
return db_mod
|
||||
|
||||
|
||||
def test_set_and_get_forum_group(fresh_db):
|
||||
db = fresh_db
|
||||
db.get_or_create_tg_user(111, "usr-111", "Alice")
|
||||
assert db.get_forum_group(111) is None
|
||||
db.set_forum_group(111, 999888)
|
||||
assert db.get_forum_group(111) == 999888
|
||||
|
||||
|
||||
def test_set_forum_thread_and_get_by_thread(fresh_db):
|
||||
db = fresh_db
|
||||
db.get_or_create_tg_user(222, "usr-222", "Bob")
|
||||
chat_id = db.create_chat(222, "Чат #1")
|
||||
assert db.get_chat_by_thread(222, 42) is None
|
||||
db.set_forum_thread(chat_id, 42)
|
||||
chat = db.get_chat_by_thread(222, 42)
|
||||
assert chat is not None
|
||||
assert chat["chat_id"] == chat_id
|
||||
assert chat["forum_thread_id"] == 42
|
||||
|
||||
|
||||
def test_get_chat_by_thread_wrong_user(fresh_db):
|
||||
db = fresh_db
|
||||
db.get_or_create_tg_user(333, "usr-333", "Carol")
|
||||
chat_id = db.create_chat(333, "Чат #1")
|
||||
db.set_forum_thread(chat_id, 77)
|
||||
assert db.get_chat_by_thread(999, 77) is None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить тесты — убедиться что падают**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
PYTHONPATH=.worktrees/telegram pytest tests/adapter/test_forum_db.py -v
|
||||
```
|
||||
|
||||
Ожидаем: `ImportError` — функции ещё не существуют.
|
||||
|
||||
- [ ] **Step 3: Добавить миграцию и новые функции в `db.py`**
|
||||
|
||||
В `init_db()` добавить после `CREATE TABLE IF NOT EXISTS chats`:
|
||||
|
||||
```python
|
||||
def init_db() -> None:
|
||||
with _conn() as con:
|
||||
con.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS tg_users (
|
||||
tg_user_id INTEGER PRIMARY KEY,
|
||||
platform_user_id TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
forum_group_id INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chats (
|
||||
chat_id TEXT PRIMARY KEY,
|
||||
tg_user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
archived_at TIMESTAMP,
|
||||
forum_thread_id INTEGER,
|
||||
FOREIGN KEY(tg_user_id) REFERENCES tg_users(tg_user_id)
|
||||
);
|
||||
""")
|
||||
# Миграция для существующих БД
|
||||
try:
|
||||
con.execute("ALTER TABLE tg_users ADD COLUMN forum_group_id INTEGER")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
con.execute("ALTER TABLE chats ADD COLUMN forum_thread_id INTEGER")
|
||||
except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
Добавить в конец файла:
|
||||
|
||||
```python
|
||||
def set_forum_group(tg_user_id: int, group_id: int) -> None:
|
||||
with _conn() as con:
|
||||
con.execute(
|
||||
"UPDATE tg_users SET forum_group_id = ? WHERE tg_user_id = ?",
|
||||
(group_id, tg_user_id),
|
||||
)
|
||||
|
||||
|
||||
def get_forum_group(tg_user_id: int) -> int | None:
|
||||
with _conn() as con:
|
||||
row = con.execute(
|
||||
"SELECT forum_group_id FROM tg_users WHERE tg_user_id = ?",
|
||||
(tg_user_id,),
|
||||
).fetchone()
|
||||
return row["forum_group_id"] if row else None
|
||||
|
||||
|
||||
def set_forum_thread(chat_id: str, thread_id: int) -> None:
|
||||
with _conn() as con:
|
||||
con.execute(
|
||||
"UPDATE chats SET forum_thread_id = ? WHERE chat_id = ?",
|
||||
(thread_id, chat_id),
|
||||
)
|
||||
|
||||
|
||||
def get_chat_by_thread(tg_user_id: int, thread_id: int) -> dict | None:
|
||||
with _conn() as con:
|
||||
row = con.execute(
|
||||
"SELECT * FROM chats WHERE tg_user_id = ? AND forum_thread_id = ? "
|
||||
"AND archived_at IS NULL",
|
||||
(tg_user_id, thread_id),
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Запустить тесты — убедиться что проходят**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
PYTHONPATH=.worktrees/telegram pytest tests/adapter/test_forum_db.py -v
|
||||
```
|
||||
|
||||
Ожидаем: `3 passed`.
|
||||
|
||||
- [ ] **Step 5: Убедиться что все тесты проекта не сломались**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
PYTHONPATH=.worktrees/telegram pytest tests/ -v
|
||||
```
|
||||
|
||||
Ожидаем: все тесты `passed`.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot/.worktrees/telegram
|
||||
git add adapter/telegram/db.py ../../tests/adapter/
|
||||
git commit -m "feat: db migration + forum_group_id/forum_thread_id functions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: ForumSetupState в states.py
|
||||
|
||||
**Files:**
|
||||
- Modify: `adapter/telegram/states.py`
|
||||
|
||||
- [ ] **Step 1: Добавить ForumSetupState**
|
||||
|
||||
```python
|
||||
# adapter/telegram/states.py
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
|
||||
class ChatState(StatesGroup):
|
||||
idle = State()
|
||||
waiting_response = State()
|
||||
|
||||
|
||||
class SettingsState(StatesGroup):
|
||||
menu = State()
|
||||
soul_editing = State()
|
||||
confirm_action = State()
|
||||
|
||||
|
||||
class ForumSetupState(StatesGroup):
|
||||
waiting_for_group = State() # ждём пересылку из группы
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Проверить синтаксис**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
uv run python -m py_compile .worktrees/telegram/adapter/telegram/states.py && echo OK
|
||||
```
|
||||
|
||||
Ожидаем: `OK`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot/.worktrees/telegram
|
||||
git add adapter/telegram/states.py
|
||||
git commit -m "feat: add ForumSetupState"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: converter.py — is_forum_message и resolve_chat_id
|
||||
|
||||
**Files:**
|
||||
- Modify: `adapter/telegram/converter.py`
|
||||
|
||||
- [ ] **Step 1: Добавить функции в converter.py**
|
||||
|
||||
Добавить в конец файла (после `format_outgoing`):
|
||||
|
||||
```python
|
||||
def is_forum_message(message: Message) -> bool:
|
||||
"""Сообщение пришло из Forum-темы (не из General и не из DM)."""
|
||||
return (
|
||||
message.message_thread_id is not None
|
||||
and message.chat.type in ("supergroup", "group")
|
||||
)
|
||||
|
||||
|
||||
def resolve_forum_chat_id(message: Message) -> str | None:
|
||||
"""
|
||||
Для Forum-сообщения ищет chat_id (UUID) по forum_thread_id в БД.
|
||||
Возвращает None если тема не зарегистрирована.
|
||||
"""
|
||||
from adapter.telegram import db
|
||||
tg_user_id = message.from_user.id
|
||||
thread_id = message.message_thread_id
|
||||
chat = db.get_chat_by_thread(tg_user_id, thread_id)
|
||||
return chat["chat_id"] if chat else None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Проверить синтаксис**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
uv run python -m py_compile .worktrees/telegram/adapter/telegram/converter.py && echo OK
|
||||
```
|
||||
|
||||
Ожидаем: `OK`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot/.worktrees/telegram
|
||||
git add adapter/telegram/converter.py
|
||||
git commit -m "feat: add is_forum_message and resolve_forum_chat_id to converter"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: handlers/forum.py — /forum и онбординг
|
||||
|
||||
**Files:**
|
||||
- Create: `adapter/telegram/handlers/forum.py`
|
||||
|
||||
- [ ] **Step 1: Создать handlers/forum.py**
|
||||
|
||||
```python
|
||||
# adapter/telegram/handlers/forum.py
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import Bot, F, Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
from adapter.telegram import db
|
||||
from adapter.telegram.states import ChatState, ForumSetupState
|
||||
|
||||
router = Router(name="forum")
|
||||
|
||||
|
||||
async def _check_forum_admin(bot: Bot, group_id: int) -> bool:
|
||||
"""Проверяет что бот — администратор с правом управления темами."""
|
||||
try:
|
||||
me = await bot.get_me()
|
||||
member = await bot.get_chat_member(group_id, me.id)
|
||||
return (
|
||||
member.status in ("administrator", "creator")
|
||||
and getattr(member, "can_manage_topics", False)
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.message(Command("forum"))
|
||||
async def cmd_forum(message: Message, state: FSMContext) -> None:
|
||||
await state.set_state(ForumSetupState.waiting_for_group)
|
||||
await message.answer(
|
||||
"📋 Подключение Forum-группы\n\n"
|
||||
"1. Создай супергруппу в Telegram\n"
|
||||
"2. Включи Topics: настройки группы → Topics\n"
|
||||
"3. Добавь меня как администратора с правом управления темами\n"
|
||||
"4. Перешли мне любое сообщение из этой группы\n\n"
|
||||
"Или /cancel чтобы отменить."
|
||||
)
|
||||
|
||||
|
||||
@router.message(ForumSetupState.waiting_for_group, Command("cancel"))
|
||||
async def cmd_cancel_forum(message: Message, state: FSMContext) -> None:
|
||||
await state.set_state(ChatState.idle)
|
||||
await message.answer("❌ Настройка форума отменена.")
|
||||
|
||||
|
||||
@router.message(ForumSetupState.waiting_for_group, F.forward_from_chat)
|
||||
async def handle_group_forward(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
group = message.forward_from_chat
|
||||
|
||||
if group.type != "supergroup":
|
||||
await message.answer(
|
||||
"⚠️ Это не супергруппа. Нужна именно супергруппа с включёнными Topics."
|
||||
)
|
||||
return
|
||||
|
||||
group_id = group.id
|
||||
|
||||
if not await _check_forum_admin(message.bot, group_id):
|
||||
await message.answer(
|
||||
"⚠️ Не могу управлять темами в этой группе.\n\n"
|
||||
"Убедись что:\n"
|
||||
"• Я добавлен как администратор\n"
|
||||
"• У меня есть право «Управление темами»"
|
||||
)
|
||||
return
|
||||
|
||||
tg_id = message.from_user.id
|
||||
db.set_forum_group(tg_id, group_id)
|
||||
|
||||
# Создать Forum-темы для всех существующих активных DM-чатов
|
||||
chats = db.get_user_chats(tg_id)
|
||||
created = 0
|
||||
for chat in chats:
|
||||
if chat.get("forum_thread_id"):
|
||||
continue # уже есть тема
|
||||
try:
|
||||
topic = await message.bot.create_forum_topic(
|
||||
chat_id=group_id,
|
||||
name=chat["name"],
|
||||
)
|
||||
db.set_forum_thread(chat["chat_id"], topic.message_thread_id)
|
||||
created += 1
|
||||
except Exception:
|
||||
pass # не страшно — тему можно создать позже через /new
|
||||
|
||||
await state.set_state(ChatState.idle)
|
||||
await message.answer(
|
||||
f"✅ Группа «{group.title}» подключена!\n"
|
||||
f"Создано тем в форуме: {created} из {len(chats)}.\n\n"
|
||||
"Теперь можешь писать как в DM, так и в темах форума."
|
||||
)
|
||||
|
||||
|
||||
@router.message(ForumSetupState.waiting_for_group)
|
||||
async def handle_forward_wrong(message: Message) -> None:
|
||||
await message.answer(
|
||||
"Жду пересланное сообщение из группы. "
|
||||
"Перешли любое сообщение из своей супергруппы."
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Проверить синтаксис**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
uv run python -m py_compile .worktrees/telegram/adapter/telegram/handlers/forum.py && echo OK
|
||||
```
|
||||
|
||||
Ожидаем: `OK`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot/.worktrees/telegram
|
||||
git add adapter/telegram/handlers/forum.py
|
||||
git commit -m "feat: add handlers/forum.py — /forum onboarding flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: handlers/chat.py — Forum-маршрутизация
|
||||
|
||||
**Files:**
|
||||
- Modify: `adapter/telegram/handlers/chat.py`
|
||||
|
||||
- [ ] **Step 1: Обновить импорты в chat.py**
|
||||
|
||||
Заменить блок импортов целиком:
|
||||
|
||||
```python
|
||||
# adapter/telegram/handlers/chat.py
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from adapter.telegram import db
|
||||
from adapter.telegram.converter import (
|
||||
format_outgoing,
|
||||
from_message,
|
||||
is_forum_message,
|
||||
resolve_forum_chat_id,
|
||||
)
|
||||
from adapter.telegram.keyboards.chat import chats_list_keyboard
|
||||
from adapter.telegram.keyboards.confirm import confirm_keyboard
|
||||
from adapter.telegram.states import ChatState
|
||||
from core.handler import EventDispatcher
|
||||
from core.protocol import OutgoingMessage, OutgoingUI
|
||||
|
||||
router = Router(name="chat")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Обновить `_send_outgoing` — добавить Forum-вариант**
|
||||
|
||||
Заменить функцию `_send_outgoing`:
|
||||
|
||||
```python
|
||||
async def _send_outgoing(
|
||||
message: Message,
|
||||
chat_name: str,
|
||||
events: list,
|
||||
forum_group_id: int | None = None,
|
||||
forum_thread_id: int | None = None,
|
||||
) -> None:
|
||||
for event in events:
|
||||
if forum_group_id and forum_thread_id:
|
||||
# Ответ в Forum-тему (без тега)
|
||||
text = event.text if isinstance(event, (OutgoingMessage, OutgoingUI)) else str(event)
|
||||
if isinstance(event, OutgoingUI) and event.buttons:
|
||||
action_id = event.buttons[0].payload.get("action_id", "unknown")
|
||||
kb = confirm_keyboard(action_id)
|
||||
await message.bot.send_message(
|
||||
forum_group_id, text,
|
||||
message_thread_id=forum_thread_id,
|
||||
reply_markup=kb,
|
||||
)
|
||||
else:
|
||||
await message.bot.send_message(
|
||||
forum_group_id, text,
|
||||
message_thread_id=forum_thread_id,
|
||||
)
|
||||
else:
|
||||
# Ответ в DM с тегом
|
||||
if isinstance(event, OutgoingUI) and event.buttons:
|
||||
action_id = event.buttons[0].payload.get("action_id", "unknown")
|
||||
kb = confirm_keyboard(action_id)
|
||||
await message.answer(format_outgoing(chat_name, event), reply_markup=kb)
|
||||
elif isinstance(event, (OutgoingMessage, OutgoingUI)):
|
||||
await message.answer(format_outgoing(chat_name, event))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Обновить `handle_message` — Forum-маршрутизация**
|
||||
|
||||
Заменить функцию `handle_message`:
|
||||
|
||||
```python
|
||||
@router.message(ChatState.idle, (F.text | F.photo | F.document | F.voice) & ~F.text.startswith("/"))
|
||||
async def handle_message(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
dispatcher: EventDispatcher,
|
||||
) -> None:
|
||||
tg_id = message.from_user.id
|
||||
|
||||
# Определяем chat_id и канал ответа
|
||||
if is_forum_message(message):
|
||||
chat_id = resolve_forum_chat_id(message)
|
||||
if not chat_id:
|
||||
await message.reply(
|
||||
"Эта тема не зарегистрирована как чат. "
|
||||
"Введи /new в этой теме чтобы создать чат."
|
||||
)
|
||||
return
|
||||
chat = db.get_chat_by_thread(tg_id, message.message_thread_id)
|
||||
chat_name = chat["name"]
|
||||
forum_group_id = message.chat.id
|
||||
forum_thread_id = message.message_thread_id
|
||||
else:
|
||||
data = await state.get_data()
|
||||
chat_id = data.get("active_chat_id")
|
||||
chat_name = data.get("active_chat_name", "Чат")
|
||||
forum_group_id = None
|
||||
forum_thread_id = None
|
||||
|
||||
if not chat_id:
|
||||
await message.answer("Нет активного чата. Введите /start")
|
||||
return
|
||||
|
||||
await state.set_state(ChatState.waiting_response)
|
||||
|
||||
async def _typing_loop():
|
||||
while True:
|
||||
await message.bot.send_chat_action(message.chat.id, "typing")
|
||||
await asyncio.sleep(4)
|
||||
|
||||
task = asyncio.create_task(_typing_loop())
|
||||
try:
|
||||
tg_user = db.get_or_create_tg_user(tg_id, str(tg_id), message.from_user.full_name)
|
||||
platform_user_id = tg_user.get("platform_user_id", str(tg_id))
|
||||
|
||||
incoming = from_message(message, chat_id)
|
||||
incoming.user_id = platform_user_id
|
||||
events = await dispatcher.dispatch(incoming)
|
||||
finally:
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
await state.set_state(ChatState.idle)
|
||||
await _send_outgoing(message, chat_name, events, forum_group_id, forum_thread_id)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Обновить `cmd_new_chat` — ветвление DM vs Forum**
|
||||
|
||||
Заменить функцию `cmd_new_chat`:
|
||||
|
||||
```python
|
||||
@router.message(Command("new"))
|
||||
async def cmd_new_chat(message: Message, state: FSMContext) -> None:
|
||||
tg_id = message.from_user.id
|
||||
args = message.text.split(maxsplit=1)
|
||||
name = args[1].strip() if len(args) > 1 else None
|
||||
|
||||
if is_forum_message(message):
|
||||
# /new в Forum-теме — регистрируем эту тему как чат
|
||||
thread_id = message.message_thread_id
|
||||
existing = db.get_chat_by_thread(tg_id, thread_id)
|
||||
if existing:
|
||||
await message.reply(f"Эта тема уже зарегистрирована как [{existing['name']}].")
|
||||
return
|
||||
|
||||
count = db.count_chats(tg_id)
|
||||
chat_name = name or f"Чат #{count + 1}"
|
||||
chat_id = db.create_chat(tg_id, chat_name)
|
||||
db.set_forum_thread(chat_id, thread_id)
|
||||
|
||||
await message.reply(f"✅ [{chat_name}] зарегистрирован. Пиши здесь!")
|
||||
else:
|
||||
# /new в DM
|
||||
count = db.count_chats(tg_id)
|
||||
chat_name = name or f"Чат #{count + 1}"
|
||||
chat_id = db.create_chat(tg_id, chat_name)
|
||||
|
||||
# Если есть форум-группа — создать тему и там
|
||||
group_id = db.get_forum_group(tg_id)
|
||||
if group_id:
|
||||
try:
|
||||
topic = await message.bot.create_forum_topic(
|
||||
chat_id=group_id,
|
||||
name=chat_name,
|
||||
)
|
||||
db.set_forum_thread(chat_id, topic.message_thread_id)
|
||||
except Exception:
|
||||
pass # не блокирует создание DM-чата
|
||||
|
||||
await state.update_data(active_chat_id=chat_id, active_chat_name=chat_name)
|
||||
await state.set_state(ChatState.idle)
|
||||
await message.answer(f"✅ [{chat_name}] создан. Пиши!")
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Проверить синтаксис**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
uv run python -m py_compile .worktrees/telegram/adapter/telegram/handlers/chat.py && echo OK
|
||||
```
|
||||
|
||||
Ожидаем: `OK`.
|
||||
|
||||
- [ ] **Step 6: Запустить все тесты**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
PYTHONPATH=.worktrees/telegram pytest tests/ -v
|
||||
```
|
||||
|
||||
Ожидаем: все тесты `passed`.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot/.worktrees/telegram
|
||||
git add adapter/telegram/handlers/chat.py
|
||||
git commit -m "feat: forum routing in handle_message and cmd_new_chat"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: bot.py — регистрация forum.router
|
||||
|
||||
**Files:**
|
||||
- Modify: `adapter/telegram/bot.py`
|
||||
|
||||
- [ ] **Step 1: Добавить импорт и регистрацию router**
|
||||
|
||||
В блоке импортов добавить:
|
||||
|
||||
```python
|
||||
from adapter.telegram.handlers import auth, chat, confirm, forum, settings
|
||||
```
|
||||
|
||||
В `main()` после `dp.include_router(auth.router)`:
|
||||
|
||||
```python
|
||||
dp.include_router(auth.router)
|
||||
dp.include_router(forum.router) # ← добавить
|
||||
dp.include_router(chat.router)
|
||||
dp.include_router(settings.router)
|
||||
dp.include_router(confirm.router)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Проверить синтаксис**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
uv run python -m py_compile .worktrees/telegram/adapter/telegram/bot.py && echo OK
|
||||
```
|
||||
|
||||
Ожидаем: `OK`.
|
||||
|
||||
- [ ] **Step 3: Финальный прогон всех тестов**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot
|
||||
PYTHONPATH=.worktrees/telegram pytest tests/ -v
|
||||
```
|
||||
|
||||
Ожидаем: все тесты `passed`.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd /Users/a/MAI/sem2/lambda/surfaces-bot/.worktrees/telegram
|
||||
git add adapter/telegram/bot.py
|
||||
git commit -m "feat: register forum router in bot.py"
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue