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/echo/main.py)
|
||||||
- [Обработчик доступных событий](https://github.com/love-apples/maxapi/blob/main/examples/events/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)
|
- [Обработчики с 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)
|
- [Демонстрация роутинга, 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)
|
- [Получение 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/middleware_in_handlers/main.py)
|
||||||
- [Вебхуки](https://github.com/love-apples/maxapi/tree/main/examples/webhook)
|
- [Вебхуки](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/keyboard/main.py)
|
||||||
- [Миддлварь в роутерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_for_router/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)
|
- [Свой фильтр на 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 __future__ import annotations
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
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.text_style import TextStyle
|
||||||
from ..enums.parse_mode import ParseMode
|
from ..enums.parse_mode import ParseMode
|
||||||
@@ -11,14 +11,6 @@ from ..enums.chat_type import ChatType
|
|||||||
from ..enums.message_link_type import MessageLinkType
|
from ..enums.message_link_type import MessageLinkType
|
||||||
|
|
||||||
from .attachments.attachment import Attachment
|
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
|
from .users import User
|
||||||
|
|
||||||
@@ -26,19 +18,6 @@ from .users import User
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..bot import Bot
|
from ..bot import Bot
|
||||||
from ..types.input_media import InputMedia, InputMediaBuffer
|
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):
|
class MarkupElement(BaseModel):
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
from typing import List, Optional, Union
|
from typing import List, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from ...types.attachments.location import Location
|
from ...types.attachments import Attachments
|
||||||
|
|
||||||
|
|
||||||
from .update import Update
|
from .update import Update
|
||||||
|
|
||||||
from ...enums.parse_mode import ParseMode
|
from ...enums.parse_mode import ParseMode
|
||||||
|
|
||||||
from ...types.message import NewMessageLink
|
from ...types.message import NewMessageLink
|
||||||
from ...types.attachments.share import Share
|
|
||||||
from ...types.callback import Callback
|
from ...types.callback import Callback
|
||||||
from ...types.message import Message
|
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):
|
class MessageForCallback(BaseModel):
|
||||||
|
|
||||||
@@ -37,18 +30,7 @@ class MessageForCallback(BaseModel):
|
|||||||
|
|
||||||
text: Optional[str] = None
|
text: Optional[str] = None
|
||||||
attachments: Optional[
|
attachments: Optional[
|
||||||
List[
|
List[Attachments]
|
||||||
Union[
|
|
||||||
AttachmentButton,
|
|
||||||
Audio,
|
|
||||||
Video,
|
|
||||||
File,
|
|
||||||
Image,
|
|
||||||
Sticker,
|
|
||||||
Share,
|
|
||||||
Location
|
|
||||||
]
|
|
||||||
]
|
|
||||||
] = Field(default_factory=list) # type: ignore
|
] = Field(default_factory=list) # type: ignore
|
||||||
link: Optional[NewMessageLink] = None
|
link: Optional[NewMessageLink] = None
|
||||||
notify: Optional[bool] = True
|
notify: Optional[bool] = True
|
||||||
|
|||||||
Reference in New Issue
Block a user