Remove break in hooks / improve ban management

This commit is contained in:
Capybara-z
2025-11-14 17:32:41 +03:00
parent eae52bb799
commit b249f3c694
5 changed files with 292 additions and 57 deletions
+219 -38
View File
@@ -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_ = (
"🚫 <b>Управление банами</b>\n\n"
"📛 <b>Забанившие бота</b> — пользователи, которые заблокировали бота вручную.\n"
"🔒 <b>Ручной бан</b> — пользователи, которых вы забанили через админку.\n\n"
"👻 <b>Теневые баны</b> — пользователи, действия которых игнорируются.\n"
"🔒 <b>Ручные баны</b> — пользователи, которых вы забанили через админку.\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_ = (
"📛 <b>Забанившие бота</b>\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 (
"👻 <b>Теневые баны</b>\n\n"
"Пользователи, действия которых игнорируются ботом.\n"
"Они не получают уведомлений о бане.\n\n"
"💡 <b>Можно добавить несколько пользователей за раз:</b>\n"
"Отправьте список Telegram ID (один на строке).\n"
"Пример:\n<code>123456789\n987654321\n555666777</code>\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_ = (
"🔒 <b>Ручные баны</b>\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"
"Пример:\n<code>123456789\n987654321</code>",
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"✅ Успешно добавлено в теневой бан: <b>{len(tg_ids)}</b> пользователей.")
await message.answer(
f"✅ Успешно добавлено в теневой бан: <b>{len(tg_ids)}</b> пользователей.",
reply_markup=build_shadow_bans_kb(),
)
await state.clear()
+73 -13
View File
@@ -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()
@@ -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}")
@@ -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}")
-2
View File
@@ -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}")