ban middleware/ ban users

This commit is contained in:
Vladless
2025-05-19 03:59:26 +03:00
parent 8ea78943fe
commit cbd2f58499
9 changed files with 227 additions and 24 deletions
+18
View File
@@ -36,6 +36,14 @@ BEGIN
END IF;
END$$;
CREATE TABLE IF NOT EXISTS manual_bans (
tg_id BIGINT PRIMARY KEY,
banned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
reason TEXT,
banned_by BIGINT,
until TIMESTAMP WITH TIME ZONE
);
CREATE TABLE IF NOT EXISTS payments (
id SERIAL PRIMARY KEY,
tg_id BIGINT NOT NULL,
@@ -85,6 +93,16 @@ BEGIN
ALTER TABLE keys ADD COLUMN alias TEXT;
END IF;
END$$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'keys' AND column_name = 'tariff_id'
) THEN
ALTER TABLE keys ADD COLUMN tariff_id INTEGER REFERENCES tariffs(id);
END IF;
END$$;
CREATE TABLE IF NOT EXISTS referrals (
referred_tg_id BIGINT PRIMARY KEY NOT NULL,
+38 -9
View File
@@ -8,17 +8,21 @@ from filters.admin import IsAdminFilter
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from .keyboard import build_bans_kb
import csv
import io
router = Router()
@router.callback_query(
AdminPanelCallback.filter(F.action == "bans"),
IsAdminFilter(),
)
@router.callback_query(AdminPanelCallback.filter(F.action == "bans"), IsAdminFilter())
async def handle_bans(callback_query: CallbackQuery):
text = "🚫 Заблокировавшие бота\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!"
text = (
"🚫 <b>Управление банами</b>\n\n"
"📛 <b>Забанившие бота</b> — пользователи, которые заблокировали бота вручную.\n"
"🔒 <b>Ручной бан</b> — пользователи, которых вы забанили через админку.\n\n"
"⬇ Выберите нужный раздел:"
)
await callback_query.message.edit_text(
text=text,
@@ -35,10 +39,6 @@ async def handle_bans_export(callback_query: CallbackQuery, session: Any):
try:
banned_users = await session.fetch("SELECT tg_id, blocked_at FROM blocked_users")
import csv
import io
csv_output = io.StringIO()
writer = csv.writer(csv_output)
writer.writerow(["tg_id", "blocked_at"])
@@ -92,3 +92,32 @@ async def handle_bans_delete_banned(callback_query: CallbackQuery, session: Any)
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: Any):
try:
rows = await session.fetch("SELECT tg_id, banned_at, reason, until FROM manual_bans")
import csv
import io
csv_output = io.StringIO()
writer = csv.writer(csv_output)
writer.writerow(["tg_id", "banned_at", "reason", "until"])
for user in rows:
writer.writerow([user["tg_id"], user["banned_at"], user["reason"], user["until"]])
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="📥 Экспорт вручную забаненных пользователей",
)
except Exception as e:
await callback_query.message.answer(
text=f"❗ Ошибка при экспорте: {e}",
reply_markup=build_admin_back_kb("bans"),
)
+7 -4
View File
@@ -4,10 +4,13 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
def build_bans_kb() -> InlineKeyboardMarkup:
def build_bans_kb():
builder = InlineKeyboardBuilder()
builder.button(text="📄 Выгрузить в CSV", callback_data=AdminPanelCallback(action="bans_export").pack())
builder.button(text="🗑️ Удалить из БД", callback_data=AdminPanelCallback(action="bans_delete_banned").pack())
builder.row(build_admin_back_btn("management"))
builder.button(text="📛 Забанившие бота", callback_data=AdminPanelCallback(action="bans_export").pack())
builder.button(text="📛 Забаненные вручную", callback_data=AdminPanelCallback(action="manual_bans_export").pack())
builder.button(text="🗑️ Очистить забанивших", callback_data=AdminPanelCallback(action="bans_delete_banned").pack())
builder.button(text="🔙 Назад",callback_data=AdminPanelCallback(action="management").pack())
builder.adjust(1)
return builder.as_markup()
+1 -1
View File
@@ -9,7 +9,7 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
def build_management_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="💾 Создать резервную копию", callback_data=AdminPanelCallback(action="backups").pack())
builder.button(text="🚫 Заблокировавшие бота", callback_data=AdminPanelCallback(action="bans").pack())
builder.button(text="📛 Управление банами", callback_data=AdminPanelCallback(action="bans").pack())
builder.button(text="🔄 Перезагрузить бота", callback_data=AdminPanelCallback(action="restart").pack())
builder.button(text="🌐 Сменить домен", callback_data=AdminPanelCallback(action="change_domain").pack())
builder.button(text="🔑 Восстановить пробники", callback_data=AdminPanelCallback(action="restore_trials").pack())
+5 -1
View File
@@ -28,7 +28,7 @@ class AdminUserKeyEditorCallback(CallbackData, prefix="admin_users_key"):
edit: bool = False
def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup:
def build_user_edit_kb(tg_id: int, key_records: list, is_banned: bool = False) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
current_time = datetime.now(tz=timezone.utc)
@@ -64,6 +64,10 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup:
builder.button(
text="❌ Удалить клиента", callback_data=AdminUserEditorCallback(action="users_delete_user", tg_id=tg_id).pack()
)
builder.button(
text="✅ Разблокировать" if is_banned else "🚫 Заблокировать",
callback_data=AdminUserEditorCallback(action="users_unban" if is_banned else "users_ban", tg_id=tg_id).pack(),
)
builder.row(build_editor_btn("🔄 Обновить данные", tg_id, edit=True))
builder.row(build_admin_back_btn())
builder.adjust(1)
+93 -8
View File
@@ -77,6 +77,11 @@ class UserEditorState(StatesGroup):
selecting_country = State()
class BanUserStates(StatesGroup):
waiting_for_reason = State()
waiting_for_ban_duration = State()
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_hwid_menu"), IsAdminFilter())
async def handle_hwid_menu(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
email = callback_data.data
@@ -706,13 +711,6 @@ async def handle_delete_user_confirm(
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter())
async def handle_editor(
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
):
await process_user_search(callback_query.message, state, session, callback_data.tg_id, callback_data.edit)
async def process_user_search(
message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False
) -> None:
@@ -736,6 +734,10 @@ async def process_user_search(
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id)
is_banned = await session.fetchval(
"SELECT 1 FROM manual_bans WHERE tg_id = $1 AND (until IS NULL OR until > NOW())", tg_id
)
text = (
f"<b>📊 Информация о пользователе</b>"
f"\n\n🆔 ID: <b>{tg_id}</b>"
@@ -746,7 +748,7 @@ async def process_user_search(
f"\n👥 Количество рефералов: <b>{referral_count}</b>"
)
kb = build_user_edit_kb(tg_id, key_records)
kb = build_user_edit_kb(tg_id, key_records, is_banned=bool(is_banned))
if edit:
try:
@@ -1095,3 +1097,86 @@ async def handle_reset_traffic(callback_query: CallbackQuery, callback_data: Adm
await callback_query.message.edit_text(
"❌ Произошла ошибка при сбросе трафика. Попробуйте позже.", reply_markup=build_editor_kb(tg_id)
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_ban"), IsAdminFilter())
async def handle_user_ban(callback: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext):
await state.set_state(BanUserStates.waiting_for_reason)
await state.update_data(tg_id=callback_data.tg_id)
await callback.message.edit_text(
text="✏️ Введите причину блокировки (или <code>-</code>, чтобы пропустить):",
reply_markup=build_admin_back_kb(f"users_edit|{callback_data.tg_id}"),
)
@router.message(BanUserStates.waiting_for_reason, IsAdminFilter())
async def handle_ban_reason_input(message: Message, state: FSMContext):
await state.update_data(reason=message.text.strip())
await state.set_state(BanUserStates.waiting_for_ban_duration)
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
await message.answer(
"⏳ Введите срок блокировки в днях (0 — навсегда):",
reply_markup=build_admin_back_kb(f"users_edit|{tg_id}"),
)
@router.message(BanUserStates.waiting_for_ban_duration, IsAdminFilter())
async def handle_ban_duration_input(message: Message, state: FSMContext, session: Any):
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
reason = user_data.get("reason") if user_data.get("reason") != "-" else None
try:
days = int(message.text.strip())
until = None
if days > 0:
from datetime import datetime, timedelta, timezone
until = datetime.now(timezone.utc) + timedelta(days=days)
await session.execute(
"""
INSERT INTO manual_bans (tg_id, reason, banned_by, until)
VALUES ($1, $2, $3, $4)
ON CONFLICT (tg_id) DO UPDATE
SET reason = $2, until = $4, banned_at = NOW()
""",
tg_id,
reason,
message.from_user.id,
until,
)
text = (
f"✅ Пользователь <code>{tg_id}</code> забанен "
f"{'навсегда' if not until else f'до {until:%Y-%m-%d %H:%M}'}. "
f"Нажмите кнопку ниже для возврата в профиль."
)
await message.answer(text=text, reply_markup=build_editor_kb(tg_id, edit=True))
except ValueError:
await message.answer("❗ Введите корректное число дней.")
finally:
await state.clear()
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_unban"), IsAdminFilter())
async def handle_user_unban(callback: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
await session.execute("DELETE FROM manual_bans WHERE tg_id = $1", callback_data.tg_id)
text = (
f"✅ Пользователь <code>{callback_data.tg_id}</code> разблокирован. "
f"Нажмите кнопку ниже для возврата в профиль."
)
await callback.message.edit_text(text=text, reply_markup=build_editor_kb(callback_data.tg_id, edit=True))
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter())
async def handle_users_editor(callback: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any, state: FSMContext):
await process_user_search(callback.message, state=state, session=session, tg_id=callback_data.tg_id, edit=callback_data.edit)
+1 -1
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -9,12 +9,14 @@ from .maintenance import MaintenanceModeMiddleware
from .session import SessionMiddleware
from .throttling import ThrottlingMiddleware
from .user import UserMiddleware
from middlewares.ban_checker import BanCheckerMiddleware
def register_middleware(
dispatcher: Dispatcher,
middlewares: Iterable[BaseMiddleware | type[BaseMiddleware]] | None = None,
exclude: Iterable[str] | None = None,
pool=None
) -> None:
"""Регистрирует middleware в диспетчере."""
if middlewares is None:
@@ -27,6 +29,9 @@ def register_middleware(
"user": UserMiddleware(),
}
if pool:
available_middlewares["ban_checker"] = BanCheckerMiddleware(pool)
exclude_set = set(exclude or [])
middlewares = [middleware for name, middleware in available_middlewares.items() if name not in exclude_set]
+59
View File
@@ -0,0 +1,59 @@
from typing import Callable, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.types import Message, CallbackQuery
from asyncpg import Pool
from pytz import timezone
from config import SUPPORT_CHAT_URL
TZ = timezone("Europe/Moscow") # или другая зона, если нужно
class BanCheckerMiddleware(BaseMiddleware):
def __init__(self, pool: Pool):
self.pool = pool
async def __call__(
self,
handler: Callable[[Any, dict[str, Any]], Awaitable[Any]],
event: Message | CallbackQuery,
data: dict[str, Any],
) -> Any:
tg_id = (
event.from_user.id
if isinstance(event, (Message, CallbackQuery))
else None
)
if tg_id is None:
return await handler(event, data)
async with self.pool.acquire() as conn:
record = await conn.fetchrow(
"""
SELECT until FROM manual_bans
WHERE tg_id = $1 AND (until IS NULL OR until > NOW())
""",
tg_id,
)
if record:
until = record["until"]
if until:
until_local = until.astimezone(TZ).strftime("%Y-%m-%d %H:%M")
text = (
f"🚫 Вы заблокированы до <b>{until_local}</b> по МСК.\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
else:
text = (
f"🚫 Вы заблокированы <b>навсегда</b>.\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
if isinstance(event, Message):
await event.answer(text, parse_mode="HTML")
elif isinstance(event, CallbackQuery):
await event.answer(text, show_alert=True)
return
return await handler(event, data)