"""
Обработчики административных команд.
Отвечают за модерацию пользователей и управление ботом.
"""
from typing import Dict, Callable, List, Tuple
from datetime import datetime, timedelta
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes
from .base_handler import BaseHandler
from services.user_service import UserService
from services.moderation_service import ModerationService
class AdminHandlers(BaseHandler):
"""
Обработчики административных команд.
Команды модерации:
- /warn [пользователь] [причина] - выдать предупреждение
- /mute [пользователь] [время] [причина] - заглушить пользователя
- /unmute [пользователь] - снять заглушку
- /ban [пользователь] [причина] - забанить пользователя
- /unban [пользователь] - разбанить пользователя
- /kick [пользователь] [причина] - кикнуть пользователя
- /admin_stats - статистика модерации
Планировщик постов:
- /schedule_post [время] [текст] - запланировать пост
- /list_posts - показать запланированные посты
- /delete_post [id] - удалить пост
- /publish_now [id] - опубликовать пост немедленно
Система ошибок:
- /report_error [тип] [заголовок] [описание] - создать отчет об ошибке
- /admin_errors [статус] - просмотр списка ошибок
- /analyze_error_ai [id] - анализ ошибки с помощью ИИ
- /process_all_errors_ai - обработка всех ошибок ИИ
- /add_error_to_todo [id] - добавить ошибку в TODO список
- /add_all_analyzed_to_todo - добавить все проанализированные ошибки в TODO
Управление триггерами:
- /trigger_add [ключевые_слова] [ответ] - добавить триггер
- /trigger_list - показать список триггеров с inline клавиатурой
- /trigger_edit [id] - редактировать триггер
- /trigger_delete [id] - удалить триггер с подтверждением
- /trigger_toggle [id] - включить/выключить триггер
Экспорт данных:
- /export_stats [формат] - экспорт статистики пользователей в CSV/Excel
"""
def __init__(self, config, metrics, user_service: UserService, moderation_service: ModerationService):
"""
Инициализация обработчика.
Args:
config: Конфигурация приложения
metrics: Сборщик метрик
user_service: Сервис управления пользователями
moderation_service: Сервис модерации
"""
super().__init__(config, metrics)
self.user_service = user_service
self.moderation_service = moderation_service
def get_command_handlers(self) -> Dict[str, Callable]:
"""Получение обработчиков команд"""
return {
# Модерационные функции перенесены в ModerationHandlers
'admin_stats': self.handle_admin_stats,
'schedule_post': self.handle_schedule_post,
'list_posts': self.handle_list_posts,
'delete_post': self.handle_delete_post,
'publish_now': self.handle_publish_now,
'report_error': self.handle_report_error,
'admin_errors': self.handle_admin_errors,
'analyze_error_ai': self.handle_analyze_error_ai,
'process_all_errors_ai': self.handle_process_all_errors_ai,
'add_error_to_todo': self.handle_add_error_to_todo,
'add_all_analyzed_to_todo': self.handle_add_all_analyzed_errors_to_todo,
'export_stats': self.handle_export_stats,
# Управление триггерами
'trigger_add': self.handle_trigger_add,
'trigger_list': self.handle_trigger_list,
'trigger_edit': self.handle_trigger_edit,
'trigger_delete': self.handle_trigger_delete,
'trigger_toggle': self.handle_trigger_toggle,
# Команда для списка чатов администратора
'admin_chats': self.handle_admin_chats,
}
def get_callback_handlers(self) -> Dict[str, Callable]:
"""Получение обработчиков callback запросов"""
return {
'admin_moderate_user': self.handle_moderate_user,
'admin_confirm_action': self.handle_confirm_action,
'trigger_manage': self.handle_trigger_manage,
'trigger_edit': self.handle_trigger_edit_callback,
'trigger_delete': self.handle_trigger_delete_callback,
'trigger_toggle': self.handle_trigger_toggle_callback,
}
def get_message_handlers(self) -> Dict[str, Callable]:
"""Получение обработчиков сообщений"""
return {}
async def handle_admin_stats(self, update: Update, context: ContextTypes):
"""Обработка команды /admin_stats"""
await self.safe_execute(update, context, "admin_stats", self._handle_admin_stats)
async def _handle_admin_stats(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /admin_stats"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
# Получаем статистику модерации
stats = await self.moderation_service.get_moderation_stats()
# Получаем статистику пользователей
# Здесь можно добавить получение статистики пользователей
response_text = (
"📊 Статистика модерации\n\n"
f"🔇 Активных заглушек: {stats['active_mutes']}\n"
f"🚫 Активных банов: {stats['active_bans']}\n"
f"📋 Всего действий: {stats['total_actions']}\n"
f"🔍 Фильтр нецензурной лексики: {'Включен' if stats['profanity_filter_enabled'] else 'Выключен'}\n\n"
"📈 Статистика пользователей\n"
"👥 Всего пользователей: -\n"
f"🏆 Топ ранг: -\n"
f"💬 Сообщений сегодня: -"
)
await self.send_response(update, response_text, parse_mode='HTML')
async def handle_moderate_user(self, update: Update, context: ContextTypes):
"""Обработка модерации пользователя"""
query = update.callback_query
await query.answer()
# Здесь будет логика модерации пользователя через callback
await query.edit_message_text("Функция модерации через меню в разработке")
async def handle_confirm_action(self, update: Update, context: ContextTypes):
"""Обработка подтверждения действия"""
query = update.callback_query
await query.answer()
# Здесь будет логика подтверждения действий модерации
await query.edit_message_text("Функция подтверждения действий в разработке")
# ===== ПЛАНИРОВЩИК ПОСТОВ =====
async def handle_schedule_post(self, update: Update, context: ContextTypes):
"""Обработка команды /schedule_post"""
await self.safe_execute(update, context, "schedule_post", self._handle_schedule_post)
async def _handle_schedule_post(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /schedule_post"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 2:
await self.send_response(update,
"Использование: /schedule_post [время] [текст]\n\n"
"Форматы времени:\n"
"• Абсолютное: 2024-01-15 14:30:00\n"
"• Абсолютное: 2024-01-15 14:30\n"
"• Относительное: +30m (минуты)\n"
"• Относительное: +2h (часы)\n"
"• Относительное: +1d (дни)\n\n"
"Пример: /schedule_post +2h Важное объявление!"
)
return
time_str = context.args[0]
text = ' '.join(context.args[1:])
# Валидация текста поста
if not text or not text.strip():
await self.send_response(update, "❌ Текст поста не может быть пустым")
return
if len(text) > 4000:
await self.send_response(update, "❌ Текст поста слишком длинный (максимум 4000 символов)")
return
# Валидация времени
if not time_str or not time_str.strip():
await self.send_response(update, "❌ Укажите время публикации")
return
if len(time_str) > 100:
await self.send_response(update, "❌ Строка времени слишком длинная")
return
try:
schedule_time = self._parse_schedule_time(time_str)
except ValueError as e:
await self.send_response(update, f"❌ Ошибка формата времени: {str(e)[:100]}")
return
if schedule_time <= datetime.now():
await self.send_response(update, "❌ Время публикации должно быть в будущем")
return
# Проверка, что время не слишком далеко в будущем (максимум 1 год)
max_future_time = datetime.now() + timedelta(days=365)
if schedule_time > max_future_time:
await self.send_response(update, "❌ Время публикации не может быть больше чем через 1 год")
return
# Показываем предварительный просмотр поста
preview_text = (
"📝 Предварительный просмотр поста:\n\n"
f"📅 Время публикации: {schedule_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"📋 Текст поста:\n{text}\n\n"
"❓ Что вы хотите сделать?"
)
keyboard = [
[InlineKeyboardButton("✅ Опубликовать сейчас", callback_data=f'confirm_schedule_now_{len(text)}')],
[InlineKeyboardButton("📝 Изменить текст", callback_data='edit_text')],
[InlineKeyboardButton("🖼 Добавить картинку", callback_data='add_image')],
[InlineKeyboardButton("❌ Отменить", callback_data='cancel_schedule')]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# Сохраняем данные для подтверждения в user_data
context.user_data['schedule_draft'] = {
'time_str': time_str,
'text': text,
'schedule_time': schedule_time,
'chat_id': update.effective_chat.id,
'created_by': user.id
}
await self.send_response(update, preview_text, parse_mode='HTML', reply_markup=reply_markup)
async def handle_list_posts(self, update: Update, context: ContextTypes):
"""Обработка команды /list_posts"""
await self.safe_execute(update, context, "list_posts", self._handle_list_posts)
async def _handle_list_posts(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /list_posts"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
# Здесь нужно получить репозиторий запланированных постов
# Пока используем заглушку
posts = [] # В будущем: self.scheduled_post_repo.get_posts(chat_id=update.effective_chat.id)
if not posts:
await self.send_response(update, "📭 Нет запланированных постов")
return
response = "📋 Запланированные посты:\n\n"
for post in posts:
post_id, chat_id, text, image_path, schedule_time, created_by, status, published_at, created_at, creator_name = post
response += f"🆔 {post_id}\n"
response += f"📅 {schedule_time}\n"
response += f"👤 Создал: {creator_name or 'Неизвестен'}\n"
response += f"📝 {text[:100]}{'...' if len(text) > 100 else ''}\n"
if image_path:
response += f"🖼 Изображение: {image_path}\n"
response += "\n" + "─" * 30 + "\n"
await self.send_response(update, response, parse_mode='HTML')
async def handle_delete_post(self, update: Update, context: ContextTypes):
"""Обработка команды /delete_post"""
await self.safe_execute(update, context, "delete_post", self._handle_delete_post)
async def _handle_delete_post(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /delete_post"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 1:
await self.send_response(update,
"Использование: /delete_post [ID_поста]\n\n"
"Пример: /delete_post 1"
)
return
post_id_str = context.args[0]
# Валидация ID поста
try:
post_id = int(post_id_str)
except ValueError:
await self.send_response(update, "❌ ID поста должен быть числом")
return
# Проверка диапазона ID
if post_id < 1:
await self.send_response(update, "❌ ID поста должен быть положительным числом")
return
# Здесь нужно удалить пост из репозитория
# Пока используем заглушку
success = False # В будущем: self.scheduled_post_repo.delete_post(post_id, user.id)
if success:
await self.send_response(update, f"✅ Пост #{post_id} успешно удален")
else:
await self.send_response(update, f"❌ Пост #{post_id} не найден или у вас нет прав на его удаление")
async def handle_publish_now(self, update: Update, context: ContextTypes):
"""Обработка команды /publish_now"""
await self.safe_execute(update, context, "publish_now", self._handle_publish_now)
async def _handle_publish_now(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /publish_now"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 1:
await self.send_response(update,
"Использование: /publish_now [ID_поста]\n\n"
"Пример: /publish_now 1"
)
return
post_id_str = context.args[0]
# Валидация ID поста
try:
post_id = int(post_id_str)
except ValueError:
await self.send_response(update, "❌ ID поста должен быть числом")
return
# Здесь нужно опубликовать пост немедленно
# Пока используем заглушку
success = False # В будущем: self.scheduled_post_repo.publish_post_now(post_id, user.id)
if success:
await self.send_response(update, f"✅ Пост #{post_id} успешно опубликован")
else:
await self.send_response(update, f"❌ Пост #{post_id} не найден или у вас нет прав на его публикацию")
def _parse_schedule_time(self, time_str: str):
"""Парсинг времени публикации из строки"""
import re
current_time = datetime.now()
# Абсолютное время: 2024-01-15 14:30:00 или 2024-01-15 14:30
absolute_pattern = r'^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$'
match = re.match(absolute_pattern, time_str)
if match:
year, month, day, hour, minute = map(int, match.groups()[:5])
second = int(match.group(6)) if match.group(6) else 0
return datetime(year, month, day, hour, minute, second)
# Относительное время: +30m, +2h, +1d
relative_pattern = r'^(\+|\-)(\d+)([mhd])$'
match = re.match(relative_pattern, time_str)
if match:
sign, amount, unit = match.groups()
amount = int(amount)
if unit == 'm': # минуты
delta = timedelta(minutes=amount)
elif unit == 'h': # часы
delta = timedelta(hours=amount)
elif unit == 'd': # дни
delta = timedelta(days=amount)
if sign == '-':
delta = -delta
return current_time + delta
raise ValueError("Неверный формат времени. Используйте абсолютное время (2024-01-15 14:30) или относительное (+30m, +2h, +1d)")
# ===== СИСТЕМА ОШИБОК И ИИ АНАЛИЗА =====
async def handle_report_error(self, update: Update, context: ContextTypes):
"""Обработка команды /report_error"""
await self.safe_execute(update, context, "report_error", self._handle_report_error)
async def _handle_report_error(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /report_error"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
# Проверка корректности аргументов команды
if len(context.args) < 2:
await self.send_response(update,
"❌ Использование: /report_error <тип> <заголовок> [описание]\n\n"
"Типы ошибок:\n"
"• bug - ошибка в работе бота\n"
"• feature - предложение новой функции\n"
"• crash - критическая ошибка/падение\n"
"• ui - проблема интерфейса\n"
"• security - проблема безопасности\n"
"• improvement - предложение улучшения\n"
"• other - другое\n\n"
"Пример: /report_error bug Не работает команда /weather Описание проблемы..."
)
return
error_type = context.args[0].lower()
title = context.args[1]
description = ' '.join(context.args[2:]) if len(context.args) > 2 else ""
# Валидация типа ошибки
valid_types = ['bug', 'feature', 'crash', 'ui', 'security', 'improvement', 'other']
if error_type not in valid_types:
await self.send_response(update,
f"❌ Неверный тип ошибки: {error_type}\n"
f"Доступные типы: {', '.join(valid_types)}"
)
return
# Валидация заголовка
if len(title.strip()) == 0:
await self.send_response(update, "❌ Заголовок ошибки не может быть пустым")
return
if len(title) > 200:
await self.send_response(update, "❌ Заголовок слишком длинный (максимум 200 символов)")
return
# Валидация описания
if len(description) > 2000:
await self.send_response(update, "❌ Описание слишком длинное (максимум 2000 символов)")
return
# Определение приоритета по умолчанию
priority_map = {
'crash': 'critical',
'security': 'high',
'bug': 'medium',
'ui': 'medium',
'feature': 'low',
'improvement': 'low',
'other': 'medium'
}
priority = priority_map.get(error_type, 'medium')
# Здесь нужно добавить сохранение в базу данных через репозиторий
# Пока используем заглушку
# error_id = self.error_repository.add_error(user.id, error_type, title, description, priority)
await self.send_response(update,
"✅ Отчет об ошибке успешно отправлен!\n\n"
f"🆔 ID ошибки: [ID будет присвоен]\n"
f"📋 Тип: {error_type}\n"
f"📝 Заголовок: {title}\n"
f"⭐ Приоритет: {priority}\n\n"
"Спасибо за ваш вклад в улучшение бота!"
)
# Здесь будет отправка уведомления разработчику
# await self._send_developer_notification(context, f"🚨 Новая ошибка: {title}")
async def handle_admin_errors(self, update: Update, context: ContextTypes):
"""Обработка команды /admin_errors"""
await self.safe_execute(update, context, "admin_errors", self._handle_admin_errors)
async def _handle_admin_errors(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /admin_errors"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
# Получаем параметр фильтрации (по статусу)
status_filter = None
if context.args and len(context.args) > 0:
status_filter = context.args[0].lower()
valid_statuses = ['new', 'in_progress', 'resolved', 'rejected']
if status_filter not in valid_statuses:
await self.send_response(update,
f"❌ Неверный статус: {status_filter}\n"
f"Доступные статусы: {', '.join(valid_statuses)}"
)
return
# Здесь нужно получить ошибки из репозитория
# Пока используем заглушку
errors = []
if not errors:
status_text = f" со статусом '{status_filter}'" if status_filter else ""
await self.send_response(update, f"📭 Нет ошибок{status_text}")
return
# Формируем ответ с информацией об ошибках
response = "📋 Список ошибок и отчетов\n\n"
for error in errors:
# Определяем эмодзи для типа ошибки
type_emojis = {
'bug': '🐛', 'feature': '✨', 'crash': '💥',
'ui': '🎨', 'security': '🔒', 'improvement': '📈', 'other': '📝'
}
type_emoji = type_emojis.get(error.get('error_type', 'other'), '📝')
# Определяем эмодзи для приоритета
priority_emojis = {
'critical': '🔴', 'high': '🟠', 'medium': '🟡', 'low': '🔵'
}
priority_emoji = priority_emojis.get(error.get('priority', 'medium'), '🟡')
# Определяем эмодзи для статуса
status_emojis = {
'new': '🆕', 'in_progress': '🔄', 'resolved': '✅', 'rejected': '❌'
}
status_emoji = status_emojis.get(error.get('status', 'new'), '❓')
response += (
f"{type_emoji} #{error.get('id', 'N/A')} {priority_emoji} {status_emoji}\n"
f"📝 {error.get('title', 'Без заголовка')}\n"
f"👤 {error.get('admin_name', 'Неизвестен')} | 📅 {error.get('created_at', 'Неизвестна')[:10]}\n"
f"📋 Тип: {error.get('error_type', 'неизвестен')} | Статус: {error.get('status', 'неизвестен')}\n"
)
description = error.get('description', '')
if description and len(description) > 100:
response += f"📄 Описание: {description[:100]}...\n"
elif description:
response += f"📄 Описание: {description}\n"
response += "\n" + "─" * 40 + "\n"
# Проверяем, не превышает ли длина лимит Telegram (4096 символов)
if len(response) > 4000:
response = response[:3997] + "..."
await self.send_response(update, response, parse_mode='HTML')
async def handle_analyze_error_ai(self, update: Update, context: ContextTypes):
"""Обработка команды /analyze_error_ai"""
await self.safe_execute(update, context, "analyze_error_ai", self._handle_analyze_error_ai)
async def _handle_analyze_error_ai(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /analyze_error_ai"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 1:
await self.send_response(update,
"❌ Использование: /analyze_error_ai \n\n"
"Пример: /analyze_error_ai 1"
)
return
try:
error_id = int(context.args[0])
except ValueError:
await self.send_response(update, "❌ ID ошибки должен быть числом")
return
# Здесь нужно получить ошибку из репозитория и проанализировать с помощью ИИ
# Пока используем заглушку
await self.send_response(update,
f"🤖 Анализ ошибки #{error_id} с помощью ИИ...\n\n"
f"Анализ будет выполнен и сохранен в базу данных.\n"
f"Используйте /admin_errors для просмотра результатов."
)
async def handle_process_all_errors_ai(self, update: Update, context: ContextTypes):
"""Обработка команды /process_all_errors_ai"""
await self.safe_execute(update, context, "process_all_errors_ai", self._handle_process_all_errors_ai)
async def _handle_process_all_errors_ai(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /process_all_errors_ai"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
await self.send_response(update,
"🤖 Начинаю обработку всех новых ошибок с помощью ИИ...\n\n"
"Это может занять некоторое время. Используйте /admin_errors для просмотра результатов."
)
async def handle_add_error_to_todo(self, update: Update, context: ContextTypes):
"""Обработка команды /add_error_to_todo"""
await self.safe_execute(update, context, "add_error_to_todo", self._handle_add_error_to_todo)
async def _handle_add_error_to_todo(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /add_error_to_todo"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 1:
await self.send_response(update,
"❌ Использование: /add_error_to_todo [приоритет]\n\n"
"Приоритет (опционально): high, medium, low\n"
"Пример: /add_error_to_todo 1 high"
)
return
try:
error_id = int(context.args[0])
except ValueError:
await self.send_response(update, "❌ ID ошибки должен быть числом")
return
priority = context.args[1].lower() if len(context.args) > 1 else 'medium'
valid_priorities = ['high', 'medium', 'low']
if priority not in valid_priorities:
await self.send_response(update, f"❌ Неверный приоритет: {priority}. Доступные: {', '.join(valid_priorities)}")
return
# Здесь нужно добавить ошибку в TODO файл
# Пока используем заглушку
await self.send_response(update,
f"✅ Ошибка #{error_id} успешно добавлена в TODO список!\n\n"
f"📝 Приоритет: {priority}\n"
f"📋 Ошибка добавлена в раздел '{priority.upper()}' в файле TODO.md"
)
async def handle_add_all_analyzed_errors_to_todo(self, update: Update, context: ContextTypes):
"""Обработка команды /add_all_analyzed_to_todo"""
await self.safe_execute(update, context, "add_all_analyzed_to_todo", self._handle_add_all_analyzed_errors_to_todo)
async def _handle_add_all_analyzed_errors_to_todo(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /add_all_analyzed_to_todo"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
await self.send_response(update,
"📝 Начинаю добавление всех проанализированных ошибок в TODO список...\n\n"
"Это может занять некоторое время."
)
async def _send_developer_notification(self, context: ContextTypes, message: str):
"""Отправка уведомления разработчику об ошибке"""
try:
# Проверяем настройки уведомлений разработчика
if self.config.bot_config.enable_developer_notifications and self.config.bot_config.developer_chat_id:
await context.bot.send_message(
chat_id=self.config.bot_config.developer_chat_id,
text=message,
parse_mode='HTML',
disable_web_page_preview=True
)
return True
return False
except Exception as e:
print(f"Ошибка при отправке уведомления разработчику: {e}")
return False
# ===== УПРАВЛЕНИЕ ТРИГГЕРАМИ =====
async def handle_trigger_add(self, update: Update, context: ContextTypes):
"""Обработка команды /trigger_add"""
await self.safe_execute(update, context, "trigger_add", self._handle_trigger_add)
async def _handle_trigger_add(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /trigger_add"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 2:
await self.send_response(update,
"❌ Использование: /trigger_add <ключевые_слова> <ответ>\n\n"
"Ключевые слова - слова через запятую, которые будут активировать триггер\n"
"Пример: /trigger_add привет,здравствуй Привет! Как дела?\n\n"
"Триггеры поддерживают:\n"
"• Несколько ключевых слов через запятую\n"
"• Регулярные выражения (если начинается с /regex/)\n"
"• Регистронезависимый поиск"
)
return
keywords_str = context.args[0]
response_text = ' '.join(context.args[1:])
# Валидация ключевых слов
keywords = [kw.strip() for kw in keywords_str.split(',') if kw.strip()]
if not keywords:
await self.send_response(update, "❌ Не указаны ключевые слова")
return
if len(keywords) > 10:
await self.send_response(update, "❌ Слишком много ключевых слов (максимум 10)")
return
for keyword in keywords:
if len(keyword) > 50:
await self.send_response(update, f"❌ Слишком длинное ключевое слово: {keyword[:20]}...")
return
if len(keyword) < 2:
await self.send_response(update, f"❌ Слишком короткое ключевое слово: {keyword}")
return
# Валидация ответа
if not response_text or not response_text.strip():
await self.send_response(update, "❌ Ответ триггера не может быть пустым")
return
if len(response_text) > 1000:
await self.send_response(update, "❌ Ответ триггера слишком длинный (максимум 1000 символов)")
return
# Проверяем, существует ли уже такой триггер
# Пока используем заглушку - в будущем проверка через репозиторий
existing_trigger = None # self.trigger_repo.get_trigger_by_keywords(keywords)
if existing_trigger:
await self.send_response(update,
f"❌ Триггер с ключевыми словами '{keywords_str}' уже существует!\n"
f"Используйте /trigger_edit для изменения существующего триггера."
)
return
# Создаем триггер
# Пока используем заглушку - в будущем сохранение через репозиторий
trigger_data = {
'keywords': keywords,
'response': response_text,
'created_by': user.id,
'created_at': datetime.now(),
'enabled': True
}
# self.trigger_repo.add_trigger(trigger_data)
trigger_id = 1 # В будущем: self.trigger_repo.add_trigger(trigger_data)
await self.send_response(update,
f"✅ Триггер успешно добавлен!\n\n"
f"🆔 ID: {trigger_id}\n"
f"🔑 Ключевые слова: {', '.join(keywords)}\n"
f"📝 Ответ: {response_text[:100]}{'...' if len(response_text) > 100 else ''}\n"
f"📊 Статус: {'Включен' if trigger_data['enabled'] else 'Выключен'}"
)
async def handle_trigger_list(self, update: Update, context: ContextTypes):
"""Обработка команды /trigger_list"""
await self.safe_execute(update, context, "trigger_list", self._handle_trigger_list)
async def _handle_trigger_list(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /trigger_list"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
# Получаем список триггеров
# Пока используем заглушку - в будущем получение через репозиторий
triggers = [] # self.trigger_repo.get_all_triggers()
if not triggers:
await self.send_response(update, "📭 Нет созданных триггеров")
return
# Создаем inline клавиатуру для управления триггерами
keyboard = []
response_text = "📋 Список триггеров\n\n"
for i, trigger in enumerate(triggers[:10]): # Ограничиваем до 10 для одного сообщения
trigger_id = trigger.get('id', i+1)
keywords = trigger.get('keywords', [])
response = trigger.get('response', '')
enabled = trigger.get('enabled', True)
status_emoji = "🟢" if enabled else "🔴"
keywords_display = ', '.join(keywords[:3]) # Показываем первые 3 ключевых слова
if len(keywords) > 3:
keywords_display += "..."
response_text += (
f"{status_emoji} #{trigger_id}\n"
f"🔑 {keywords_display}\n"
f"📝 {response[:50]}{'...' if len(response) > 50 else ''}\n\n"
)
# Кнопки управления для каждого триггера (максимум 2 кнопки в ряд)
keyboard.append([
InlineKeyboardButton(f"{'Выключить' if enabled else 'Включить'} #{trigger_id}",
callback_data=f'trigger_toggle_{trigger_id}'),
InlineKeyboardButton(f"Удалить #{trigger_id}",
callback_data=f'trigger_delete_{trigger_id}')
])
# Кнопка для добавления нового триггера
keyboard.append([InlineKeyboardButton("➕ Добавить триггер", callback_data='trigger_add_new')])
reply_markup = InlineKeyboardMarkup(keyboard)
# Ограничиваем длину сообщения Telegram
if len(response_text) > 4000:
response_text = response_text[:3997] + "..."
await self.send_response(update, response_text, parse_mode='HTML', reply_markup=reply_markup)
async def handle_trigger_edit(self, update: Update, context: ContextTypes):
"""Обработка команды /trigger_edit"""
await self.safe_execute(update, context, "trigger_edit", self._handle_trigger_edit)
async def _handle_trigger_edit(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /trigger_edit"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 1:
await self.send_response(update,
"❌ Использование: /trigger_edit \n\n"
"Пример: /trigger_edit 1\n\n"
"После выполнения команды будет показан редактор триггера."
)
return
trigger_id_str = context.args[0]
# Валидация ID триггера
try:
trigger_id = int(trigger_id_str)
except ValueError:
await self.send_response(update, "❌ ID триггера должен быть числом")
return
# Получаем триггер
# Пока используем заглушку - в будущем получение через репозиторий
trigger = None # self.trigger_repo.get_trigger_by_id(trigger_id)
if not trigger:
await self.send_response(update, f"❌ Триггер #{trigger_id} не найден")
return
# Сохраняем ID триггера для редактирования
context.user_data['editing_trigger_id'] = trigger_id
# Показываем форму редактирования
current_keywords = ', '.join(trigger.get('keywords', []))
current_response = trigger.get('response', '')
response_text = (
f"📝 Редактирование триггера #{trigger_id}\n\n"
f"🔑 Текущие ключевые слова: {current_keywords}\n"
f"📝 Текущий ответ: {current_response}\n\n"
"Отправьте новые ключевые слова (через запятую) или 'cancel' для отмены:"
)
await self.send_response(update, response_text, parse_mode='HTML')
async def handle_trigger_delete(self, update: Update, context: ContextTypes):
"""Обработка команды /trigger_delete"""
await self.safe_execute(update, context, "trigger_delete", self._handle_trigger_delete)
async def _handle_trigger_delete(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /trigger_delete"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 1:
await self.send_response(update,
"❌ Использование: /trigger_delete \n\n"
"Пример: /trigger_delete 1"
)
return
trigger_id_str = context.args[0]
# Валидация ID триггера
try:
trigger_id = int(trigger_id_str)
except ValueError:
await self.send_response(update, "❌ ID триггера должен быть числом")
return
# Получаем триггер для подтверждения
# Пока используем заглушку - в будущем получение через репозиторий
trigger = None # self.trigger_repo.get_trigger_by_id(trigger_id)
if not trigger:
await self.send_response(update, f"❌ Триггер #{trigger_id} не найден")
return
# Создаем клавиатуру для подтверждения удаления
keywords = trigger.get('keywords', [])
keywords_display = ', '.join(keywords[:3])
if len(keywords) > 3:
keywords_display += "..."
response_text = (
f"🗑️ Подтверждение удаления триггера #{trigger_id}\n\n"
f"🔑 Ключевые слова: {keywords_display}\n"
f"📝 Ответ: {trigger.get('response', '')[:100]}{'...' if len(trigger.get('response', '')) > 100 else ''}\n\n"
"Это действие нельзя отменить!"
)
keyboard = [
[InlineKeyboardButton("✅ Подтвердить удаление", callback_data=f'trigger_confirm_delete_{trigger_id}')],
[InlineKeyboardButton("❌ Отменить", callback_data='trigger_cancel_delete')]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await self.send_response(update, response_text, parse_mode='HTML', reply_markup=reply_markup)
async def handle_trigger_toggle(self, update: Update, context: ContextTypes):
"""Обработка команды /trigger_toggle"""
await self.safe_execute(update, context, "trigger_toggle", self._handle_trigger_toggle)
async def _handle_trigger_toggle(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /trigger_toggle"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
if len(context.args) < 1:
await self.send_response(update,
"❌ Использование: /trigger_toggle \n\n"
"Пример: /trigger_toggle 1"
)
return
trigger_id_str = context.args[0]
# Валидация ID триггера
try:
trigger_id = int(trigger_id_str)
except ValueError:
await self.send_response(update, "❌ ID триггера должен быть числом")
return
# Переключаем статус триггера
# Пока используем заглушку - в будущем через репозиторий
success = False # self.trigger_repo.toggle_trigger(trigger_id, user.id)
if success:
# Получаем обновленный статус
new_status = True # self.trigger_repo.get_trigger_status(trigger_id)
status_text = "включен" if new_status else "выключен"
await self.send_response(update, f"✅ Триггер #{trigger_id} успешно {status_text}")
else:
await self.send_response(update, f"❌ Триггер #{trigger_id} не найден или у вас нет прав на его изменение")
# ===== CALLBACK ОБРАБОТЧИКИ ДЛЯ ТРИГГЕРОВ =====
async def handle_trigger_manage(self, update: Update, context: ContextTypes):
"""Обработка управления триггерами через callback"""
query = update.callback_query
await query.answer()
# Пока используем заглушку
await query.edit_message_text("Функция управления триггерами в разработке")
async def handle_trigger_edit_callback(self, update: Update, context: ContextTypes):
"""Обработка callback редактирования триггера"""
query = update.callback_query
await query.answer()
# Пока используем заглушку
await query.edit_message_text("Функция редактирования триггера в разработке")
async def handle_trigger_delete_callback(self, update: Update, context: ContextTypes):
"""Обработка callback удаления триггера"""
query = update.callback_query
await query.answer()
# Пока используем заглушку
await query.edit_message_text("Функция удаления триггера в разработке")
async def handle_trigger_toggle_callback(self, update: Update, context: ContextTypes):
"""Обработка callback переключения статуса триггера"""
query = update.callback_query
await query.answer()
# Извлекаем ID триггера из callback_data
callback_data = query.data
if not callback_data.startswith('trigger_toggle_'):
return
try:
trigger_id = int(callback_data.split('_')[2])
except (IndexError, ValueError):
await query.edit_message_text("❌ Ошибка: некорректный ID триггера")
return
# Переключаем статус триггера
# Пока используем заглушку - в будущем через репозиторий
success = True # self.trigger_repo.toggle_trigger(trigger_id, query.from_user.id)
if success:
# Получаем обновленный статус
new_status = True # self.trigger_repo.get_trigger_status(trigger_id)
status_emoji = "🟢" if new_status else "🔴"
status_text = "включен" if new_status else "выключен"
await query.edit_message_text(
f"{status_emoji} Статус триггера #{trigger_id} изменен: {status_text}",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("⬅️ Назад к списку", callback_data='trigger_back_to_list')]
])
)
else:
await query.edit_message_text(f"❌ Триггер #{trigger_id} не найден или у вас нет прав на его изменение")
async def handle_admin_chats(self, update: Update, context: ContextTypes):
"""Обработка команды /admin_chats - показывает список чатов, где пользователь является администратором"""
await self.safe_execute(update, context, "admin_chats", self._handle_admin_chats)
async def _handle_admin_chats(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /admin_chats"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
try:
# Получаем список чатов, где пользователь является администратором
# Для этого нужно получить доступ к Telegram API через context.bot
bot = context.bot
# Получаем список чатов из конфигурации (как fallback)
# В реальной реализации нужно получить список чатов через Telegram API
chats_info = []
# Пример данных - в будущем заменить на реальный API вызов
# Для демонстрации используем тестовые данные
chats_info = [
{
'chat_id': -1001234567890,
'chat_title': 'Тестовый чат 1',
'user_status': 'administrator',
'permissions': ['can_delete_messages', 'can_restrict_members', 'can_promote_members']
},
{
'chat_id': -1001987654321,
'chat_title': 'Модерируемый чат 2',
'user_status': 'administrator',
'permissions': ['can_delete_messages', 'can_restrict_members']
}
]
# Формируем ответ
if not chats_info:
await self.send_response(update, "ℹ️ Вы не являетесь администратором ни в одном чате")
return
response_text = "🏢 Чаты, где вы администратор:\n\n"
for chat_info in chats_info:
chat_title = chat_info.get('chat_title', 'Неизвестный чат')
permissions = chat_info.get('permissions', [])
# Определяем уровень прав
if 'can_promote_members' in permissions:
rights_level = "👑 Полные права"
elif 'can_restrict_members' in permissions:
rights_level = "🛡️ Модераторские права"
else:
rights_level = "⚖️ Ограниченные права"
response_text += f"📍 {chat_title}\n"
response_text += f"🔑 Права: {rights_level}\n"
response_text += f"🆔 ID: {chat_info['chat_id']}\n\n"
# Добавляем кнопки для выбора чата для управления
keyboard = []
for chat_info in chats_info:
chat_title = chat_info.get('chat_title', 'Неизвестный чат')
chat_id = chat_info['chat_id']
keyboard.append([
InlineKeyboardButton(f"🎯 Управлять: {chat_title[:20]}...",
callback_data=f'admin_select_chat_{chat_id}')
])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data='menu_admin')])
reply_markup = InlineKeyboardMarkup(keyboard)
await self.send_response(update, response_text, parse_mode='HTML', reply_markup=reply_markup)
except Exception as e:
self.logger.error(f"Ошибка при получении списка чатов администратора: {e}")
await self.send_response(update, "❌ Ошибка при получении списка чатов")
# ===== ЭКСПОРТ СТАТИСТИКИ =====
async def handle_export_stats(self, update: Update, context: ContextTypes):
"""Обработка команды /export_stats"""
await self.safe_execute(update, context, "export_stats", self._handle_export_stats)
async def _handle_export_stats(self, update: Update, context: ContextTypes):
"""Внутренняя обработка команды /export_stats"""
user = update.effective_user
# Проверяем права администратора
await self.require_admin(update, user.id)
# Получаем формат экспорта
export_format = 'csv'
if context.args and len(context.args) > 0:
export_format = context.args[0].lower()
if export_format not in ['csv', 'excel']:
await self.send_response(update,
"❌ Неверный формат. Используйте:\n"
"/export_stats csv\n"
"/export_stats excel"
)
return
try:
# Получаем статистику пользователей
users_data = await self._get_users_statistics()
if not users_data:
await self.send_response(update, "❌ Нет данных для экспорта")
return
# Генерируем файл
if export_format == 'csv':
filename, file_content = self._generate_csv_export(users_data)
else:
filename, file_content = self._generate_excel_export(users_data)
# Отправляем файл пользователю
await update.message.reply_document(
document=file_content,
filename=filename,
caption=f"📊 Экспорт статистики пользователей ({export_format.upper()})\n"
f"Всего пользователей: {len(users_data)}"
)
except Exception as e:
await self.send_response(update, f"❌ Ошибка при экспорте статистики: {str(e)[:100]}")
async def _get_users_statistics(self) -> List[Dict]:
"""Получение статистики пользователей"""
# Здесь нужно получить данные из базы данных
# Пока используем заглушку
return [
{
'id': 1,
'telegram_id': 123456789,
'username': 'testuser',
'first_name': 'Тест',
'last_name': 'Пользователь',
'reputation': 1500,
'rank': 'Активист',
'message_count': 245,
'game_wins': 12,
'donations_total': 500.0,
'warnings': 1,
'joined_date': '2024-01-15',
'last_activity': '2024-01-20'
}
]
def _generate_csv_export(self, users_data: List[Dict]) -> Tuple[str, bytes]:
"""Генерация CSV файла"""
import csv
import io
output = io.StringIO()
writer = csv.writer(output, delimiter=';')
# Заголовок
writer.writerow([
'ID', 'Telegram ID', 'Username', 'Имя', 'Фамилия',
'Репутация', 'Ранг', 'Сообщений', 'Побед в играх',
'Сумма донатов', 'Предупреждений', 'Дата присоединения', 'Последняя активность'
])
# Данные пользователей
for user in users_data:
writer.writerow([
user['id'],
user['telegram_id'],
user['username'] or '',
user['first_name'],
user['last_name'] or '',
user['reputation'],
user['rank'],
user['message_count'],
user['game_wins'],
user['donations_total'],
user['warnings'],
user['joined_date'],
user['last_activity']
])
csv_content = output.getvalue().encode('utf-8-sig')
return f'bot_stats_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv', csv_content
def _generate_excel_export(self, users_data: List[Dict]) -> Tuple[str, bytes]:
"""Генерация Excel файла"""
try:
import pandas as pd
import io
# Создаем DataFrame
df = pd.DataFrame(users_data)
# Переименовываем колонки для удобства
column_names = {
'id': 'ID',
'telegram_id': 'Telegram ID',
'username': 'Username',
'first_name': 'Имя',
'last_name': 'Фамилия',
'reputation': 'Репутация',
'rank': 'Ранг',
'message_count': 'Сообщений',
'game_wins': 'Побед в играх',
'donations_total': 'Сумма донатов',
'warnings': 'Предупреждений',
'joined_date': 'Дата присоединения',
'last_activity': 'Последняя активность'
}
df = df.rename(columns=column_names)
# Создаем Excel файл в памяти
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Статистика пользователей', index=False)
# Настраиваем ширину колонок
worksheet = writer.sheets['Статистика пользователей']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
worksheet.column_dimensions[column_letter].width = adjusted_width
excel_content = output.getvalue()
return f'bot_stats_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx', excel_content
except ImportError:
# Если pandas недоступен, возвращаем CSV
return self._generate_csv_export(users_data)
def _add_error_to_todo_file(self, error_id: int, title: str, error_type: str, priority: str) -> bool:
"""Добавление ошибки в TODO файл"""
try:
import os
todo_file_path = os.path.join('telegram_bot', 'TODO.md')
# Читаем текущий файл TODO
if os.path.exists(todo_file_path):
with open(todo_file_path, 'r', encoding='utf-8') as file:
content = file.read()
else:
content = "# 📋 TODO - Список задач разработки\n\n"
# Определяем раздел для добавления задачи
priority_sections = {
'high': '## 🚀 Приоритетные задачи (High Priority)',
'medium': '## 🎯 Средний приоритет (Medium Priority)',
'low': '## 🔮 Низкий приоритет (Low Priority)'
}
section_title = priority_sections.get(priority, priority_sections['medium'])
task_text = f"- [ ] #{error_id} {title} (Тип: {error_type})"
# Находим место для вставки
lines = content.split('\n')
insert_index = -1
for i, line in enumerate(lines):
if line.startswith(section_title):
# Находим следующий подраздел или задачи в этом разделе
for j in range(i + 1, len(lines)):
next_line = lines[j]
if next_line.startswith('### ') and 'Система ошибок' in next_line:
# Вставляем перед подразделом "Система ошибок"
insert_index = j
break
elif next_line.startswith('## ') and next_line != lines[i]:
# Вставляем перед следующим основным разделом
insert_index = j
break
if insert_index == -1:
# Если не нашли место, вставляем в конец раздела
insert_index = len(lines)
break
if insert_index == -1:
# Если не нашли подходящий раздел, добавляем в конец файла
insert_index = len(lines)
# Вставляем задачу
lines.insert(insert_index, task_text)
# Записываем обновленный файл
with open(todo_file_path, 'w', encoding='utf-8') as file:
file.write('\n'.join(lines))
return True
except Exception as e:
print(f"Ошибка при добавлении ошибки в TODO файл: {e}")
return False