mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
1375 lines
74 KiB
Python
1375 lines
74 KiB
Python
"""
|
|
Обработчики пользовательских команд.
|
|
Отвечают за взаимодействие с пользователями и управление профилями.
|
|
"""
|
|
|
|
from typing import Dict, Callable, List, Tuple
|
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
from telegram.ext import ContextTypes
|
|
from .base_handler import BaseHandler
|
|
|
|
# Используем абсолютные импорты для избежания проблем с относительными импортами
|
|
import sys
|
|
import os
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from services.user_service import UserService
|
|
from services.welcome_service import WelcomeService
|
|
from utils.formatters import MessageFormatter, KeyboardFormatter
|
|
# Исправлено: теперь используем SQLite репозиторий вместо PostgreSQL database.py
|
|
from database.repository import UserRepository
|
|
|
|
# Банковские реквизиты для донатов
|
|
BANK_DETAILS_TEXT = """
|
|
🏦 Банк: Сбербанк
|
|
💳 Номер карты: 4276 3800 1234 5678
|
|
👤 Получатель: ИВАНОВ И.И.
|
|
|
|
📱 QIWI Кошелек: +79123456789
|
|
|
|
💰 ЮMoney: 410011234567890
|
|
|
|
После оплаты отправьте чек или подтверждение перевода администратору.
|
|
Спасибо за поддержку! ❤️
|
|
"""
|
|
|
|
# Список нецензурных слов для фильтрации
|
|
PROFANITY_WORDS = {
|
|
'пизда', 'пизде', 'пизду', 'пизды', 'хуй', 'хуя', 'хуе', 'хуи', 'хуйня', 'хуйню', 'хуйне',
|
|
'блядь', 'бляди', 'блядь', 'сука', 'суки', 'суку', 'ебать', 'ебал', 'ебла', 'ебли', 'ебло',
|
|
'ебан', 'ебана', 'ебану', 'ебаны', 'пидор', 'пидора', 'пидору', 'пидоры', 'пидар', 'пидара',
|
|
'пидару', 'пидары', 'гандон', 'гандона', 'гандону', 'гандоны', 'член', 'члена', 'члене',
|
|
'члену', 'члены', 'мудак', 'мудака', 'мудаку', 'мудаки', 'уебок', 'уебка', 'уебку', 'уебки',
|
|
'fuck', 'shit', 'bitch', 'ass', 'asshole', 'bastard', 'damn', 'cunt', 'dick', 'cock', 'pussy'
|
|
}
|
|
|
|
|
|
class UserHandlers(BaseHandler):
|
|
"""
|
|
Обработчики пользовательских команд.
|
|
|
|
Команды:
|
|
- /start - начало работы с ботом
|
|
- /help - помощь
|
|
- /rank - просмотр ранга
|
|
- /leaderboard - таблица лидеров
|
|
- /info - информация о пользователе
|
|
"""
|
|
|
|
def __init__(self, config, metrics, user_service: UserService, error_repo=None):
|
|
"""
|
|
Инициализация обработчика.
|
|
|
|
Args:
|
|
config: Конфигурация приложения
|
|
metrics: Сборщик метрик
|
|
user_service: Сервис управления пользователями
|
|
error_repo: Репозиторий ошибок (опционально)
|
|
"""
|
|
super().__init__(config, metrics, MessageFormatter())
|
|
self.keyboard_formatter = KeyboardFormatter()
|
|
self.user_service = user_service
|
|
self.welcome_service = WelcomeService()
|
|
self.error_repo = error_repo
|
|
|
|
# Исправление: добавляем недостающий атрибут
|
|
self._user_service = user_service
|
|
|
|
def get_command_handlers(self) -> Dict[str, Callable]:
|
|
"""Получение обработчиков команд"""
|
|
return {
|
|
'start': self.handle_start,
|
|
'help': self.handle_help,
|
|
'rank': self.handle_rank,
|
|
'leaderboard': self.handle_leaderboard,
|
|
'info': self.handle_info,
|
|
'weather': self.handle_weather,
|
|
'news': self.handle_news,
|
|
'translate': self.handle_translate,
|
|
'donate': self.handle_donate,
|
|
}
|
|
|
|
def get_callback_handlers(self) -> Dict[str, Callable]:
|
|
"""Получение обработчиков callback запросов"""
|
|
return {
|
|
'menu_main': self.handle_main_menu,
|
|
'menu_help': self.handle_help_menu,
|
|
'menu_rank': self.handle_rank_menu,
|
|
'menu_donate': self.handle_donate_menu,
|
|
'menu_leaderboard': self.handle_leaderboard_menu,
|
|
'donate_100': self.handle_donation_callback,
|
|
'donate_500': self.handle_donation_callback,
|
|
'donate_1000': self.handle_donation_callback,
|
|
'donate_2500': self.handle_donation_callback,
|
|
'donate_5000': self.handle_donation_callback,
|
|
'donate_custom': self.handle_donation_custom,
|
|
'donate_confirm': self.handle_donate_confirm_callback,
|
|
'menu_moderation': lambda u, c: None, # Stub - handled by AdminHandlers
|
|
'menu_admin': lambda u, c: None, # Stub - handled by AdminHandlers
|
|
'menu_triggers': lambda u, c: None, # Stub - handled by AdminHandlers
|
|
}
|
|
|
|
def get_message_handlers(self) -> Dict[str, Callable]:
|
|
"""Получение обработчиков сообщений"""
|
|
return {
|
|
'text': self.handle_text_message,
|
|
'voice': self.handle_voice_message,
|
|
'audio': self.handle_audio_message,
|
|
'video': self.handle_video_message,
|
|
}
|
|
|
|
def _generate_personalized_help(self, user_role, user_name=None):
|
|
"""Генерация персонализированной справки на основе роли пользователя"""
|
|
# Получаем доступные команды для роли
|
|
available_commands = self.welcome_service.get_available_commands_for_role(user_role)
|
|
|
|
# Формируем персонализированный заголовок
|
|
header_name = f", {user_name}" if user_name else ""
|
|
help_text = f"[HELP] <b>Команды бота{header_name}</b>\n\n"
|
|
|
|
# Добавляем команды по категориям
|
|
if 'user_commands' in available_commands:
|
|
help_text += "<b>📋 Основные команды:</b>\n"
|
|
for cmd in available_commands['user_commands'][:8]: # Ограничиваем количество для читаемости
|
|
help_text += f"• {cmd}\n"
|
|
help_text += "\n"
|
|
|
|
if 'games' in available_commands:
|
|
help_text += "<b>🎮 Игры:</b>\n"
|
|
for cmd in available_commands['games'][:5]: # Показываем основные игровые команды
|
|
help_text += f"• {cmd}\n"
|
|
help_text += "\n"
|
|
|
|
if 'moderation' in available_commands:
|
|
help_text += "<b>🛡️ Модерация:</b>\n"
|
|
for cmd in available_commands['moderation']:
|
|
help_text += f"• {cmd}\n"
|
|
help_text += "\n"
|
|
|
|
if 'admin' in available_commands:
|
|
help_text += "<b>👑 Администрирование:</b>\n"
|
|
for cmd in available_commands['admin'][:8]: # Ограничиваем количество
|
|
help_text += f"• {cmd}\n"
|
|
help_text += "\n"
|
|
|
|
if 'super_admin' in available_commands:
|
|
help_text += "<b>🔧 Система:</b>\n"
|
|
for cmd in available_commands['super_admin']:
|
|
help_text += f"• {cmd}\n"
|
|
help_text += "\n"
|
|
|
|
# Добавляем информацию о донатах для всех
|
|
help_text += "<b>💰 Поддержка проекта:</b>\n"
|
|
help_text += "• /donate - Поддержать проект донатом\n\n"
|
|
|
|
help_text += "<b>💡 Для получения дополнительной информации используйте меню бота.</b>"
|
|
|
|
return help_text
|
|
|
|
async def handle_start(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /start"""
|
|
await self.safe_execute(update, context, "start", self._handle_start)
|
|
|
|
async def _handle_start(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /start"""
|
|
user = update.effective_user
|
|
|
|
# Создаем или получаем профиль пользователя
|
|
# Раскомментировал вызов user_service.get_or_create_user() после тестирования
|
|
profile = await self.user_service.get_or_create_user(
|
|
user.id, user.username, user.first_name, user.last_name
|
|
)
|
|
|
|
# Получаем роль пользователя как enum
|
|
role_enum = self.user_service.get_user_role_enum(user.id)
|
|
print(f"[DEBUG] User {user.id} has role: {role_enum.value}") # ОТЛАДКА
|
|
|
|
# Получаем персонализированное приветствие от сервиса приветствий
|
|
welcome_text = self.welcome_service.get_welcome_message(
|
|
role_enum,
|
|
user.first_name
|
|
)
|
|
|
|
# Определяем тип чата
|
|
chat_type = "private" if update.message.chat.type == "private" else "group"
|
|
|
|
# Создаем клавиатуру главного меню с учетом роли пользователя и типа чата
|
|
keyboard = self.keyboard_formatter.create_main_menu(role_enum.value, chat_type)
|
|
print(f"[DEBUG] Created menu with {len(keyboard.inline_keyboard)} rows for role {role_enum.value} in {chat_type} chat") # ОТЛАДКА
|
|
|
|
# Выводим кнопки меню для отладки
|
|
for i, row in enumerate(keyboard.inline_keyboard):
|
|
for button in row:
|
|
print(f"[DEBUG] Menu button {i}: {button.text} -> {button.callback_data}") # ОТЛАДКА
|
|
|
|
reply_markup = keyboard
|
|
|
|
if update.message:
|
|
await update.message.reply_text(welcome_text, reply_markup=reply_markup)
|
|
else:
|
|
await self.send_response(update, welcome_text, reply_markup=reply_markup)
|
|
|
|
async def handle_help(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /help"""
|
|
await self.safe_execute(update, context, "help", self._handle_help)
|
|
|
|
async def _handle_help(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /help"""
|
|
user = update.effective_user
|
|
|
|
# Получаем роль пользователя как enum
|
|
role_enum = self.user_service.get_user_role_enum(user.id)
|
|
print(f"[DEBUG] Help handler - User {user.id} has role: {role_enum.value}") # ОТЛАДКА
|
|
|
|
# Получаем персонализированную справку через WelcomeService
|
|
help_text = self._generate_personalized_help(role_enum, user.first_name)
|
|
|
|
# Определяем тип чата для меню
|
|
chat_type = "private" if update.effective_chat.type == "private" else "group"
|
|
|
|
keyboard = self.keyboard_formatter.create_main_menu(role_enum.value, chat_type)
|
|
print(f"[DEBUG] Help handler - Created menu with {len(keyboard.inline_keyboard)} rows for role {role_enum.value} in {chat_type} chat") # ОТЛАДКА
|
|
|
|
# Определяем, откуда пришел запрос - из команды или из callback кнопки
|
|
message_sent = False
|
|
|
|
if update.message:
|
|
# Вызов из команды /help
|
|
await update.message.reply_text(help_text, parse_mode='HTML', reply_markup=keyboard)
|
|
message_sent = True
|
|
elif update.callback_query and update.callback_query.message:
|
|
# Вызов из кнопки меню помощи
|
|
try:
|
|
# Получаем текущий текст и клавиатуру сообщения для проверки необходимости редактирования
|
|
current_text = update.callback_query.message.text
|
|
current_markup = update.callback_query.message.reply_markup
|
|
|
|
# Проверяем, нужно ли редактировать сообщение
|
|
text_changed = current_text != help_text if current_text and help_text else False
|
|
markup_changed = self._compare_keyboards(current_markup, keyboard)
|
|
|
|
if text_changed or markup_changed:
|
|
await update.callback_query.edit_message_text(help_text, parse_mode='HTML', reply_markup=keyboard)
|
|
self.logger.debug("Сообщение помощи успешно отредактировано")
|
|
else:
|
|
# Логируем, что редактирование не требуется
|
|
self.logger.debug("Редактирование сообщения помощи не требуется - содержимое идентично")
|
|
message_sent = True
|
|
except Exception as e:
|
|
# Если редактирование не удалось (например, сообщение уже отредактировано), отправляем новое
|
|
self.logger.warning(f"Не удалось отредактировать сообщение в help: {e}")
|
|
await update.callback_query.message.reply_text(help_text, parse_mode='HTML', reply_markup=keyboard)
|
|
message_sent = True
|
|
|
|
# Если сообщение не было отправлено ни одним из способов
|
|
if not message_sent:
|
|
try:
|
|
# Fallback - отправляем в чат по умолчанию
|
|
if update.effective_chat:
|
|
await update.effective_chat.send_message(help_text, parse_mode='HTML', reply_markup=keyboard)
|
|
else:
|
|
print("Не удалось отправить сообщение помощи: нет доступного чата")
|
|
except Exception as e:
|
|
await self.send_response(update, "Не удалось отправить сообщение помощи", reply_markup=keyboard)
|
|
|
|
async def handle_rank(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /rank"""
|
|
await self.safe_execute(update, context, "rank", self._handle_rank)
|
|
|
|
async def _handle_rank(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /rank"""
|
|
user = update.effective_user
|
|
|
|
# Получаем профиль пользователя
|
|
profile = await self.user_service.get_or_create_user(
|
|
user.id, user.username, user.first_name, user.last_name
|
|
)
|
|
|
|
if not profile:
|
|
error_text = "❌ Ошибка: не удалось получить профиль пользователя"
|
|
await self.send_response(update, error_text)
|
|
return
|
|
|
|
# Получаем прогресс до следующего ранга
|
|
rank_progress = self.user_service.get_rank_progress(profile.reputation)
|
|
|
|
if not rank_progress:
|
|
error_text = "❌ Ошибка: не удалось получить информацию о прогрессе ранга"
|
|
await self.send_response(update, error_text)
|
|
return
|
|
|
|
# Формируем сообщение
|
|
rank_text = (
|
|
f"🏆 <b>Ваш ранг:</b>\n\n"
|
|
f"⭐ Очки: {profile.reputation}\n"
|
|
f"👑 Ранг: {profile.rank}\n"
|
|
f"💬 Сообщений: {profile.message_count}\n"
|
|
f"⚠️ Предупреждений: {profile.warnings}\n\n"
|
|
)
|
|
|
|
if rank_progress.get('next_rank') != profile.rank:
|
|
rank_text += (
|
|
f"📈 Прогресс до {rank_progress.get('next_rank', 'следующего ранга')}:\n"
|
|
f" {rank_progress.get('progress', 0)}/{rank_progress.get('needed', 0)} очков\n"
|
|
f" ({rank_progress.get('percentage', 0):.1f}%)\n"
|
|
)
|
|
|
|
# Используем унифицированный метод отправки сообщений
|
|
await self.send_response(update, rank_text, parse_mode='HTML')
|
|
|
|
async def handle_leaderboard(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /leaderboard"""
|
|
await self.safe_execute(update, context, "leaderboard", self._handle_leaderboard)
|
|
|
|
async def _handle_leaderboard(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /leaderboard"""
|
|
# Получаем топ пользователей
|
|
top_users = await self.user_service.get_top_users(10)
|
|
|
|
# Формируем сообщение
|
|
if top_users:
|
|
leaderboard_text = "🏆 <b>Таблица лидеров:</b>\n\n"
|
|
|
|
for i, (user_id, username, first_name, score) in enumerate(top_users, 1):
|
|
name = username if username else first_name or f"Пользователь {user_id}"
|
|
medal = self._get_medal(i)
|
|
leaderboard_text += f"{medal} <b>{name}</b> - {score} очков\n"
|
|
else:
|
|
leaderboard_text = "📭 Таблица лидеров пуста"
|
|
|
|
await self.send_response(update, leaderboard_text, parse_mode='HTML')
|
|
|
|
async def handle_info(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /info"""
|
|
await self.safe_execute(update, context, "info", self._handle_info)
|
|
|
|
async def _handle_info(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /info"""
|
|
user = update.effective_user
|
|
|
|
# Проверяем права администратора для просмотра чужих профилей
|
|
is_admin = await self.is_admin(update, user.id)
|
|
|
|
if context.args and is_admin:
|
|
# Администратор просматривает чужой профиль
|
|
target_user_id = self.extract_user_id_from_args(context.args)
|
|
if target_user_id:
|
|
profile = await self.user_service.get_or_create_user(target_user_id, None, None, None)
|
|
if not profile:
|
|
error_text = "❌ Ошибка: не удалось получить профиль пользователя"
|
|
await self.send_response(update, error_text)
|
|
return
|
|
print(f"[DEBUG] _handle_info: showing profile for target_user_id={target_user_id}, warnings={profile.warnings}")
|
|
info_text = self._format_user_info(profile, target_user_id)
|
|
else:
|
|
info_text = "❌ Неверный ID пользователя"
|
|
else:
|
|
# Пользователь просматривает свой профиль
|
|
profile = await self.user_service.get_or_create_user(
|
|
user.id, user.username, user.first_name, user.last_name
|
|
)
|
|
if not profile:
|
|
error_text = "❌ Ошибка: не удалось получить профиль пользователя"
|
|
await self.send_response(update, error_text)
|
|
return
|
|
print(f"[DEBUG] _handle_info: showing profile for user_id={user.id}, warnings={profile.warnings}")
|
|
# Исправлено: теперь UserService использует SQLite репозиторий, данные должны быть согласованы
|
|
info_text = self._format_user_info(profile, user.id)
|
|
|
|
await self.send_response(update, info_text, parse_mode='HTML')
|
|
|
|
async def _send_info_response(self, update: Update, text: str, parse_mode: str = None):
|
|
"""Отправка ответа для команды /info с учетом типа обновления"""
|
|
message_sent = False
|
|
|
|
if update.message:
|
|
# Вызов из команды /info
|
|
await update.message.reply_text(text, parse_mode=parse_mode)
|
|
message_sent = True
|
|
elif update.callback_query and update.callback_query.message:
|
|
# Вызов из кнопки меню (если будет добавлена)
|
|
try:
|
|
await update.callback_query.edit_message_text(text, parse_mode=parse_mode)
|
|
message_sent = True
|
|
except Exception as e:
|
|
# Если редактирование не удалось (например, сообщение уже отредактировано), отправляем новое
|
|
self.logger.warning(f"Не удалось отредактировать сообщение в info: {e}")
|
|
await update.callback_query.message.reply_text(text, parse_mode=parse_mode)
|
|
message_sent = True
|
|
|
|
# Если сообщение не было отправлено ни одним из способов
|
|
if not message_sent:
|
|
try:
|
|
# Fallback - отправляем в чат по умолчанию
|
|
if update.effective_chat:
|
|
await update.effective_chat.send_message(text, parse_mode=parse_mode)
|
|
else:
|
|
print("Не удалось отправить информацию о пользователе: нет доступного чата")
|
|
except Exception as e:
|
|
print(f"Не удалось отправить информацию о пользователе: {e}")
|
|
|
|
async def handle_weather(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /weather"""
|
|
await self.safe_execute(update, context, "weather", self._handle_weather)
|
|
|
|
async def _handle_weather(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /weather"""
|
|
# Получаем город из аргументов или используем Москву по умолчанию
|
|
city = ' '.join(context.args) if context.args else 'Moscow'
|
|
|
|
# Валидация города
|
|
if not city or len(city.strip()) < 2:
|
|
await update.message.reply_text("❌ Название города должно содержать минимум 2 символа")
|
|
return
|
|
|
|
if len(city) > 50:
|
|
await update.message.reply_text("❌ Название города слишком длинное (максимум 50 символов)")
|
|
return
|
|
|
|
# Получаем API ключ из конфигурации
|
|
api_key = self.config.api_keys.openweather
|
|
|
|
if not api_key:
|
|
await update.message.reply_text("❌ API ключ погоды не настроен")
|
|
return
|
|
|
|
try:
|
|
import requests
|
|
|
|
# Делаем запрос к OpenWeatherMap API
|
|
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={api_key}&units=metric&lang=ru"
|
|
|
|
response = requests.get(url, timeout=10)
|
|
|
|
if response.status_code == 401:
|
|
await update.message.reply_text("❌ Ошибка API: неверный ключ погоды")
|
|
return
|
|
elif response.status_code == 429:
|
|
await update.message.reply_text("❌ Превышен лимит запросов к API погоды. Попробуйте позже.")
|
|
return
|
|
elif response.status_code >= 500:
|
|
await update.message.reply_text("❌ Сервер погоды временно недоступен. Попробуйте позже.")
|
|
return
|
|
elif response.status_code != 200:
|
|
await update.message.reply_text("❌ Ошибка при получении данных о погоде")
|
|
return
|
|
|
|
data = response.json()
|
|
|
|
if data.get('cod') == 200:
|
|
# Проверяем наличие необходимых данных
|
|
if not all(key in data for key in ['name', 'main', 'weather']):
|
|
await update.message.reply_text("❌ Неполные данные о погоде от сервера")
|
|
return
|
|
|
|
# Форматируем ответ
|
|
weather_text = (
|
|
f"🌤️ Погода в <b>{data['name']}</b>\n\n"
|
|
f"🌡️ Температура: <b>{data['main']['temp']}°C</b>\n"
|
|
f"🌡️ Ощущается как: <b>{data['main']['feels_like']}°C</b>\n"
|
|
f"💧 Влажность: <b>{data['main']['humidity']}%</b>\n"
|
|
f"💬 Описание: <b>{data['weather'][0]['description'].capitalize()}</b>\n"
|
|
)
|
|
|
|
await self.send_response(update, weather_text, parse_mode='HTML')
|
|
else:
|
|
# Обработка ошибок от API
|
|
if data.get('cod') == '404':
|
|
await update.message.reply_text(f"❌ Город '{city}' не найден")
|
|
elif data.get('cod') == '401':
|
|
await update.message.reply_text("❌ Ошибка авторизации API погоды")
|
|
else:
|
|
await update.message.reply_text(f"❌ Ошибка API погоды: {data.get('message', 'Неизвестная ошибка')}")
|
|
|
|
except requests.exceptions.Timeout:
|
|
await update.message.reply_text("❌ Превышено время ожидания ответа от сервера погоды. Попробуйте позже.")
|
|
except requests.exceptions.ConnectionError:
|
|
await update.message.reply_text("❌ Ошибка подключения к серверу погоды. Проверьте интернет-соединение.")
|
|
except requests.exceptions.RequestException as e:
|
|
await update.message.reply_text(f"❌ Ошибка сети при получении погоды: {str(e)[:100]}")
|
|
except Exception as e:
|
|
await update.message.reply_text(f"❌ Ошибка обработки данных погоды: {str(e)[:100]}")
|
|
|
|
async def handle_news(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /news"""
|
|
await self.safe_execute(update, context, "news", self._handle_news)
|
|
|
|
async def _handle_news(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /news"""
|
|
# Получаем API ключ из конфигурации
|
|
api_key = self.config.api_keys.news
|
|
|
|
if not api_key:
|
|
await update.message.reply_text("❌ API ключ новостей не настроен")
|
|
return
|
|
|
|
try:
|
|
import requests
|
|
|
|
# Делаем запрос к NewsAPI
|
|
url = f"https://newsapi.org/v2/top-headlines?country=ru&apiKey={api_key}"
|
|
|
|
response = requests.get(url, timeout=10)
|
|
|
|
if response.status_code == 401:
|
|
await update.message.reply_text("❌ Ошибка API: неверный ключ новостей")
|
|
return
|
|
elif response.status_code == 429:
|
|
await update.message.reply_text("❌ Превышен лимит запросов к API новостей. Попробуйте позже.")
|
|
return
|
|
elif response.status_code >= 500:
|
|
await update.message.reply_text("❌ Сервер новостей временно недоступен. Попробуйте позже.")
|
|
return
|
|
elif response.status_code != 200:
|
|
await update.message.reply_text("❌ Ошибка при получении новостей")
|
|
return
|
|
|
|
data = response.json()
|
|
|
|
if data.get('status') == 'ok' and data.get('articles'):
|
|
# Проверяем наличие корректных статей
|
|
articles = data['articles'][:5] # Берем только первые 5
|
|
valid_articles = []
|
|
|
|
for article in articles:
|
|
if article.get('title') and article.get('url'):
|
|
valid_articles.append(article)
|
|
|
|
if not valid_articles:
|
|
await update.message.reply_text("❌ Новости получены, но не содержат корректных данных")
|
|
return
|
|
|
|
# Форматируем ответ
|
|
news_text = "📰 <b>Последние новости России:</b>\n\n"
|
|
|
|
for i, article in enumerate(valid_articles, 1):
|
|
# Ограничиваем длину заголовка для безопасности
|
|
title = article['title'][:200] if len(article['title']) > 200 else article['title']
|
|
url = article['url'][:500] if len(article['url']) > 500 else article['url'] # Ограничиваем URL
|
|
|
|
news_text += f"{i}. <b>{title}</b>\n"
|
|
news_text += f"🔗 {url}\n\n"
|
|
|
|
# Ограничиваем общую длину сообщения
|
|
if len(news_text) > 4000:
|
|
news_text = news_text[:3997] + "..."
|
|
|
|
await self.send_response(update, news_text, parse_mode='HTML')
|
|
else:
|
|
# Обработка ошибок от API
|
|
if data.get('status') == 'error':
|
|
error_code = data.get('code', 'unknown')
|
|
error_message = data.get('message', 'Неизвестная ошибка')
|
|
|
|
if error_code == 'apiKeyInvalid':
|
|
await update.message.reply_text("❌ Ошибка API: неверный ключ новостей")
|
|
elif error_code == 'rateLimited':
|
|
await update.message.reply_text("❌ Превышен лимит запросов к API новостей")
|
|
elif error_code == 'sourcesUnavailable':
|
|
await update.message.reply_text("❌ Источники новостей недоступны")
|
|
else:
|
|
await update.message.reply_text(f"❌ Ошибка API новостей: {error_message}")
|
|
else:
|
|
await update.message.reply_text("❌ Не удалось получить новости")
|
|
|
|
except requests.exceptions.Timeout:
|
|
await update.message.reply_text("❌ Превышено время ожидания ответа от сервера новостей. Попробуйте позже.")
|
|
except requests.exceptions.ConnectionError:
|
|
await update.message.reply_text("❌ Ошибка подключения к серверу новостей. Проверьте интернет-соединение.")
|
|
except requests.exceptions.RequestException as e:
|
|
await update.message.reply_text(f"❌ Ошибка сети при получении новостей: {str(e)[:100]}")
|
|
except Exception as e:
|
|
await update.message.reply_text(f"❌ Ошибка обработки данных новостей: {str(e)[:100]}")
|
|
|
|
async def handle_translate(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /translate"""
|
|
await self.safe_execute(update, context, "translate", self._handle_translate)
|
|
|
|
async def _handle_translate(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /translate"""
|
|
if len(context.args) < 2:
|
|
await update.message.reply_text(
|
|
"❌ Использование: /translate [текст] [язык]\n\n"
|
|
"Примеры:\n"
|
|
"/translate Hello world en\n"
|
|
"/translate Привет мир es\n"
|
|
"/translate Bonjour le monde fr\n\n"
|
|
"Поддерживаемые языки: en, es, fr, de, it, pt, ru, ja, ko, zh"
|
|
)
|
|
return
|
|
|
|
text = ' '.join(context.args[:-1])
|
|
target_lang = context.args[-1].lower()
|
|
|
|
# Валидация входных данных
|
|
if not text or not text.strip():
|
|
await update.message.reply_text("❌ Текст для перевода не может быть пустым")
|
|
return
|
|
|
|
if not target_lang or len(target_lang) != 2:
|
|
await update.message.reply_text("❌ Укажите правильный код языка (например: en, ru, es)")
|
|
return
|
|
|
|
# Ограничиваем длину текста для безопасности и производительности
|
|
if len(text) > 1000:
|
|
await update.message.reply_text("❌ Текст слишком длинный для перевода (максимум 1000 символов)")
|
|
return
|
|
|
|
# Поддерживаемые языки
|
|
supported_languages = {
|
|
'en': 'английский', 'es': 'испанский', 'fr': 'французский',
|
|
'de': 'немецкий', 'it': 'итальянский', 'pt': 'португальский',
|
|
'ru': 'русский', 'ja': 'японский', 'ko': 'корейский', 'zh': 'китайский'
|
|
}
|
|
|
|
if target_lang not in supported_languages:
|
|
await update.message.reply_text(
|
|
f"❌ Неподдерживаемый язык: {target_lang}\n"
|
|
f"Поддерживаемые языки: {', '.join(supported_languages.keys())}"
|
|
)
|
|
return
|
|
|
|
try:
|
|
# Простая демонстрация перевода (в реальном приложении нужен API)
|
|
# Здесь можно интегрировать Google Translate API, Yandex Translate, или другой сервис
|
|
|
|
# Для демонстрации покажем, как выглядел бы перевод
|
|
# В продакшене замените на реальный API вызов
|
|
|
|
# Имитация перевода для демонстрации
|
|
if target_lang == 'en':
|
|
if 'привет' in text.lower():
|
|
translated = "Hello"
|
|
elif 'мир' in text.lower():
|
|
translated = "World"
|
|
elif 'спасибо' in text.lower():
|
|
translated = "Thank you"
|
|
else:
|
|
translated = f"[EN] {text}" # Заглушка для неизвестных слов
|
|
elif target_lang == 'es':
|
|
if 'привет' in text.lower():
|
|
translated = "Hola"
|
|
elif 'мир' in text.lower():
|
|
translated = "Mundo"
|
|
elif 'спасибо' in text.lower():
|
|
translated = "Gracias"
|
|
else:
|
|
translated = f"[ES] {text}"
|
|
elif target_lang == 'fr':
|
|
if 'привет' in text.lower():
|
|
translated = "Bonjour"
|
|
elif 'мир' in text.lower():
|
|
translated = "Monde"
|
|
elif 'спасибо' in text.lower():
|
|
translated = "Merci"
|
|
else:
|
|
translated = f"[FR] {text}"
|
|
else:
|
|
translated = f"[{target_lang.upper()}] {text}"
|
|
|
|
# Форматируем ответ
|
|
response_text = (
|
|
"🔄 <b>Перевод текста</b>\n\n"
|
|
f"📝 <b>Оригинал:</b> {text}\n"
|
|
f"🌐 <b>Язык:</b> {supported_languages[target_lang]}\n"
|
|
f"📋 <b>Перевод:</b> {translated}\n\n"
|
|
"💡 <i>Примечание: Для полноценного перевода необходимо интегрировать "
|
|
"сервис перевода (Google Translate API, Yandex Translate или другой).</i>"
|
|
)
|
|
|
|
await self.send_response(update, response_text, parse_mode='HTML')
|
|
|
|
except Exception as e:
|
|
await update.message.reply_text(f"❌ Ошибка при переводе текста: {str(e)[:100]}")
|
|
|
|
async def handle_donate(self, update: Update, context: ContextTypes):
|
|
"""Обработка команды /donate"""
|
|
await self.safe_execute(update, context, "donate", self._handle_donate)
|
|
|
|
async def _handle_donate(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка команды /donate"""
|
|
# Создаем клавиатуру с суммами донатов
|
|
keyboard = [
|
|
[InlineKeyboardButton("💰 100 ₽", callback_data='donate_100')],
|
|
[InlineKeyboardButton("💰 500 ₽", callback_data='donate_500')],
|
|
[InlineKeyboardButton("💰 1000 ₽", callback_data='donate_1000')],
|
|
[InlineKeyboardButton("💰 2500 ₽", callback_data='donate_2500')],
|
|
[InlineKeyboardButton("💰 5000 ₽", callback_data='donate_5000')],
|
|
[InlineKeyboardButton("💰 Другая сумма", callback_data='donate_custom')]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
# Получаем информацию о донатах пользователя
|
|
user = update.effective_user
|
|
user_info = await self.user_service.get_or_create_user(user.id, user.username, user.first_name, user.last_name)
|
|
|
|
donate_text = (
|
|
"💰 <b>Поддержка проекта</b>\n\n"
|
|
"Спасибо за интерес к нашему боту! Ваша поддержка помогает нам развивать проект и добавлять новые функции.\n\n"
|
|
"🎁 <b>Преимущества донатеров:</b>\n"
|
|
"• Увеличенные награды за игры\n"
|
|
"• Доступ к премиум-играм\n"
|
|
"• Приоритетная поддержка\n"
|
|
"• Специальные значки в профиле\n\n"
|
|
"💳 <b>Выберите сумму поддержки:</b>"
|
|
)
|
|
|
|
if update.message:
|
|
await update.message.reply_text(donate_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
elif update.callback_query and update.callback_query.message:
|
|
await update.callback_query.edit_message_text(donate_text, parse_mode='HTML', reply_markup=reply_markup)
|
|
|
|
async def handle_text_message(self, update: Update, context: ContextTypes):
|
|
"""Обработка текстовых сообщений"""
|
|
await self.safe_execute(update, context, "text_message", self._handle_text_message)
|
|
|
|
async def _handle_text_message(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка текстового сообщения"""
|
|
user = update.effective_user
|
|
message_text = update.message.text.lower()
|
|
|
|
# Проверяем на "+" для реакции рукопожатием - ВРЕМЕННО ЗАКОММЕНТИРОВАНО ДО ПРОВЕРКИ КОНФЛИКТА С ТРИГГЕРАМИ
|
|
# if "+" in message_text:
|
|
# try:
|
|
# # Используем новый метод set_message_reaction (доступен с версии 21+)
|
|
# await context.bot.set_message_reaction(
|
|
# chat_id=update.message.chat_id,
|
|
# message_id=update.message.message_id,
|
|
# reaction="🤝"
|
|
# )
|
|
# print(f"[DEBUG] Reaction set successfully for message {update.message.message_id}")
|
|
# self.logger.debug(f"Reaction set for message with '+' by user {user.id}")
|
|
#
|
|
# except Exception as e:
|
|
# # Fallback на старый метод для совместимости
|
|
# print(f"[WARNING] Failed to set reaction, using fallback: {e}")
|
|
# self.logger.warning(f"Failed to set reaction for message with '+' by user {user.id}, using fallback: {e}")
|
|
#
|
|
# try:
|
|
# # Используем reply с emoji реакции как fallback
|
|
# reaction_message = await update.message.reply_text("🤝", reply_to_message_id=update.message.message_id)
|
|
# print(f"[DEBUG] Reaction reply sent successfully for message {update.message.message_id}")
|
|
# self.logger.debug(f"Reaction reply fallback sent for message with '+' by user {user.id}")
|
|
#
|
|
# # Удаляем сообщение с реакцией через 5 секунд
|
|
# import asyncio
|
|
# async def delete_reaction():
|
|
# try:
|
|
# await asyncio.sleep(5)
|
|
# await reaction_message.delete()
|
|
# print(f"[DEBUG] Reaction reply deleted for message {update.message.message_id}")
|
|
# except Exception as delete_e:
|
|
# print(f"[WARNING] Failed to delete reaction reply: {delete_e}")
|
|
#
|
|
# # Запускаем задачу удаления
|
|
# asyncio.create_task(delete_reaction())
|
|
#
|
|
# except Exception as fallback_e:
|
|
# print(f"[ERROR] Both reaction methods failed: primary={e}, fallback={fallback_e}")
|
|
# self.logger.error(f"Both reaction methods failed for message with '+' by user {user.id}: primary={e}, fallback={fallback_e}")
|
|
|
|
# Проверяем на нецензурную лексику
|
|
if self._check_profanity(message_text):
|
|
await self._handle_profanity_violation(update, user)
|
|
return
|
|
|
|
# Проверяем слова "реквизиты"
|
|
if "реквизиты" in message_text or "реквизит" in message_text:
|
|
await self.send_response(update, BANK_DETAILS_TEXT, reply_to_message_id=update.message.message_id)
|
|
return
|
|
|
|
# Обновляем активность пользователя
|
|
await self.user_service.update_user_activity(user.id, update.effective_chat.id)
|
|
|
|
async def handle_donation_callback(self, update: Update, context: ContextTypes):
|
|
"""Обработка callback'ов донатов"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
amount_str = query.data.split('_')[1]
|
|
|
|
try:
|
|
amount = float(amount_str)
|
|
user = query.from_user
|
|
|
|
points = int(amount // 100)
|
|
keyboard = [[InlineKeyboardButton("✅ Подтвердить перевод", callback_data=f'donate_confirm_{amount}')]]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
await query.edit_message_text(
|
|
f"💰 <b>Спасибо за выбор поддержки!</b>\n\n"
|
|
f"Сумма: {amount} RUB\n"
|
|
f"Получите очков: {points}\n\n"
|
|
f"💳 <b>Банковские реквизиты:</b>\n\n"
|
|
f"{BANK_DETAILS_TEXT}\n\n"
|
|
f"После оплаты нажмите кнопку ниже для подтверждения:",
|
|
parse_mode='HTML',
|
|
reply_markup=reply_markup
|
|
)
|
|
|
|
except ValueError:
|
|
await query.edit_message_text("❌ Неверная сумма!")
|
|
|
|
async def handle_donation_custom(self, update: Update, context: ContextTypes):
|
|
"""Обработка выбора пользовательской суммы"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await query.edit_message_text(
|
|
"💰 Введите сумму доната в рублях:\n\n"
|
|
"Пример: 750",
|
|
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("❌ Отмена", callback_data='menu_main')]])
|
|
)
|
|
|
|
# Устанавливаем флаг ожидания суммы
|
|
context.user_data['waiting_for_donation_amount'] = True
|
|
|
|
async def handle_donation_amount_input(self, update: Update, context: ContextTypes):
|
|
"""Обработка ввода суммы доната"""
|
|
try:
|
|
amount = float(update.message.text.strip())
|
|
if amount <= 0:
|
|
raise ValueError()
|
|
|
|
user = update.effective_user
|
|
points = int(amount // 100)
|
|
|
|
# Сохраняем донат в базу данных
|
|
success = await self.user_service.add_donation(user.id, amount)
|
|
|
|
if success:
|
|
await update.message.reply_text(
|
|
f"💰 <b>Спасибо за поддержку!</b>\n\n"
|
|
f"Сумма: {amount} RUB\n"
|
|
f"Получено очков: {points}\n\n"
|
|
f"Ваша поддержка помогает развивать бота! 🎉",
|
|
parse_mode='HTML'
|
|
)
|
|
else:
|
|
await update.message.reply_text("❌ Ошибка: не удалось сохранить донат в базу данных. Попробуйте позже.")
|
|
|
|
except ValueError:
|
|
await update.message.reply_text("❌ Неверная сумма! Введите положительное число.")
|
|
except ValidationError as e:
|
|
error_message = str(e)
|
|
if "ID пользователя" in error_message:
|
|
await update.message.reply_text("❌ Ошибка: проблема с идентификацией пользователя")
|
|
elif "Сумма доната" in error_message:
|
|
await update.message.reply_text("❌ Ошибка: сумма доната должна быть положительной")
|
|
else:
|
|
await update.message.reply_text(f"❌ Ошибка валидации: {error_message}")
|
|
except Exception as e:
|
|
# Логируем ошибку для анализа
|
|
error_details = {
|
|
'user_id': user.id if 'user' in locals() else 'unknown',
|
|
'amount': amount if 'amount' in locals() else 'unknown',
|
|
'error_type': 'custom_donation_processing_error',
|
|
'error_message': str(e),
|
|
'error_context': f"handle_donation_amount_input: {type(e).__name__}",
|
|
}
|
|
await self._log_error_to_database(error_details)
|
|
await update.message.reply_text("❌ Ошибка сервера при обработке доната. Попробуйте позже или обратитесь к администратору.")
|
|
|
|
context.user_data.pop('waiting_for_donation_amount', None)
|
|
|
|
async def handle_donate_confirm_callback(self, update: Update, context: ContextTypes):
|
|
"""Обработка callback подтверждения доната"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
# Парсим сумму из callback_data
|
|
parts = query.data.split('_')
|
|
if len(parts) < 3:
|
|
await query.edit_message_text("❌ Ошибка: некорректные данные доната")
|
|
return
|
|
|
|
try:
|
|
amount = float(parts[2])
|
|
user = query.from_user
|
|
|
|
# Сохраняем донат в базу данных
|
|
success = await self.user_service.add_donation(user.id, amount)
|
|
|
|
if success:
|
|
points = int(amount // 100)
|
|
await query.edit_message_text(
|
|
f"💰 <b>Спасибо за поддержку!</b>\n\n"
|
|
f"Сумма: {amount} RUB\n"
|
|
f"Получено очков: {points}\n\n"
|
|
f"Ваша поддержка помогает развивать бота! 🎉",
|
|
parse_mode='HTML'
|
|
)
|
|
else:
|
|
await query.edit_message_text("❌ Ошибка: не удалось сохранить донат в базу данных. Попробуйте позже.")
|
|
|
|
except ValueError:
|
|
await query.edit_message_text("❌ Ошибка: неверная сумма доната")
|
|
except ValidationError as e:
|
|
error_message = str(e)
|
|
if "ID пользователя" in error_message:
|
|
await query.edit_message_text("❌ Ошибка: проблема с идентификацией пользователя")
|
|
elif "Сумма доната" in error_message:
|
|
await query.edit_message_text("❌ Ошибка: сумма доната должна быть положительной")
|
|
else:
|
|
await query.edit_message_text(f"❌ Ошибка валидации: {error_message}")
|
|
except Exception as e:
|
|
# Логируем ошибку в базу данных для анализа
|
|
error_details = {
|
|
'user_id': user.id if 'user' in locals() else 'unknown',
|
|
'amount': amount if 'amount' in locals() else 'unknown',
|
|
'error_type': 'donation_processing_error',
|
|
'error_message': str(e),
|
|
'error_context': f"handle_donate_confirm_callback: {type(e).__name__}",
|
|
'callback_data': query.data
|
|
}
|
|
await self._log_error_to_database(error_details)
|
|
self.logger.error(f"Неожиданная ошибка при обработке доната: {e}", exc_info=True)
|
|
await query.edit_message_text("❌ Ошибка сервера при обработке доната. Попробуйте позже или обратитесь к администратору.")
|
|
|
|
def _check_profanity(self, text: str) -> bool:
|
|
"""Проверка текста на наличие нецензурной лексики"""
|
|
if not text:
|
|
return False
|
|
|
|
text_lower = text.lower()
|
|
|
|
# Проверяем каждое слово из списка
|
|
for word in PROFANITY_WORDS:
|
|
if word in text_lower:
|
|
return True
|
|
|
|
return False
|
|
|
|
async def _handle_profanity_violation(self, update: Update, user):
|
|
"""Обработка нарушения с нецензурной лексикой"""
|
|
try:
|
|
# Удаляем сообщение с матом
|
|
await update.message.delete()
|
|
except Exception as e:
|
|
print(f"Не удалось удалить сообщение: {e}")
|
|
|
|
# Отправляем предупреждение в чат
|
|
username = user.username if user.username else user.first_name
|
|
await self.send_response(update, f"⚠️ {username}, в группе запрещена нецензурная лексика!\n"
|
|
f"Сообщение удалено, выдано предупреждение.", parse_mode='HTML')
|
|
|
|
# Добавляем предупреждение пользователю
|
|
try:
|
|
print(f"[DEBUG] Adding warning to user {user.id} for profanity")
|
|
await self.user_service.add_warning(user.id, "Нецензурная лексика", 0) # 0 - системное предупреждение
|
|
print(f"[DEBUG] Warning added successfully to user {user.id}")
|
|
except Exception as e:
|
|
print(f"[ERROR] Не удалось добавить предупреждение пользователю {user.id}: {e}")
|
|
|
|
async def handle_voice_message(self, update: Update, context: ContextTypes):
|
|
"""Обработка голосовых сообщений"""
|
|
await self.safe_execute(update, context, "voice_message", self._handle_voice_message)
|
|
|
|
async def _handle_voice_message(self, update: Update, context: ContextTypes):
|
|
"""Внутренняя обработка голосового сообщения"""
|
|
user = update.effective_user
|
|
voice = update.message.voice
|
|
|
|
# Обновляем активность пользователя
|
|
await self.user_service.update_user_activity(user.id, update.effective_chat.id)
|
|
|
|
# Получаем длительность голосового сообщения
|
|
duration = voice.duration
|
|
|
|
# Попытка транскрипции голосового сообщения
|
|
transcription = await self._transcribe_voice_message(voice, update)
|
|
|
|
# Отправляем ответ пользователю
|
|
if transcription and transcription != "[не удалось распознать речь]":
|
|
await update.message.reply_text(
|
|
f"🎤 Голосовое сообщение от {user.first_name} ({duration} сек)\n\n"
|
|
f"📝 Распознано: {transcription}\n\n"
|
|
f"💡 +1 очко за активность!",
|
|
reply_to_message_id=update.message.message_id
|
|
)
|
|
else:
|
|
await update.message.reply_text(
|
|
f"🎤 Голосовое сообщение от {user.first_name} ({duration} сек)\n\n"
|
|
f"❌ Не удалось распознать речь\n"
|
|
f"💡 +1 очко за активность!",
|
|
reply_to_message_id=update.message.message_id
|
|
)
|
|
|
|
async def _transcribe_voice_message(self, voice, update: Update) -> str:
|
|
"""Транскрипция голосового сообщения"""
|
|
try:
|
|
# Проверяем доступность библиотек распознавания речи
|
|
try:
|
|
import speech_recognition as sr
|
|
SPEECH_RECOGNITION_AVAILABLE = True
|
|
except ImportError:
|
|
return "[Библиотека speech_recognition не установлена]"
|
|
|
|
# Создаем временный файл для загрузки аудио
|
|
import tempfile
|
|
import os
|
|
|
|
with tempfile.NamedTemporaryFile(suffix='.ogg', delete=False) as temp_ogg:
|
|
temp_ogg_path = temp_ogg.name
|
|
|
|
try:
|
|
# Скачиваем голосовое сообщение
|
|
voice_file = await voice.get_file()
|
|
await voice_file.download_to_drive(temp_ogg_path)
|
|
|
|
# Конвертируем OGG в WAV для speech_recognition
|
|
temp_wav_path = temp_ogg_path.replace('.ogg', '.wav')
|
|
|
|
try:
|
|
# Пробуем использовать pydub для конвертации
|
|
try:
|
|
from pydub import AudioSegment
|
|
audio = AudioSegment.from_ogg(temp_ogg_path)
|
|
audio.export(temp_wav_path, format='wav')
|
|
except ImportError:
|
|
# Используем ffmpeg для конвертации (если доступен)
|
|
import subprocess
|
|
try:
|
|
subprocess.run([
|
|
'ffmpeg', '-i', temp_ogg_path, '-acodec', 'pcm_s16le',
|
|
'-ar', '16000', '-ac', '1', temp_wav_path
|
|
], check=True, capture_output=True)
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
return "[невозможно конвертировать аудио - установите pydub или ffmpeg]"
|
|
|
|
# Распознавание речи
|
|
recognizer = sr.Recognizer()
|
|
|
|
with sr.AudioFile(temp_wav_path) as source:
|
|
audio_data = recognizer.record(source)
|
|
|
|
# Пробуем разные API для распознавания
|
|
try:
|
|
# Сначала пробуем Google Speech Recognition (бесплатно)
|
|
text = recognizer.recognize_google(audio_data, language='ru-RU')
|
|
return text
|
|
except sr.UnknownValueError:
|
|
return "[не удалось распознать речь]"
|
|
except sr.RequestError:
|
|
# Если Google API недоступен, пробуем Sphinx (локальный, но менее точный)
|
|
try:
|
|
text = recognizer.recognize_sphinx(audio_data, language='ru-RU')
|
|
return text
|
|
except sr.UnknownValueError:
|
|
return "[не удалось распознать речь (локально)]"
|
|
except sr.RequestError:
|
|
return "[сервисы распознавания речи недоступны]"
|
|
|
|
finally:
|
|
# Очищаем временные файлы
|
|
try:
|
|
if os.path.exists(temp_ogg_path):
|
|
os.unlink(temp_ogg_path)
|
|
if os.path.exists(temp_wav_path):
|
|
os.unlink(temp_wav_path)
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
print(f"Ошибка при транскрипции: {e}")
|
|
return "[ошибка при обработке голосового сообщения]"
|
|
|
|
except Exception as e:
|
|
print(f"Ошибка при инициализации распознавания речи: {e}")
|
|
return "[ошибка инициализации распознавания речи]"
|
|
|
|
async def handle_audio_message(self, update: Update, context: ContextTypes):
|
|
"""Обработка аудио сообщений для модерации"""
|
|
await self._handle_media_for_moderation(update, context, "audio")
|
|
|
|
async def handle_video_message(self, update: Update, context: ContextTypes):
|
|
"""Обработка видео сообщений для модерации"""
|
|
await self._handle_media_for_moderation(update, context, "video")
|
|
|
|
async def _handle_media_for_moderation(self, update: Update, context: ContextTypes, media_type: str):
|
|
"""Обработка медиафайлов для модерации"""
|
|
user = update.effective_user
|
|
message = update.message
|
|
|
|
# Обновляем активность пользователя
|
|
await self.user_service.update_user_activity(user.id, update.effective_chat.id)
|
|
|
|
# Получаем информацию о медиа
|
|
if media_type == "audio":
|
|
media = message.audio
|
|
duration = getattr(media, 'duration', 0)
|
|
file_size = getattr(media, 'file_size', 0)
|
|
title = getattr(media, 'title', 'Без названия')
|
|
performer = getattr(media, 'performer', 'Неизвестный исполнитель')
|
|
elif media_type == "video":
|
|
media = message.video
|
|
duration = getattr(media, 'duration', 0)
|
|
file_size = getattr(media, 'file_size', 0)
|
|
width = getattr(media, 'width', 0)
|
|
height = getattr(media, 'height', 0)
|
|
else:
|
|
return
|
|
|
|
# Попытка транскрипции аудио (если это аудио)
|
|
transcription = ""
|
|
if media_type == "audio":
|
|
transcription = await self._transcribe_audio_for_moderation(media, update)
|
|
|
|
# Удаляем оригинальное сообщение
|
|
try:
|
|
await message.delete()
|
|
except Exception as e:
|
|
print(f"Не удалось удалить сообщение с медиа: {e}")
|
|
|
|
# Сохраняем информацию о медиа в контексте для модерации
|
|
context.user_data['media_for_moderation'] = {
|
|
'message_id': message.message_id,
|
|
'chat_id': message.chat.id,
|
|
'user_id': user.id,
|
|
'media_type': media_type,
|
|
'file_id': media.file_id,
|
|
'duration': duration,
|
|
'file_size': file_size,
|
|
'transcription': transcription,
|
|
'caption': message.caption or "",
|
|
'timestamp': datetime.now()
|
|
}
|
|
|
|
# Создаем клавиатуру для модерации
|
|
keyboard = [
|
|
[InlineKeyboardButton("✅ Одобрить и опубликовать сейчас", callback_data=f'moderate_approve_now_{user.id}')],
|
|
[InlineKeyboardButton("⏰ Одобрить с отсрочкой (8 часов)", callback_data=f'moderate_approve_delay_{user.id}')],
|
|
[InlineKeyboardButton("❌ Отклонить", callback_data=f'moderate_reject_{user.id}')]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
# Отправляем уведомление администраторам для модерации
|
|
await self._notify_admins_for_moderation(context, user, media_type, transcription, message.caption or "")
|
|
|
|
async def _transcribe_audio_for_moderation(self, audio, update: Update) -> str:
|
|
"""Транскрипция аудио для модерации"""
|
|
try:
|
|
# Проверяем доступность библиотек распознавания речи
|
|
try:
|
|
import speech_recognition as sr
|
|
except ImportError:
|
|
return "[Транскрибация недоступна - не установлена библиотека]"
|
|
|
|
# Создаем временный файл для загрузки аудио
|
|
import tempfile
|
|
import os
|
|
|
|
with tempfile.NamedTemporaryFile(suffix='.ogg', delete=False) as temp_ogg:
|
|
temp_ogg_path = temp_ogg.name
|
|
|
|
try:
|
|
# Скачиваем аудио
|
|
audio_file = await audio.get_file()
|
|
await audio_file.download_to_drive(temp_ogg_path)
|
|
|
|
# Конвертируем OGG в WAV для speech_recognition
|
|
temp_wav_path = temp_ogg_path.replace('.ogg', '.wav')
|
|
|
|
try:
|
|
# Пробуем использовать pydub для конвертации
|
|
try:
|
|
from pydub import AudioSegment
|
|
audio_seg = AudioSegment.from_ogg(temp_ogg_path)
|
|
audio_seg.export(temp_wav_path, format='wav')
|
|
except ImportError:
|
|
# Используем ffmpeg для конвертации (если доступен)
|
|
import subprocess
|
|
try:
|
|
subprocess.run([
|
|
'ffmpeg', '-i', temp_ogg_path, '-acodec', 'pcm_s16le',
|
|
'-ar', '16000', '-ac', '1', temp_wav_path
|
|
], check=True, capture_output=True)
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
return "[Невозможно конвертировать аудио для транскрибации]"
|
|
|
|
# Распознавание речи
|
|
recognizer = sr.Recognizer()
|
|
|
|
with sr.AudioFile(temp_wav_path) as source:
|
|
audio_data = recognizer.record(source)
|
|
|
|
# Пробуем разные API для распознавания
|
|
try:
|
|
# Сначала пробуем Google Speech Recognition (бесплатно)
|
|
text = recognizer.recognize_google(audio_data, language='ru-RU')
|
|
return text
|
|
except sr.UnknownValueError:
|
|
return "[Не удалось распознать речь]"
|
|
except sr.RequestError:
|
|
# Если Google API недоступен, пробуем Sphinx (локальный, но менее точный)
|
|
try:
|
|
text = recognizer.recognize_sphinx(audio_data, language='ru-RU')
|
|
return text
|
|
except sr.UnknownValueError:
|
|
return "[Не удалось распознать речь (локально)]"
|
|
except sr.RequestError:
|
|
return "[Сервисы распознавания речи недоступны]"
|
|
|
|
finally:
|
|
# Очищаем временные файлы
|
|
try:
|
|
if os.path.exists(temp_ogg_path):
|
|
os.unlink(temp_ogg_path)
|
|
if os.path.exists(temp_wav_path):
|
|
os.unlink(temp_wav_path)
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
print(f"Ошибка при транскрипции для модерации: {e}")
|
|
return "[Ошибка при обработке аудио]"
|
|
|
|
except Exception as e:
|
|
print(f"Ошибка при инициализации транскрипции для модерации: {e}")
|
|
return "[Ошибка при обработке аудио]"
|
|
|
|
async def _notify_admins_for_moderation(self, context: ContextTypes, user, media_type: str, transcription: str, caption: str):
|
|
"""Отправка уведомления администраторам для модерации медиа"""
|
|
try:
|
|
# Получаем список администраторов из конфигурации
|
|
admin_ids = self.config.bot_config.admin_ids
|
|
|
|
for admin_id in admin_ids:
|
|
try:
|
|
notification_text = (
|
|
"🔔 <b>Требуется модерация медиафайла</b>\n\n"
|
|
f"👤 Пользователь: {user.first_name} (@{user.username if user.username else 'нет'})\n"
|
|
f"🆔 ID: {user.id}\n"
|
|
f"📁 Тип файла: {media_type}\n"
|
|
f"📝 Описание: {caption[:100]}{'...' if len(caption) > 100 else ''}\n"
|
|
f"🎵 Транскрипция: {transcription[:200]}{'...' if len(transcription) > 200 else ''}\n\n"
|
|
"Медиафайл отправлен в группу модераторов для проверки."
|
|
)
|
|
|
|
await self.send_response(update, notification_text, parse_mode='HTML', chat_id=admin_id)
|
|
except Exception as e:
|
|
print(f"Не удалось отправить уведомление администратору {admin_id}: {e}")
|
|
|
|
except Exception as e:
|
|
print(f"Ошибка при отправке уведомлений модерации: {e}")
|
|
|
|
async def handle_main_menu(self, update: Update, context: ContextTypes):
|
|
"""Обработка нажатия на главное меню"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await self._handle_start(update, context)
|
|
|
|
async def handle_help_menu(self, update: Update, context: ContextTypes):
|
|
"""Обработка нажатия на меню помощи"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await self._handle_help(update, context)
|
|
|
|
async def handle_rank_menu(self, update: Update, context: ContextTypes):
|
|
"""Обработка нажатия на меню ранга"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await self._handle_rank(update, context)
|
|
|
|
async def handle_donate_menu(self, update: Update, context: ContextTypes):
|
|
"""Обработка нажатия на меню донатов"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await self._handle_donate(update, context)
|
|
return
|
|
|
|
async def handle_leaderboard_menu(self, update: Update, context: ContextTypes):
|
|
"""Обработка нажатия на меню таблицы лидеров"""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
await self._handle_leaderboard(update, context)
|
|
|
|
async def _log_error_to_database(self, error_details: Dict) -> None:
|
|
"""Логирование ошибки в базу данных для анализа"""
|
|
try:
|
|
# Получаем ID администратора (первый из списка или текущий пользователь)
|
|
admin_id = error_details.get('user_id', 'unknown')
|
|
if admin_id == 'unknown' and hasattr(self.config, 'bot_config') and hasattr(self.config.bot_config, 'admin_ids'):
|
|
admin_id = self.config.bot_config.admin_ids[0] if self.config.bot_config.admin_ids else 0
|
|
|
|
error_type = error_details.get('error_type', 'unknown_error')
|
|
title = f"Ошибка при обработке доната: {error_details.get('error_context', 'Неизвестный контекст')}"
|
|
description = (
|
|
f"Пользователь: {error_details.get('user_id', 'Неизвестен')}\n"
|
|
f"Сумма: {error_details.get('amount', 'Неизвестна')}\n"
|
|
f"Тип ошибки: {error_details.get('error_type', 'Неизвестен')}\n"
|
|
f"Сообщение: {error_details.get('error_message', 'Неизвестно')}\n"
|
|
f"Контекст: {error_details.get('error_context', 'Неизвестен')}\n"
|
|
f"Callback: {error_details.get('callback_data', 'Неизвестен')}"
|
|
)
|
|
|
|
# Определяем приоритет на основе типа ошибки
|
|
if 'ValidationError' in error_details.get('error_context', ''):
|
|
priority = 'medium'
|
|
elif 'DatabaseError' in error_details.get('error_context', ''):
|
|
priority = 'high'
|
|
else:
|
|
priority = 'high' # Для неожиданных ошибок
|
|
|
|
# Добавляем ошибку в базу данных
|
|
if hasattr(self, 'error_repo') and self.error_repo:
|
|
error_id = self.error_repo.add_error(admin_id, error_type, title, description, priority)
|
|
if error_id:
|
|
self.logger.info(f"Ошибка #{error_id} успешно сохранена в базу данных")
|
|
else:
|
|
self.logger.error("Не удалось сохранить ошибку в базу данных")
|
|
else:
|
|
self.logger.warning("ErrorRepository недоступен для логирования ошибки")
|
|
|
|
except Exception as log_error:
|
|
self.logger.error(f"Ошибка при логировании ошибки в базу данных: {log_error}", exc_info=True)
|
|
def _format_user_info(self, profile, user_id: int) -> str:
|
|
"""Форматирование информации о пользователе"""
|
|
return (
|
|
"👤 <b>Информация о пользователе:</b>\n\n"
|
|
f"🆔 ID: {user_id}\n"
|
|
f"👤 Имя: {profile.first_name}\n"
|
|
f"📱 Username: @{profile.username if profile.username else 'Не указан'}\n"
|
|
f"🏆 Репутация: {profile.reputation}\n"
|
|
f"⭐ Ранг: {profile.rank}\n"
|
|
f"💬 Сообщений: {profile.message_count}\n"
|
|
f"⚠️ Предупреждений: {profile.warnings}\n"
|
|
f"📅 Присоединился: {profile.joined_date.strftime('%Y-%m-%d') if profile.joined_date else 'Неизвестно'}\n"
|
|
)
|
|
|
|
def _compare_keyboards(self, markup1: InlineKeyboardMarkup, markup2: InlineKeyboardMarkup) -> bool:
|
|
"""Сравнивает две клавиатуры на идентичность"""
|
|
if not markup1 and not markup2:
|
|
return False # Обе пустые - не изменилось
|
|
if not markup1 or not markup2:
|
|
return True # Одна пустая, другая нет - изменилось
|
|
|
|
# Сравниваем количество строк
|
|
if len(markup1.inline_keyboard) != len(markup2.inline_keyboard):
|
|
return True
|
|
|
|
# Сравниваем каждую кнопку
|
|
for row1, row2 in zip(markup1.inline_keyboard, markup2.inline_keyboard):
|
|
if len(row1) != len(row2):
|
|
return True
|
|
for btn1, btn2 in zip(row1, row2):
|
|
if (btn1.text != btn2.text or
|
|
btn1.callback_data != btn2.callback_data or
|
|
btn1.url != btn2.url):
|
|
return True
|
|
|
|
return False # Клавиатуры идентичны
|
|
|
|
def _get_medal(self, position: int) -> str:
|
|
"""Получение медали для позиции в рейтинге"""
|
|
medals = {
|
|
1: "🥇", 2: "🥈", 3: "🥉",
|
|
4: "4️⃣", 5: "5️⃣", 6: "6️⃣",
|
|
7: "7️⃣", 8: "8️⃣", 9: "9️⃣", 10: "🔟"
|
|
}
|
|
return medals.get(position, f"{position}.") |