Отправка репортов на сообщения
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