ban middleware/ ban users
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user