Compare commits
3 Commits
b59d97da8a
...
e0569de1c5
| Author | SHA1 | Date | |
|---|---|---|---|
| e0569de1c5 | |||
| ff4575fe84 | |||
| e922132319 |
@@ -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)
|
||||
61
examples/callback_payload/main.py
Normal file
61
examples/callback_payload/main.py
Normal 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())
|
||||
175
maxapi/filters/callback_payload.py
Normal file
175
maxapi/filters/callback_payload.py
Normal 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
|
||||
@@ -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')]
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user