First commit

This commit is contained in:
2026-01-23 04:45:55 +07:00
commit 0b251c5967
118 changed files with 9580 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
from aiogram import Dispatcher, Bot
from configs import ImportantID
from .error_mdw import ErrorHandlingMiddleware
from .logging_mdw import LoggingMiddleware
from .msg_mdw import MessageCounterMiddleware
from .referal_mdw import ReferralMiddleware
from .spam_mdw import RateLimitMiddleware
from .subscription_mdw import SubscriptionMiddleware
from .time_mdw import TimingMiddleware
from .ban_user_mdw import BanCheckMiddleware
# Настройки экспорта
__all__ = (
"LoggingMiddleware",
"SubscriptionMiddleware",
"RateLimitMiddleware",
"ErrorHandlingMiddleware",
"TimingMiddleware",
"MessageCounterMiddleware",
"setup_middlewares",
"ReferralMiddleware",
"BanCheckMiddleware",
)
def setup_middlewares(dp: Dispatcher, bot: Bot, channel_ids: list[int | str] = None) -> None:
"""
Регистрирует все middleware в диспетчере.
"""
channel_ids: list = channel_ids or []
# Middleware для ВСЕХ событий (update level)
middlewares_updates: list = [
TimingMiddleware(), # Замер времени
LoggingMiddleware(), # Логирование
ErrorHandlingMiddleware(admin_ids=ImportantID.ADMIN_ID), # Обработка ошибок
]
# Middleware только для СООБЩЕНИЙ (message level)
middlewares_msg: list = [
BanCheckMiddleware(),
# RateLimitMiddleware(rate_limit=3, time_period=5.0), # Антифлуд
# SubscriptionMiddleware(bot=bot, channel_ids=channel_ids), # Проверка подписки
MessageCounterMiddleware(), # Подсчет сообщений
ReferralMiddleware(), # Проверка реф-ссылок
]
# Регистрируем middleware для всех событий
for middleware in middlewares_updates:
dp.update.middleware(middleware)
# Регистрируем middleware только для сообщений
for middleware in middlewares_msg:
dp.message.middleware(middleware)

View File

@@ -0,0 +1,108 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import Message, CallbackQuery, TelegramObject
from database import db
__all__ = ("BanCheckMiddleware",)
class BanCheckMiddleware(BaseMiddleware):
"""
Middleware для проверки забанен ли пользователь.
Если пользователь забанен в боте - блокирует все его действия.
"""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Проверяет каждый входящий запрос на наличие пользователя в черном списке.
Args:
handler: Следующий обработчик
event: Событие (сообщение, callback и т.д.)
data: Данные контекста
Returns:
Результат обработчика или None если пользователь забанен
"""
# Извлекаем информацию о пользователе из события
user = await self._extract_user(event)
if user is None:
# Не смогли определить пользователя - пропускаем
return await handler(event, data)
# Проверяем в базе данных статус пользователя
user_db = await db.get_user(user.id)
if user_db and user_db.status == "banned":
# Пользователь забанен - блокируем запрос
await self._send_ban_message(event, data)
return None
# Пользователь не забанен - пропускаем запрос дальше
return await handler(event, data)
@staticmethod
async def _extract_user(event: TelegramObject) -> Any:
"""
Извлекает пользователя из разных типов событий.
Args:
event: Событие Telegram
Returns:
Объект пользователя или None
"""
if isinstance(event, Message):
return event.from_user
elif isinstance(event, CallbackQuery):
return event.from_user
# Можно добавить другие типы событий при необходимости
return None
@staticmethod
async def _send_ban_message(event: TelegramObject, data: Dict[str, Any]) -> None:
"""
Отправляет сообщение о бане пользователю.
Args:
event: Событие которое triggered проверку
data: Данные контекста с ботом
"""
bot = data.get('bot')
if not bot:
return
chat_id = None
message_id = None
# Определяем куда отправлять сообщение в зависимости от типа события
if isinstance(event, Message):
chat_id = event.chat.id
message_id = event.message_id
elif isinstance(event, CallbackQuery) and event.message:
chat_id = event.message.chat.id
message_id = event.message.message_id
if chat_id:
try:
if isinstance(event, CallbackQuery):
# Для callback запросов отвечаем уведомлением
await event.answer("🚫 Вы заблокированы в боте!", show_alert=True)
else:
# Для сообщений отправляем новое сообщение
await bot.send_message(
chat_id=chat_id,
text="🚫 Вы заблокированы в боте!",
reply_to_message_id=message_id
)
except Exception:
# Игнорируем ошибки отправки (пользователь мог заблокировать бота)
pass

View File

@@ -0,0 +1,201 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import Bot, BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
from middleware.loggers import logger
class ErrorHandlingMiddleware(BaseMiddleware):
"""
Middleware для глобальной обработки ошибок в хендлерах.
Зачем нужен:
- Централизованная обработка исключений
- Уведомление администраторов об ошибках
- Graceful degradation при сбоях
"""
def __init__(self, admin_ids: list[int]):
"""
Инициализация middleware обработки ошибок.
Args:
admin_ids: Список ID администраторов для уведомлений
"""
self.admin_ids = admin_ids
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Перехватывает и обрабатывает ошибки в хендлерах.
"""
try:
return await handler(event, data)
except Exception as e:
# Получаем информацию о пользователе безопасным способом
user_str: str = self._extract_user_info(event)
# Логируем ошибку
error_message: str = f"Ошибка в хендлере: {type(e).__name__}: {str(e)}"
logger.error(
text=error_message,
log_type="HANDLER_ERROR",
user=user_str
)
# Уведомляем администраторов
await self._notify_admins(error_message, event, user_str)
# Отправляем пользователю сообщение об ошибке
await self._send_error_message(event, user_str)
return None
@staticmethod
def _extract_user_info(event: TelegramObject) -> str:
"""
Безопасно извлекает информацию о пользователе из события.
Args:
event: Объект события
Returns:
Строка с идентификатором пользователя
"""
user_str: str = "@System"
# Для Message и CallbackQuery
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update (который содержит message или callback_query)
elif isinstance(event, Update):
# Пытаемся найти пользователя в различных полях Update
user_object = None
if event.message and event.message.from_user:
user_object = event.message.from_user
elif event.edited_message and event.edited_message.from_user:
user_object = event.edited_message.from_user
elif event.callback_query and event.callback_query.from_user:
user_object = event.callback_query.from_user
elif event.channel_post and event.channel_post.from_user:
user_object = event.channel_post.from_user
elif event.edited_channel_post and event.edited_channel_post.from_user:
user_object = event.edited_channel_post.from_user
if user_object:
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
return user_str
@staticmethod
def _extract_event_text(event: TelegramObject) -> str:
"""
Безопасно извлекает текст из события.
Args:
event: Объект события
Returns:
Текст события или пустая строка
"""
event_text: str = ""
# Для Message
if isinstance(event, Message) and hasattr(event, 'text') and event.text:
event_text: str = event.text
# Для CallbackQuery
elif isinstance(event, CallbackQuery) and hasattr(event, 'data') and event.data:
event_text: str = f"callback: {event.data}"
# Для Update
elif isinstance(event, Update):
if event.message and event.message.text:
event_text: str = event.message.text
elif event.callback_query and event.callback_query.data:
event_text: str = f"callback: {event.callback_query.data}"
elif event.edited_message and event.edited_message.text:
event_text: str = event.edited_message.text
return event_text[:100] + "..." if len(event_text) > 100 else event_text
async def _notify_admins(
self,
error_message: str,
event: TelegramObject,
user_str: str
) -> None:
"""Уведомляет администраторов об ошибке."""
bot: Bot = event.bot if hasattr(event, 'bot') else None
if bot:
for admin_id in self.admin_ids:
try:
event_info: str = f"Событие: {type(event).__name__}"
event_text: str = self._extract_event_text(event)
if event_text:
event_info += f", текст: {event_text}"
full_message: str = (
f"🚨 Ошибка в боте:\n\n"
f"Пользователь: {user_str}\n"
f"Ошибка: {error_message}\n"
f"{event_info}"
)
await bot.send_message(admin_id, full_message)
logger.info(
text=f"Администратор {admin_id} уведомлен об ошибке",
log_type="ADMIN_NOTIFIED",
user=user_str
)
except Exception as e:
logger.error(
text=f"Не удалось уведомить админа {admin_id}: {e}",
log_type="ADMIN_NOTIFY_ERROR",
user=user_str
)
@staticmethod
async def _send_error_message(
event: TelegramObject,
user_str: str
) -> None:
"""Отправляет пользователю сообщение об ошибке."""
error_text: str = (
"⚠️ Произошла непредвиденная ошибка. "
"Разработчики уже уведомлены и работают над исправлением.\n\n"
"Попробуйте повторить действие позже или нажмите /start"
)
try:
if isinstance(event, Message):
await event.answer(error_text)
elif isinstance(event, CallbackQuery):
await event.message.answer(error_text)
await event.answer()
elif isinstance(event, Update) and event.message:
await event.message.answer(error_text)
logger.info(
text="Пользователю отправлено сообщение об ошибке",
log_type="ERROR_MESSAGE_SENT",
user=user_str
)
except Exception as e:
logger.error(
text=f"Не удалось отправить сообщение об ошибке: {e}",
log_type="ERROR_MESSAGE_FAILED",
user=user_str
)

View File

@@ -0,0 +1,272 @@
from typing import Callable, Awaitable, Any, Dict, Optional, Tuple, Set
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Update, Message, CallbackQuery, MaybeInaccessibleMessageUnion, User
from bot.utils import type_msg
from configs import BotSettings, COMMANDS # импортируем настройки и команды
from middleware.loggers import logger # ваш глобальный логгер
class LoggingMiddleware(BaseMiddleware):
"""
Middleware для логирования апдейтов с определением типа события,
пользователя и добавлением префикса проекта к типу лога.
Автоматически добавляет префикс проекта (например, 'PRIMO-') к типам логов:
- PRIMO-UPDATE: общий апдейт без определенного типа
- PRIMO-MSG: текстовое сообщение от пользователя
- PRIMO-CMD: команда (сообщение, начинающееся с любого префикса)
- PRIMO-CBD: callback query от инлайн-кнопок
"""
# Префикс проекта для логов
PROJECT_PREFIX: str = "PRIMO"
# Кэш для всех команд из COMMANDS
_all_commands: Optional[Set[str]] = None
def __init__(self):
super().__init__()
# Предварительно загружаем все команды
self._load_all_commands()
def _load_all_commands(self) -> None:
"""Загружает все команды из COMMANDS в множество для быстрого поиска."""
if self._all_commands is None:
self._all_commands = set()
for command_list in COMMANDS.values():
self._all_commands.update(command_list)
def _is_command(self, text: str) -> bool:
"""
Проверяет, является ли текст командой с любым префиксом.
Args:
text: Текст для проверки
Returns:
True если это команда, False если нет
"""
if not text:
return False
# Проверяем все префиксы из BotSettings
for prefix in BotSettings.PREFIX:
if text.startswith(prefix):
# Извлекаем команду без префикса
command_without_prefix = text[len(prefix):].strip()
# Проверяем, есть ли такая команда в нашем списке
if command_without_prefix in self._all_commands:
return True
# Также проверяем команды с префиксом / (стандартные)
if text.startswith('/'):
command_without_slash = text[1:].strip()
if command_without_slash in self._all_commands:
return True
return False
@staticmethod
def _extract_command_name(text: str) -> str:
"""
Извлекает название команды из текста.
Args:
text: Текст команды с префиксом
Returns:
Название команды без префикса
"""
for prefix in BotSettings.PREFIX:
if text.startswith(prefix):
return text[len(prefix):].strip()
if text.startswith('/'):
return text[1:].strip()
return text
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Обрабатывает входящее событие, определяет его тип, логирует с префиксом проекта
и передает следующему обработчику.
Args:
handler: Следующий обработчик в цепочке middleware
event: Входящее событие для обработки (Update, Message, CallbackQuery)
data: Словарь с контекстными данными FSM
Returns:
Результат выполнения следующего обработчика
Raises:
Exception: Любое исключение, возникшее при обработке хендлером
"""
# Определяем тип события и информацию для логирования
log_type: str
log_text: str
message_obj: Optional[Message]
log_type, log_text, message_obj = self._determine_event_type(event)
# Добавляем префикс проекта к типу лога
prefixed_log_type: str = f"{log_type}"
# Определяем информацию о пользователе
user_str: str = self._extract_user_info(event, message_obj)
# Логируем получение события с префиксом проекта
logger.info(
text=log_text,
log_type=prefixed_log_type,
user=user_str
)
try:
# Передаем событие следующему обработчику
result: Any = await handler(event, data)
# Логируем успешное выполнение для команд
if log_type == "CMD":
logger.info(
text=f"[SUCCESS] команда обработана",
log_type=prefixed_log_type,
user=user_str
)
return result
except Exception as e:
# Логируем ошибку при обработке с префиксом проекта
logger.error(
text=f"Ошибка обработки: {str(e)}",
log_type=prefixed_log_type,
user=user_str
)
raise
def _determine_event_type(
self,
event: TelegramObject
) -> Tuple[str, str, Optional[Message]]:
"""
Определяет тип события и извлекает информацию для логирования.
Args:
event: Объект события для анализа
Returns:
Кортеж из (тип_лога, текст_лога, объект_сообщения)
"""
log_type: str = "UPDATE"
log_text: str = f"Получен апдейт: {type(event).__name__}"
message_obj: Optional[Message] = None
# Обработка Update объектов (основной тип в middleware)
if isinstance(event, Update):
# Пытаемся найти сообщение в различных полях Update
message_obj: Message | None = (
event.message or
event.edited_message or
event.channel_post or
event.edited_channel_post
)
if message_obj and message_obj.text:
if self._is_command(message_obj.text):
log_type: str = "CMD"
log_text: str = f"использовал команду '{message_obj.text}'"
else:
log_type: str = "MSG"
log_text: str = f"получено сообщение: {message_obj.text!r}"
elif message_obj:
# Не текстовое сообщение (фото, видео и т.д.)
log_type: str = "MSG"
log_text: str = f"получено сообщение: '{type_msg(message_obj)}'"
elif event.callback_query:
# Обработка callback query
callback: CallbackQuery = event.callback_query
log_type: str = "CBD"
log_text: str = f"получен callback: {callback.data!r}"
if callback.message:
message_obj: Optional[MaybeInaccessibleMessageUnion] = callback.message
# Прямая обработка Message (если мидлварь зарегистрирован на messages)
elif isinstance(event, Message):
message_obj: Message | None = event
if event.text and self._is_command(event.text):
log_type: str = "CMD"
log_text: str = f"использовал команду '{event.text}'"
elif event.text:
log_type: str = "MSG"
log_text: str = f"получено сообщение: {event.text!r}"
else:
log_type: str = "MSG"
log_text: str = f"получено сообщение типа: {event.content_type}"
# Прямая обработка CallbackQuery (если мидлварь зарегистрирован на callbacks)
elif isinstance(event, CallbackQuery):
log_type: str = "CBD"
log_text: str = f"получен callback: {event.data!r}"
if event.message:
message_obj = event.message
return log_type, log_text, message_obj
@staticmethod
def _extract_user_info(
event: TelegramObject,
message: Optional[Message] = None
) -> str:
"""
Извлекает информацию о пользователе из события.
Args:
event: Объект события (Update, Message или CallbackQuery)
message: Объект Message (если уже определен)
Returns:
Строка с идентификатором пользователя в формате '@username' или 'id<user_id>'
"""
user_str: str = "@System"
# Для CallbackQuery извлекаем пользователя из самого callback'а
if isinstance(event, CallbackQuery) and hasattr(event, 'from_user') and event.from_user:
user: User | None = event.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Message извлекаем пользователя из сообщения
elif isinstance(event, Message) and hasattr(event, 'from_user') and event.from_user:
user: User | None = event.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update с callback_query
elif (isinstance(event, Update) and
event.callback_query and
hasattr(event.callback_query, 'from_user') and
event.callback_query.from_user):
user: User | None = event.callback_query.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update с сообщением
elif (isinstance(event, Update) and
(event.message or event.edited_message) and
hasattr(event.message or event.edited_message, 'from_user')):
msg: Message | None = event.message or event.edited_message
if msg and msg.from_user:
user: Optional[User] = msg.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Если передан message объект
elif message and hasattr(message, 'from_user') and message.from_user:
user: Optional[User] = message.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
return user_str

View File

@@ -0,0 +1,57 @@
import logging
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.enums import ChatType
from aiogram.types import Message
from database import db
logger = logging.getLogger(__name__)
class MessageCounterMiddleware(BaseMiddleware):
"""
Middleware для подсчёта сообщений в группах и супергруппах.
"""
async def __call__(
self,
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
event: Any,
data: Dict[str, Any]
) -> Any:
if not isinstance(event, Message):
return await handler(event, data)
# Проверяем, что сообщение пришло из группового чата и не от бота
if (event.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP) and
not event.from_user.is_bot):
try:
await self.process_group_message(event)
except Exception as e:
logger.error(msg=f"Ошибка при обработке сообщения: {e}", exc_info=True)
return await handler(event, data)
@staticmethod
async def process_group_message(message: Message) -> None:
"""
Обработка сообщения из группового чата.
"""
user_id: int = message.from_user.id
message_text: str = message.text or message.caption or ""
# Добавляем пользователя (если его ещё нет)
await db.add_user(
user_id=user_id,
username=message.from_user.username,
full_name=message.from_user.full_name,
)
# Сохраняем сообщение
await db.add_message(
user_id=user_id,
message_text=message_text,
created_at=message.date,
)

View File

@@ -0,0 +1,59 @@
from typing import Callable, Awaitable, Any, Dict, Optional
from aiogram import BaseMiddleware
from aiogram.filters.command import CommandObject
from aiogram.types import TelegramObject, Message
from middleware.loggers import logger
class ReferralMiddleware(BaseMiddleware):
"""
Middleware для перехвата и обработки реферальных ссылок (?start=...).
Основные задачи:
- Отслеживание перехода по deep-link (например, /start ref123)
- Централизованное логирование
- Возможность передачи кода дальше в хендлеры
- Подготовка к сохранению кода в базу данных
"""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
"""
Перехватывает входящие сообщения и извлекает deep-link аргумент,
если пользователь зашёл по реферальной ссылке.
Args:
handler: Следующий обработчик в цепочке middleware
event: Входящее событие (Message, CallbackQuery и др.)
data: Контекстные данные, доступные хендлеру
Returns:
Результат работы следующего обработчика
"""
# Проверяем, что событие — это именно сообщение
if isinstance(event, Message):
# Извлекаем объект команды (если был установлен фильтр CommandStart)
command: Optional[CommandObject] = data.get("command")
# Проверяем, что это именно команда /start с аргументом
if command and command.command.casefold() == "start" and command.args:
ref_code: str = command.args
user_id: int = event.from_user.id
username: Optional[str] = event.from_user.username
# 👉 Здесь можно сохранить код в БД
logger.debug(
f"[Referral] user={user_id}, username={username}, ref={ref_code}"
)
# Пробрасываем реф-код в data, чтобы использовать в хендлере
data["ref_code"] = ref_code
# Передаём управление дальше
return await handler(event, data)

View File

@@ -0,0 +1,98 @@
import time
from collections import defaultdict
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery
from middleware.loggers import logger # ваш логгер
class RateLimitMiddleware(BaseMiddleware):
"""
Middleware для ограничения частоты запросов от пользователей (анти-спам).
Зачем нужен:
- Защита от DDoS и флуда
- Предотвращение злоупотребления ботом
- Контроль нагрузки на сервер
"""
def __init__(self, rate_limit: int = 10, time_period: float = 2.0):
"""
Инициализация rate limit middleware.
Args:
rate_limit: Максимальное количество запросов за период
time_period: Период времени в секундах
"""
self.rate_limit = rate_limit
self.time_period = time_period
self.user_calls: Dict[int, list[float]] = defaultdict(list)
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
log: bool = False,
) -> Any:
"""
Проверяет rate limit перед обработкой запроса.
"""
# Пропускаем не-сообщения и не-колбэки
if not isinstance(event, (Message, CallbackQuery)):
return await handler(event, data)
user_id: int = event.from_user.id
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
current_time: float = time.time()
# Очищаем старые запросы
self.user_calls[user_id]: dict[int, list[float]] = [
call_time for call_time in self.user_calls[user_id]
if current_time - call_time < self.time_period
]
# Логируем текущее состояние rate limit
if log:
logger.debug(
text=f"Rate limit: {len(self.user_calls[user_id])}/{self.rate_limit} за {self.time_period}сек",
log_type="RATE_LIMIT_STATUS",
user=user_str
)
# Проверяем текущий лимит
if len(self.user_calls[user_id]) >= self.rate_limit:
# Логируем попытку спама
if log:
logger.warning(
text=f"Превышен rate limit ({self.rate_limit}/{self.time_period}сек)",
log_type="RATE_LIMIT_EXCEEDED",
user=user_str
)
# Отправляем сообщение о превышении лимита
if isinstance(event, Message):
await event.answer(
text="⏳ Слишком много запросов! Пожалуйста, подождите немного.",
)
elif isinstance(event, CallbackQuery):
await event.answer(
text="⏳ Подождите немного перед следующим действием.",
show_alert=True
)
return None
# Добавляем текущий запрос и продолжаем обработку
self.user_calls[user_id].append(current_time)
logger.debug(
text=f"Запрос добавлен в rate limit",
log_type="RATE_LIMIT_ADDED",
user=user_str
)
return await handler(event, data)

View File

@@ -0,0 +1,110 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware, Bot
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import TelegramObject, Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from middleware.loggers import logger
class SubscriptionMiddleware(BaseMiddleware):
"""
Middleware для проверки подписки пользователя на необходимые каналы.
Блокирует обработку команд, если пользователь не подписан.
Зачем нужен:
- Автоматическая проверка подписки для всех входящих сообщений
- Единая точка управления подписками
- Предотвращение доступа к функционалу без подписки
"""
def __init__(self, bot: Bot, channel_ids: list[int | str]):
"""
Инициализация middleware проверки подписки.
Args:
bot: Экземпляр бота
channel_ids: Список ID каналов/чатов для проверки подписки
"""
self.bot = bot
self.channel_ids = channel_ids
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Проверяет подписку пользователя перед обработкой команды.
"""
# Пропускаем не-сообщения и не-колбэки
if not isinstance(event, (Message, CallbackQuery)):
return await handler(event, data)
user_id: int = event.from_user.id
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
# Логируем начало проверки подписки
logger.info(
text=f"Проверка подписки для пользователя",
log_type="SUBSCRIPTION_CHECK",
user=user_str
)
# Проверяем подписку на все required каналы
not_subscribed_channels: list[str] = []
for channel_id in self.channel_ids:
try:
member = await self.bot.get_chat_member(
chat_id=channel_id,
user_id=user_id
)
# Проверяем, что пользователь является участником
if member.status not in ['member', 'administrator', 'creator']:
not_subscribed_channels.append(str(channel_id))
except TelegramBadRequest as e:
logger.error(
text=f"Ошибка проверки подписки на канал {channel_id}: {e}",
log_type="SUBSCRIPTION_ERROR",
user=user_str
)
# Если пользователь не подписан на некоторые каналы
if not_subscribed_channels:
logger.warning(
text=f"Пользователь не подписан на каналы: {', '.join(not_subscribed_channels)}",
log_type="SUBSCRIPTION_FAILED",
user=user_str
)
warning_text: str = (
"📢 Для использования бота необходимо подписаться на наши каналы!\n\n"
"После подписки нажмите /start для продолжения."
)
# Создаем кнопку "Проверить подписку"
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
if isinstance(event, Message):
await event.answer(warning_text, reply_markup=ikb.as_markup())
elif isinstance(event, CallbackQuery):
await event.message.answer(warning_text, reply_markup=ikb.as_markup())
await event.answer()
return None
# Логируем успешную проверку подписки
logger.info(
text="Пользователь подписан на все required каналы",
log_type="SUBSCRIPTION_SUCCESS",
user=user_str
)
# Если подписка есть, продолжаем обработку
return await handler(event, data)

View File

@@ -0,0 +1,83 @@
from time import time
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
from middleware.loggers import logger
class TimingMiddleware(BaseMiddleware):
"""
Middleware для измерения времени выполнения хендлеров.
Зачем нужен:
- Мониторинг производительности хендлеров
- Выявление медленных запросов
- Оптимизация кода бота
"""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
perm: str = None,
) -> Any:
"""
Измеряет время выполнения хендлера.
"""
start_time: float = time()
try:
result: Any = await handler(event, data)
return result
finally:
execution_time: float = time() - start_time
# Получаем информацию о пользователе безопасным способом
user_str: str = "@System"
# Для Message и CallbackQuery
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update (который содержит message или callback_query)
elif isinstance(event, Update):
# Пытаемся найти пользователя в различных полях Update
user_object = None
if event.message and event.message.from_user:
user_object = event.message.from_user
elif event.edited_message and event.edited_message.from_user:
user_object = event.edited_message.from_user
elif event.callback_query and event.callback_query.from_user:
user_object = event.callback_query.from_user
elif event.channel_post and event.channel_post.from_user:
user_object = event.channel_post.from_user
elif event.edited_channel_post and event.edited_channel_post.from_user:
user_object = event.edited_channel_post.from_user
if user_object:
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
# Логируем время выполнения
if execution_time > 1.0 and perm: # Медленные запросы
logger.warning(
text=f"Медленный хендлер: {execution_time:.2f}сек",
log_type="SLOW_HANDLER",
user=user_str
)
elif execution_time > 0.5 and perm == "medium": # Средние запросы
logger.info(
text=f"Среднее время выполнения: {execution_time:.3f}сек",
log_type="HANDLER_TIMING",
user=user_str
)
elif perm == "fast": # Быстрые запросы
logger.debug(
text=f"Быстрое выполнение: {execution_time:.3f}сек",
log_type="HANDLER_TIMING_FAST",
user=user_str
)