Compare commits

...

3 Commits

6 changed files with 277 additions and 54 deletions

View File

@@ -1,12 +1,13 @@
## ⭐️ Примеры
- [Эхо бот](https://github.com/love-apples/maxapi/blob/main/examples/echo/main.py)
- [Обработчик доступных событий](https://github.com/love-apples/maxapi/blob/main/examples/events/main.py)
- [Обработчики с MagicFilter](https://github.com/love-apples/maxapi/blob/main/examples/magic_filters/main.py)
- [Демонстрация роутинга, InputMedia и механика контекста](https://github.com/love-apples/maxapi/tree/main/examples/router_with_input_media) (audio.mp3 для команды /media)
- [Получение ID](https://github.com/love-apples/maxapi/tree/main/examples/get_ids/main.py)
- [Миддлварь в хендлерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_in_handlers/main.py)
- [Вебхуки](https://github.com/love-apples/maxapi/tree/main/examples/webhook)
- [Клавиатуры](https://github.com/love-apples/maxapi/tree/main/examples/keyboard/main.py)
- [Миддлварь в роутерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_for_router/main.py)
- [BaseFilter](https://github.com/love-apples/maxapi/tree/main/examples/base_filter/main.py)
- [Обработчик доступных событий](https://github.com/love-apples/maxapi/blob/main/examples/events/main.py)
- [Обработчики с MagicFilter](https://github.com/love-apples/maxapi/blob/main/examples/magic_filters/main.py)
- [Демонстрация роутинга, InputMedia и механика контекста](https://github.com/love-apples/maxapi/tree/main/examples/router_with_input_media) (audio.mp3 для команды /media)
- [Получение ID](https://github.com/love-apples/maxapi/tree/main/examples/get_ids/main.py)
- [Миддлварь в хендлерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_in_handlers/main.py)
- [Вебхуки](https://github.com/love-apples/maxapi/tree/main/examples/webhook)
- [Клавиатуры](https://github.com/love-apples/maxapi/tree/main/examples/keyboard/main.py)
- [Миддлварь в роутерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_for_router/main.py)
- [Свой фильтр на BaseFilter](https://github.com/love-apples/maxapi/tree/main/examples/base_filter/main.py)
- [Фильтр callback payload](https://github.com/love-apples/maxapi/tree/main/examples/callback_payload/main.py)

View File

@@ -0,0 +1,61 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher, F
from maxapi.filters.callback_payload import CallbackPayload
from maxapi.filters.command import CommandStart
from maxapi.types import (
CallbackButton,
MessageCreated,
MessageCallback,
)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
logging.basicConfig(level=logging.INFO)
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
class MyPayload(CallbackPayload, prefix='mypayload'):
foo: str
action: str
class AnotherPayload(CallbackPayload, prefix='another'):
bar: str
value: int
@dp.message_created(CommandStart())
async def show_keyboard(event: MessageCreated):
kb = InlineKeyboardBuilder()
kb.row(
CallbackButton(
text='Первая кнопка',
payload=MyPayload(foo='123', action='edit').pack(),
),
CallbackButton(
text='Вторая кнопка',
payload=AnotherPayload(bar='abc', value=42).pack(),
),
)
await event.message.answer('Нажми кнопку!', attachments=[kb.as_markup()])
@dp.message_callback(MyPayload.filter(F.foo == '123'))
async def on_first_callback(event: MessageCallback, payload: MyPayload):
await event.answer(new_text=f'Первая кнопка: foo={payload.foo}, action={payload.action}')
@dp.message_callback(AnotherPayload.filter())
async def on_second_callback(event: MessageCallback, payload: AnotherPayload):
await event.answer(new_text=f'Вторая кнопка: bar={payload.bar}, value={payload.value}')
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,175 @@
from __future__ import annotations
from typing import Any, ClassVar, List, Optional, Type, TYPE_CHECKING
from magic_filter import MagicFilter
from pydantic import BaseModel
from ..types.updates.message_callback import MessageCallback
from ..types.updates import UpdateUnion
from .filter import BaseFilter
PAYLOAD_MAX = 1024
class CallbackPayload(BaseModel):
"""
Базовый класс для сериализации/десериализации callback payload.
Атрибуты:
prefix (str): Префикс для payload (используется при pack/unpack) (по умолчанию название класса).
separator (str): Разделитель между значениями (по умолчанию '|').
"""
if TYPE_CHECKING:
prefix: ClassVar[str]
separator: ClassVar[str]
def __init_subclass__(cls, **kwargs: Any) -> None:
"""
Автоматически проставляет prefix и separator при наследовании.
"""
cls.prefix = kwargs.get('prefix', str(cls.__name__))
cls.separator = kwargs.get('separator', '|')
def pack(self) -> str:
"""
Собирает данные payload в строку для передачи в callback payload.
Raises:
ValueError: Если в значении встречается разделитель или payload слишком длинный.
Returns:
str: Сериализованный payload.
"""
values = [self.prefix]
for name in self.attrs():
value = getattr(self, name)
str_value = '' if value is None else str(value)
if self.separator in str_value:
raise ValueError(
f'Символ разделителя "{self.separator}" не должен встречаться в значении поля {name}'
)
values.append(str_value)
data = self.separator.join(values)
if len(data.encode()) > PAYLOAD_MAX:
raise ValueError(
f'Payload слишком длинный! Максимум: {PAYLOAD_MAX} байт'
)
return data
@classmethod
def unpack(cls, data: str):
"""
Десериализует payload из строки.
Args:
data (str): Строка payload (из callback payload).
Raises:
ValueError: Некорректный prefix или количество аргументов.
Returns:
CallbackPayload: Экземпляр payload с заполненными полями.
"""
parts = data.split(cls.separator)
if not parts[0] == cls.prefix:
raise ValueError('Некорректный prefix')
field_names = cls.attrs()
if not len(parts) - 1 == len(field_names):
raise ValueError(
f'Ожидалось {len(field_names)} аргументов, получено {len(parts) - 1}'
)
kwargs = dict(zip(field_names, parts[1:]))
return cls(**kwargs)
@classmethod
def attrs(cls) -> List[str]:
"""
Возвращает список полей для сериализации/десериализации (исключая prefix и separator).
Returns:
List[str]: Имена полей модели.
"""
return [
k for k in cls.model_fields.keys()
if k not in ('prefix', 'separator')
]
@classmethod
def filter(cls, rule: Optional[MagicFilter] = None) -> PayloadFilter:
"""
Создаёт PayloadFilter для фильтрации callback-ивентов по payload.
Args:
rule (Optional[MagicFilter]): Фильтр на payload.
Returns:
PayloadFilter: Экземпляр фильтра для хэндлера.
"""
return PayloadFilter(model=cls, rule=rule)
class PayloadFilter(BaseFilter):
"""
Фильтр для MessageCallback по payload.
"""
def __init__(self, model: Type[CallbackPayload], rule: Optional[MagicFilter]):
"""
Args:
model (Type[CallbackPayload]): Класс payload для распаковки.
rule (Optional[MagicFilter]): Фильтр (условие) для payload.
"""
self.model = model
self.rule = rule
async def __call__(self, event: UpdateUnion):
"""
Проверяет event на MessageCallback и применяет фильтр к payload.
Args:
event (UpdateUnion): Обновление/событие.
Returns:
dict | bool: dict с payload при совпадении, иначе False.
"""
if not isinstance(event, MessageCallback):
return False
if not event.callback.payload:
return False
try:
payload = self.model.unpack(event.callback.payload)
except Exception:
return False
if not self.rule or self.rule.resolve(payload):
return {'payload': payload}
return False

View File

@@ -0,0 +1,25 @@
from typing import Annotated, Union
from pydantic import Field
from ..attachments.share import Share
from ..attachments.buttons.attachment_button import AttachmentButton
from ..attachments.sticker import Sticker
from ..attachments.file import File
from ..attachments.image import Image
from ..attachments.video import Video
from ..attachments.audio import Audio
from ..attachments.location import Location
from ..attachments.contact import Contact
Attachments = Annotated[Union[
Audio,
Video,
File,
Image,
Sticker,
Share,
Location,
AttachmentButton,
Contact
], Field(discriminator='type')]

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
from pydantic import BaseModel, Field
from typing import Annotated, Any, Optional, List, Union, TYPE_CHECKING
from typing import Any, Optional, List, Union, TYPE_CHECKING
from ..types.attachments.contact import Contact
from ..types.attachments import Attachments
from ..enums.text_style import TextStyle
from ..enums.parse_mode import ParseMode
@@ -11,14 +11,6 @@ from ..enums.chat_type import ChatType
from ..enums.message_link_type import MessageLinkType
from .attachments.attachment import Attachment
from .attachments.share import Share
from .attachments.buttons.attachment_button import AttachmentButton
from .attachments.sticker import Sticker
from .attachments.file import File
from .attachments.image import Image
from .attachments.video import Video
from .attachments.audio import Audio
from .attachments.location import Location
from .users import User
@@ -26,19 +18,6 @@ from .users import User
if TYPE_CHECKING:
from ..bot import Bot
from ..types.input_media import InputMedia, InputMediaBuffer
Attachments = Annotated[Union[
Audio,
Video,
File,
Image,
Sticker,
Share,
Location,
AttachmentButton,
Contact
], Field(discriminator='type')]
class MarkupElement(BaseModel):

View File

@@ -1,25 +1,18 @@
from typing import List, Optional, Union
from typing import List, Optional
from pydantic import BaseModel, Field
from ...types.attachments.location import Location
from ...types.attachments import Attachments
from .update import Update
from ...enums.parse_mode import ParseMode
from ...types.message import NewMessageLink
from ...types.attachments.share import Share
from ...types.callback import Callback
from ...types.message import Message
from ..attachments.buttons.attachment_button import AttachmentButton
from ..attachments.sticker import Sticker
from ..attachments.file import File
from ..attachments.image import Image
from ..attachments.video import Video
from ..attachments.audio import Audio
class MessageForCallback(BaseModel):
@@ -37,18 +30,7 @@ class MessageForCallback(BaseModel):
text: Optional[str] = None
attachments: Optional[
List[
Union[
AttachmentButton,
Audio,
Video,
File,
Image,
Sticker,
Share,
Location
]
]
List[Attachments]
] = Field(default_factory=list) # type: ignore
link: Optional[NewMessageLink] = None
notify: Optional[bool] = True