""" Telegram bot handlers для обработки команд пользователей """ import logging from typing import Optional from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove from telegram.ext import ( Application, CommandHandler, ContextTypes, MessageHandler, filters, ConversationHandler, ) from telegram.constants import ParseMode from telegram.error import TelegramError from django.conf import settings from django.utils import timezone from .telegram_models import TelegramUser from employees.models import Employee logger = logging.getLogger(__name__) # Состояния для ConversationHandler WAITING_FOR_CONTACT = 1 class TelegramBotHandlers: """ Обработчики команд Telegram бота """ @staticmethod async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /start - приветствие и инструкции """ user = update.effective_user # Проверяем, привязан ли аккаунт try: tg_user = TelegramUser.objects.get( telegram_id=user.id, is_active=True ) # Аккаунт уже привязан employee = tg_user.user await update.message.reply_text( f"👋 Привет, {employee.first_name}!\n\n" f"Ваш аккаунт уже привязан к EUSRR.\n\n" f"Доступные команды:\n" f"/settings - Настройки уведомлений\n" f"/unlink - Отвязать аккаунт\n" f"/help - Справка", parse_mode=ParseMode.HTML ) # Обновляем время взаимодействия tg_user.update_last_interaction() except TelegramUser.DoesNotExist: # Аккаунт не привязан - показываем инструкцию await update.message.reply_text( "👋 Добро пожаловать в бот EUSRR!\n\n" "Этот бот будет отправлять вам уведомления из системы EUSRR.\n\n" "🔗 Для привязки аккаунта:\n" "1. Войдите в личный кабинет EUSRR\n" "2. Перейдите в настройки уведомлений\n" "3. Нажмите 'Привязать Telegram'\n" "4. Получите код привязки\n" "5. Отправьте команду: /link ВАШ_КОД\n\n" "❓ Помощь: /help", parse_mode=ParseMode.HTML ) @staticmethod async def link_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /link CODE - запрашивает код и контакт для привязки """ user = update.effective_user # Проверяем что код указан if not context.args or len(context.args) == 0: await update.message.reply_text( "❌ Укажите код привязки.\n\n" "Использование: /link ВАШ_КОД\n\n" "Код можно получить в настройках уведомлений EUSRR.", parse_mode=ParseMode.HTML ) return link_code = context.args[0].upper() # Проверяем что аккаунт еще не привязан if TelegramUser.objects.filter( telegram_id=user.id, is_active=True ).exists(): await update.message.reply_text( "⚠️ Ваш Telegram уже привязан к аккаунту EUSRR.\n\n" "Если хотите привязать другой аккаунт, " "сначала используйте /unlink", parse_mode=ParseMode.HTML ) return # Ищем пользователя с таким кодом try: tg_user = TelegramUser.objects.select_related('user').get( link_code=link_code, telegram_id__isnull=True ) # Проверяем срок действия кода if not tg_user.is_link_code_valid(): await update.message.reply_text( "❌ Код привязки истёк.\n\n" "Пожалуйста, получите новый код в настройках EUSRR.", parse_mode=ParseMode.HTML ) return # Сохраняем код в контексте для проверки контакта context.user_data['pending_link_code'] = link_code context.user_data['pending_tg_user_id'] = tg_user.id # Создаем кнопку "Поделиться контактом" from telegram import KeyboardButton, ReplyKeyboardMarkup keyboard = ReplyKeyboardMarkup( [[KeyboardButton("📱 Поделиться контактом", request_contact=True)]], one_time_keyboard=True, resize_keyboard=True ) await update.message.reply_text( "🔐 Подтверждение личности\n\n" "Для безопасности нажмите кнопку ниже, " "чтобы поделиться вашим контактом.\n\n" "Мы проверим что он совпадает с данными, " "указанными при регистрации в EUSRR.", parse_mode=ParseMode.HTML, reply_markup=keyboard ) except TelegramUser.DoesNotExist: await update.message.reply_text( "❌ Неверный код привязки.\n\n" "Проверьте код и попробуйте снова.\n" "Если код истёк, получите новый в настройках EUSRR.", parse_mode=ParseMode.HTML ) @staticmethod async def contact_received( update: Update, context: ContextTypes.DEFAULT_TYPE ): """ Обработка полученного контакта для верификации """ from telegram import ReplyKeyboardRemove user = update.effective_user contact = update.message.contact # Проверяем что контакт от самого пользователя if contact.user_id != user.id: await update.message.reply_text( "❌ Пожалуйста, поделитесь своим контактом, " "а не контактом другого пользователя.", parse_mode=ParseMode.HTML, reply_markup=ReplyKeyboardRemove() ) return # Проверяем что есть pending привязка pending_code = context.user_data.get('pending_link_code') pending_tg_user_id = context.user_data.get('pending_tg_user_id') if not pending_code or not pending_tg_user_id: await update.message.reply_text( "❌ Нет активной попытки привязки.\n\n" "Сначала используйте команду /link КОД", parse_mode=ParseMode.HTML, reply_markup=ReplyKeyboardRemove() ) return try: tg_user = TelegramUser.objects.select_related('user').get( id=pending_tg_user_id, link_code=pending_code ) employee = tg_user.user # Проверяем совпадение данных telegram_field = employee.telegram.strip().lower() # Получаем username и phone из контакта contact_username = f"@{contact.username}".lower() \ if contact.username else None contact_phone = contact.phone_number # Проверка совпадения match_found = False match_type = "" if telegram_field: # Проверка по username if contact_username and telegram_field == contact_username: match_found = True match_type = f"username ({contact_username})" # Проверка по номеру телефона elif contact_phone: # Нормализуем номера (убираем + и пробелы) normalized_contact = contact_phone.replace('+', '') \ .replace(' ', '').replace('-', '') normalized_field = telegram_field.replace('+', '') \ .replace(' ', '').replace('-', '') if normalized_contact == normalized_field: match_found = True match_type = f"номер телефона ({contact_phone})" if not match_found: await update.message.reply_text( "❌ Данные не совпадают\n\n" f"Ваш контакт:\n" f"Username: {contact_username or 'не указан'}\n" f"Телефон: {contact_phone or 'не указан'}\n\n" f"В профиле EUSRR указано: {telegram_field or 'не указано'}\n\n" "Обновите поле 'Telegram' в вашем профиле EUSRR " "и попробуйте снова.", parse_mode=ParseMode.HTML, reply_markup=ReplyKeyboardRemove() ) # Очищаем pending данные context.user_data.clear() return # ✅ Данные совпали - привязываем аккаунт tg_user.telegram_id = user.id tg_user.telegram_username = user.username or '' tg_user.first_name = user.first_name or '' tg_user.last_name = user.last_name or '' tg_user.is_active = True tg_user.is_blocked = False tg_user.clear_link_code() tg_user.update_last_interaction() tg_user.save() await update.message.reply_text( f"✅ Аккаунт успешно привязан!\n\n" f"Подтверждено через: {match_type}\n\n" f"👤 Привязан к: {employee.get_full_name()}\n" f"📧 Email: {employee.email}\n\n" f"Теперь вы будете получать уведомления EUSRR в Telegram.\n\n" f"Доступные команды:\n" f"/settings - Настройки уведомлений\n" f"/unlink - Отвязать аккаунт\n" f"/help - Справка", parse_mode=ParseMode.HTML, reply_markup=ReplyKeyboardRemove() ) logger.info( f"Telegram привязан через {match_type}: " f"{user.id} -> {employee.email}" ) # Очищаем pending данные context.user_data.clear() except TelegramUser.DoesNotExist: await update.message.reply_text( "❌ Код привязки истёк или недействителен.\n\n" "Получите новый код в настройках EUSRR.", parse_mode=ParseMode.HTML, reply_markup=ReplyKeyboardRemove() ) context.user_data.clear() @staticmethod async def unlink_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /unlink - отвязка аккаунта """ user = update.effective_user try: tg_user = TelegramUser.objects.get( telegram_id=user.id, is_active=True ) employee_name = tg_user.user.get_full_name() # Отвязываем аккаунт tg_user.is_active = False tg_user.save() await update.message.reply_text( f"✅ Аккаунт отвязан\n\n" f"Связь с {employee_name} удалена.\n" f"Вы больше не будете получать уведомления.\n\n" f"Для повторной привязки используйте /link", parse_mode=ParseMode.HTML ) logger.info(f"Telegram аккаунт отвязан: {user.id}") except TelegramUser.DoesNotExist: await update.message.reply_text( "⚠️ Ваш аккаунт не привязан к EUSRR.\n\n" "Используйте /start для инструкций по привязке.", parse_mode=ParseMode.HTML ) @staticmethod async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /settings - показать текущие настройки """ user = update.effective_user try: tg_user = TelegramUser.objects.get( telegram_id=user.id, is_active=True ) employee = tg_user.user # Получаем настройки уведомлений from .models import UserNotificationSettings, NotificationCategory settings_text = f"⚙️ Настройки уведомлений\n\n" settings_text += f"👤 Пользователь: {employee.get_full_name()}\n" settings_text += f"📧 Email: {employee.email}\n" settings_text += f"🔗 Telegram: @{tg_user.telegram_username or 'не указан'}\n\n" settings_text += "Категории уведомлений:\n\n" categories = NotificationCategory.objects.all() for category in categories: try: user_settings = UserNotificationSettings.objects.get( user=employee, category=category ) # Эмодзи для категорий emoji_map = { 'communications': '💬', 'documents': '📄', 'requests': '📋', 'calendar': '📅', 'department': '👥', 'profile': '👤', 'feed': '📰', 'system': '⚙️', } emoji = emoji_map.get(category.code, '🔔') status = "✅" if user_settings.send_telegram else "❌" settings_text += f"{emoji} {category.name}: {status}\n" except UserNotificationSettings.DoesNotExist: pass # Создаем кнопку для перехода в настройки site_url = getattr(settings, 'SITE_URL', 'http://localhost:9000') keyboard = InlineKeyboardMarkup([ [InlineKeyboardButton( "🔧 Изменить настройки", url=f"{site_url}/notifications/settings/" )] ]) await update.message.reply_text( settings_text, parse_mode=ParseMode.HTML, reply_markup=keyboard ) tg_user.update_last_interaction() except TelegramUser.DoesNotExist: await update.message.reply_text( "⚠️ Ваш аккаунт не привязан к EUSRR.\n\n" "Используйте /start для инструкций по привязке.", parse_mode=ParseMode.HTML ) @staticmethod async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Команда /help - справка """ await update.message.reply_text( "📖 Справка по командам бота EUSRR\n\n" "Основные команды:\n\n" "/start - Приветствие и инструкции\n" "/link КОД - Привязать аккаунт EUSRR\n" "/unlink - Отвязать аккаунт\n" "/settings - Показать текущие настройки\n" "/help - Эта справка\n\n" "Как привязать аккаунт:\n" "1. Войдите в личный кабинет EUSRR\n" "2. Откройте настройки уведомлений\n" "3. Нажмите 'Привязать Telegram'\n" "4. Скопируйте код привязки\n" "5. Отправьте: /link ВАШ_КОД\n\n" "О боте:\n" "Этот бот отправляет уведомления из системы EUSRR " "о новых сообщениях, документах, заявках и других событиях.\n\n" "Настроить какие уведомления получать можно в личном кабинете EUSRR.", parse_mode=ParseMode.HTML ) @staticmethod async def unknown_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Обработчик неизвестных команд и текстовых сообщений """ await update.message.reply_text( "❓ Неизвестная команда.\n\n" "Используйте /help для списка доступных команд.", parse_mode=ParseMode.HTML ) def create_telegram_bot_application() -> Optional[Application]: """ Создает и настраивает приложение Telegram бота Returns: Настроенное приложение или None если токен не настроен """ token = getattr(settings, 'TELEGRAM_BOT_TOKEN', None) if not token: logger.error("TELEGRAM_BOT_TOKEN не настроен в settings") return None # Создаем приложение application = Application.builder().token(token).build() # Регистрируем handlers handlers = TelegramBotHandlers application.add_handler(CommandHandler("start", handlers.start_command)) application.add_handler(CommandHandler("link", handlers.link_command)) application.add_handler(CommandHandler("unlink", handlers.unlink_command)) application.add_handler( CommandHandler("settings", handlers.settings_command) ) application.add_handler(CommandHandler("help", handlers.help_command)) # Обработчик контактов (для верификации при привязке) application.add_handler( MessageHandler(filters.CONTACT, handlers.contact_received) ) # Обработчик неизвестных команд и текстовых сообщений application.add_handler( MessageHandler( filters.COMMAND | filters.TEXT, handlers.unknown_command ) ) logger.info("Telegram bot application создан") return application async def run_telegram_bot(): """ Запускает Telegram бота в режиме polling """ application = create_telegram_bot_application() if not application: logger.error("Не удалось создать Telegram bot application") return logger.info("Запуск Telegram бота...") try: # Инициализация await application.initialize() await application.start() # Запуск polling await application.updater.start_polling( allowed_updates=Update.ALL_TYPES ) logger.info("Telegram бот запущен и слушает обновления") # Ожидание остановки await application.updater.stop() await application.stop() await application.shutdown() except Exception as e: logger.error(f"Ошибка при запуске Telegram бота: {e}", exc_info=True)