Files
new_lottery_bot/src/handlers/admin_chat_handlers.py
Trevor1985 f3f8d0f737
All checks were successful
continuous-integration/drone/push Build is passing
Fix text dialog routing and club card registration recovery
2026-09-13 21:39:40 +09:00

392 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Админские обработчики для управления чатом"""
from html import escape
from src.utils.input_validation import numeric_id
from src.utils.errors import public_error
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from src.filters.case_insensitive import CaseInsensitiveCommand as Command
from sqlalchemy.ext.asyncio import AsyncSession
from src.filters.case_insensitive import CaseInsensitiveCommand
from src.core.chat_services import (
ChatSettingsService,
BanService,
ChatMessageService
)
from src.core.services import UserService
from src.core.database import async_session_maker
from src.core.config import ADMIN_IDS
from src.core.permissions import admin_only
router = Router(name='admin_chat_router')
async def reply_target(message: Message):
"""Resolve the human author of a bot copy only within the operator's dialog."""
replied = message.reply_to_message
if replied.from_user and not replied.from_user.is_bot:
return replied.from_user.id
from sqlalchemy import select
from src.core.models import User, P2PMessage
async with async_session_maker() as session:
original = await ChatMessageService.get_message_by_telegram_id(
session, replied.message_id, chat_id=message.chat.id)
if original:
return await session.scalar(select(User.telegram_id).where(User.id == original.user_id))
operator_id = select(User.id).where(User.telegram_id == message.from_user.id).scalar_subquery()
return await session.scalar(select(User.telegram_id).join(P2PMessage, P2PMessage.sender_id == User.id).where(
P2PMessage.recipient_id == operator_id, P2PMessage.recipient_message_id == replied.message_id))
def get_chat_mode_keyboard() -> InlineKeyboardMarkup:
"""Клавиатура выбора режима чата"""
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="📢 Рассылка всем", callback_data="chat_mode:broadcast"),
InlineKeyboardButton(text="➡️ Пересылка в канал", callback_data="chat_mode:forward")
],
[InlineKeyboardButton(text="❌ Закрыть", callback_data="close_menu")]
])
@router.message(CaseInsensitiveCommand("chat_mode"))
@admin_only
async def cmd_chat_mode(message: Message):
"""Команда управления режимом чата (регистронезависимо)"""
async with async_session_maker() as session:
settings = await ChatSettingsService.get_or_create_settings(session)
mode_text = "📢 Рассылка всем пользователям" if settings.mode == 'broadcast' else "➡️ Пересылка в канал"
await message.answer(
f"🎛 <b>Управление режимом чата</b>\n\n"
f"Текущий режим: {mode_text}\n\n"
f"Выберите режим работы:",
reply_markup=get_chat_mode_keyboard(),
parse_mode="HTML"
)
@router.callback_query(F.data.startswith("chat_mode:"))
async def process_chat_mode(callback: CallbackQuery):
"""Обработка выбора режима чата"""
mode = callback.data.split(":")[1]
async with async_session_maker() as session:
settings = await ChatSettingsService.set_mode(session, mode)
mode_text = "📢 Рассылка всем пользователям" if mode == 'broadcast' else "➡️ Пересылка в канал"
await callback.message.edit_text(
f"✅ Режим чата изменен!\n\n"
f"Новый режим: {mode_text}",
reply_markup=None
)
await callback.answer("✅ Режим изменен")
@router.message(CaseInsensitiveCommand("set_forward"))
@admin_only
async def cmd_set_forward(message: Message):
"""Установить ID канала для пересылки (регистронезависимо)"""
args = message.text.split(maxsplit=1)
if len(args) < 2:
await message.answer(
"📝 <b>Использование:</b>\n"
"/set_forward <chat_id>\n\n"
"Пример: /set_forward -1001234567890\n\n"
"💡 Чтобы узнать ID канала/группы:\n"
"1. Добавьте бота в канал/группу\n"
"2. Напишите любое сообщение\n"
"3. Перешлите его боту @userinfobot",
parse_mode="HTML"
)
return
try:
chat_id = str(numeric_id(args[1], signed=True))
except ValueError as error:
await message.answer(str(error))
return
async with async_session_maker() as session:
settings = await ChatSettingsService.set_forward_chat(session, chat_id)
await message.answer(
f"✅ ID канала для пересылки установлен!\n\n"
f"Chat ID: <code>{chat_id}</code>\n\n"
f"Теперь переключитесь в режим пересылки командой /chat_mode",
parse_mode="HTML"
)
@router.message(CaseInsensitiveCommand("global_ban"))
@admin_only
async def cmd_global_ban(message: Message):
"""Включить/выключить глобальный бан чата (регистронезависимо)"""
async with async_session_maker() as session:
settings = await ChatSettingsService.get_or_create_settings(session)
# Переключаем состояние
new_state = not settings.global_ban
settings = await ChatSettingsService.set_global_ban(session, new_state)
if new_state:
await message.answer(
"🔇 <b>Глобальный бан включен</b>\n\n"
"Теперь только администраторы могут отправлять сообщения в чат",
parse_mode="HTML"
)
else:
await message.answer(
"🔊 <b>Глобальный бан выключен</b>\n\n"
"Все пользователи снова могут отправлять сообщения",
parse_mode="HTML"
)
@router.message(CaseInsensitiveCommand("ban"))
@admin_only
async def cmd_ban(message: Message):
"""Забанить пользователя (регистронезависимо)"""
# Проверяем является ли это ответом на сообщение
if message.reply_to_message:
target_user_id = await reply_target(message)
if target_user_id is None:
await message.answer("Не удалось определить автора. Укажите его Telegram ID в команде.")
return
reason = message.text.split(maxsplit=1)[1] if len(message.text.split(maxsplit=1)) > 1 else None
else:
args = message.text.split(maxsplit=2)
if len(args) < 2:
await message.answer(
"📝 <b>Использование:</b>\n\n"
"1. Ответьте на сообщение пользователя: /ban [причина]\n"
"2. Укажите ID: /ban <user_id> [причина]\n\n"
"Пример: /ban 123456789 Спам",
parse_mode="HTML"
)
return
try:
target_user_id = numeric_id(args[1])
reason = args[2] if len(args) > 2 else None
except ValueError:
await message.answer("❌ Неверный ID пользователя")
return
async with async_session_maker() as session:
# Получаем пользователя
user = await UserService.get_user_by_telegram_id(session, target_user_id)
if not user:
await message.answer("❌ Пользователь не найден в базе")
return
# Получаем админа
admin = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Баним
ban = await BanService.ban_user(
session,
user_id=user.id,
telegram_id=target_user_id,
banned_by=admin.id,
reason=reason
)
reason_text = f"\n📝 Причина: {escape(reason[:500])}" if reason else ""
await message.answer(
f"🚫 <b>Пользователь забанен</b>\n\n"
f"👤 Пользователь: {escape(user.nickname or user.first_name or 'Неизвестен')}\n"
f"🆔 ID: <code>{target_user_id}</code>"
f"{reason_text}",
parse_mode="HTML"
)
@router.message(CaseInsensitiveCommand("unban"))
@admin_only
async def cmd_unban(message: Message):
"""Разбанить пользователя (регистронезависимо)"""
# Проверяем является ли это ответом на сообщение
if message.reply_to_message:
target_user_id = await reply_target(message)
if target_user_id is None:
await message.answer("Не удалось определить автора. Укажите его Telegram ID в команде.")
return
else:
args = message.text.split()
if len(args) < 2:
await message.answer(
"📝 <b>Использование:</b>\n\n"
"1. Ответьте на сообщение пользователя: /unban\n"
"2. Укажите ID: /unban <user_id>\n\n"
"Пример: /unban 123456789",
parse_mode="HTML"
)
return
try:
target_user_id = numeric_id(args[1])
except ValueError:
await message.answer("❌ Неверный ID пользователя")
return
async with async_session_maker() as session:
# Разбаниваем
success = await BanService.unban_user(session, target_user_id)
if success:
await message.answer(
f"✅ <b>Пользователь разбанен</b>\n\n"
f"🆔 ID: <code>{target_user_id}</code>\n\n"
f"Теперь пользователь может отправлять сообщения",
parse_mode="HTML"
)
else:
await message.answer("❌ Пользователь не был забанен")
@router.message(CaseInsensitiveCommand("banlist"))
@admin_only
async def cmd_banlist(message: Message):
"""Показать список заблокированных пользователей (регистронезависимо)"""
async with async_session_maker() as session:
banned_users = await BanService.get_banned_users(session, active_only=True)
if not banned_users:
await message.answer("📋 Список банов пуст")
return
text = "🚫 <b>Забаненные пользователи</b>\n\n"
for ban in banned_users:
user = ban.user
admin = ban.admin
text += f"👤 {escape(user.nickname or user.first_name or 'Неизвестен')} (<code>{ban.telegram_id}</code>)\n"
text += f"🔨 Забанил: {escape((admin.nickname or admin.first_name or 'Неизвестен') if admin else 'Неизвестен')}\n"
if ban.reason:
text += f"📝 Причина: {escape(ban.reason[:500])}\n"
text += f"📅 Дата: {ban.banned_at.strftime('%d.%m.%Y %H:%M')}\n"
text += "\n"
await message.answer(text, parse_mode="HTML")
@router.message(CaseInsensitiveCommand("delete_msg"))
@admin_only
async def cmd_delete_message(message: Message):
"""Удалить сообщение из чата (пометить как удаленное) (регистронезависимо)"""
if not message.reply_to_message:
await message.answer(
"📝 <b>Использование:</b>\n\n"
"Ответьте на сообщение которое хотите удалить командой /delete_msg",
parse_mode="HTML"
)
return
async with async_session_maker() as session:
# Получаем админа
admin = await UserService.get_or_create_user(
session,
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Находим сообщение в базе по telegram_message_id
from sqlalchemy import select
from src.core.models import ChatMessage
chat_message = await ChatMessageService.get_message_by_telegram_id(
session, message.reply_to_message.message_id, chat_id=message.chat.id,
)
if not chat_message:
await message.answer("❌ Сообщение не найдено в базе данных")
return
# Помечаем как удаленное
success = await ChatMessageService.delete_message(
session,
message_id=chat_message.id,
deleted_by=admin.id
)
if success:
# Пытаемся удалить сообщение у всех пользователей
if chat_message.forwarded_message_ids:
deleted_count = 0
for user_telegram_id, msg_id in chat_message.forwarded_message_ids.items():
try:
await message.bot.delete_message(int(user_telegram_id), msg_id)
deleted_count += 1
except Exception as e:
print(f"Failed to delete message {msg_id} for user {user_telegram_id}: {public_error(e)}")
await message.answer(
f"✅ <b>Сообщение удалено</b>\n\n"
f"🗑 Удалено у {deleted_count} пользователей",
parse_mode="HTML"
)
else:
await message.answer("✅ Сообщение помечено как удаленное")
else:
await message.answer("Не удалось удалить сообщение")
@router.message(CaseInsensitiveCommand("chat_stats"))
@admin_only
async def cmd_chat_stats(message: Message):
"""Статистика чата (регистронезависимо)"""
async with async_session_maker() as session:
settings = await ChatSettingsService.get_or_create_settings(session)
banned_users = await BanService.get_banned_users(session, active_only=True)
recent_messages = await ChatMessageService.get_recent_messages(session, limit=100)
mode_text = "📢 Рассылка всем" if settings.mode == 'broadcast' else "➡️ Пересылка в канал"
global_ban_text = "🔇 Включен" if settings.global_ban else "🔊 Выключен"
text = (
f"📊 <b>Статистика чата</b>\n\n"
f"🎛 Режим: {mode_text}\n"
f"🚫 Глобальный бан: {global_ban_text}\n"
f"👥 Забанено пользователей: {len(banned_users)}\n"
f"💬 Сообщений за последнее время: {len(recent_messages)}\n"
)
if settings.mode == 'forward' and settings.forward_chat_id:
text += f"\n➡️ ID канала: <code>{settings.forward_chat_id}</code>"
await message.answer(text, parse_mode="HTML")
@router.callback_query(F.data == "close_menu")
async def close_menu(callback: CallbackQuery):
"""Закрыть меню"""
await callback.message.delete()
await callback.answer()