Files
bottohelp/handlers/admin_handlers.py
T

1331 lines
64 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Обработчики административных команд.
Отвечают за модерацию пользователей и управление ботом.
"""
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 = (
"📊 <b>Статистика модерации</b>\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"
"📈 <b>Статистика пользователей</b>\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 = (
"📝 <b>Предварительный просмотр поста:</b>\n\n"
f"📅 <b>Время публикации:</b> {schedule_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"📋 <b>Текст поста:</b>\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 = "📋 <b>Запланированные посты:</b>\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"🆔 <b>{post_id}</b>\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 = "📋 <b>Список ошибок и отчетов</b>\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} <b>#{error.get('id', 'N/A')}</b> {priority_emoji} {status_emoji}\n"
f"📝 <b>{error.get('title', 'Без заголовка')}</b>\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 <ID_ошибки>\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 <ID_ошибки> [приоритет]\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 = "📋 <b>Список триггеров</b>\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} <b>#{trigger_id}</b>\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 <ID_триггера>\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"📝 <b>Редактирование триггера #{trigger_id}</b>\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 <ID_триггера>\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"🗑️ <b>Подтверждение удаления триггера #{trigger_id}</b>\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 <ID_триггера>\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 = "🏢 <b>Чаты, где вы администратор:</b>\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"📍 <b>{chat_title}</b>\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