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}")