Первый коммит
This commit is contained in:
11
bot/filters/__init__.py
Normal file
11
bot/filters/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Модуль фильтров для aiogram
|
||||
"""
|
||||
from .subscription import *
|
||||
from .admin import *
|
||||
from .spam import *
|
||||
from .modes import *
|
||||
from .chat_type import *
|
||||
from .msg_content import *
|
||||
from .chat_rights import *
|
||||
from .callback import *
|
||||
109
bot/filters/admin.py
Normal file
109
bot/filters/admin.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Фильтры для проверки прав администратора
|
||||
"""
|
||||
from typing import Union
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from configs import settings
|
||||
from database import get_manager
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ('IsSuperAdmin', 'IsAdmin', 'IsOwner')
|
||||
|
||||
|
||||
class IsSuperAdmin(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли пользователь суперадминистратором (из .env).
|
||||
|
||||
Суперадмины имеют полный доступ ко всем командам бота.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("addadmin"), IsSuperAdmin())
|
||||
async def add_admin_command(message: Message):
|
||||
await message.answer("Добавление админа...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
user_id = event.from_user.id
|
||||
is_super_admin = user_id in settings.OWNER_ID
|
||||
|
||||
if not is_super_admin:
|
||||
logger.warning(
|
||||
f"Попытка доступа к команде суперадмина от user_id={user_id}",
|
||||
log_type='SECURITY',
|
||||
message=event if isinstance(event, Message) else None
|
||||
)
|
||||
|
||||
return is_super_admin
|
||||
|
||||
|
||||
class IsAdmin(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли пользователь администратором (суперадмин или доп. админ).
|
||||
|
||||
Администраторы могут управлять банвордами, но не могут добавлять других админов.
|
||||
Список дополнительных админов загружается из БД через BanWordsManager.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("addword"), IsAdmin())
|
||||
async def add_word_command(message: Message):
|
||||
await message.answer("Добавление банворда...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
user_id = event.from_user.id
|
||||
|
||||
# Проверка суперадмина
|
||||
if user_id in settings.OWNER_ID:
|
||||
return True
|
||||
|
||||
# Проверка доп. админа из БД (через кэш)
|
||||
manager = get_manager()
|
||||
is_db_admin = manager.is_admin_cached(user_id)
|
||||
|
||||
if not is_db_admin:
|
||||
logger.warning(
|
||||
f"Попытка доступа к админ-команде от user_id={user_id}",
|
||||
log_type='SECURITY',
|
||||
message=event if isinstance(event, Message) else None
|
||||
)
|
||||
|
||||
return is_db_admin
|
||||
|
||||
|
||||
class IsOwner(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли пользователь первым владельцем бота (OWNER_ID[0]).
|
||||
|
||||
Используется для критических операций (например, полная очистка данных).
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("reset_all"), IsOwner())
|
||||
async def reset_command(message: Message):
|
||||
await message.answer("⚠️ Сброс всех данных...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
user_id = event.from_user.id
|
||||
|
||||
# Берём первого суперадмина как владельца
|
||||
owner_id = settings.OWNER_ID[0] if settings.OWNER_ID else None
|
||||
|
||||
is_owner = user_id == owner_id
|
||||
|
||||
if not is_owner:
|
||||
logger.warning(
|
||||
f"Попытка доступа к команде владельца от user_id={user_id}",
|
||||
log_type='SECURITY',
|
||||
message=event if isinstance(event, Message) else None
|
||||
)
|
||||
|
||||
return is_owner
|
||||
253
bot/filters/callback.py
Normal file
253
bot/filters/callback.py
Normal file
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Фильтры для обработки callback-запросов
|
||||
"""
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = (
|
||||
'CallbackStartsWith',
|
||||
'CallbackEndsWith',
|
||||
'CallbackContains',
|
||||
'CallbackMatches',
|
||||
'CallbackIn'
|
||||
)
|
||||
|
||||
|
||||
class CallbackStartsWith(BaseFilter):
|
||||
"""
|
||||
Проверяет, начинается ли callback_data с указанного префикса.
|
||||
|
||||
Attributes:
|
||||
prefix: Префикс для проверки (строка или список строк)
|
||||
ignore_case: Игнорировать регистр
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Один префикс
|
||||
@router.callback_query(CallbackStartsWith("menu:"))
|
||||
async def menu_handler(callback: CallbackQuery):
|
||||
await callback.answer("Меню")
|
||||
|
||||
# Несколько префиксов
|
||||
@router.callback_query(CallbackStartsWith(["admin:", "mod:"]))
|
||||
async def admin_handler(callback: CallbackQuery):
|
||||
await callback.answer("Админ панель")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, prefix: Union[str, list[str]], ignore_case: bool = True):
|
||||
"""
|
||||
Args:
|
||||
prefix: Префикс или список префиксов
|
||||
ignore_case: Игнорировать регистр букв
|
||||
"""
|
||||
self.prefixes = [prefix] if isinstance(prefix, str) else prefix
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
if self.ignore_case:
|
||||
self.prefixes = [p.lower() for p in self.prefixes]
|
||||
|
||||
async def __call__(self, callback: CallbackQuery) -> Union[bool, dict]:
|
||||
if not callback.data:
|
||||
return False
|
||||
|
||||
data = callback.data.lower() if self.ignore_case else callback.data
|
||||
|
||||
for prefix in self.prefixes:
|
||||
if data.startswith(prefix):
|
||||
# Извлекаем данные после префикса
|
||||
value = callback.data[len(prefix):]
|
||||
|
||||
logger.debug(
|
||||
f"Callback с префиксом '{prefix}': {callback.data}",
|
||||
log_type='CALLBACK'
|
||||
)
|
||||
|
||||
return {
|
||||
'matched': True,
|
||||
'prefix': prefix,
|
||||
'value': value,
|
||||
'full_data': callback.data
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class CallbackEndsWith(BaseFilter):
|
||||
"""
|
||||
Проверяет, заканчивается ли callback_data на указанный суффикс.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.callback_query(CallbackEndsWith(":confirm"))
|
||||
async def confirm_handler(callback: CallbackQuery, matched: dict):
|
||||
action = matched['value']
|
||||
await callback.answer(f"Подтверждение: {action}")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, suffix: Union[str, list[str]], ignore_case: bool = True):
|
||||
"""
|
||||
Args:
|
||||
suffix: Суффикс или список суффиксов
|
||||
ignore_case: Игнорировать регистр букв
|
||||
"""
|
||||
self.suffixes = [suffix] if isinstance(suffix, str) else suffix
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
if self.ignore_case:
|
||||
self.suffixes = [s.lower() for s in self.suffixes]
|
||||
|
||||
async def __call__(self, callback: CallbackQuery) -> Union[bool, dict]:
|
||||
if not callback.data:
|
||||
return False
|
||||
|
||||
data = callback.data.lower() if self.ignore_case else callback.data
|
||||
|
||||
for suffix in self.suffixes:
|
||||
if data.endswith(suffix):
|
||||
# Извлекаем данные до суффикса
|
||||
value = callback.data[:-len(suffix)]
|
||||
|
||||
return {
|
||||
'matched': True,
|
||||
'suffix': suffix,
|
||||
'value': value,
|
||||
'full_data': callback.data
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class CallbackContains(BaseFilter):
|
||||
"""
|
||||
Проверяет, содержит ли callback_data указанную подстроку.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.callback_query(CallbackContains("delete"))
|
||||
async def delete_handler(callback: CallbackQuery):
|
||||
await callback.answer("Удаление...")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, substring: Union[str, list[str]], ignore_case: bool = True):
|
||||
"""
|
||||
Args:
|
||||
substring: Подстрока или список подстрок
|
||||
ignore_case: Игнорировать регистр букв
|
||||
"""
|
||||
self.substrings = [substring] if isinstance(substring, str) else substring
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
if self.ignore_case:
|
||||
self.substrings = [s.lower() for s in self.substrings]
|
||||
|
||||
async def __call__(self, callback: CallbackQuery) -> Union[bool, dict]:
|
||||
if not callback.data:
|
||||
return False
|
||||
|
||||
data = callback.data.lower() if self.ignore_case else callback.data
|
||||
|
||||
for substring in self.substrings:
|
||||
if substring in data:
|
||||
return {
|
||||
'matched': True,
|
||||
'substring': substring,
|
||||
'full_data': callback.data
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class CallbackMatches(BaseFilter):
|
||||
"""
|
||||
Проверяет callback_data по regex паттерну.
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Паттерн: user_123, user_456 и т.д.
|
||||
@router.callback_query(CallbackMatches(r"^user_(\d+)$"))
|
||||
async def user_handler(callback: CallbackQuery, matched: dict):
|
||||
user_id = matched['groups']
|
||||
await callback.answer(f"Пользователь {user_id}")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, pattern: Union[str, re.Pattern], flags: int = 0):
|
||||
"""
|
||||
Args:
|
||||
pattern: Regex паттерн (строка или скомпилированный Pattern)
|
||||
flags: Флаги для regex (например, re.IGNORECASE)
|
||||
"""
|
||||
if isinstance(pattern, str):
|
||||
self.pattern = re.compile(pattern, flags)
|
||||
else:
|
||||
self.pattern = pattern
|
||||
|
||||
async def __call__(self, callback: CallbackQuery) -> Union[bool, dict]:
|
||||
if not callback.data:
|
||||
return False
|
||||
|
||||
match = self.pattern.match(callback.data)
|
||||
|
||||
if match:
|
||||
logger.debug(
|
||||
f"Callback соответствует паттерну {self.pattern.pattern}: {callback.data}",
|
||||
log_type='CALLBACK'
|
||||
)
|
||||
|
||||
return {
|
||||
'matched': True,
|
||||
'pattern': self.pattern.pattern,
|
||||
'groups': match.groups(),
|
||||
'groupdict': match.groupdict(),
|
||||
'full_data': callback.data
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class CallbackIn(BaseFilter):
|
||||
"""
|
||||
Проверяет, находится ли callback_data в списке разрешенных значений.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.callback_query(CallbackIn(["yes", "no", "cancel"]))
|
||||
async def choice_handler(callback: CallbackQuery):
|
||||
choice = callback.data
|
||||
await callback.answer(f"Выбрано: {choice}")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, values: list[str], ignore_case: bool = True):
|
||||
"""
|
||||
Args:
|
||||
values: Список разрешенных значений
|
||||
ignore_case: Игнорировать регистр букв
|
||||
"""
|
||||
self.values = values
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
if self.ignore_case:
|
||||
self.values = [v.lower() for v in values]
|
||||
|
||||
async def __call__(self, callback: CallbackQuery) -> Union[bool, dict]:
|
||||
if not callback.data:
|
||||
return False
|
||||
|
||||
data = callback.data.lower() if self.ignore_case else callback.data
|
||||
|
||||
if data in self.values:
|
||||
return {
|
||||
'matched': True,
|
||||
'value': callback.data
|
||||
}
|
||||
|
||||
return False
|
||||
324
bot/filters/chat_rights.py
Normal file
324
bot/filters/chat_rights.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
Фильтры для проверки прав пользователей в чатах
|
||||
"""
|
||||
from typing import Any, Union
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.enums import ChatMemberStatus
|
||||
|
||||
from configs import settings
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = (
|
||||
'IsBotOwner',
|
||||
'IsChatCreator',
|
||||
'IsChatAdmin',
|
||||
'IsModerator',
|
||||
'CanDeleteMessages',
|
||||
'CanRestrictMembers',
|
||||
'CanPinMessages'
|
||||
)
|
||||
|
||||
|
||||
class IsBotOwner(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли пользователь владельцем бота (из .env).
|
||||
|
||||
Attributes:
|
||||
send_error_message: Отправлять ли сообщение об ошибке доступа
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Без сообщения об ошибке
|
||||
@router.message(Command("reset"), IsOwner())
|
||||
async def reset_command(message: Message):
|
||||
await message.answer("🔄 Сброс данных...")
|
||||
|
||||
# С сообщением об ошибке
|
||||
@router.message(Command("secret"), IsOwner(send_error_message=True))
|
||||
async def secret_command(message: Message):
|
||||
await message.answer("🔐 Секретная команда выполнена")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, send_error_message: bool = False) -> None:
|
||||
"""
|
||||
Args:
|
||||
send_error_message: Если True, отправляет сообщение при отказе в доступе
|
||||
"""
|
||||
self.send_error_message = send_error_message
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
event: Union[Message, CallbackQuery],
|
||||
bot: Bot
|
||||
) -> Union[bool, dict[str, Any]]:
|
||||
"""
|
||||
Проверка владельца бота.
|
||||
|
||||
Returns:
|
||||
bool или dict: True/dict если владелец, False иначе
|
||||
"""
|
||||
if not event.from_user:
|
||||
return False
|
||||
|
||||
user_id = event.from_user.id
|
||||
is_owner = user_id in settings.OWNER_ID
|
||||
|
||||
if not is_owner:
|
||||
logger.warning(
|
||||
f"Попытка доступа к команде владельца от user_id={user_id}",
|
||||
log_type='SECURITY',
|
||||
message=event if isinstance(event, Message) else None
|
||||
)
|
||||
|
||||
if self.send_error_message:
|
||||
error_text = "⛔ Эта команда доступна только владельцу бота!"
|
||||
|
||||
if isinstance(event, Message):
|
||||
await event.answer(error_text)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.answer(error_text, show_alert=True)
|
||||
|
||||
return False
|
||||
|
||||
# Возвращаем информацию для handler
|
||||
return {
|
||||
'is_owner': True,
|
||||
'user_id': user_id,
|
||||
'owner_ids': settings.OWNER_ID
|
||||
}
|
||||
|
||||
|
||||
class IsChatCreator(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли пользователь создателем чата.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("transfer"), IsChatCreator())
|
||||
async def transfer_ownership(message: Message):
|
||||
await message.answer("👑 Передача владения чатом...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> Union[bool, dict]:
|
||||
try:
|
||||
member = await bot.get_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=message.from_user.id
|
||||
)
|
||||
|
||||
is_creator = member.status == ChatMemberStatus.CREATOR
|
||||
|
||||
if is_creator:
|
||||
return {
|
||||
'is_creator': True,
|
||||
'user_id': message.from_user.id,
|
||||
'chat_id': message.chat.id
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError) as e:
|
||||
logger.error(
|
||||
f"Ошибка проверки создателя чата: {e}",
|
||||
log_type='CHAT_RIGHTS',
|
||||
message=message
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class IsChatAdmin(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли пользователь администратором чата (или создателем).
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("ban"), IsChatAdmin())
|
||||
async def ban_user(message: Message):
|
||||
await message.answer("🔨 Бан пользователя...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> Union[bool, dict]:
|
||||
try:
|
||||
member = await bot.get_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=message.from_user.id
|
||||
)
|
||||
|
||||
is_admin = member.status in (
|
||||
ChatMemberStatus.ADMINISTRATOR,
|
||||
ChatMemberStatus.CREATOR
|
||||
)
|
||||
|
||||
if is_admin:
|
||||
return {
|
||||
'is_admin': True,
|
||||
'status': member.status.value,
|
||||
'user_id': message.from_user.id,
|
||||
'chat_id': message.chat.id
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError) as e:
|
||||
logger.error(
|
||||
f"Ошибка проверки администратора чата: {e}",
|
||||
log_type='CHAT_RIGHTS',
|
||||
message=message
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class IsModerator(BaseFilter):
|
||||
"""
|
||||
Проверяет, имеет ли администратор модераторские права:
|
||||
- Удаление сообщений
|
||||
- Ограничение пользователей
|
||||
- Закрепление сообщений
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("warn"), IsModerator())
|
||||
async def warn_user(message: Message):
|
||||
await message.answer("⚠️ Предупреждение пользователю...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> Union[bool, dict]:
|
||||
try:
|
||||
member = await bot.get_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=message.from_user.id
|
||||
)
|
||||
|
||||
# Создатель всегда модератор
|
||||
if member.status == ChatMemberStatus.CREATOR:
|
||||
return {
|
||||
'is_moderator': True,
|
||||
'status': 'creator',
|
||||
'user_id': message.from_user.id
|
||||
}
|
||||
|
||||
# Проверка прав администратора
|
||||
if member.status != ChatMemberStatus.ADMINISTRATOR:
|
||||
return False
|
||||
|
||||
# Проверка модераторских прав
|
||||
required_rights = [
|
||||
getattr(member, 'can_delete_messages', False),
|
||||
getattr(member, 'can_restrict_members', False),
|
||||
getattr(member, 'can_pin_messages', False),
|
||||
]
|
||||
|
||||
has_all_rights = all(required_rights)
|
||||
|
||||
if has_all_rights:
|
||||
return {
|
||||
'is_moderator': True,
|
||||
'status': 'administrator',
|
||||
'can_delete': required_rights[0],
|
||||
'can_restrict': required_rights[1],
|
||||
'can_pin': required_rights[2],
|
||||
'user_id': message.from_user.id
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError) as e:
|
||||
logger.error(
|
||||
f"Ошибка проверки модератора: {e}",
|
||||
log_type='CHAT_RIGHTS',
|
||||
message=message
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class CanDeleteMessages(BaseFilter):
|
||||
"""
|
||||
Проверяет право на удаление сообщений.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("clear"), CanDeleteMessages())
|
||||
async def clear_messages(message: Message):
|
||||
await message.answer("🗑️ Очистка сообщений...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
try:
|
||||
member = await bot.get_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=message.from_user.id
|
||||
)
|
||||
|
||||
if member.status == ChatMemberStatus.CREATOR:
|
||||
return True
|
||||
|
||||
return getattr(member, 'can_delete_messages', False)
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError):
|
||||
return False
|
||||
|
||||
|
||||
class CanRestrictMembers(BaseFilter):
|
||||
"""
|
||||
Проверяет право на ограничение пользователей (бан, мут).
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("mute"), CanRestrictMembers())
|
||||
async def mute_user(message: Message):
|
||||
await message.answer("🔇 Мут пользователя...")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
try:
|
||||
member = await bot.get_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=message.from_user.id
|
||||
)
|
||||
|
||||
if member.status == ChatMemberStatus.CREATOR:
|
||||
return True
|
||||
|
||||
return getattr(member, 'can_restrict_members', False)
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError):
|
||||
return False
|
||||
|
||||
|
||||
class CanPinMessages(BaseFilter):
|
||||
"""
|
||||
Проверяет право на закрепление сообщений.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("pin"), CanPinMessages())
|
||||
async def pin_message(message: Message):
|
||||
if message.reply_to_message:
|
||||
await message.reply_to_message.pin()
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
try:
|
||||
member = await bot.get_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=message.from_user.id
|
||||
)
|
||||
|
||||
if member.status == ChatMemberStatus.CREATOR:
|
||||
return True
|
||||
|
||||
return getattr(member, 'can_pin_messages', False)
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError):
|
||||
return False
|
||||
105
bot/filters/chat_type.py
Normal file
105
bot/filters/chat_type.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Фильтры для проверки типов чатов
|
||||
"""
|
||||
from typing import Union
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.enums import ChatType
|
||||
|
||||
__all__ = ('IsPrivateChat', 'IsGroupChat', 'IsSuperGroupChat', 'IsChannelChat', 'IsAnyGroup')
|
||||
|
||||
|
||||
class IsPrivateChat(BaseFilter):
|
||||
"""
|
||||
Проверяет, что сообщение из личного чата (приватный диалог с ботом).
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("start"), IsPrivateChat())
|
||||
async def start_private(message: Message):
|
||||
await message.answer("Привет в личке!")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
if isinstance(event, CallbackQuery):
|
||||
event = event.message
|
||||
|
||||
return event.chat.type == ChatType.PRIVATE
|
||||
|
||||
|
||||
class IsGroupChat(BaseFilter):
|
||||
"""
|
||||
Проверяет, что сообщение из обычной группы (не супергруппы).
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(IsGroupChat())
|
||||
async def group_message(message: Message):
|
||||
await message.answer("Это обычная группа")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
if isinstance(event, CallbackQuery):
|
||||
event = event.message
|
||||
|
||||
return event.chat.type == ChatType.GROUP
|
||||
|
||||
|
||||
class IsSuperGroupChat(BaseFilter):
|
||||
"""
|
||||
Проверяет, что сообщение из супергруппы.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(IsSuperGroupChat())
|
||||
async def supergroup_message(message: Message):
|
||||
await message.answer("Это супергруппа")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
if isinstance(event, CallbackQuery):
|
||||
event = event.message
|
||||
|
||||
return event.chat.type == ChatType.SUPERGROUP
|
||||
|
||||
|
||||
class IsChannelChat(BaseFilter):
|
||||
"""
|
||||
Проверяет, что сообщение из канала.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(IsChannelChat())
|
||||
async def channel_message(message: Message):
|
||||
await message.answer("Это канал")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
if isinstance(event, CallbackQuery):
|
||||
event = event.message
|
||||
|
||||
return event.chat.type == ChatType.CHANNEL
|
||||
|
||||
|
||||
class IsAnyGroup(BaseFilter):
|
||||
"""
|
||||
Проверяет, что сообщение из любой группы (обычная или супергруппа).
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(Command("admin"), IsAnyGroup())
|
||||
async def admin_command(message: Message):
|
||||
await message.answer("Команда доступна только в группах")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
if isinstance(event, CallbackQuery):
|
||||
event = event.message
|
||||
|
||||
return event.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)
|
||||
184
bot/filters/modes.py
Normal file
184
bot/filters/modes.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
Фильтры для проверки активных режимов бота (silence, conflict)
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ('IsSilenceActive', 'IsConflictModeActive')
|
||||
|
||||
|
||||
class IsSilenceActive(BaseFilter):
|
||||
"""
|
||||
Проверяет, активен ли режим тишины (silence mode).
|
||||
|
||||
В режиме тишины удаляются ВСЕ сообщения (кроме админов).
|
||||
|
||||
Attributes:
|
||||
silence_until: Время до которого активен режим (None = неактивен)
|
||||
|
||||
Example:
|
||||
```python
|
||||
# В handler-файле
|
||||
silence_filter = IsSilenceActive()
|
||||
|
||||
@router.message(silence_filter)
|
||||
async def silence_mode_active(message: Message):
|
||||
# Удаляем все сообщения в режиме тишины
|
||||
await message.delete()
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, silence_until: Optional[datetime] = None):
|
||||
"""
|
||||
Args:
|
||||
silence_until: Datetime до которого активен режим
|
||||
"""
|
||||
self.silence_until = silence_until
|
||||
|
||||
def update_silence_until(self, new_datetime: Optional[datetime]) -> None:
|
||||
"""
|
||||
Обновляет время окончания режима тишины.
|
||||
|
||||
Args:
|
||||
new_datetime: Новое время окончания или None для отключения
|
||||
"""
|
||||
self.silence_until = new_datetime
|
||||
|
||||
if new_datetime:
|
||||
logger.info(
|
||||
f"Режим тишины активирован до {new_datetime.strftime('%H:%M:%S')}",
|
||||
log_type='SILENCE'
|
||||
)
|
||||
else:
|
||||
logger.info("Режим тишины отключен", log_type='SILENCE')
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""
|
||||
Проверяет, активен ли режим сейчас.
|
||||
|
||||
Returns:
|
||||
bool: True если режим активен
|
||||
"""
|
||||
if self.silence_until is None:
|
||||
return False
|
||||
|
||||
# Проверка истечения времени
|
||||
if datetime.now() >= self.silence_until:
|
||||
logger.info("Режим тишины автоматически завершен", log_type='SILENCE')
|
||||
self.silence_until = None
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def __call__(self, event: Message) -> Optional[dict]:
|
||||
"""
|
||||
Проверка активности режима тишины.
|
||||
|
||||
Returns:
|
||||
dict или None: Информация о режиме если активен, иначе None
|
||||
"""
|
||||
if self.is_active():
|
||||
remaining = (self.silence_until - datetime.now()).total_seconds()
|
||||
logger.debug(
|
||||
f"Режим тишины активен (осталось {remaining:.0f}с)",
|
||||
log_type='SILENCE',
|
||||
message=event
|
||||
)
|
||||
return {
|
||||
'is_active': True,
|
||||
'until': self.silence_until,
|
||||
'remaining_seconds': remaining
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class IsConflictModeActive(BaseFilter):
|
||||
"""
|
||||
Проверяет, активен ли режим антиконфликта (conflict mode).
|
||||
|
||||
В режиме антиконфликта удаляются сообщения с конфликтными словами.
|
||||
|
||||
Attributes:
|
||||
conflict_until: Время до которого активен режим (None = неактивен)
|
||||
|
||||
Example:
|
||||
```python
|
||||
conflict_filter = IsConflictModeActive()
|
||||
|
||||
@router.message(conflict_filter)
|
||||
async def conflict_mode_active(message: Message):
|
||||
# Проверяем на конфликтные слова и удаляем
|
||||
if has_conflict_words(message.text):
|
||||
await message.delete()
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, conflict_until: Optional[datetime] = None):
|
||||
"""
|
||||
Args:
|
||||
conflict_until: Datetime до которого активен режим
|
||||
"""
|
||||
self.conflict_until = conflict_until
|
||||
|
||||
def update_conflict_until(self, new_datetime: Optional[datetime]) -> None:
|
||||
"""
|
||||
Обновляет время окончания режима антиконфликта.
|
||||
|
||||
Args:
|
||||
new_datetime: Новое время окончания или None для отключения
|
||||
"""
|
||||
self.conflict_until = new_datetime
|
||||
|
||||
if new_datetime:
|
||||
logger.info(
|
||||
f"Режим антиконфликта активирован до {new_datetime.strftime('%H:%M:%S')}",
|
||||
log_type='CONFLICT'
|
||||
)
|
||||
else:
|
||||
logger.info("Режим антиконфликта отключен", log_type='CONFLICT')
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""
|
||||
Проверяет, активен ли режим сейчас.
|
||||
|
||||
Returns:
|
||||
bool: True если режим активен
|
||||
"""
|
||||
if self.conflict_until is None:
|
||||
return False
|
||||
|
||||
# Проверка истечения времени
|
||||
if datetime.now() >= self.conflict_until:
|
||||
logger.info("Режим антиконфликта автоматически завершен", log_type='CONFLICT')
|
||||
self.conflict_until = None
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def __call__(self, event: Message) -> Optional[dict]:
|
||||
"""
|
||||
Проверка активности режима антиконфликта.
|
||||
|
||||
Returns:
|
||||
dict или None: Информация о режиме если активен, иначе None
|
||||
"""
|
||||
if self.is_active():
|
||||
remaining = (self.conflict_until - datetime.now()).total_seconds()
|
||||
logger.debug(
|
||||
f"Режим антиконфликта активен (осталось {remaining:.0f}с)",
|
||||
log_type='CONFLICT',
|
||||
message=event
|
||||
)
|
||||
return {
|
||||
'is_active': True,
|
||||
'until': self.conflict_until,
|
||||
'remaining_seconds': remaining
|
||||
}
|
||||
|
||||
return None
|
||||
395
bot/filters/msg_content.py
Normal file
395
bot/filters/msg_content.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Фильтры для проверки содержимого сообщений
|
||||
"""
|
||||
import re
|
||||
from typing import Optional, Union
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, ContentType
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = (
|
||||
'IsReply',
|
||||
'IsForwarded',
|
||||
'HasMedia',
|
||||
'ContainsURL',
|
||||
'HasText',
|
||||
'HasCaption',
|
||||
'HasEntities',
|
||||
'MediaType'
|
||||
)
|
||||
|
||||
|
||||
class IsReply(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли сообщение ответом на другое сообщение.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(IsReply())
|
||||
async def handle_reply(message: Message):
|
||||
original = message.reply_to_message
|
||||
await message.answer(f"Это ответ на: {original.text}")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, dict]:
|
||||
is_reply = message.reply_to_message is not None
|
||||
|
||||
if is_reply:
|
||||
return {
|
||||
'is_reply': True,
|
||||
'reply_to_message': message.reply_to_message,
|
||||
'reply_to_user_id': message.reply_to_message.from_user.id if message.reply_to_message.from_user else None
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class IsForwarded(BaseFilter):
|
||||
"""
|
||||
Проверяет, является ли сообщение пересланным.
|
||||
|
||||
Поддерживает:
|
||||
- Пересылку от пользователей (forward_from)
|
||||
- Пересылку из каналов/групп (forward_from_chat)
|
||||
- Скрытую пересылку (forward_sender_name)
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(IsForwarded())
|
||||
async def handle_forwarded(message: Message, forward_info: dict):
|
||||
await message.answer(f"Переслано из: {forward_info['origin']}")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, dict]:
|
||||
# Проверка различных типов пересылки
|
||||
is_forwarded = (
|
||||
message.forward_origin is not None or # Новый API (aiogram 3.x)
|
||||
message.forward_from is not None or
|
||||
message.forward_from_chat is not None or
|
||||
message.forward_sender_name is not None
|
||||
)
|
||||
|
||||
if is_forwarded:
|
||||
origin = "неизвестно"
|
||||
|
||||
if message.forward_from:
|
||||
origin = f"пользователь @{message.forward_from.username or message.forward_from.id}"
|
||||
elif message.forward_from_chat:
|
||||
origin = f"чат {message.forward_from_chat.title or message.forward_from_chat.id}"
|
||||
elif message.forward_sender_name:
|
||||
origin = f"скрытый пользователь ({message.forward_sender_name})"
|
||||
|
||||
logger.debug(
|
||||
f"Обнаружено пересланное сообщение из: {origin}",
|
||||
log_type='FORWARD',
|
||||
message=message
|
||||
)
|
||||
|
||||
return {
|
||||
'is_forwarded': True,
|
||||
'origin': origin,
|
||||
'forward_date': message.forward_date
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class HasMedia(BaseFilter):
|
||||
"""
|
||||
Проверяет, содержит ли сообщение медиа-контент.
|
||||
|
||||
Attributes:
|
||||
media_types: Список типов медиа для проверки (если None, проверяются все)
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Любое медиа
|
||||
@router.message(HasMedia())
|
||||
async def handle_media(message: Message):
|
||||
await message.answer("Получено медиа!")
|
||||
|
||||
# Только фото и видео
|
||||
@router.message(HasMedia(['photo', 'video']))
|
||||
async def handle_visual(message: Message):
|
||||
await message.answer("Фото или видео!")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, media_types: Optional[list[str]] = None):
|
||||
"""
|
||||
Args:
|
||||
media_types: Список типов медиа ('photo', 'video', 'document', и т.д.)
|
||||
Если None, проверяются все типы
|
||||
"""
|
||||
self.media_types = media_types
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, dict]:
|
||||
# Все возможные типы медиа
|
||||
media_checks = {
|
||||
'photo': message.photo,
|
||||
'video': message.video,
|
||||
'document': message.document,
|
||||
'audio': message.audio,
|
||||
'voice': message.voice,
|
||||
'video_note': message.video_note,
|
||||
'sticker': message.sticker,
|
||||
'animation': message.animation,
|
||||
}
|
||||
|
||||
# Если указаны конкретные типы, проверяем только их
|
||||
if self.media_types:
|
||||
has_media = any(
|
||||
media_checks[media_type]
|
||||
for media_type in self.media_types
|
||||
if media_type in media_checks
|
||||
)
|
||||
detected_type = next(
|
||||
(media_type for media_type in self.media_types if media_checks.get(media_type)),
|
||||
None
|
||||
)
|
||||
else:
|
||||
# Проверяем все типы
|
||||
has_media = any(media_checks.values())
|
||||
detected_type = next(
|
||||
(media_type for media_type, value in media_checks.items() if value),
|
||||
None
|
||||
)
|
||||
|
||||
if has_media:
|
||||
return {
|
||||
'has_media': True,
|
||||
'media_type': detected_type,
|
||||
'content': media_checks[detected_type]
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ContainsURL(BaseFilter):
|
||||
"""
|
||||
Проверяет, содержит ли сообщение ссылки.
|
||||
|
||||
Поддерживает:
|
||||
- HTTP/HTTPS ссылки
|
||||
- Telegram ссылки (t.me, tg://)
|
||||
- Проверку через entities (более точная)
|
||||
|
||||
Attributes:
|
||||
strict: Использовать строгую проверку через entities
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(ContainsURL())
|
||||
async def handle_url(message: Message, url_info: dict):
|
||||
urls = url_info['urls']
|
||||
await message.answer(f"Обнаружено {len(urls)} ссылок")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, strict: bool = False):
|
||||
"""
|
||||
Args:
|
||||
strict: Если True, проверяет через entities (игнорирует текст в коде/pre)
|
||||
"""
|
||||
self.strict = strict
|
||||
# Паттерн для поиска URL
|
||||
self.url_pattern = re.compile(
|
||||
r'https?://[^\s]+|' # http(s)://
|
||||
r't\.me/[^\s]+|' # t.me/
|
||||
r'tg://[^\s]+', # tg://
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, dict]:
|
||||
if not message.text and not message.caption:
|
||||
return False
|
||||
|
||||
text = message.text or message.caption
|
||||
|
||||
if self.strict and message.entities:
|
||||
# Строгая проверка через entities
|
||||
url_entities = [
|
||||
entity for entity in message.entities
|
||||
if entity.type in ('url', 'text_link')
|
||||
]
|
||||
|
||||
if url_entities:
|
||||
urls = []
|
||||
for entity in url_entities:
|
||||
if entity.type == 'url':
|
||||
url = text[entity.offset:entity.offset + entity.length]
|
||||
urls.append(url)
|
||||
elif entity.type == 'text_link':
|
||||
urls.append(entity.url)
|
||||
|
||||
return {
|
||||
'contains_url': True,
|
||||
'urls': urls,
|
||||
'url_count': len(urls)
|
||||
}
|
||||
else:
|
||||
# Простая проверка через regex
|
||||
urls = self.url_pattern.findall(text)
|
||||
|
||||
if urls:
|
||||
return {
|
||||
'contains_url': True,
|
||||
'urls': urls,
|
||||
'url_count': len(urls)
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class HasText(BaseFilter):
|
||||
"""
|
||||
Проверяет, содержит ли сообщение текст.
|
||||
|
||||
Attributes:
|
||||
min_length: Минимальная длина текста (по умолчанию 1)
|
||||
max_length: Максимальная длина текста (по умолчанию None)
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Любой текст
|
||||
@router.message(HasText())
|
||||
async def handle_text(message: Message):
|
||||
await message.answer("Получен текст!")
|
||||
|
||||
# Текст от 10 до 100 символов
|
||||
@router.message(HasText(min_length=10, max_length=100))
|
||||
async def handle_medium_text(message: Message):
|
||||
await message.answer("Текст подходящей длины!")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, min_length: int = 1, max_length: Optional[int] = None):
|
||||
self.min_length = min_length
|
||||
self.max_length = max_length
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, dict]:
|
||||
if not message.text:
|
||||
return False
|
||||
|
||||
text_length = len(message.text)
|
||||
|
||||
# Проверка длины
|
||||
if text_length < self.min_length:
|
||||
return False
|
||||
|
||||
if self.max_length and text_length > self.max_length:
|
||||
return False
|
||||
|
||||
return {
|
||||
'has_text': True,
|
||||
'text_length': text_length,
|
||||
'text': message.text
|
||||
}
|
||||
|
||||
|
||||
class HasCaption(BaseFilter):
|
||||
"""
|
||||
Проверяет, есть ли у медиа подпись.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(HasCaption())
|
||||
async def handle_caption(message: Message):
|
||||
await message.answer(f"Подпись: {message.caption}")
|
||||
```
|
||||
"""
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, dict]:
|
||||
if message.caption:
|
||||
return {
|
||||
'has_caption': True,
|
||||
'caption': message.caption,
|
||||
'caption_length': len(message.caption)
|
||||
}
|
||||
return False
|
||||
|
||||
|
||||
class HasEntities(BaseFilter):
|
||||
"""
|
||||
Проверяет наличие entities (упоминания, хештеги, команды и т.д.).
|
||||
|
||||
Attributes:
|
||||
entity_types: Список типов entities для проверки
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Любые entities
|
||||
@router.message(HasEntities())
|
||||
async def handle_entities(message: Message):
|
||||
pass
|
||||
|
||||
# Только упоминания и хештеги
|
||||
@router.message(HasEntities(['mention', 'hashtag']))
|
||||
async def handle_mentions(message: Message):
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, entity_types: Optional[list[str]] = None):
|
||||
"""
|
||||
Args:
|
||||
entity_types: Список типов ('mention', 'hashtag', 'bot_command', и т.д.)
|
||||
"""
|
||||
self.entity_types = entity_types
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, dict]:
|
||||
if not message.entities:
|
||||
return False
|
||||
|
||||
if self.entity_types:
|
||||
# Фильтруем по типам
|
||||
matching_entities = [
|
||||
entity for entity in message.entities
|
||||
if entity.type in self.entity_types
|
||||
]
|
||||
|
||||
if matching_entities:
|
||||
return {
|
||||
'has_entities': True,
|
||||
'entities': matching_entities,
|
||||
'entity_count': len(matching_entities)
|
||||
}
|
||||
else:
|
||||
# Любые entities
|
||||
return {
|
||||
'has_entities': True,
|
||||
'entities': message.entities,
|
||||
'entity_count': len(message.entities)
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class MediaType(BaseFilter):
|
||||
"""
|
||||
Проверяет точный тип контента сообщения.
|
||||
|
||||
Attributes:
|
||||
content_type: Тип контента из ContentType enum
|
||||
|
||||
Example:
|
||||
```python
|
||||
@router.message(MediaType(ContentType.PHOTO))
|
||||
async def handle_photo(message: Message):
|
||||
await message.answer("Это фото!")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, content_type: Union[ContentType, str]):
|
||||
"""
|
||||
Args:
|
||||
content_type: Тип контента (ContentType enum или строка)
|
||||
"""
|
||||
self.content_type = content_type if isinstance(content_type, str) else content_type.value
|
||||
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
return message.content_type == self.content_type
|
||||
111
bot/filters/spam.py
Normal file
111
bot/filters/spam.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Фильтры для проверки сообщений на спам и банворды
|
||||
"""
|
||||
from typing import Optional, Callable
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ('HasSpam', 'IsWhitelisted')
|
||||
|
||||
|
||||
class HasSpam(BaseFilter):
|
||||
"""
|
||||
Проверяет, содержит ли сообщение запрещенные слова (спам).
|
||||
|
||||
Attributes:
|
||||
check_spam_func: Функция проверки спама (передается при инициализации)
|
||||
|
||||
Example:
|
||||
```python
|
||||
from utils.spam_checker import check_spam
|
||||
|
||||
@router.message(HasSpam(check_spam))
|
||||
async def spam_detected(message: Message):
|
||||
await message.delete()
|
||||
await message.answer("⚠️ Сообщение содержит запрещенные слова")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, check_spam_func: Callable[[str], bool]):
|
||||
"""
|
||||
Args:
|
||||
check_spam_func: Функция для проверки спама
|
||||
"""
|
||||
self.check_spam = check_spam_func
|
||||
|
||||
async def __call__(self, message: Message) -> Optional[dict]:
|
||||
"""
|
||||
Проверка сообщения на спам.
|
||||
|
||||
Returns:
|
||||
dict или None: Информация о найденном спаме или None
|
||||
"""
|
||||
if not message.text:
|
||||
return None
|
||||
|
||||
text_lower = message.text.lower()
|
||||
has_spam = self.check_spam(text_lower)
|
||||
|
||||
if has_spam:
|
||||
logger.warning(
|
||||
f"Обнаружен спам в сообщении",
|
||||
log_type='SPAM',
|
||||
message=message
|
||||
)
|
||||
return {'has_spam': True, 'text': text_lower}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class IsWhitelisted(BaseFilter):
|
||||
"""
|
||||
Проверяет, содержит ли сообщение слова из белого списка (исключения).
|
||||
|
||||
Используется для защиты от ложных срабатываний спам-фильтра.
|
||||
|
||||
Attributes:
|
||||
check_whitelist_func: Функция проверки белого списка
|
||||
|
||||
Example:
|
||||
```python
|
||||
from utils.spam_checker import check_whitelist
|
||||
|
||||
@router.message(IsWhitelisted(check_whitelist))
|
||||
async def whitelisted_message(message: Message):
|
||||
# Сообщение содержит исключение, пропускаем проверку спама
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, check_whitelist_func: Callable[[str], bool]):
|
||||
"""
|
||||
Args:
|
||||
check_whitelist_func: Функция для проверки белого списка
|
||||
"""
|
||||
self.check_whitelist = check_whitelist_func
|
||||
|
||||
async def __call__(self, message: Message) -> Optional[bool]:
|
||||
"""
|
||||
Проверка на наличие в белом списке.
|
||||
|
||||
Returns:
|
||||
bool или None: True если в белом списке, None если нет
|
||||
"""
|
||||
if not message.text:
|
||||
return None
|
||||
|
||||
text_lower = message.text.lower()
|
||||
is_whitelisted = self.check_whitelist(text_lower)
|
||||
|
||||
if is_whitelisted:
|
||||
logger.debug(
|
||||
f"Сообщение содержит исключение из белого списка",
|
||||
log_type='WHITELIST',
|
||||
message=message
|
||||
)
|
||||
return True
|
||||
|
||||
return None
|
||||
246
bot/filters/subscription.py
Normal file
246
bot/filters/subscription.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Фильтр проверки подписки пользователя на каналы/группы
|
||||
"""
|
||||
from typing import Union, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.enums import ChatMemberStatus
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ('IsSubscribed', 'SubscriptionChecker')
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChannelInfo:
|
||||
"""Информация о канале для проверки подписки"""
|
||||
id: Union[str, int]
|
||||
name: Optional[str] = None
|
||||
invite_link: Optional[str] = None
|
||||
|
||||
|
||||
class SubscriptionChecker:
|
||||
"""
|
||||
Вспомогательный класс для проверки подписок.
|
||||
Может использоваться отдельно от фильтра.
|
||||
"""
|
||||
|
||||
# Статусы, считающиеся подпиской
|
||||
SUBSCRIBED_STATUSES: set[str] = {
|
||||
ChatMemberStatus.MEMBER,
|
||||
ChatMemberStatus.ADMINISTRATOR,
|
||||
ChatMemberStatus.CREATOR
|
||||
}
|
||||
|
||||
# Статусы, означающие отсутствие подписки
|
||||
NOT_SUBSCRIBED_STATUSES: set[str] = {
|
||||
ChatMemberStatus.LEFT,
|
||||
ChatMemberStatus.KICKED,
|
||||
ChatMemberStatus.RESTRICTED # Опционально
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def is_subscribed(
|
||||
cls,
|
||||
bot: Bot,
|
||||
user_id: int,
|
||||
channel_id: Union[str, int]
|
||||
) -> bool:
|
||||
"""
|
||||
Проверяет подписку одного пользователя на один канал.
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
user_id: ID пользователя
|
||||
channel_id: ID или username канала
|
||||
|
||||
Returns:
|
||||
bool: True если подписан
|
||||
"""
|
||||
try:
|
||||
member = await bot.get_chat_member(
|
||||
chat_id=channel_id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
is_sub = member.status in cls.SUBSCRIBED_STATUSES
|
||||
|
||||
logger.debug(
|
||||
f"Проверка подписки user={user_id} на канал={channel_id}: {member.status} ({'✅' if is_sub else '❌'})",
|
||||
log_type='SUBSCRIPTION'
|
||||
)
|
||||
|
||||
return is_sub
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
logger.warning(
|
||||
f"Канал {channel_id} недоступен или неверный ID: {e}",
|
||||
log_type='SUBSCRIPTION'
|
||||
)
|
||||
return False
|
||||
|
||||
except TelegramForbiddenError as e:
|
||||
logger.error(
|
||||
f"Бот не имеет доступа к каналу {channel_id}: {e}",
|
||||
log_type='SUBSCRIPTION'
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Непредвиденная ошибка проверки подписки на {channel_id}: {e}",
|
||||
log_type='SUBSCRIPTION'
|
||||
)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def check_all_channels(
|
||||
cls,
|
||||
bot: Bot,
|
||||
user_id: int,
|
||||
channels: list[Union[str, int]]
|
||||
) -> dict[Union[str, int], bool]:
|
||||
"""
|
||||
Проверяет подписку на несколько каналов одновременно.
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
user_id: ID пользователя
|
||||
channels: Список ID/username каналов
|
||||
|
||||
Returns:
|
||||
dict: Словарь {channel_id: is_subscribed}
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for channel in channels:
|
||||
results[channel] = await cls.is_subscribed(bot, user_id, channel)
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
async def get_not_subscribed_channels(
|
||||
cls,
|
||||
bot: Bot,
|
||||
user_id: int,
|
||||
channels: list[Union[str, int]]
|
||||
) -> list[Union[str, int]]:
|
||||
"""
|
||||
Возвращает список каналов, на которые пользователь НЕ подписан.
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
user_id: ID пользователя
|
||||
channels: Список ID/username каналов
|
||||
|
||||
Returns:
|
||||
list: Список каналов без подписки
|
||||
"""
|
||||
not_subscribed = []
|
||||
|
||||
for channel in channels:
|
||||
if not await cls.is_subscribed(bot, user_id, channel):
|
||||
not_subscribed.append(channel)
|
||||
|
||||
return not_subscribed
|
||||
|
||||
|
||||
class IsSubscribed(BaseFilter):
|
||||
"""
|
||||
Фильтр для проверки подписки пользователя на каналы/группы.
|
||||
|
||||
Поддерживает:
|
||||
- Публичные каналы (username: "@channel_name")
|
||||
- Приватные каналы/группы (ID: -1001234567890)
|
||||
- Проверку всех или хотя бы одного канала
|
||||
- Работу с Message и CallbackQuery
|
||||
|
||||
Attributes:
|
||||
channels: Список ID или username каналов для проверки
|
||||
require_all: Требовать подписку на все каналы (True) или хотя бы один (False)
|
||||
|
||||
Examples:
|
||||
>> # Проверка подписки на один канал
|
||||
>> @router.message(IsSubscribed(["@my_channel"]))
|
||||
>> async def handler(message: Message):
|
||||
... await message.answer("Ты подписан!")
|
||||
|
||||
>> # Проверка на несколько каналов (все обязательны)
|
||||
>> @router.message(IsSubscribed(["@channel1", -1001234567890], require_all=True))
|
||||
>> async def handler(message: Message):
|
||||
... await message.answer("Ты подписан на все каналы!")
|
||||
|
||||
>> # Проверка на несколько каналов (хотя бы один)
|
||||
>> @router.message(IsSubscribed(["@channel1", "@channel2"], require_all=False))
|
||||
>> async def handler(message: Message):
|
||||
... await message.answer("Ты подписан хотя бы на один канал!")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channels: list[Union[str, int]],
|
||||
require_all: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Инициализация фильтра.
|
||||
|
||||
Args:
|
||||
channels: Список ID или username каналов
|
||||
require_all: True = все каналы, False = хотя бы один
|
||||
"""
|
||||
if not channels:
|
||||
raise ValueError("Список каналов не может быть пустым")
|
||||
|
||||
self.channels = channels
|
||||
self.require_all = require_all
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
event: Union[Message, CallbackQuery],
|
||||
bot: Bot
|
||||
) -> Union[bool, dict]:
|
||||
"""
|
||||
Проверка подписки.
|
||||
|
||||
Args:
|
||||
event: Message или CallbackQuery
|
||||
bot: Экземпляр бота
|
||||
|
||||
Returns:
|
||||
bool или dict: True/False для простой проверки,
|
||||
dict с деталями для сложной логики
|
||||
"""
|
||||
user_id = event.from_user.id
|
||||
|
||||
# Проверка всех каналов
|
||||
results = await SubscriptionChecker.check_all_channels(
|
||||
bot, user_id, self.channels
|
||||
)
|
||||
|
||||
# Логика проверки
|
||||
if self.require_all:
|
||||
# Все каналы обязательны
|
||||
is_passed = all(results.values())
|
||||
else:
|
||||
# Хотя бы один канал
|
||||
is_passed = any(results.values())
|
||||
|
||||
# Логирование
|
||||
if not is_passed:
|
||||
not_subscribed = [ch for ch, sub in results.items() if not sub]
|
||||
logger.info(
|
||||
f"Пользователь {user_id} не подписан на: {not_subscribed}",
|
||||
log_type='SUBSCRIPTION',
|
||||
message=event if isinstance(event, Message) else None
|
||||
)
|
||||
|
||||
# Возвращаем результат + детали для handler
|
||||
return {
|
||||
'is_subscribed': is_passed,
|
||||
'subscription_results': results,
|
||||
'not_subscribed_channels': [ch for ch, sub in results.items() if not sub]
|
||||
} if not is_passed else is_passed
|
||||
Reference in New Issue
Block a user