This commit is contained in:
2026-02-20 03:12:47 +07:00
parent 5d350d0885
commit 5aca4e8438
23 changed files with 2291 additions and 1330 deletions

View File

@@ -2,11 +2,12 @@
Обработчики команды /report для пользователей
"""
from datetime import datetime
from aiogram import Router, F
from aiogram.exceptions import TelegramBadRequest
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
@@ -18,29 +19,13 @@ __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)
@@ -49,11 +34,9 @@ def format_user(user: User) -> str:
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
return full_name
def format_datetime(dt: datetime) -> str:
@@ -72,7 +55,8 @@ def get_report_keyboard(
chat_id: int,
message_id: int,
reported_user_id: int,
report_id: str
report_id: str,
message_thread_id: int | None = None
) -> InlineKeyboardBuilder:
"""
Создает клавиатуру для репорта.
@@ -82,17 +66,19 @@ def get_report_keyboard(
message_id: ID сообщения
reported_user_id: ID пользователя, на которого пожаловались
report_id: Уникальный ID репорта
message_thread_id: ID топика (если есть)
"""
ikb = InlineKeyboardBuilder()
# Кнопки действий
thread_id = message_thread_id if message_thread_id is not None else 0
ikb.button(
text="🚫 Забанить",
callback_data=f"report:ban:{chat_id}:{reported_user_id}:{report_id}"
callback_data=f"report:ban:{chat_id}:{reported_user_id}:{report_id}:{thread_id}"
)
ikb.button(
text="🗑 Удалить",
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}"
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}:{thread_id}"
)
ikb.button(
text="✅ Закрыть",
@@ -115,17 +101,10 @@ async def report_cmd(message: Message) -> None:
"""
Отправляет жалобу на сообщение администраторам.
Доступно всем пользователям.
Использование:
/report — в ответ на сообщение
/report <причина> — в ответ на сообщение с указанием причины
Пример:
/report спам
/report оскорбления
"""
# Проверяем, что команда в ответ на сообщение
if not message.reply_to_message:
await message.answer(
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
@@ -133,7 +112,7 @@ async def report_cmd(message: Message) -> None:
"1. Ответьте на сообщение нарушителя\n"
"2. Напишите <code>/report</code> или <code>/report причина</code>\n\n"
"Пример: <code>/report спам</code>",
parse_mode="HTML"
parse_mode="HTML",
)
return
@@ -141,103 +120,104 @@ async def report_cmd(message: Message) -> None:
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")
await message.answer(
"❌ <b>Ошибка получения данных пользователя</b>",
parse_mode="HTML",
)
return
# Нельзя пожаловаться на самого себя
if reported_user.id == reporter.id:
await message.answer(
"⚠️ <b>Нельзя пожаловаться на самого себя</b>",
parse_mode="HTML"
parse_mode="HTML",
)
return
# Нельзя пожаловаться на бота
if reported_user.is_bot:
await message.answer(
"⚠️ <b>Нельзя пожаловаться на бота</b>",
parse_mode="HTML"
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"
parse_mode="HTML",
)
return
# Извлекаем причину (опционально)
parts = message.text.split(maxsplit=1)
parts = (message.text or "").split(maxsplit=1)
reason = parts[1] if len(parts) > 1 else "Не указана"
# Генерируем ID репорта
report_id = generate_report_id()
# === ФОРМИРУЕМ СООБЩЕНИЕ РЕПОРТА ===
# thread/topic исходного сообщения (если репортят из топика)
original_message_thread_id = reported_message.message_thread_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>Chat ID:</b> <code>{message.chat.id}</code>\n"
if original_message_thread_id:
report_text += f"📌 <b>Topic ID:</b> <code>{original_message_thread_id}</code>\n"
report_text += "\n"
# Причина
report_text += f"📝 <b>Причина:</b> {reason}\n\n"
report_text += "📄 <b>Текст сообщения:</b>\n"
# Текст сообщения
report_text += f"📄 <b>Текст сообщения:</b>\n"
message_content = None
if reported_message.text:
truncated_text = truncate_text(reported_message.text, max_length=300)
report_text += f"<code>{truncated_text}</code>\n\n"
message_content = reported_message.text
elif reported_message.caption:
truncated_caption = truncate_text(reported_message.caption, max_length=300)
report_text += f"<code>{truncated_caption}</code>\n\n"
message_content = reported_message.caption
else:
content_type = reported_message.content_type
report_text += f"<i>[{content_type}]</i>\n\n"
report_text += f"<i>[{reported_message.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
report_id=report_id,
message_thread_id=original_message_thread_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()
)
report_chat_id = settings.REPORT_CHAT_ID
report_thread_id = settings.REPORT_THREAD_ID
# Нормализуем: 0 считаем как "без топика"
if report_thread_id == 0:
report_thread_id = None
if report_chat_id:
send_params = {
"chat_id": report_chat_id,
"text": report_text,
"parse_mode": "HTML",
"reply_markup": keyboard.as_markup(),
}
if report_thread_id is not None:
send_params["message_thread_id"] = report_thread_id # отправка в конкретный топик
await message.bot.send_message(**send_params)
else:
# Отправляем всем владельцам
sent_count = 0
for owner_id in settings.OWNER_ID:
try:
@@ -254,24 +234,38 @@ async def report_cmd(message: Message) -> None:
if sent_count == 0:
raise Exception("Не удалось отправить репорт ни одному владельцу")
# Подтверждение пользователю
await manager.log_report(
report_id=report_id,
reporter_id=reporter.id,
reporter_username=reporter.username,
reported_user_id=reported_user.id,
reported_username=reported_user.username,
chat_id=message.chat.id,
chat_title=chat_title,
message_id=reported_message.message_id,
message_thread_id=original_message_thread_id,
message_text=message_content,
reason=reason
)
await message.answer(
"✅ <b>Жалоба отправлена администраторам</b>\n\n"
"Спасибо за бдительность! Администраторы рассмотрят вашу жалобу.",
parse_mode="HTML"
parse_mode="HTML",
)
# Логирование
logger.info(
f"Репорт #{report_id}: {reporter.id}{reported_user.id} в чате {message.chat.id}",
f"Репорт #{report_id}: {reporter.id}{reported_user.id} в чате {message.chat.id}"
+ (f" (топик {original_message_thread_id})" if original_message_thread_id else ""),
log_type="REPORT"
)
except Exception as e:
logger.error(f"Ошибка отправки репорта: {e}", log_type="REPORT")
await message.answer(
"❌ <b>Ошибка отправки жалобы</b>\n\nПопробуйте позже или обратитесь к администратору напрямую.",
parse_mode="HTML"
"❌ <b>Ошибка отправки жалобы</b>\n\n"
"Попробуйте позже или обратитесь к администратору напрямую.",
parse_mode="HTML",
)
@@ -280,30 +274,28 @@ async def report_cmd(message: Message) -> None:
@router.callback_query(F.data.startswith("report:ban:"), IsAdmin())
async def report_ban_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Забанить'"""
manager = get_manager()
try:
# Парсим данные: report:ban:chat_id:user_id:report_id
parts = callback.data.split(":")
parts = (callback.data or "").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
await callback.bot.ban_chat_member(chat_id=chat_id, user_id=user_id)
await manager.repo.update_report_status(
report_id=report_id,
status="banned",
processed_by=callback.from_user.id
)
admin_name = format_user(callback.from_user)
updated_text = (callback.message.text if callback.message else "") + f"\n\n✅ <b>Пользователь забанен</b> ({admin_name})"
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n✅ <b>Пользователь забанен</b> ({admin_name})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
if callback.message:
await callback.message.edit_text(text=updated_text, parse_mode="HTML")
await callback.answer("✅ Пользователь забанен", show_alert=True)
@@ -323,30 +315,28 @@ async def report_ban_callback(callback: CallbackQuery) -> None:
@router.callback_query(F.data.startswith("report:delete:"), IsAdmin())
async def report_delete_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Удалить'"""
manager = get_manager()
try:
# Парсим данные: report:delete:chat_id:message_id:report_id
parts = callback.data.split(":")
parts = (callback.data or "").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
await callback.bot.delete_message(chat_id=chat_id, message_id=message_id)
await manager.repo.update_report_status(
report_id=report_id,
status="deleted",
processed_by=callback.from_user.id
)
admin_name = format_user(callback.from_user)
updated_text = (callback.message.text if callback.message else "") + f"\n\n🗑 <b>Сообщение удалено</b> ({admin_name})"
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n🗑 <b>Сообщение удалено</b> ({admin_name})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
if callback.message:
await callback.message.edit_text(text=updated_text, parse_mode="HTML")
await callback.answer("✅ Сообщение удалено", show_alert=True)
@@ -365,25 +355,28 @@ async def report_delete_callback(callback: CallbackQuery) -> None:
@router.callback_query(F.data.startswith("report:close:"), IsAdmin())
async def report_close_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Закрыть'"""
"""Обрабатывает нажатие кнопки 'Закрыть' (и удаляет сообщение репорта)"""
manager = get_manager()
try:
# Парсим данные: report:close:report_id
parts = callback.data.split(":")
parts = (callback.data or "").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 manager.repo.update_report_status(
report_id=report_id,
status="closed",
processed_by=callback.from_user.id
)
await callback.answer("✅ Репорт закрыт")
# Удаляем сообщение с репортом в админ-чате/топике
if callback.message:
try:
await callback.message.delete()
except TelegramBadRequest as e:
logger.warning(f"Не удалось удалить сообщение репорта: {e}", log_type="REPORT")
logger.info(
f"Репорт #{report_id} закрыт админом {callback.from_user.id}",
log_type="REPORT"
@@ -394,15 +387,11 @@ async def report_close_callback(callback: CallbackQuery) -> None:
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"
@@ -425,23 +414,44 @@ async def report_help_cmd(message: Message) -> None:
await message.answer(text, parse_mode="HTML")
@router.message(Command(*COMMANDS.get("reportstats", ["reportstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@router.message(
Command(*COMMANDS.get("reportstats", ["reportstats"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin()
)
async def report_stats_cmd(message: Message) -> None:
"""
Показывает статистику по репортам (для админов).
"""Показывает статистику по репортам (для админов)"""
manager = get_manager()
TODO: Реализовать сохранение статистики в БД
"""
text = (
"📊 <b>СТАТИСТИКА РЕПОРТОВ</b>\n\n"
"⚠️ <i>Функция в разработке</i>\n\n"
"Планируется:\n"
"Всего репортов за всё время\n"
"• Топ жалобщиков\n"
"• Топ нарушителей\n"
"• Распределение по причинам\n"
"• Статистика обработки\n\n"
"💡 <i>Для реализации нужно добавить таблицу reports в БД</i>"
)
stats = await manager.repo.get_report_stats()
top_reporters = await manager.repo.get_top_reporters(limit=5)
top_reported = await manager.repo.get_top_reported_users(limit=5)
if not stats:
await message.answer("❌ <b>Ошибка получения статистики</b>", parse_mode="HTML")
return
text = "📊 <b>СТАТИСТИКА РЕПОРТОВ</b>\n\n"
text += "📈 <b>Общая статистика:</b>\n"
text += f"├─ Всего репортов: <b>{stats.get('total', 0)}</b>\n"
text += f"├─ В ожидании: <b>{stats.get('pending', 0)}</b>\n"
text += f"├─ Закрыто: <b>{stats.get('closed', 0)}</b>\n"
text += f"├─ Забанено: <b>{stats.get('banned', 0)}</b>\n"
text += f"└─ Удалено: <b>{stats.get('deleted', 0)}</b>\n\n"
if top_reporters:
text += "👥 <b>Топ жалобщиков:</b>\n"
for i, (user_id, username, count) in enumerate(top_reporters, 1):
username_display = f"@{username}" if username and not username.startswith("id") else (username or f"id{user_id}")
text += f"{i}. {username_display} — <b>{count}</b> реп.\n"
text += "\n"
if top_reported:
text += "⚠️ <b>Топ нарушителей:</b>\n"
for i, (user_id, username, count) in enumerate(top_reported, 1):
username_display = f"@{username}" if username and not username.startswith("id") else (username or f"id{user_id}")
text += f"{i}. {username_display} — <b>{count}</b> жалоб\n"
text += "\n"
text += f"🕐 <b>Обновлено:</b> {format_datetime(datetime.now())}"
await message.answer(text, parse_mode="HTML")