Initial commit: TrueConf Chatbot КЛЕВЕР
This commit is contained in:
+682
@@ -0,0 +1,682 @@
|
||||
from utils.texts import *
|
||||
# /opt/trueconf_bot/lk/handlers.py
|
||||
import re
|
||||
import os
|
||||
import csv
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
import smtplib
|
||||
from datetime import datetime, timedelta
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from ldap3 import Server, Connection, ALL
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
from trueconf import Router, Message
|
||||
from trueconf.types import FSInputFile
|
||||
|
||||
import config.config as config
|
||||
import lk.payslip_service as ps
|
||||
import lk.documents_service as ds
|
||||
from utils.states import get_state, set_state, clear_state
|
||||
|
||||
# 🔌 Импортируем централизованную функцию сбора статистики из main
|
||||
from utils.stats_logger import log_menu_stats
|
||||
|
||||
# 🔌 Импортируем функции безопасности из изолированного утилитного модуля 2FA
|
||||
from utils.otp_service import generate_otp_code, send_otp_via_api, get_user_phone_from_ad
|
||||
|
||||
# 🔌 Импортируем вынесенную бизнес-логику запросов по CARD_ID
|
||||
from lk.info_service import (
|
||||
get_vacation_records,
|
||||
get_rewards_text,
|
||||
get_sick_leaves_text,
|
||||
get_tabel_report_text,
|
||||
get_lk_profile_by_card_id,
|
||||
get_missions_records,
|
||||
get_transfers_records
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =========================================================
|
||||
# ГЛОБАЛЬНЫЙ МАРШРУТИЗАТОР ЦЕЛЬНЫХ VK-ЭМОДЗИ И СЕССИЙ ПАГИНАЦИИ
|
||||
# =========================================================
|
||||
EMOJI_DIGITS = {
|
||||
"0": "0⃣",
|
||||
"1": "1⃣",
|
||||
"2": "2⃣",
|
||||
"3": "3⃣",
|
||||
"4": "4⃣",
|
||||
"5": "5⃣",
|
||||
"6": "6⃣",
|
||||
"7": "7⃣",
|
||||
"8": "8⃣",
|
||||
"9": "9⃣",
|
||||
"10": "🔟"
|
||||
}
|
||||
|
||||
# Список из 7 справок строго по структуру портала
|
||||
DOC_TYPES = {
|
||||
"1": "Справка о доходах физического лица (2-НДФЛ)",
|
||||
"2": "Справка об удержаниях за ДМС",
|
||||
"3": "Справка о средней заработной плате",
|
||||
"4": "Справка о получении единовременного пособия по рождению ребенка",
|
||||
"5": "Справка о получении ежемесячного пособия и назначении отпуска по уходу за ребенком",
|
||||
"6": "Справка об удержаниях по исполнительным листам",
|
||||
"7": "Копия трудовой книжки"
|
||||
}
|
||||
|
||||
LK_DOCS_MENU_TEXT = (
|
||||
"📄 <b>Выберите тип документа:</b>\n\n" +
|
||||
"\n".join([f"{EMOJI_DIGITS[k]} — {v}" for k, v in DOC_TYPES.items()]) +
|
||||
f"\n\n<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||
)
|
||||
|
||||
USER_COOLDOWN_SEC = 1.5
|
||||
EMAIL_COOLDOWN_SEC = 10.0
|
||||
|
||||
user_last_request = {}
|
||||
last_email_time = 0.0
|
||||
user_docs_session = {}
|
||||
user_page_sessions = {}
|
||||
|
||||
# 🔒 Репозиторий временных блокировок 2FA (хранит user_id: timestamp_разблокировки)
|
||||
user_payslip_blocks = {}
|
||||
|
||||
router = Router()
|
||||
|
||||
# =========================================================
|
||||
# СИСТЕМНЫЕ ФУНКЦИИ И ОШИБКИ
|
||||
# =========================================================
|
||||
def log_and_notify_error(user_id: str, action: str, error_details: str):
|
||||
global last_email_time
|
||||
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "lk", "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, "lk_errors.log")
|
||||
current_time = time.time()
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
log_entry = f"[{timestamp}] User: {user_id} | Action: {action} | Error: {error_details}\n"
|
||||
|
||||
with open(log_file, "a", encoding="utf-8") as f:
|
||||
f.write(log_entry)
|
||||
|
||||
if current_time - last_email_time < EMAIL_COOLDOWN_SEC:
|
||||
return
|
||||
last_email_time = current_time
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = str(getattr(config, 'DEFAULT_EMAIL_FROM', 'bot@noreply.com'))
|
||||
to_emails = getattr(config, 'ALERTS_SUPPORT_EMAILS', 'admin@example.com')
|
||||
msg['To'] = ", ".join(to_emails) if isinstance(to_emails, list) else str(to_emails)
|
||||
msg['Subject'] = f"TrueConf Bot Error: ЛК - {action}"
|
||||
body = f"Обнаружена ошибка в ЛК.\nВремя: {timestamp}\nПользователь: {user_id}\nДействие: {action}\nДетали:\n{error_details}"
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
import lk.info_service as info_service
|
||||
with smtplib.SMTP(getattr(config, 'SMTP_SERVER', 'localhost'), int(getattr(config, 'SMTP_PORT', 25))) as server:
|
||||
server.send_message(msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def handle_lk_error(msg: Message, user_id: str, action: str, error_details: str):
|
||||
await asyncio.to_thread(log_and_notify_error, user_id, action, error_details)
|
||||
error_msg = lk_error_handling(EMOJI_DIGITS)
|
||||
await msg.answer(error_msg, parse_mode="html")
|
||||
|
||||
# =========================================================
|
||||
# ИНТЕГРАЦИЯ С ACTIVE DIRECTORY ЧЕРЕЗ extensionAttribute2
|
||||
# =========================================================
|
||||
def get_card_id_from_ad(user_id: str) -> str:
|
||||
try:
|
||||
server = Server(config.AD_SERVER, get_info=ALL)
|
||||
conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
||||
safe_user_id = escape_filter_chars(user_id)
|
||||
short_username = user_id.split('@')[0]
|
||||
search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))"
|
||||
|
||||
conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2'])
|
||||
|
||||
if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value:
|
||||
return str(conn.entries[0].extensionAttribute2.value).strip()
|
||||
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
||||
except Exception as e: raise Exception(f"AD Card Query Error: {e}")
|
||||
|
||||
# =========================================================
|
||||
# МОДНОЕ УНИВЕРСАЛЬНОЕ ЯДРО ПАГИНАЦИИ СПИСКОВ ДЛЯ ЧАТА
|
||||
# =========================================================
|
||||
async def send_paginated_page(msg: Message, user_id: str):
|
||||
session = user_page_sessions.get(user_id)
|
||||
if not session: return
|
||||
|
||||
current_page = session["current_page"]
|
||||
per_page = session["per_page"]
|
||||
items = session["items"]
|
||||
|
||||
total_items = len(items)
|
||||
total_pages = (total_items + per_page - 1) // per_page if total_items > 0 else 1
|
||||
|
||||
start_idx = current_page * per_page
|
||||
end_idx = start_idx + per_page
|
||||
page_items = items[start_idx:end_idx]
|
||||
|
||||
if session["type"] == "vacation":
|
||||
body_text = session["static_header"] + "\n📜 <b>История отпусков (полный архив):</b>\n"
|
||||
body_text += "\n".join(page_items) + "\n" if page_items else "• <i>История отпусков пуста</i>\n"
|
||||
elif session["type"] == "missions":
|
||||
body_text = "🧳 <b>Информация о командировках (архив):</b>\n\n"
|
||||
body_text += "\n\n".join(page_items) + "\n" if page_items else "• <i>Записи о командировках отсутствуют</i>\n"
|
||||
elif session["type"] == "transfers":
|
||||
body_text = "🔄 <b>История кадровых перемещений:</b>\n\n"
|
||||
body_text += "\n\n".join(page_items) + "\n" if page_items else "• <i>Записи о перемещениях отсутствуют</i>\n"
|
||||
|
||||
body_text += f"\n📄 <b>Страница {current_page + 1} из {total_pages}</b>\n\n"
|
||||
|
||||
nav_lines = []
|
||||
if current_page > 0: nav_lines.append(f"{EMOJI_DIGITS['7']} — Предыдущая страница")
|
||||
if current_page < total_pages - 1: nav_lines.append(f"{EMOJI_DIGITS['8']} — Следующая страница")
|
||||
nav_lines.append(f"{EMOJI_DIGITS['9']} — Назад")
|
||||
nav_lines.append(f"{EMOJI_DIGITS['0']} — В главное меню")
|
||||
|
||||
body_text += "\n".join(nav_lines)
|
||||
await msg.answer(body_text, parse_mode="html")
|
||||
|
||||
# =========================================================
|
||||
# ДИНАМИЧЕСКАЯ СБОРКА ГЛАВНОГО МЕНЮ ЛК
|
||||
# =========================================================
|
||||
async def send_dynamic_lk_main_menu(msg: Message, user_id: str):
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
profile = await asyncio.to_thread(get_lk_profile_by_card_id, card_id)
|
||||
|
||||
header = (
|
||||
f"👤 <b>Личный кабинет</b>\n\n"
|
||||
f"🏢 <b>Компания:</b> {profile['company']}\n"
|
||||
f"📂 <b>Отдел:</b> {profile['dept']}\n"
|
||||
f"💼 <b>Должность:</b> {profile['job']}\n"
|
||||
f"⏳ <b>Страховой стаж:</b> {profile['stag_o']}\n"
|
||||
f"📈 <b>Стаж на предприятии:</b> {profile['stag_p']}\n\n"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось собрать динамический заголовок ЛК: {e}")
|
||||
header = "👤 <b>Личный кабинет</b>\n\n"
|
||||
|
||||
menu_text = lk_main_menu_text(header, EMOJI_DIGITS)
|
||||
await msg.answer(menu_text, parse_mode="html")
|
||||
|
||||
async def send_tabel_months_menu(msg: Message, user_id: str):
|
||||
now = datetime(2026, 6, 3)
|
||||
prev_1 = now.replace(day=1) - timedelta(days=1)
|
||||
prev_2 = prev_1.replace(day=1) - timedelta(days=1)
|
||||
months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
|
||||
menu_text = lk_tabel_months_menu_text(prev_1, prev_2, months_ru, EMOJI_DIGITS)
|
||||
set_state(user_id, "LK_TABEL_MONTHS")
|
||||
await msg.answer(menu_text, parse_mode="html")
|
||||
|
||||
def wrapper_get_months(card_id: str) -> list:
|
||||
import lk.info_service as info_service
|
||||
conn = info_service.get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
try: return ps.get_latest_payslip_months(cursor, card_id)
|
||||
finally: conn.close()
|
||||
|
||||
def wrapper_get_payslip(card_id: str, target_cmonth: int):
|
||||
import lk.info_service as info_service
|
||||
conn = info_service.get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
try: return ps.build_payslip_data(cursor, card_id, target_cmonth)
|
||||
except Exception as e: raise Exception(f"Payslip Build Error: {e}")
|
||||
finally: conn.close()
|
||||
|
||||
async def send_payslip_months_menu(msg: Message, user_id: str, card_id: str):
|
||||
try:
|
||||
months_data = await asyncio.to_thread(wrapper_get_months, card_id)
|
||||
months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
menu_text = lk_payslip_months_menu_text(months_data, months_ru, EMOJI_DIGITS)
|
||||
set_state(user_id, "LK_PAYSLIP_MONTHS")
|
||||
await msg.answer(menu_text, parse_mode="html")
|
||||
except Exception as e: raise Exception(str(e))
|
||||
|
||||
async def build_confirmation_text(user_id: str, doc: dict) -> str:
|
||||
confirm_txt = f"📝 <b>Подтверждение заказа:</b>\n\nДокумент: {doc['doc_name']}\nКоличество: {doc['quantity']} шт."
|
||||
if doc.get('period'): confirm_txt += f"\nУточнение: {doc['period']}"
|
||||
if getattr(config, 'DOCUMENTS_TEST_MODE', False):
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
import lk.info_service as info_service
|
||||
with info_service.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
user_info = await asyncio.to_thread(ds.get_user_info_for_docs, cursor, card_id)
|
||||
|
||||
orig_to = user_info.get('hr_email', '').strip()
|
||||
orig_cc = user_info.get('wb_email', '').strip() if doc['doc_id'] == "7" else ""
|
||||
requested_recipients = orig_to
|
||||
if orig_cc: requested_recipients += f" (Копия: {orig_cc})"
|
||||
test_email = getattr(config, 'DOCUMENTS_TARGET_EMAIL', 'mailonline@sibcem.ru')
|
||||
confirm_txt += f"\n\n⚠️ <b>Тест-режим.</b>\nКому: <code>{requested_recipients}</code>\nЗамена: <code>{test_email}</code>"
|
||||
except Exception as e: confirm_txt += f"\n\n⚠️ <b>Тест-режим.</b>\n(Ошибка БД: {e})"
|
||||
confirm_txt += f"\n\n<i>{EMOJI_DIGITS['1']} — Да, отправить\n{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||
return confirm_txt
|
||||
|
||||
# =========================================================
|
||||
# ОСНОВНОЙ ОБРАБОТЧИК МАРШРУТОВ ЛК
|
||||
# =========================================================
|
||||
@router.message()
|
||||
async def lk_router_handler(msg: Message):
|
||||
user_id = msg.from_user.id
|
||||
current_time = time.time()
|
||||
if current_time - user_last_request.get(user_id, 0) < USER_COOLDOWN_SEC: return
|
||||
user_last_request[user_id] = current_time
|
||||
|
||||
current_state = get_state(user_id)
|
||||
nav_text = lk_nav_text(EMOJI_DIGITS)
|
||||
|
||||
if not msg.text: return
|
||||
cmd = msg.text.strip().lower()
|
||||
|
||||
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
||||
if cmd == emoji_num:
|
||||
cmd = raw_num
|
||||
break
|
||||
|
||||
if current_state is None:
|
||||
from utils.menu import MENU_MAP
|
||||
if MENU_MAP.get(cmd) == "LK":
|
||||
msg.handled = True
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
return
|
||||
return
|
||||
|
||||
if current_state == "LK_MODE":
|
||||
msg.handled = True
|
||||
if cmd == "0":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Выход в главное меню")
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "1":
|
||||
block_expiry = user_payslip_blocks.get(user_id, 0)
|
||||
if time.time() < block_expiry:
|
||||
rem_minutes = int((block_expiry - time.time()) // 60) + 1
|
||||
await msg.answer(
|
||||
f"❌ <b>Доступ временно заблокирован.</b>\n"
|
||||
f"Вы трижды ввели неверный код подтверждения.\n"
|
||||
f"Доступ к расчетным листам ограничен на {int(getattr(config, 'OTP_BLOCK_DURATION_SEC', 1800) // 60)} минут.\n\n"
|
||||
f"⏱ Осталось подождать: <b>{rem_minutes} мин.</b>{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
return
|
||||
|
||||
log_menu_stats(user_id, "Личный кабинет", "Расчётный лист")
|
||||
await msg.answer(lk_payslip_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
await send_payslip_months_menu(msg, user_id, card_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Меню расчетных листков", str(e))
|
||||
elif cmd == "2":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Информация о днях отпуска")
|
||||
await msg.answer(lk_vacation_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
static_header, history_lines = await asyncio.to_thread(get_vacation_records, card_id)
|
||||
user_page_sessions[user_id] = {"type": "vacation", "current_page": 0, "per_page": 5, "static_header": static_header, "items": history_lines}
|
||||
set_state(user_id, "LK_VACATION_VIEW")
|
||||
await send_paginated_page(msg, user_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Справка по отпускам", str(e))
|
||||
elif cmd == "3":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Больничные")
|
||||
await msg.answer(lk_sick_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
sick_info = await asyncio.to_thread(get_sick_leaves_text, card_id)
|
||||
set_state(user_id, "LK_SICK_VIEW")
|
||||
await msg.answer(sick_info + nav_text, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Просмотр больничных", str(e))
|
||||
elif cmd == "4":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Командировки")
|
||||
await msg.answer(lk_missions_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
mission_lines = await asyncio.to_thread(get_missions_records, card_id)
|
||||
user_page_sessions[user_id] = {"type": "missions", "current_page": 0, "per_page": 4, "items": mission_lines}
|
||||
set_state(user_id, "LK_MISSIONS_VIEW")
|
||||
await send_paginated_page(msg, user_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Просмотр командировок", str(e))
|
||||
elif cmd == "5":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Награды")
|
||||
await msg.answer(lk_rewards_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
rewards_info = await asyncio.to_thread(get_rewards_text, card_id)
|
||||
set_state(user_id, "LK_REWARDS_VIEW")
|
||||
await msg.answer(rewards_info + nav_text, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Запрос наград", str(e))
|
||||
elif cmd == "6":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Переводы")
|
||||
await msg.answer(lk_transfers_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
transfer_lines = await asyncio.to_thread(get_transfers_records, card_id)
|
||||
user_page_sessions[user_id] = {"type": "transfers", "current_page": 0, "per_page": 4, "items": transfer_lines}
|
||||
set_state(user_id, "LK_TRANSFERS_VIEW")
|
||||
await send_paginated_page(msg, user_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Просмотр переводов", str(e))
|
||||
elif cmd == "7":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Табель учёта рабочего времени")
|
||||
await send_tabel_months_menu(msg, user_id)
|
||||
elif cmd == "8":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Справки")
|
||||
set_state(user_id, "LK_DOCS_MENU")
|
||||
await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Вместо спама копии меню ЛК выводим строгое предупреждение
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_TABEL_MONTHS":
|
||||
msg.handled = True
|
||||
if cmd == "0":
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif cmd in ["1", "2"]:
|
||||
log_menu_stats(user_id, "Личный кабинет", "Просмотр табеля за месяц")
|
||||
await msg.answer(lk_tabel_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
now = datetime(2026, 6, 3)
|
||||
target_date = now.replace(day=1) - timedelta(days=1) if cmd == "1" else (now.replace(day=1) - timedelta(days=1)).replace(day=1) - timedelta(days=1)
|
||||
tabel_report = await asyncio.to_thread(get_tabel_report_text, card_id, target_date.year, target_date.month)
|
||||
set_state(user_id, "LK_TABEL_VIEW")
|
||||
await msg.answer(tabel_report + nav_text, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Формирование табеля", str(e))
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state in ["LK_VACATION_VIEW", "LK_MISSIONS_VIEW", "LK_TRANSFERS_VIEW"]:
|
||||
msg.handled = True
|
||||
session = user_page_sessions.get(user_id)
|
||||
if cmd == "0":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif cmd == "7" and session and session["current_page"] > 0:
|
||||
session["current_page"] -= 1
|
||||
await send_paginated_page(msg, user_id)
|
||||
elif cmd == "8" and session:
|
||||
if session["current_page"] < (len(session["items"]) + session["per_page"] - 1) // session["per_page"] - 1:
|
||||
session["current_page"] += 1
|
||||
await send_paginated_page(msg, user_id)
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_PAYSLIP_AUTH":
|
||||
msg.handled = True
|
||||
session = user_page_sessions.get(user_id)
|
||||
|
||||
if cmd == "0":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
return
|
||||
elif cmd == "9":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_PAYSLIP_MONTHS")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
await send_payslip_months_menu(msg, user_id, card_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Возврат к меню месяцев", str(e))
|
||||
return
|
||||
|
||||
if not session:
|
||||
await msg.answer("⚠️ <i>Сессия авторизации не найдена. Начните сначала.</i>", parse_mode="html")
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
return
|
||||
|
||||
if time.time() > session.get("expires_at", 0):
|
||||
log_menu_stats(user_id, "Личный кабинет", "Ошибка 2FA: Истекло время жизни кода")
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_MODE")
|
||||
await msg.answer(
|
||||
f"⏱ <b>Время действия кода подтверждения изменилось или истекло.</b>\n"
|
||||
f"Пожалуйста, запросите расчетный лист заново.{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
return
|
||||
|
||||
if cmd == session.get("correct_code"):
|
||||
log_menu_stats(user_id, "Личный кабинет", "Успешная проверка 2FA расчетного листа")
|
||||
await msg.answer(lk_auth_success(EMOJI_DIGITS), parse_mode="html")
|
||||
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
target_cmonth = session["target_cmonth"]
|
||||
|
||||
m_name = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
tmonth = (target_cmonth - 1) % 12 + 1
|
||||
tyear = (target_cmonth - 1) // 12
|
||||
|
||||
payslip_info, pdf_path = await asyncio.to_thread(wrapper_get_payslip, card_id, target_cmonth)
|
||||
|
||||
set_state(user_id, "LK_PAYSLIP_VIEW")
|
||||
await msg.answer_document(
|
||||
FSInputFile(pdf_path, filename=f"Расчетный_лист_{m_name.get(tmonth, '00')}_{tyear}.pdf"),
|
||||
caption=payslip_info + f"\n\n<i>{EMOJI_DIGITS['9']} — Назад к выбору месяца\n{EMOJI_DIGITS['0']} — В главное меню</i>",
|
||||
parse_mode="html"
|
||||
)
|
||||
if os.path.exists(pdf_path): os.remove(pdf_path)
|
||||
except Exception as e:
|
||||
await handle_lk_error(msg, user_id, "Просмотр детализации после 2FA", str(e))
|
||||
finally:
|
||||
user_page_sessions.pop(user_id, None)
|
||||
else:
|
||||
session["attempts_left"] -= 1
|
||||
if session["attempts_left"] <= 0:
|
||||
log_menu_stats(user_id, "Личный кабинет", "Ошибка 2FA: Превышен лимит попыток ввода, бан")
|
||||
|
||||
block_duration = int(getattr(config, 'OTP_BLOCK_DURATION_SEC', 1800))
|
||||
user_payslip_blocks[user_id] = time.time() + block_duration
|
||||
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_MODE")
|
||||
await msg.answer(
|
||||
f"❌ <b>Превышено максимальное количество попыток ввода кода.</b>\n"
|
||||
f"Доступ к финансовым документам заблокирован на <b>{block_duration // 60} минут</b>.{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
else:
|
||||
await msg.answer(
|
||||
f"⚠️ <b>Неверный код подтверждения.</b>\n"
|
||||
f"Осталось попыток: {session['attempts_left']}. Попробуйте еще раз или вернитесь назад.{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
|
||||
elif current_state in ["LK_REWARDS_VIEW", "LK_PAYSLIP_MONTHS", "LK_SICK_VIEW", "LK_TABEL_VIEW", "LK_STUB_VIEW"]:
|
||||
msg.handled = True
|
||||
if cmd == "0":
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
if current_state == "LK_TABEL_VIEW": await send_tabel_months_menu(msg, user_id)
|
||||
else: set_state(user_id, "LK_MODE"); await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif current_state == "LK_PAYSLIP_MONTHS" and cmd.isdigit():
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
months_data = await asyncio.to_thread(wrapper_get_months, card_id)
|
||||
idx = int(cmd) - 1
|
||||
|
||||
if 0 <= idx < len(months_data):
|
||||
target_cmonth = months_data[idx]
|
||||
|
||||
await msg.answer(lk_auth_processing(EMOJI_DIGITS), parse_mode="html")
|
||||
user_phone = await asyncio.to_thread(get_user_phone_from_ad, user_id)
|
||||
|
||||
otp_code = generate_otp_code(4)
|
||||
sms_success, channel_info = await send_otp_via_api(user_phone, otp_code)
|
||||
|
||||
if sms_success:
|
||||
log_menu_stats(user_id, "Личный кабинет", f"Запрос кода 2FA для расчетного листа ({channel_info})")
|
||||
masked_phone = f"{user_phone[:5]}***{user_phone[-4:]}" if len(user_phone) > 8 else user_phone
|
||||
|
||||
otp_ttl = int(getattr(config, 'OTP_TTL_SEC', 180))
|
||||
max_attempts = int(getattr(config, 'OTP_MAX_ATTEMPTS', 3))
|
||||
|
||||
user_page_sessions[user_id] = {
|
||||
"correct_code": otp_code,
|
||||
"target_cmonth": target_cmonth,
|
||||
"expires_at": time.time() + otp_ttl,
|
||||
"attempts_left": max_attempts
|
||||
}
|
||||
|
||||
set_state(user_id, "LK_PAYSLIP_AUTH")
|
||||
await msg.answer(
|
||||
f"🔐 <b>Двухфакторная авторизация</b>\n\n"
|
||||
f"Код подтверждения отправлен <b>{channel_info}</b> на номер <code>{masked_phone}</code>.\n"
|
||||
f"Пожалуйста, введите 4-значный код из сообщения в ответ:\n\n"
|
||||
f"<i>Код действителен в течение {otp_ttl // 60} минут. У вас есть {max_attempts} попытки.</i>{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Внутренний каскадный шлюз недоступен: {channel_info}")
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Инициализация 2FA расчетного листа", str(e))
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_PAYSLIP_VIEW":
|
||||
msg.handled = True
|
||||
if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
try: card_id = await asyncio.to_thread(get_card_id_from_ad, user_id); await send_payslip_months_menu(msg, user_id, card_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Возврат в меню месяцев", str(e))
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_MENU":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": user_docs_session.pop(user_id, None); set_state(user_id, "LK_MODE"); await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif cmd in DOC_TYPES:
|
||||
user_docs_session[user_id] = {'doc_id': cmd, 'doc_name': DOC_TYPES[cmd]}
|
||||
log_menu_stats(user_id, "Личный кабинет", f"Выбор справки: {DOC_TYPES[cmd]}")
|
||||
set_state(user_id, "LK_DOCS_QUANTITY")
|
||||
await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Заменен спам списка документов на чистую ошибку ввода
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_QUANTITY":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||
elif cmd.isdigit() and 0 < int(cmd) <= 5:
|
||||
user_docs_session[user_id]['quantity'] = int(cmd)
|
||||
doc_id = user_docs_session[user_id]['doc_id']
|
||||
|
||||
if doc_id in ["1", "2", "6"]:
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
current_year = datetime.now().year
|
||||
years_text = lk_years_menu(current_year, EMOJI_DIGITS)
|
||||
await msg.answer(years_text, parse_mode="html")
|
||||
elif doc_id == "3":
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
await msg.answer(lk_docs_avg_salary_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
elif doc_id == "5":
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
await msg.answer(lk_docs_child_age_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else:
|
||||
user_docs_session[user_id]['period'] = ""
|
||||
set_state(user_id, "LK_DOCS_CONFIRM")
|
||||
confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id])
|
||||
await msg.answer(confirm_txt, parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Вместо кастомной подсказки выводим единый стандарт
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_PERIOD":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": set_state(user_id, "LK_DOCS_QUANTITY"); await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else:
|
||||
doc_id = user_docs_session[user_id]['doc_id']
|
||||
if doc_id in ["1", "2", "6"]:
|
||||
if cmd in ["1", "2", "3", "4", "5", "6", "7", "8"]:
|
||||
user_docs_session[user_id]['period'] = f"за {datetime.now().year - (int(cmd) - 1)} год"
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Убран спам меню годов при абракадабре
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
return
|
||||
elif doc_id == "5":
|
||||
if cmd == "1": user_docs_session[user_id]['period'] = "до 1.5 лет"
|
||||
elif cmd == "2": user_docs_session[user_id]['period'] = "до 3 лет"
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Убран спам меню возраста при некорректном вводе
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
return
|
||||
elif doc_id == "3":
|
||||
user_docs_session[user_id]['period'] = f"за {cmd} мес."
|
||||
|
||||
set_state(user_id, "LK_DOCS_CONFIRM")
|
||||
confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id])
|
||||
await msg.answer(confirm_txt, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_CONFIRM":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
doc_id = user_docs_session.get(user_id, {}).get('doc_id')
|
||||
if doc_id in ["1", "2", "3", "5", "6"]:
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
if doc_id in ["1", "2", "6"]:
|
||||
current_year = datetime.now().year
|
||||
years_text = lk_years_menu(current_year, EMOJI_DIGITS)
|
||||
await msg.answer(years_text, parse_mode="html")
|
||||
elif doc_id == "3": await msg.answer(lk_docs_avg_salary_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
elif doc_id == "5": await msg.answer(lk_docs_child_age_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else: set_state(user_id, "LK_DOCS_QUANTITY"); await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
elif cmd == "1":
|
||||
await msg.answer("⏳ <i>Отправка заявки...</i>", parse_mode="html")
|
||||
try:
|
||||
doc = user_docs_session[user_id]
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
|
||||
import lk.info_service as info_service
|
||||
with info_service.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
user_info = await asyncio.to_thread(ds.get_user_info_for_docs, cursor, card_id)
|
||||
|
||||
success = await asyncio.to_thread(ds.generate_mail_message, doc['doc_name'], doc['quantity'], user_info, doc.get('period', ''))
|
||||
if success:
|
||||
log_menu_stats(user_id, "Личный кабинет", f"Заказ справки: {doc['doc_name']}")
|
||||
await msg.answer(f"✅ <b>Ваша заявка успешно отправлена!</b>\nСпециалисты свяжутся с вами по готовности документа.\n\n<i>{EMOJI_DIGITS['9']} — Назад в список справок\n{EMOJI_DIGITS['0']} — В главное меню</i>", parse_mode="html")
|
||||
set_state(user_id, "LK_DOCS_SUCCESS")
|
||||
else: raise Exception("Внутренняя ошибка SMTP при отправке письма")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, f"Заказ справки {user_docs_session.get(user_id, {}).get('doc_id')}", str(e)); set_state(user_id, "LK_MODE")
|
||||
finally: user_docs_session.pop(user_id, None)
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Удален спам-текст подтверждения
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_SUCCESS":
|
||||
msg.handled = True
|
||||
if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Унифицировано под общий шаблон ошибки
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
Reference in New Issue
Block a user