intra_max_chatbot/bot/handlers/start.py
2026-04-03 00:30:24 +03:00

314 lines
10 KiB
Python

"""
Обработчики авторизации:
- 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)