# Research: aiogram 3.x Forum Topics API Based on: Telegram Bot API 7.0+, aiogram 3.x docs, GitHub examples. ## Создание супергруппы и тем ### Ключевое ограничение: бот не может создать группу сам Telegram Bot API **не позволяет боту программно создавать группы**. Пользователь должен: 1. Создать супергруппу вручную 2. Добавить бота как администратора 3. Включить Topics (`ForumTopic` feature) 4. Переслать сообщение из группы боту — бот получит `chat_id` Это фундаментальное ограничение. Флоу адаптируется так: бот просит пользователя создать группу, пользователь пересылает любое сообщение оттуда. ### Проверка прав бота в группе ```python from aiogram import Bot from aiogram.exceptions import TelegramBadRequest async def check_bot_admin_rights(bot: Bot, chat_id: int) -> bool: """Проверяет что бот является администратором с правом управления темами.""" try: member = await bot.get_chat_member(chat_id, (await bot.get_me()).id) return ( member.status in ("administrator", "creator") and getattr(member, "can_manage_topics", False) ) except TelegramBadRequest: return False ``` --- ## Управление темами (Forum Topics) ### Создание темы ```python from aiogram.types import ForumTopic async def create_chat_topic(bot: Bot, group_id: int, name: str) -> int: """ Создаёт тему в Forum-группе. Возвращает message_thread_id (используется для отправки сообщений в тему). """ topic: ForumTopic = await bot.create_forum_topic( chat_id=group_id, name=name, # до 128 символов # icon_color — опционально (7322096, 16766590, 13338331, 9367192, 16749490, 16478047) ) return topic.message_thread_id # Пример: создать "Чат 1" # thread_id = await create_chat_topic(bot, group_id, "Чат 1") ``` ### Переименование, закрытие, удаление ```python async def rename_topic(bot: Bot, group_id: int, thread_id: int, new_name: str) -> None: await bot.edit_forum_topic( chat_id=group_id, message_thread_id=thread_id, name=new_name, ) async def archive_topic(bot: Bot, group_id: int, thread_id: int) -> None: """Закрывает тему (архивирует). Пользователи не могут писать в закрытую тему.""" await bot.close_forum_topic( chat_id=group_id, message_thread_id=thread_id, ) async def reopen_topic(bot: Bot, group_id: int, thread_id: int) -> None: await bot.reopen_forum_topic( chat_id=group_id, message_thread_id=thread_id, ) async def delete_topic(bot: Bot, group_id: int, thread_id: int) -> None: """Удаляет тему и все её сообщения. Необратимо.""" await bot.delete_forum_topic( chat_id=group_id, message_thread_id=thread_id, ) ``` --- ## Отправка и получение сообщений в темах ### Отправить сообщение в конкретную тему ```python async def send_to_topic( bot: Bot, group_id: int, thread_id: int, text: str, ) -> None: await bot.send_message( chat_id=group_id, message_thread_id=thread_id, # ключевой параметр text=text, ) ``` ### Router фильтр для сообщений из конкретной темы ```python from aiogram import Router, F from aiogram.types import Message chat_router = Router() # Слушать ВСЕ сообщения в темах (кроме General) @chat_router.message(F.message_thread_id.is_not(None)) async def on_topic_message(message: Message) -> None: thread_id = message.message_thread_id group_id = message.chat.id # ... обработка ``` ### Подводный камень #1: message_thread_id может быть None Сообщения в «General» теме (или если Topics выключены) имеют `message_thread_id = None`. ```python @router.message() async def handler(message: Message): if message.message_thread_id is None: # Сообщение в General или не в Forum-группе return # Сообщение в конкретной теме thread_id = message.message_thread_id ``` --- ## FSM паттерны для онбординга ### Многошаговый онбординг ```python from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram import Router, F from aiogram.filters import Command from aiogram.types import Message class OnboardingState(StatesGroup): waiting_for_group = State() # ждём пересылку из группы setting_up = State() # настраиваем группу onboarding_router = Router() @onboarding_router.message(Command("start")) async def cmd_start(message: Message, state: FSMContext) -> None: current_state = await state.get_state() if current_state is not None: # Пользователь уже в процессе — напоминаем await message.answer("Ты уже в процессе настройки. Перешли сообщение из группы.") return await state.set_state(OnboardingState.waiting_for_group) await message.answer( "Привет! Для начала создай группу в Telegram:\n" "1. Создай супергруппу\n" "2. Добавь меня как администратора\n" "3. Перешли мне любое сообщение из этой группы" ) @onboarding_router.message( OnboardingState.waiting_for_group, F.forward_from_chat.type == "supergroup", ) async def handle_group_forward(message: Message, state: FSMContext, bot: Bot) -> None: group_id = message.forward_from_chat.id # Проверяем права if not await check_bot_admin_rights(bot, group_id): await message.answer( "Не могу управлять темами. Убедись что:\n" "- Я администратор группы\n" "- У меня есть право управлять темами" ) return await state.update_data(group_id=group_id) await state.set_state(OnboardingState.setting_up) # Создаём первую тему thread_id = await create_chat_topic(bot, group_id, "Чат 1") await send_to_topic(bot, group_id, thread_id, "Привет! Я готов. Пиши здесь.") await state.update_data(chat_1_thread_id=thread_id) await state.clear() # онбординг завершён await message.answer("✅ Всё готово! Пиши в Чат 1.") ``` ### Передача данных между шагами (StateData) ```python # Сохранять в StateData только то, что нужно в следующем шаге # НЕ дублировать данные из БД # Хорошо: await state.update_data(group_id=group_id, pending_name="Анализ") # Плохо: хранить большие объекты или данные которые уже в БД await state.update_data(user_profile=big_dict) # избыточно # Читать: data = await state.get_data() group_id = data["group_id"] ``` ### FSM Storage — выбор хранилища ```python from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.redis import RedisStorage # MVP (разработка): данные теряются при перезапуске storage = MemoryStorage() # Продакшн: данные переживают перезапуски from redis.asyncio import Redis storage = RedisStorage(redis=Redis(host="localhost", port=6379)) # Альтернатива без Redis (простой персистентный вариант): # pip install aiogram-sqlite-storage from aiogram_sqlite_storage.sqlitestore import SQLStorage storage = SQLStorage(db_path="fsm_states.db") dp = Dispatcher(storage=storage) ``` ### Подводный камень #2: MemoryStorage теряет данные при перезапуске Если бот упал в середине онбординга — пользователь застрянет. Используй SQLiteStorage для MVP. ### Обработка timeout состояния ```python import asyncio from datetime import datetime, timedelta # Способ 1: пассивный — при следующем обращении проверяем дату @router.message() async def any_handler(message: Message, state: FSMContext) -> None: data = await state.get_data() started_at = data.get("started_at") if started_at: elapsed = datetime.now() - datetime.fromisoformat(started_at) if elapsed > timedelta(minutes=10): await state.clear() await message.answer("Сессия истекла. Начни заново: /start") return # ... продолжение обработки # Способ 2: при старте шага сохранять timestamp await state.update_data(started_at=datetime.now().isoformat()) ``` --- ## Подводные камни (резюме) | Проблема | Решение | |----------|---------| | Бот не может создать группу | Пользователь создаёт, пересылает сообщение боту | | `message_thread_id is None` | General тема или не Forum-группа — проверяй всегда | | Бот не администратор | `check_bot_admin_rights()` перед операциями | | FSM теряется при рестарте | Используй `SQLiteStorage` вместо `MemoryStorage` | | Пользователь бросил онбординг | Сохраняй `started_at`, проверяй timeout | | StateData разрастается | Хранить только параметры текущего шага | --- ## Конкурентный анализ UX ### Бот 1: ChatGPT (@ChatGPT официальный) **Хорошо:** - Простой `/start` без лишних шагов - Typing indicator во время генерации - Кнопки «Regenerate» и «Edit» под ответом **Плохо:** - Нет разделения на чаты (одна сплошная лента) - Нет управления историей **Паттерны для нас:** typing indicator обязателен; кнопки под сообщением вместо команд --- ### Бот 2: Notion AI бот **Хорошо:** - Онбординг в 3 шага с прогресс-баром - Inline кнопки вместо команд - Явный feedback: «✅ Готово», «❌ Ошибка» **Плохо:** - Много текста в одном сообщении **Паттерны для нас:** пошаговый онбординг, явный feedback --- ### Бот 3: TaskMaster AI **Хорошо:** - Отдельный «чат» на каждую задачу (аналог наших Topics) - `/list` показывает кнопки по одной задаче на строку - Подтверждение деструктивных действий: «Удалить чат? [Да] [Нет]» **Паттерны для нас:** подтверждение перед удалением/архивацией --- ## Паттерны для Lambda Surfaces | Паттерн | Источник | Применение | |---------|----------|------------| | Typing indicator | ChatGPT | Всегда при запросе к платформе | | Inline кнопки | Notion, TaskMaster | `/new`, `/chats`, настройки | | Пошаговый онбординг | Notion | AuthPending → GroupSetup → Idle | | Явный feedback | Notion | «✅ Чат создан», «❌ Ошибка» | | Подтверждение удаления | TaskMaster | `/archive` и `/delete` | | Прогресс в треде | общий паттерн | Долгие задачи агента | ## Анти-паттерны - **Молчаливая обработка** — всегда показывай «обрабатываю...» если > 2 сек - **Команды вместо кнопок** — пользователи не помнят команды, используй кнопки - **Все настройки в одном сообщении** — разбивай на подменю - **Нет подтверждения** — всегда спрашивай перед деструктивными действиями