Первый коммит

This commit is contained in:
2026-02-17 11:24:55 +07:00
commit a06448ca4b
109 changed files with 21165 additions and 0 deletions

11
bot/filters/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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