314 lines
10 KiB
Python
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)
|