152 lines
6.1 KiB
Python
152 lines
6.1 KiB
Python
from collections.abc import Awaitable, Callable
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
import pytz
|
|
|
|
from aiogram import BaseMiddleware
|
|
from aiogram.types import CallbackQuery, Message, TelegramObject, Update
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from config import ADMIN_ID, SUPPORT_CHAT_URL
|
|
from core.cache_config import BAN_CACHE_TTL_SEC
|
|
from core.redis_cache import cache_delete, cache_get, cache_key, cache_set
|
|
from database import async_session_maker
|
|
from database.models import ManualBan, User
|
|
from logger import logger
|
|
|
|
|
|
TZ = pytz.timezone("Europe/Moscow")
|
|
_BAN_CACHE_TTL = BAN_CACHE_TTL_SEC
|
|
|
|
|
|
async def invalidate_ban_cache(tg_id: int) -> None:
|
|
"""Сбросить кэш статуса бана после добавления/снятия бана."""
|
|
await cache_delete(cache_key("ban_status", tg_id))
|
|
|
|
|
|
class BanCheckerMiddleware(BaseMiddleware):
|
|
"""Проверка банов."""
|
|
|
|
async def _load_ban_info(self, session: AsyncSession, tg_id: int) -> dict[str, Any] | None:
|
|
query = (
|
|
select(ManualBan.reason, ManualBan.until)
|
|
.join(User, ManualBan.user_id == User.id)
|
|
.where(
|
|
User.tg_id == tg_id,
|
|
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.now(timezone.utc)),
|
|
)
|
|
.limit(1)
|
|
)
|
|
result = await session.execute(query)
|
|
row = result.first()
|
|
if row:
|
|
reason, until = row
|
|
await cache_set(
|
|
cache_key("ban_status", tg_id),
|
|
{"has_ban": True, "reason": reason or "не указана", "until": until.isoformat() if until else None},
|
|
_BAN_CACHE_TTL,
|
|
)
|
|
return {"reason": reason or "не указана", "until": until}
|
|
|
|
await cache_set(cache_key("ban_status", tg_id), {"has_ban": False}, _BAN_CACHE_TTL)
|
|
return None
|
|
|
|
async def __call__(
|
|
self,
|
|
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
|
event: TelegramObject,
|
|
data: dict[str, Any],
|
|
) -> Any:
|
|
tg_id = None
|
|
obj = None
|
|
|
|
if isinstance(event, Update):
|
|
if event.message:
|
|
tg_id = event.message.from_user.id
|
|
obj = event.message
|
|
elif event.callback_query:
|
|
tg_id = event.callback_query.from_user.id
|
|
obj = event.callback_query
|
|
elif isinstance(event, Message | CallbackQuery):
|
|
tg_id = event.from_user.id
|
|
obj = event
|
|
|
|
if tg_id is None:
|
|
return await handler(event, data)
|
|
|
|
cached = await cache_get(cache_key("ban_status", tg_id))
|
|
if isinstance(cached, dict):
|
|
if not cached.get("has_ban"):
|
|
ban_info = None
|
|
else:
|
|
until_raw = cached.get("until")
|
|
until_parsed = None
|
|
if isinstance(until_raw, str):
|
|
try:
|
|
until_parsed = datetime.fromisoformat(until_raw)
|
|
except ValueError:
|
|
until_parsed = None
|
|
if until_parsed is not None and until_parsed < datetime.now(timezone.utc):
|
|
ban_info = None
|
|
await cache_delete(cache_key("ban_status", tg_id))
|
|
else:
|
|
ban_info = {
|
|
"reason": cached.get("reason") or "не указана",
|
|
"until": until_parsed,
|
|
}
|
|
else:
|
|
session = data.get("session")
|
|
if session is not None and getattr(session, "execute", None) is not None:
|
|
ban_info = await self._load_ban_info(session, tg_id)
|
|
else:
|
|
async with async_session_maker() as short_session:
|
|
ban_info = await self._load_ban_info(short_session, tg_id)
|
|
await short_session.commit()
|
|
|
|
if not ban_info:
|
|
return await handler(event, data)
|
|
|
|
reason = ban_info["reason"]
|
|
until = ban_info["until"]
|
|
|
|
admin_ids = set(ADMIN_ID) if isinstance(ADMIN_ID, list | tuple) else {ADMIN_ID}
|
|
if tg_id in admin_ids:
|
|
return await handler(event, data)
|
|
|
|
if reason == "shadow":
|
|
logger.info(f"[BanChecker] Теневой бан: пользователь {tg_id} — действия игнорируются.")
|
|
return
|
|
|
|
if until:
|
|
until_local = until.astimezone(TZ).strftime("%Y-%m-%d %H:%M")
|
|
text_html = (
|
|
f"🚫 Вы заблокированы до <b>{until_local}</b> по МСК.\n"
|
|
f"📄 Причина: <i>{reason}</i>\n\n"
|
|
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
|
|
)
|
|
text_plain = (
|
|
f"🚫 Вы заблокированы до {until_local} по МСК.\n"
|
|
f"📄 Причина: {reason}\n\n"
|
|
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
|
|
)
|
|
else:
|
|
text_html = (
|
|
f"🚫 Вы заблокированы <b>навсегда</b>.\n"
|
|
f"📄 Причина: <i>{reason}</i>\n\n"
|
|
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
|
|
)
|
|
text_plain = (
|
|
f"🚫 Вы заблокированы навсегда.\n"
|
|
f"📄 Причина: {reason}\n\n"
|
|
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
|
|
)
|
|
|
|
if isinstance(obj, Message):
|
|
await obj.answer(text_html, parse_mode="HTML")
|
|
elif isinstance(obj, CallbackQuery):
|
|
alert_text = text_plain if len(text_plain) <= 200 else text_plain[:197] + "..."
|
|
await obj.answer(alert_text, show_alert=True)
|
|
return
|