Improve Telegram forum onboarding and topic safety
This commit is contained in:
parent
2b56b98697
commit
a1b7a14138
13 changed files with 1101 additions and 376 deletions
212
adapter/telegram/handlers/forum.py
Normal file
212
adapter/telegram/handlers/forum.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Chat, Message, ReplyKeyboardRemove
|
||||
|
||||
from adapter.telegram import db
|
||||
from adapter.telegram.keyboards.forum import forum_group_request_keyboard
|
||||
from adapter.telegram.states import ChatState, ForumSetupState
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
router = Router(name="forum")
|
||||
|
||||
|
||||
def _thread_id_from_topic(topic: object) -> int | None:
|
||||
thread_id = getattr(topic, "message_thread_id", None)
|
||||
if thread_id is not None:
|
||||
return thread_id
|
||||
return getattr(topic, "thread_id", None)
|
||||
|
||||
|
||||
def _resolve_forwarded_chat(message: Message) -> Chat | None:
|
||||
forwarded_chat = getattr(message, "forward_from_chat", None)
|
||||
if forwarded_chat is not None:
|
||||
return forwarded_chat
|
||||
|
||||
forward_origin = getattr(message, "forward_origin", None)
|
||||
if forward_origin is None:
|
||||
return None
|
||||
|
||||
sender_chat = getattr(forward_origin, "sender_chat", None)
|
||||
if sender_chat is not None:
|
||||
return sender_chat
|
||||
|
||||
return getattr(forward_origin, "chat", None)
|
||||
|
||||
|
||||
def _forward_debug_payload(message: Message) -> dict[str, object]:
|
||||
forward_origin = getattr(message, "forward_origin", None)
|
||||
forwarded_chat = _resolve_forwarded_chat(message)
|
||||
return {
|
||||
"has_forward_from_chat": getattr(message, "forward_from_chat", None) is not None,
|
||||
"has_forward_origin": forward_origin is not None,
|
||||
"forward_origin_type": getattr(forward_origin, "type", None),
|
||||
"forwarded_chat_id": getattr(forwarded_chat, "id", None),
|
||||
"forwarded_chat_type": getattr(forwarded_chat, "type", None),
|
||||
"forwarded_chat_is_forum": getattr(forwarded_chat, "is_forum", None),
|
||||
}
|
||||
|
||||
|
||||
async def _send_message(
|
||||
message: Message,
|
||||
text: str,
|
||||
*,
|
||||
reply_markup=None,
|
||||
thread_id: int | None = None,
|
||||
) -> None:
|
||||
if thread_id is None:
|
||||
await message.answer(text, reply_markup=reply_markup)
|
||||
return
|
||||
|
||||
await message.bot.send_message(
|
||||
message.chat.id,
|
||||
text,
|
||||
reply_markup=reply_markup,
|
||||
message_thread_id=thread_id,
|
||||
)
|
||||
|
||||
|
||||
async def _complete_group_link(message: Message, state: FSMContext, forwarded_chat: Chat) -> None:
|
||||
bot_user = await message.bot.get_me()
|
||||
member = await message.bot.get_chat_member(forwarded_chat.id, bot_user.id)
|
||||
can_manage_topics = getattr(member, "can_manage_topics", False)
|
||||
is_admin = member.status in ("administrator", "creator")
|
||||
if not is_admin or (member.status == "administrator" and not can_manage_topics):
|
||||
logger.warning(
|
||||
"Forum onboarding failed: bot lacks forum admin rights",
|
||||
tg_user_id=message.from_user.id,
|
||||
forum_group_id=forwarded_chat.id,
|
||||
member_status=member.status,
|
||||
can_manage_topics=can_manage_topics,
|
||||
)
|
||||
await message.answer(
|
||||
"Я не вижу прав на управление темами. "
|
||||
"Добавь меня администратором с правом `can_manage_topics` и попробуй снова.",
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
return
|
||||
|
||||
tg_user_id = message.from_user.id
|
||||
db.set_forum_group(tg_user_id, forwarded_chat.id)
|
||||
logger.info(
|
||||
"Forum group linked",
|
||||
tg_user_id=tg_user_id,
|
||||
forum_group_id=forwarded_chat.id,
|
||||
forum_group_title=getattr(forwarded_chat, "title", None),
|
||||
)
|
||||
|
||||
created_topics = 0
|
||||
for chat in db.get_user_chats(tg_user_id):
|
||||
if chat.get("forum_thread_id") is not None:
|
||||
continue
|
||||
|
||||
topic = await message.bot.create_forum_topic(
|
||||
chat_id=forwarded_chat.id,
|
||||
name=chat["name"],
|
||||
)
|
||||
thread_id = _thread_id_from_topic(topic)
|
||||
if thread_id is None:
|
||||
logger.warning("Forum topic created without thread id", chat_id=chat["chat_id"])
|
||||
continue
|
||||
|
||||
db.set_forum_thread(chat["chat_id"], thread_id)
|
||||
created_topics += 1
|
||||
logger.info(
|
||||
"Forum topic linked to chat",
|
||||
tg_user_id=tg_user_id,
|
||||
chat_id=chat["chat_id"],
|
||||
forum_group_id=forwarded_chat.id,
|
||||
forum_thread_id=thread_id,
|
||||
)
|
||||
|
||||
await state.set_state(ChatState.idle)
|
||||
logger.info(
|
||||
"Forum onboarding completed",
|
||||
tg_user_id=tg_user_id,
|
||||
forum_group_id=forwarded_chat.id,
|
||||
created_topics=created_topics,
|
||||
)
|
||||
await message.answer(
|
||||
f"✅ Группа подключена. Создал {created_topics} тем(ы) для существующих чатов.",
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("forum"))
|
||||
async def cmd_forum(message: Message, state: FSMContext) -> None:
|
||||
await state.set_state(ForumSetupState.waiting_for_group)
|
||||
logger.info("Forum onboarding started", tg_user_id=message.from_user.id)
|
||||
await message.answer(
|
||||
"Выбери forum-группу кнопкой ниже. Бот должен уже быть добавлен туда "
|
||||
"администратором с правом управления темами.\n\n"
|
||||
"Если кнопка не сработает, можно переслать сообщение из группы как fallback.",
|
||||
reply_markup=forum_group_request_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@router.message(ForumSetupState.waiting_for_group)
|
||||
async def handle_group_forward(message: Message, state: FSMContext) -> None:
|
||||
chat_shared = getattr(message, "chat_shared", None)
|
||||
if chat_shared is not None:
|
||||
logger.info(
|
||||
"Forum onboarding chat selected via request_chat",
|
||||
tg_user_id=message.from_user.id,
|
||||
forum_group_id=chat_shared.chat_id,
|
||||
forum_group_title=getattr(chat_shared, "title", None),
|
||||
request_id=getattr(chat_shared, "request_id", None),
|
||||
)
|
||||
forwarded_chat = Chat(
|
||||
id=chat_shared.chat_id,
|
||||
type="supergroup",
|
||||
title=getattr(chat_shared, "title", None),
|
||||
is_forum=True,
|
||||
)
|
||||
await _complete_group_link(message, state, forwarded_chat)
|
||||
return
|
||||
|
||||
debug_payload = _forward_debug_payload(message)
|
||||
logger.info(
|
||||
"Forum onboarding message received",
|
||||
tg_user_id=message.from_user.id,
|
||||
**debug_payload,
|
||||
)
|
||||
|
||||
forwarded_chat = _resolve_forwarded_chat(message)
|
||||
if forwarded_chat is None:
|
||||
logger.warning(
|
||||
"Forum onboarding failed: missing forwarded chat metadata",
|
||||
tg_user_id=message.from_user.id,
|
||||
**debug_payload,
|
||||
)
|
||||
await message.answer(
|
||||
"Не вижу в сообщении данных о группе. "
|
||||
"Нажми кнопку `Выбрать forum-группу` или перешли сообщение именно из нужной супергруппы, не копируй текст вручную."
|
||||
)
|
||||
return
|
||||
|
||||
if forwarded_chat.type != "supergroup":
|
||||
logger.warning(
|
||||
"Forum onboarding failed: forwarded chat is not supergroup",
|
||||
tg_user_id=message.from_user.id,
|
||||
**debug_payload,
|
||||
)
|
||||
await message.answer(
|
||||
"Пересылка пришла не из супергруппы. Нужна именно supergroup с включёнными Topics."
|
||||
)
|
||||
return
|
||||
|
||||
if getattr(forwarded_chat, "is_forum", None) is False:
|
||||
logger.warning(
|
||||
"Forum onboarding failed: supergroup is not forum-enabled",
|
||||
tg_user_id=message.from_user.id,
|
||||
**debug_payload,
|
||||
)
|
||||
await message.answer(
|
||||
"Это супергруппа, но в ней выключены Topics. Включи Topics и попробуй снова."
|
||||
)
|
||||
return
|
||||
await _complete_group_link(message, state, forwarded_chat)
|
||||
Loading…
Add table
Add a link
Reference in a new issue