From b249f3c6943583c21bfdf489eb3a8909bd753d8f Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Fri, 14 Nov 2025 17:32:41 +0300 Subject: [PATCH] Remove break in hooks / improve ban management --- handlers/admin/bans/bans_handler.py | 257 ++++++++++++++++++--- handlers/admin/bans/keyboard.py | 86 +++++-- handlers/keys/key_mode/key_cluster_mode.py | 2 - handlers/keys/key_mode/key_country_mode.py | 2 - handlers/keys/key_view.py | 2 - 5 files changed, 292 insertions(+), 57 deletions(-) diff --git a/handlers/admin/bans/bans_handler.py b/handlers/admin/bans/bans_handler.py index a9728ef5..4c511022 100644 --- a/handlers/admin/bans/bans_handler.py +++ b/handlers/admin/bans/bans_handler.py @@ -7,17 +7,23 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import BufferedInputFile, CallbackQuery, Message -from sqlalchemy import delete, text +from aiogram.utils.keyboard import InlineKeyboardBuilder +from sqlalchemy import delete, func, or_, select from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession from database import delete_user_data -from database.models import ManualBan +from database.models import BlockedUser, Key, ManualBan from filters.admin import IsAdminFilter from logger import logger from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb -from .keyboard import build_bans_kb +from .keyboard import ( + build_bans_kb, + build_blocked_users_kb, + build_manual_bans_kb, + build_shadow_bans_kb, +) router = Router() @@ -32,25 +38,66 @@ async def handle_bans(callback_query: CallbackQuery): text_ = ( "🚫 Управление банами\n\n" "📛 Забанившие бота — пользователи, которые заблокировали бота вручную.\n" - "🔒 Ручной бан — пользователи, которых вы забанили через админку.\n\n" + "👻 Теневые баны — пользователи, действия которых игнорируются.\n" + "🔒 Ручные баны — пользователи, которых вы забанили через админку.\n\n" "⬇ Выберите нужный раздел:" ) await callback_query.message.edit_text(text=text_, reply_markup=build_bans_kb()) +@router.callback_query(AdminPanelCallback.filter(F.action == "bans_blocked_menu"), IsAdminFilter()) +async def handle_blocked_users_menu(callback_query: CallbackQuery): + text_ = ( + "📛 Забанившие бота\n\n" + "Пользователи, которые заблокировали бота вручную или удалили чат.\n" + "⬇ Выберите действие:" + ) + await callback_query.message.edit_text(text=text_, reply_markup=build_blocked_users_kb()) + + +def get_shadow_bans_menu_text() -> str: + return ( + "👻 Теневые баны\n\n" + "Пользователи, действия которых игнорируются ботом.\n" + "Они не получают уведомлений о бане.\n\n" + "💡 Можно добавить несколько пользователей за раз:\n" + "Отправьте список Telegram ID (один на строке).\n" + "Пример:\n123456789\n987654321\n555666777\n\n" + "⬇ Выберите действие:" + ) + + +@router.callback_query(AdminPanelCallback.filter(F.action == "bans_shadow_menu"), IsAdminFilter()) +async def handle_shadow_bans_menu(callback_query: CallbackQuery): + await callback_query.message.edit_text( + text=get_shadow_bans_menu_text(), + reply_markup=build_shadow_bans_kb() + ) + + +@router.callback_query(AdminPanelCallback.filter(F.action == "bans_manual_menu"), IsAdminFilter()) +async def handle_manual_bans_menu(callback_query: CallbackQuery): + text_ = ( + "🔒 Ручные баны\n\n" + "Пользователи, которых вы забанили через админку.\n" + "⬇ Выберите действие:" + ) + await callback_query.message.edit_text(text=text_, reply_markup=build_manual_bans_kb()) + + @router.callback_query(AdminPanelCallback.filter(F.action == "bans_export"), IsAdminFilter()) async def handle_bans_export(callback_query: CallbackQuery, session: AsyncSession): - kb = build_admin_back_kb("management") + kb = build_blocked_users_kb() try: - result = await session.execute(text("SELECT tg_id FROM blocked_users")) - banned_users = result.all() + result = await session.execute(select(BlockedUser.tg_id)) + banned_users = result.scalars().all() csv_output = io.StringIO() writer = csv.writer(csv_output) writer.writerow(["tg_id"]) - for user in banned_users: - writer.writerow([user.tg_id]) + for tg_id in banned_users: + writer.writerow([tg_id]) csv_output.seek(0) document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv") @@ -68,11 +115,15 @@ async def handle_bans_export(callback_query: CallbackQuery, session: AsyncSessio @router.callback_query(AdminPanelCallback.filter(F.action == "bans_delete_banned"), IsAdminFilter()) async def handle_bans_delete_banned(callback_query: CallbackQuery, session: AsyncSession): - kb = build_admin_back_kb("bans") + kb = build_blocked_users_kb() try: - result = await session.execute(text("SELECT tg_id FROM blocked_users")) - blocked_users = result.all() - blocked_ids = [user.tg_id for user in blocked_users] + stmt = ( + select(BlockedUser.tg_id) + .outerjoin(Key, BlockedUser.tg_id == Key.tg_id) + .where(Key.tg_id.is_(None)) + ) + result = await session.execute(stmt) + blocked_ids = [row[0] for row in result.all()] if not blocked_ids: await callback_query.message.answer( @@ -84,12 +135,6 @@ async def handle_bans_delete_banned(callback_query: CallbackQuery, session: Asyn for tg_id in blocked_ids: await delete_user_data(session, tg_id) - await session.execute( - text("DELETE FROM blocked_users WHERE tg_id = ANY(:blocked_ids)"), - {"blocked_ids": blocked_ids}, - ) - await session.commit() - await callback_query.message.answer( text=f"🗑️ Удалены данные о {len(blocked_ids)} пользователях и связанных записях.", reply_markup=kb, @@ -101,57 +146,190 @@ async def handle_bans_delete_banned(callback_query: CallbackQuery, session: Asyn ) -@router.callback_query(AdminPanelCallback.filter(F.action == "manual_bans_export"), IsAdminFilter()) -async def handle_manual_bans_export(callback_query: CallbackQuery, session: AsyncSession): - build_admin_back_kb("bans") +@router.callback_query(AdminPanelCallback.filter(F.action == "shadow_bans_export"), IsAdminFilter()) +async def handle_shadow_bans_export(callback_query: CallbackQuery, session: AsyncSession): + kb = build_shadow_bans_kb() try: - result = await session.execute(text("SELECT tg_id, banned_at, reason, until FROM manual_bans")) + result = await session.execute( + select(ManualBan.tg_id, ManualBan.banned_at, ManualBan.banned_by, ManualBan.until) + .where(ManualBan.reason == "shadow") + ) rows = result.all() csv_output = io.StringIO() writer = csv.writer(csv_output) - writer.writerow(["tg_id", "banned_at", "reason", "until"]) + writer.writerow(["tg_id", "banned_at", "banned_by", "until"]) for user in rows: - writer.writerow([user.tg_id, user.banned_at, user.reason, user.until]) + writer.writerow([user.tg_id, user.banned_at, user.banned_by, user.until]) + + csv_output.seek(0) + document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="shadow_bans.csv") + + await callback_query.message.answer_document( + document=document, + caption="📥 Экспорт теневых банов (CSV)", + ) + except Exception as e: + await callback_query.message.answer( + text=f"❗ Ошибка при экспорте: {e}", + reply_markup=kb, + ) + + +@router.callback_query(AdminPanelCallback.filter(F.action == "manual_bans_export"), IsAdminFilter()) +async def handle_manual_bans_export(callback_query: CallbackQuery, session: AsyncSession): + kb = build_manual_bans_kb() + try: + result = await session.execute( + select(ManualBan.tg_id, ManualBan.banned_at, ManualBan.reason, ManualBan.until, ManualBan.banned_by) + .where(or_(ManualBan.reason != "shadow", ManualBan.reason.is_(None))) + ) + rows = result.all() + + csv_output = io.StringIO() + writer = csv.writer(csv_output) + writer.writerow(["tg_id", "banned_at", "reason", "until", "banned_by"]) + + for user in rows: + writer.writerow([user.tg_id, user.banned_at, user.reason, user.until, user.banned_by]) csv_output.seek(0) document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="manual_bans.csv") await callback_query.message.answer_document( document=document, - caption="📥 Экспорт вручную забаненных пользователей", + caption="📥 Экспорт вручную забаненных пользователей (CSV)", ) except Exception as e: await callback_query.message.answer( text=f"❗ Ошибка при экспорте: {e}", - reply_markup=build_admin_back_kb("bans"), + reply_markup=kb, ) -@router.callback_query(AdminPanelCallback.filter(F.action == "bans_delete_manual"), IsAdminFilter()) -async def handle_delete_manual_banned(callback_query: CallbackQuery, session: AsyncSession): +@router.callback_query(AdminPanelCallback.filter(F.action == "bans_clear_blocked"), IsAdminFilter()) +async def handle_clear_blocked_users(callback_query: CallbackQuery, session: AsyncSession): + kb = build_blocked_users_kb() try: - await session.execute(delete(ManualBan)) + count_result = await session.execute(select(func.count()).select_from(BlockedUser)) + total_count = count_result.scalar() or 0 + + if total_count == 0: + await callback_query.message.answer( + text="📂 Нет забанивших пользователей для очистки.", + reply_markup=kb, + ) + return + + await session.execute(delete(BlockedUser)) await session.commit() - await callback_query.message.edit_text( - "🗑️ Вручную забаненные пользователи удалены.", - reply_markup=build_bans_kb(), + + await callback_query.message.answer( + text=f"🗑️ Очищено {total_count} записей забанивших пользователей из базы данных.", + reply_markup=kb, ) - logger.info("[BANS] Очищены записи из manual_bans") + logger.info(f"[BANS] Очищено {total_count} записей из blocked_users") except Exception as e: - logger.error(f"[BANS] Ошибка при очистке manual_bans: {e}") - await callback_query.message.edit_text("❌ Ошибка при удалении вручную забаненных пользователей.") + logger.error(f"[BANS] Ошибка при очистке blocked_users: {e}") + await callback_query.message.answer( + text=f"❗ Ошибка при очистке забанивших пользователей: {e}", + reply_markup=kb, + ) + + +@router.callback_query(AdminPanelCallback.filter(F.action == "bans_clear_shadow"), IsAdminFilter()) +async def handle_clear_shadow_bans(callback_query: CallbackQuery, session: AsyncSession): + kb = build_shadow_bans_kb() + try: + count_result = await session.execute( + select(func.count()).select_from(ManualBan).where(ManualBan.reason == "shadow") + ) + total_count = count_result.scalar() or 0 + + if total_count == 0: + await callback_query.message.answer( + text="📂 Нет теневых банов для очистки.", + reply_markup=kb, + ) + return + + await session.execute(delete(ManualBan).where(ManualBan.reason == "shadow")) + await session.commit() + + await callback_query.message.answer( + text=f"🗑️ Очищено {total_count} записей теневых банов из базы данных.", + reply_markup=kb, + ) + logger.info(f"[BANS] Очищено {total_count} записей теневых банов из manual_bans") + except Exception as e: + logger.error(f"[BANS] Ошибка при очистке теневых банов: {e}") + await callback_query.message.answer( + text=f"❗ Ошибка при очистке теневых банов: {e}", + reply_markup=kb, + ) + + +@router.callback_query(AdminPanelCallback.filter(F.action == "bans_clear_manual"), IsAdminFilter()) +async def handle_clear_manual_bans(callback_query: CallbackQuery, session: AsyncSession): + kb = build_manual_bans_kb() + try: + count_result = await session.execute( + select(func.count()).select_from(ManualBan).where( + or_(ManualBan.reason != "shadow", ManualBan.reason.is_(None)) + ) + ) + total_count = count_result.scalar() or 0 + + if total_count == 0: + await callback_query.message.answer( + text="📂 Нет ручных банов для очистки.", + reply_markup=kb, + ) + return + + await session.execute( + delete(ManualBan).where( + or_(ManualBan.reason != "shadow", ManualBan.reason.is_(None)) + ) + ) + await session.commit() + + await callback_query.message.answer( + text=f"🗑️ Очищено {total_count} записей ручных банов из базы данных.", + reply_markup=kb, + ) + logger.info(f"[BANS] Очищено {total_count} записей ручных банов из manual_bans") + except Exception as e: + logger.error(f"[BANS] Ошибка при очистке ручных банов: {e}") + await callback_query.message.answer( + text=f"❗ Ошибка при очистке ручных банов: {e}", + reply_markup=kb, + ) @router.callback_query(AdminPanelCallback.filter(F.action == "bans_preemptive"), IsAdminFilter()) async def handle_preemptive_ban_start(callback: CallbackQuery, state: FSMContext): await state.set_state(PreemptiveBanStates.waiting_for_preemptive_ids) + builder = InlineKeyboardBuilder() + builder.button( + text="❌ Отмена", + callback_data=AdminPanelCallback(action="bans_cancel_preemptive").pack(), + ) await callback.message.edit_text( "📥 Отправьте список Telegram ID (один на строке), которых нужно заранее забанить (теневой бан).\n\n" "Пример:\n123456789\n987654321", - reply_markup=build_admin_back_kb("bans"), + reply_markup=builder.as_markup(), + ) + + +@router.callback_query(AdminPanelCallback.filter(F.action == "bans_cancel_preemptive"), IsAdminFilter()) +async def handle_cancel_preemptive_ban(callback: CallbackQuery, state: FSMContext): + await state.clear() + await callback.message.edit_text( + text=get_shadow_bans_menu_text(), + reply_markup=build_shadow_bans_kb() ) @@ -197,5 +375,8 @@ async def handle_preemptive_ids_input(message: Message, state: FSMContext, sessi await session.execute(stmt) await session.commit() - await message.answer(f"✅ Успешно добавлено в теневой бан: {len(tg_ids)} пользователей.") + await message.answer( + f"✅ Успешно добавлено в теневой бан: {len(tg_ids)} пользователей.", + reply_markup=build_shadow_bans_kb(), + ) await state.clear() diff --git a/handlers/admin/bans/keyboard.py b/handlers/admin/bans/keyboard.py index 7c3323a3..a203ad04 100644 --- a/handlers/admin/bans/keyboard.py +++ b/handlers/admin/bans/keyboard.py @@ -8,23 +8,15 @@ def build_bans_kb(): builder.button( text="📛 Забанившие бота", - callback_data=AdminPanelCallback(action="bans_export").pack(), + callback_data=AdminPanelCallback(action="bans_blocked_menu").pack(), ) builder.button( - text="📛 Забаненные вручную", - callback_data=AdminPanelCallback(action="manual_bans_export").pack(), + text="👻 Теневые баны", + callback_data=AdminPanelCallback(action="bans_shadow_menu").pack(), ) builder.button( - text="Забанить заранее", - callback_data=AdminPanelCallback(action="bans_preemptive").pack(), - ) - builder.button( - text="🗑️ Удалить забанивших", - callback_data=AdminPanelCallback(action="bans_delete_banned").pack(), - ) - builder.button( - text="🗑️ Очистить забаненных", - callback_data=AdminPanelCallback(action="bans_delete_manual").pack(), + text="🔒 Ручные баны", + callback_data=AdminPanelCallback(action="bans_manual_menu").pack(), ) builder.button( text="🔙 Назад", @@ -33,3 +25,71 @@ def build_bans_kb(): builder.adjust(1) return builder.as_markup() + + +def build_blocked_users_kb(): + builder = InlineKeyboardBuilder() + + builder.button( + text="📥 Экспорт", + callback_data=AdminPanelCallback(action="bans_export").pack(), + ) + builder.button( + text="🗑️ Удалить забанивших", + callback_data=AdminPanelCallback(action="bans_delete_banned").pack(), + ) + builder.button( + text="🗑️ Очистить забанивших", + callback_data=AdminPanelCallback(action="bans_clear_blocked").pack(), + ) + builder.button( + text="🔙 Назад", + callback_data=AdminPanelCallback(action="bans").pack(), + ) + + builder.adjust(1) + return builder.as_markup() + + +def build_shadow_bans_kb(): + builder = InlineKeyboardBuilder() + + builder.button( + text="📥 Экспорт", + callback_data=AdminPanelCallback(action="shadow_bans_export").pack(), + ) + builder.button( + text="➕ Забанить заранее", + callback_data=AdminPanelCallback(action="bans_preemptive").pack(), + ) + builder.button( + text="🗑️ Очистить теневые баны", + callback_data=AdminPanelCallback(action="bans_clear_shadow").pack(), + ) + builder.button( + text="🔙 Назад", + callback_data=AdminPanelCallback(action="bans").pack(), + ) + + builder.adjust(1) + return builder.as_markup() + + +def build_manual_bans_kb(): + builder = InlineKeyboardBuilder() + + builder.button( + text="📥 Экспорт", + callback_data=AdminPanelCallback(action="manual_bans_export").pack(), + ) + builder.button( + text="🗑️ Очистить ручные баны", + callback_data=AdminPanelCallback(action="bans_clear_manual").pack(), + ) + builder.button( + text="🔙 Назад", + callback_data=AdminPanelCallback(action="bans").pack(), + ) + + builder.adjust(1) + return builder.as_markup() diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py index 04856a4a..82c7bdb8 100644 --- a/handlers/keys/key_mode/key_cluster_mode.py +++ b/handlers/keys/key_mode/key_cluster_mode.py @@ -192,10 +192,8 @@ async def key_cluster_mode( for hook_result in webapp_override_results: if hook_result is True or hook_result is False: use_webapp = hook_result - break elif isinstance(hook_result, dict) and "override" in hook_result: use_webapp = hook_result["override"] - break except Exception as e: logger.warning(f"[REMNAWAVE_WEBAPP_OVERRIDE] Ошибка при применении хуков: {e}") diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index 101049a9..60cb2caa 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -606,10 +606,8 @@ async def finalize_key_creation( for hook_result in webapp_override_results: if hook_result is True or hook_result is False: use_webapp = hook_result - break elif isinstance(hook_result, dict) and "override" in hook_result: use_webapp = hook_result["override"] - break except Exception as e: logger.warning(f"[REMNAWAVE_WEBAPP_OVERRIDE] Ошибка при применении хуков: {e}") diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py index dd0e128f..c5439ee5 100644 --- a/handlers/keys/key_view.py +++ b/handlers/keys/key_view.py @@ -338,10 +338,8 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p for hook_result in webapp_override_results: if hook_result is True or hook_result is False: use_webapp = hook_result - break elif isinstance(hook_result, dict) and "override" in hook_result: use_webapp = hook_result["override"] - break except Exception as e: logger.warning(f"[REMNAWAVE_WEBAPP_OVERRIDE] Ошибка при применении хуков: {e}")