intra_max_chatbot/bot/handlers/start.py

309 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Обработчики авторизации:
- 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.keyboards import contact_request_kb, main_menu_kb
from bot.logging_setup import get_logger
from bot.send_helper import bot_send
from bot.states import AuthStates
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=(
"⛔ Ваш номер не найден в системе. Доступ заблокирован.\n"
"Обратитесь к администратору для подключения."
),
)
return
if bot_user["is_authorized"]:
logger.info("✅ bot_started returning user | user_id=%s", user.user_id)
await context.set_state(None)
from bot.handlers.menu import build_main_menu_text
menu_text = "👋 С возвращением!\n\n" + await build_main_menu_text(bot_user)
await bot_send(
event.bot,
chat_id=event.chat_id,
user_id=user.user_id,
text=menu_text,
attachments=[main_menu_kb()],
)
return
# Новый пользователь — запрашиваем контакт
logger.info("👋 bot_started new user | user_id=%s", user.user_id)
await context.set_state(AuthStates.waiting_contact)
await bot_send(
event.bot,
chat_id=event.chat_id,
user_id=user.user_id,
text=(
"👋 Добро пожаловать в систему технической поддержки!\n\n"
"Для входа нажмите кнопку ниже и поделитесь своим номером телефона."
),
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(AuthStates.waiting_contact)
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text=(
"👋 Добро пожаловать!\n\n"
"Поделитесь номером телефона для доступа к системе."
),
attachments=[contact_request_kb()],
)
return
if bot_user["is_blocked"]:
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text="⛔ Доступ заблокирован. Обратитесь к администратору.",
)
return
if not bot_user["is_authorized"]:
await context.set_state(AuthStates.waiting_contact)
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text="Поделитесь номером телефона для доступа:",
attachments=[contact_request_kb()],
)
return
await context.set_state(None)
from bot.handlers.menu import build_main_menu_text
menu_text = await build_main_menu_text(bot_user)
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text=menu_text,
attachments=[main_menu_kb()],
)
@router.message_created(ContactFilter(), states=AuthStates.waiting_contact)
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="Не удалось прочитать контакт. Попробуйте ещё раз.",
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="В контакте не найден номер телефона. Попробуйте ещё раз.",
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(None)
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text=(
"⛔ Ваш номер не найден в системе.\n"
"Доступ заблокирован. Обратитесь к администратору для подключения."
),
)
return
# Авторизуем
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(None)
from bot.handlers.menu import build_main_menu_text # local import to avoid circular
menu_text = "✅ Вы успешно авторизованы!\n\n" + await build_main_menu_text(bot_user)
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text=menu_text,
attachments=[main_menu_kb()],
)
# ── /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(None)
from bot.handlers.menu import build_main_menu_text
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()],
)
# ── /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
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 = "❌ Действие отменено.\n\n"
else:
cancel_text = ""
await context.set_state(None)
from bot.handlers.menu import build_main_menu_text
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()],
)