Отправка репортов на сообщения
This commit is contained in:
457
bot/handlers/commands/users/report.py
Normal file
457
bot/handlers/commands/users/report.py
Normal 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")
|
||||||
Reference in New Issue
Block a user