Отправка репортов на сообщения

This commit is contained in:
2026-02-23 14:27:53 +07:00
parent e5225067f7
commit ab73fa121c

View File

@@ -0,0 +1,457 @@
"""
Обработчики команды /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 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")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def format_user(user: User) -> str:
"""Форматирует информацию о пользователе."""
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"
if user.username:
return f"{full_name} (@{user.username})"
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,
message_thread_id: int | None = None
) -> InlineKeyboardBuilder:
"""
Создает клавиатуру для репорта.
Args:
chat_id: ID чата, где было сообщение
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}:{thread_id}"
)
ikb.button(
text="🗑 Удалить",
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}:{thread_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 <причина> — в ответ на сообщение с указанием причины
"""
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
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 or "").split(maxsplit=1)
reason = parts[1] if len(parts) > 1 else "Не указана"
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"
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"
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:
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,
message_thread_id=original_message_thread_id
)
try:
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:
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 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",
)
logger.info(
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",
)
# ================= ОБРАБОТЧИКИ КНОПОК =================
@router.callback_query(F.data.startswith("report:ban:"), IsAdmin())
async def report_ban_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Забанить'"""
manager = get_manager()
try:
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 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})"
if callback.message:
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:
"""Обрабатывает нажатие кнопки 'Удалить'"""
manager = get_manager()
try:
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 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})"
if callback.message:
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:
"""Обрабатывает нажатие кнопки 'Закрыть' (и удаляет сообщение репорта)"""
manager = get_manager()
try:
parts = (callback.data or "").split(":")
report_id = parts[2]
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"
)
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:
"""Показывает статистику по репортам (для админов)"""
manager = get_manager()
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")