448 lines
17 KiB
Python
448 lines
17 KiB
Python
"""
|
||
Обработчики команды /report для пользователей
|
||
"""
|
||
from datetime import datetime
|
||
from aiogram import Router, F
|
||
from aiogram.filters import Command
|
||
from aiogram.types import Message, CallbackQuery, User
|
||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||
from aiogram.exceptions import TelegramBadRequest
|
||
|
||
from bot.filters.admin import IsAdmin
|
||
from configs import settings, COMMANDS
|
||
from database import get_manager
|
||
from middleware.loggers import logger
|
||
|
||
__all__ = ("router",)
|
||
|
||
router: Router = Router(name="report_router")
|
||
|
||
|
||
# ================= НАСТРОЙКИ =================
|
||
|
||
# ID чата для отправки репортов (можно вынести в configs)
|
||
# Если None, репорты отправляются всем владельцам в ЛС
|
||
REPORT_CHAT_ID = getattr(settings, 'REPORT_CHAT_ID', None)
|
||
|
||
|
||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||
|
||
def format_user(user: User) -> str:
|
||
"""
|
||
Форматирует информацию о пользователе.
|
||
|
||
Args:
|
||
user: Объект User
|
||
|
||
Returns:
|
||
Отформатированная строка с именем и username
|
||
"""
|
||
if not user:
|
||
return "Unknown User"
|
||
|
||
# Формируем имя
|
||
name_parts = []
|
||
if user.first_name:
|
||
name_parts.append(user.first_name)
|
||
if user.last_name:
|
||
name_parts.append(user.last_name)
|
||
|
||
full_name = " ".join(name_parts) if name_parts else "No Name"
|
||
|
||
# Добавляем username если есть
|
||
if user.username:
|
||
return f"{full_name} (@{user.username})"
|
||
else:
|
||
return full_name
|
||
|
||
|
||
def format_datetime(dt: datetime) -> str:
|
||
"""Форматирует datetime"""
|
||
return dt.strftime("%d.%m.%Y %H:%M:%S")
|
||
|
||
|
||
def truncate_text(text: str, max_length: int = 200) -> str:
|
||
"""Обрезает текст до указанной длины"""
|
||
if len(text) <= max_length:
|
||
return text
|
||
return text[:max_length] + "..."
|
||
|
||
|
||
def get_report_keyboard(
|
||
chat_id: int,
|
||
message_id: int,
|
||
reported_user_id: int,
|
||
report_id: str
|
||
) -> InlineKeyboardBuilder:
|
||
"""
|
||
Создает клавиатуру для репорта.
|
||
|
||
Args:
|
||
chat_id: ID чата, где было сообщение
|
||
message_id: ID сообщения
|
||
reported_user_id: ID пользователя, на которого пожаловались
|
||
report_id: Уникальный ID репорта
|
||
"""
|
||
ikb = InlineKeyboardBuilder()
|
||
|
||
# Кнопки действий
|
||
ikb.button(
|
||
text="🚫 Забанить",
|
||
callback_data=f"report:ban:{chat_id}:{reported_user_id}:{report_id}"
|
||
)
|
||
ikb.button(
|
||
text="🗑 Удалить",
|
||
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}"
|
||
)
|
||
ikb.button(
|
||
text="✅ Закрыть",
|
||
callback_data=f"report:close:{report_id}"
|
||
)
|
||
|
||
ikb.adjust(2, 1)
|
||
return ikb
|
||
|
||
|
||
def generate_report_id() -> str:
|
||
"""Генерирует уникальный ID репорта"""
|
||
return f"{int(datetime.now().timestamp() * 1000)}"
|
||
|
||
|
||
# ================= КОМАНДА РЕПОРТА =================
|
||
|
||
@router.message(Command(*COMMANDS.get("report", ["report"]), prefix=settings.PREFIX, ignore_case=True))
|
||
async def report_cmd(message: Message) -> None:
|
||
"""
|
||
Отправляет жалобу на сообщение администраторам.
|
||
|
||
Доступно всем пользователям.
|
||
|
||
Использование:
|
||
/report — в ответ на сообщение
|
||
/report <причина> — в ответ на сообщение с указанием причины
|
||
|
||
Пример:
|
||
/report спам
|
||
/report оскорбления
|
||
"""
|
||
# Проверяем, что команда в ответ на сообщение
|
||
if not message.reply_to_message:
|
||
await message.answer(
|
||
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
|
||
"Как использовать:\n"
|
||
"1. Ответьте на сообщение нарушителя\n"
|
||
"2. Напишите <code>/report</code> или <code>/report причина</code>\n\n"
|
||
"Пример: <code>/report спам</code>",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
reported_message = message.reply_to_message
|
||
reported_user = reported_message.from_user
|
||
reporter = message.from_user
|
||
|
||
# Проверка на None
|
||
if not reported_user or not reporter:
|
||
await message.answer("❌ <b>Ошибка получения данных пользователя</b>", parse_mode="HTML")
|
||
return
|
||
|
||
# Нельзя пожаловаться на самого себя
|
||
if reported_user.id == reporter.id:
|
||
await message.answer(
|
||
"⚠️ <b>Нельзя пожаловаться на самого себя</b>",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
# Нельзя пожаловаться на бота
|
||
if reported_user.is_bot:
|
||
await message.answer(
|
||
"⚠️ <b>Нельзя пожаловаться на бота</b>",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
# Нельзя пожаловаться на администратора
|
||
manager = get_manager()
|
||
is_admin = await manager.is_admin(reported_user.id) or reported_user.id in settings.OWNER_ID
|
||
|
||
if is_admin:
|
||
await message.answer(
|
||
"⚠️ <b>Нельзя пожаловаться на администратора</b>",
|
||
parse_mode="HTML"
|
||
)
|
||
return
|
||
|
||
# Извлекаем причину (опционально)
|
||
parts = message.text.split(maxsplit=1)
|
||
reason = parts[1] if len(parts) > 1 else "Не указана"
|
||
|
||
# Генерируем ID репорта
|
||
report_id = generate_report_id()
|
||
|
||
# === ФОРМИРУЕМ СООБЩЕНИЕ РЕПОРТА ===
|
||
|
||
report_text = "🚨 <b>НОВЫЙ РЕПОРТ</b>\n\n"
|
||
|
||
# Информация о жалобщике
|
||
report_text += f"👤 <b>От:</b> {format_user(reporter)} (<code>{reporter.id}</code>)\n"
|
||
|
||
# Информация о нарушителе
|
||
report_text += f"⚠️ <b>На:</b> {format_user(reported_user)} (<code>{reported_user.id}</code>)\n\n"
|
||
|
||
# Информация о чате
|
||
chat_title = message.chat.title if message.chat.title else "Личные сообщения"
|
||
report_text += f"💬 <b>Чат:</b> {chat_title}\n"
|
||
report_text += f"🆔 <b>Chat ID:</b> <code>{message.chat.id}</code>\n\n"
|
||
|
||
# Причина
|
||
report_text += f"📝 <b>Причина:</b> {reason}\n\n"
|
||
|
||
# Текст сообщения
|
||
report_text += f"📄 <b>Текст сообщения:</b>\n"
|
||
|
||
if reported_message.text:
|
||
truncated_text = truncate_text(reported_message.text, max_length=300)
|
||
report_text += f"<code>{truncated_text}</code>\n\n"
|
||
elif reported_message.caption:
|
||
truncated_caption = truncate_text(reported_message.caption, max_length=300)
|
||
report_text += f"<code>{truncated_caption}</code>\n\n"
|
||
else:
|
||
content_type = reported_message.content_type
|
||
report_text += f"<i>[{content_type}]</i>\n\n"
|
||
|
||
# Время
|
||
report_text += f"🕐 <b>Время:</b> {format_datetime(datetime.now())}\n"
|
||
report_text += f"🔗 <b>Message ID:</b> <code>{reported_message.message_id}</code>\n\n"
|
||
|
||
report_text += f"💡 <i>ID репорта: {report_id}</i>"
|
||
|
||
# Клавиатура
|
||
keyboard = get_report_keyboard(
|
||
chat_id=message.chat.id,
|
||
message_id=reported_message.message_id,
|
||
reported_user_id=reported_user.id,
|
||
report_id=report_id
|
||
)
|
||
|
||
# === ОТПРАВКА РЕПОРТА ===
|
||
|
||
try:
|
||
# Если указан админ-чат, отправляем туда
|
||
if REPORT_CHAT_ID:
|
||
await message.bot.send_message(
|
||
chat_id=REPORT_CHAT_ID,
|
||
text=report_text,
|
||
parse_mode="HTML",
|
||
reply_markup=keyboard.as_markup()
|
||
)
|
||
else:
|
||
# Отправляем всем владельцам
|
||
sent_count = 0
|
||
for owner_id in settings.OWNER_ID:
|
||
try:
|
||
await message.bot.send_message(
|
||
chat_id=owner_id,
|
||
text=report_text,
|
||
parse_mode="HTML",
|
||
reply_markup=keyboard.as_markup()
|
||
)
|
||
sent_count += 1
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отправки репорта владельцу {owner_id}: {e}", log_type="REPORT")
|
||
|
||
if sent_count == 0:
|
||
raise Exception("Не удалось отправить репорт ни одному владельцу")
|
||
|
||
# Подтверждение пользователю
|
||
await message.answer(
|
||
"✅ <b>Жалоба отправлена администраторам</b>\n\n"
|
||
"Спасибо за бдительность! Администраторы рассмотрят вашу жалобу.",
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
# Логирование
|
||
logger.info(
|
||
f"Репорт #{report_id}: {reporter.id} → {reported_user.id} в чате {message.chat.id}",
|
||
log_type="REPORT"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отправки репорта: {e}", log_type="REPORT")
|
||
await message.answer(
|
||
"❌ <b>Ошибка отправки жалобы</b>\n\nПопробуйте позже или обратитесь к администратору напрямую.",
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
|
||
# ================= ОБРАБОТЧИКИ КНОПОК =================
|
||
|
||
@router.callback_query(F.data.startswith("report:ban:"), IsAdmin())
|
||
async def report_ban_callback(callback: CallbackQuery) -> None:
|
||
"""Обрабатывает нажатие кнопки 'Забанить'"""
|
||
try:
|
||
# Парсим данные: report:ban:chat_id:user_id:report_id
|
||
parts = callback.data.split(":")
|
||
chat_id = int(parts[2])
|
||
user_id = int(parts[3])
|
||
report_id = parts[4]
|
||
|
||
# Баним пользователя
|
||
try:
|
||
await callback.bot.ban_chat_member(
|
||
chat_id=chat_id,
|
||
user_id=user_id
|
||
)
|
||
|
||
admin_name = format_user(callback.from_user)
|
||
|
||
# Обновляем сообщение
|
||
updated_text = callback.message.text + f"\n\n✅ <b>Пользователь забанен</b> ({admin_name})"
|
||
|
||
# Убираем кнопки
|
||
await callback.message.edit_text(
|
||
text=updated_text,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer("✅ Пользователь забанен", show_alert=True)
|
||
|
||
logger.info(
|
||
f"Репорт #{report_id}: пользователь {user_id} забанен админом {callback.from_user.id}",
|
||
log_type="REPORT"
|
||
)
|
||
|
||
except TelegramBadRequest as e:
|
||
await callback.answer(f"❌ Ошибка бана: {str(e)}", show_alert=True)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка обработки бана из репорта: {e}", log_type="REPORT")
|
||
await callback.answer("❌ Ошибка выполнения", show_alert=True)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("report:delete:"), IsAdmin())
|
||
async def report_delete_callback(callback: CallbackQuery) -> None:
|
||
"""Обрабатывает нажатие кнопки 'Удалить'"""
|
||
try:
|
||
# Парсим данные: report:delete:chat_id:message_id:report_id
|
||
parts = callback.data.split(":")
|
||
chat_id = int(parts[2])
|
||
message_id = int(parts[3])
|
||
report_id = parts[4]
|
||
|
||
# Удаляем сообщение
|
||
try:
|
||
await callback.bot.delete_message(
|
||
chat_id=chat_id,
|
||
message_id=message_id
|
||
)
|
||
|
||
admin_name = format_user(callback.from_user)
|
||
|
||
# Обновляем сообщение
|
||
updated_text = callback.message.text + f"\n\n🗑 <b>Сообщение удалено</b> ({admin_name})"
|
||
|
||
# Убираем кнопки
|
||
await callback.message.edit_text(
|
||
text=updated_text,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer("✅ Сообщение удалено", show_alert=True)
|
||
|
||
logger.info(
|
||
f"Репорт #{report_id}: сообщение {message_id} удалено админом {callback.from_user.id}",
|
||
log_type="REPORT"
|
||
)
|
||
|
||
except TelegramBadRequest as e:
|
||
await callback.answer(f"❌ Ошибка удаления: {str(e)}", show_alert=True)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка удаления из репорта: {e}", log_type="REPORT")
|
||
await callback.answer("❌ Ошибка выполнения", show_alert=True)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("report:close:"), IsAdmin())
|
||
async def report_close_callback(callback: CallbackQuery) -> None:
|
||
"""Обрабатывает нажатие кнопки 'Закрыть'"""
|
||
try:
|
||
# Парсим данные: report:close:report_id
|
||
parts = callback.data.split(":")
|
||
report_id = parts[2]
|
||
|
||
admin_name = format_user(callback.from_user)
|
||
|
||
# Обновляем сообщение
|
||
updated_text = callback.message.text + f"\n\n✅ <b>Репорт закрыт</b> ({admin_name})"
|
||
|
||
# Убираем кнопки
|
||
await callback.message.edit_text(
|
||
text=updated_text,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer("✅ Репорт закрыт")
|
||
|
||
logger.info(
|
||
f"Репорт #{report_id} закрыт админом {callback.from_user.id}",
|
||
log_type="REPORT"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка закрытия репорта: {e}", log_type="REPORT")
|
||
await callback.answer("❌ Ошибка выполнения", show_alert=True)
|
||
|
||
|
||
# ================= ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ =================
|
||
|
||
@router.message(Command(*COMMANDS.get("reporthelp", ["reporthelp"]), prefix=settings.PREFIX, ignore_case=True))
|
||
async def report_help_cmd(message: Message) -> None:
|
||
"""
|
||
Показывает справку по системе репортов.
|
||
|
||
Доступно всем пользователям.
|
||
"""
|
||
text = (
|
||
"🚨 <b>СИСТЕМА РЕПОРТОВ</b>\n\n"
|
||
"Используйте команду /report, чтобы пожаловаться на сообщение администраторам.\n\n"
|
||
"📝 <b>Как пожаловаться:</b>\n"
|
||
"1. Ответьте на сообщение нарушителя\n"
|
||
"2. Напишите <code>/report</code>\n"
|
||
"3. Можно указать причину: <code>/report спам</code>\n\n"
|
||
"✅ <b>Примеры:</b>\n"
|
||
"• <code>/report</code> — жалоба без причины\n"
|
||
"• <code>/report спам</code> — жалоба на спам\n"
|
||
"• <code>/report оскорбления</code> — жалоба на оскорбления\n\n"
|
||
"⚠️ <b>Важно:</b>\n"
|
||
"├─ Нельзя пожаловаться на себя\n"
|
||
"├─ Нельзя пожаловаться на ботов\n"
|
||
"├─ Нельзя пожаловаться на администраторов\n"
|
||
"└─ Ложные жалобы могут привести к бану\n\n"
|
||
"💡 <i>Администраторы получат уведомление и примут меры</i>"
|
||
)
|
||
|
||
await message.answer(text, parse_mode="HTML")
|
||
|
||
|
||
@router.message(Command(*COMMANDS.get("reportstats", ["reportstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||
async def report_stats_cmd(message: Message) -> None:
|
||
"""
|
||
Показывает статистику по репортам (для админов).
|
||
|
||
TODO: Реализовать сохранение статистики в БД
|
||
"""
|
||
text = (
|
||
"📊 <b>СТАТИСТИКА РЕПОРТОВ</b>\n\n"
|
||
"⚠️ <i>Функция в разработке</i>\n\n"
|
||
"Планируется:\n"
|
||
"• Всего репортов за всё время\n"
|
||
"• Топ жалобщиков\n"
|
||
"• Топ нарушителей\n"
|
||
"• Распределение по причинам\n"
|
||
"• Статистика обработки\n\n"
|
||
"💡 <i>Для реализации нужно добавить таблицу reports в БД</i>"
|
||
)
|
||
|
||
await message.answer(text, parse_mode="HTML")
|