""" Обработчики авторизации: - bot_started — первый запуск / перезапуск - /start команда - Шаринг контакта """ import re from maxapi.dispatcher import Router from maxapi.filters import F from maxapi.filters.contact import ContactFilter from maxapi.types.attachments.attachment import ContactAttachmentPayload from maxapi.types.attachments.contact import Contact as ContactAttachment from maxapi.types.updates.bot_started import BotStarted from maxapi.types.updates.message_created import MessageCreated from maxapi.context import BaseContext from maxapi.filters.command import Command from bot.flow_config import TRANSITIONS from bot.keyboards import contact_request_kb, main_menu_kb from bot.logging_setup import get_logger from bot.nav_helper import nav_set from bot.send_helper import bot_send from bot.texts import AuthTexts logger = get_logger("bot.handlers.start") from database.queries.bot_users import ( authorize_bot_user, block_bot_user, get_bot_user_by_max_id, upsert_bot_user, ) from database.queries.org_users import find_by_phone from database.queries.message_log import log_message router = Router(router_id="start") def normalize_phone(raw: str) -> str: """Убираем всё кроме цифр, заменяем ведущую 8 → 7.""" digits = re.sub(r"\D", "", raw) if digits.startswith("8") and len(digits) == 11: digits = "7" + digits[1:] return digits @router.bot_started() async def on_bot_started(event: BotStarted, context: BaseContext) -> None: user = event.user bot_user = await upsert_bot_user( max_user_id=user.user_id, max_chat_id=event.chat_id, username=getattr(user, "username", None), first_name=getattr(user, "name", None), ) if bot_user["is_blocked"]: logger.info("🚫 bot_started blocked | user_id=%s", user.user_id) await bot_send( event.bot, chat_id=event.chat_id, user_id=user.user_id, text=AuthTexts.BLOCKED, ) return if bot_user["is_authorized"]: logger.info("✅ bot_started returning user | user_id=%s", user.user_id) await context.set_state(TRANSITIONS["auth:authorized"]) from bot.handlers.menu import build_main_menu_text menu_text = AuthTexts.WELCOME_BACK + await build_main_menu_text(bot_user) mid = await bot_send( event.bot, chat_id=event.chat_id, user_id=user.user_id, text=menu_text, attachments=[main_menu_kb()], ) await nav_set(context, mid) return # Новый пользователь — запрашиваем контакт logger.info("👋 bot_started new user | user_id=%s", user.user_id) await context.set_state(TRANSITIONS["auth:new_user"]) await bot_send( event.bot, chat_id=event.chat_id, user_id=user.user_id, text=AuthTexts.WELCOME_NEW, attachments=[contact_request_kb()], ) @router.message_created(Command("start")) async def on_start_command(event: MessageCreated, context: BaseContext) -> None: """commands_info: Главное меню""" chat_id, user_id = event.get_ids() if user_id is None or chat_id is None: return bot_user = await get_bot_user_by_max_id(user_id) if bot_user is None: # Первый раз — как bot_started await upsert_bot_user( max_user_id=user_id, max_chat_id=chat_id, username=None, first_name=None, ) await context.set_state(TRANSITIONS["auth:new_user"]) await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=AuthTexts.WELCOME_START, attachments=[contact_request_kb()], ) return if bot_user["is_blocked"]: await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=AuthTexts.BLOCKED_SHORT, ) return if not bot_user["is_authorized"]: await context.set_state(TRANSITIONS["auth:new_user"]) await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=AuthTexts.SHARE_PHONE, attachments=[contact_request_kb()], ) return await context.set_state(TRANSITIONS["auth:authorized"]) from bot.handlers.menu import build_main_menu_text menu_text = await build_main_menu_text(bot_user) mid = await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=menu_text, attachments=[main_menu_kb()], ) await nav_set(context, mid) @router.message_created(ContactFilter(), states=TRANSITIONS["auth:new_user"]) async def on_contact_shared( event: MessageCreated, contact: ContactAttachment, context: BaseContext, ) -> None: chat_id, user_id = event.get_ids() if user_id is None or chat_id is None: return # Извлекаем телефон из VCF payload = contact.payload if not isinstance(payload, ContactAttachmentPayload): await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=AuthTexts.CONTACT_READ_ERROR, attachments=[contact_request_kb()], ) return vcf = payload.vcf raw_phone = vcf.phone if not raw_phone: await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=AuthTexts.CONTACT_NO_PHONE, attachments=[contact_request_kb()], ) return phone = normalize_phone(raw_phone) display_name = vcf.full_name # Логируем входящий контакт await log_message( direction="in", max_user_id=user_id, max_chat_id=chat_id, message_id=None, text=f"[Контакт: {raw_phone}]", ) # Ищем в белом списке org_user = await find_by_phone(phone) if org_user is None: logger.warning("🚫 phone not found, blocking | user_id=%s phone=%s", user_id, phone) await block_bot_user(max_user_id=user_id, phone=phone) await context.set_state(TRANSITIONS["auth:blocked"]) await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=AuthTexts.PHONE_NOT_FOUND, ) return # Мгновенно показываем заглушку, пока линкуем аккаунт mid = await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=AuthTexts.LOADING, ) # Авторизуем logger.info("✅ authorized | user_id=%s phone=%s org_id=%s", user_id, phone, org_user["org_id"]) await authorize_bot_user( max_user_id=user_id, phone=phone, org_id=org_user["org_id"], first_name=display_name, ) await context.set_state(TRANSITIONS["auth:contact_found"]) bot_user = await get_bot_user_by_max_id(user_id) from bot.handlers.menu import build_main_menu_text # local import to avoid circular menu_text = AuthTexts.AUTHORIZED + await build_main_menu_text(bot_user) # Заменяем заглушку финальным меню await event.bot.edit_message( message_id=mid, text=menu_text, attachments=[main_menu_kb()], ) await nav_set(context, mid) # ── /menu ───────────────────────────────────────────────────────────────────── @router.message_created(Command("menu")) async def on_menu_command(event: MessageCreated, context: BaseContext) -> None: """commands_info: Показать меню""" chat_id, user_id = event.get_ids() if user_id is None or chat_id is None: return bot_user = await get_bot_user_by_max_id(user_id) if not bot_user or not bot_user["is_authorized"]: return from bot.handlers.create_ticket import cleanup_ticket_flow await cleanup_ticket_flow(event.bot, context) await context.set_state(TRANSITIONS["nav:menu"]) from bot.handlers.menu import build_main_menu_text mid = await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=await build_main_menu_text(bot_user), attachments=[main_menu_kb()], ) await nav_set(context, mid) # ── /cancel ─────────────────────────────────────────────────────────────────── @router.message_created(Command("cancel")) async def on_cancel_command(event: MessageCreated, context: BaseContext) -> None: """commands_info: Отменить текущее действие""" chat_id, user_id = event.get_ids() if user_id is None or chat_id is None: return bot_user = await get_bot_user_by_max_id(user_id) if not bot_user or not bot_user["is_authorized"]: return from bot.handlers.create_ticket import cleanup_ticket_flow from bot.states import CommentStates, TicketStates from bot.texts import MenuTexts state = await context.get_state() active_states = [ TicketStates.waiting_description, TicketStates.collecting_attachments, TicketStates.confirming, CommentStates.waiting_text, ] if state in active_states: await cleanup_ticket_flow(event.bot, context) cancel_text = MenuTexts.CANCEL_PREFIX else: cancel_text = "" await context.set_state(TRANSITIONS["nav:cancel"]) from bot.handlers.menu import build_main_menu_text mid = await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=cancel_text + await build_main_menu_text(bot_user), attachments=[main_menu_kb()], ) await nav_set(context, mid)