212 lines
7.8 KiB
Python
212 lines
7.8 KiB
Python
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)
|