Compare commits

...

14 Commits

29 changed files with 438 additions and 117 deletions

View File

@@ -9,3 +9,4 @@
- [Вебхуки](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)

View File

@@ -0,0 +1,38 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher
from maxapi.types import MessageCreated, CommandStart, UpdateUnion
from maxapi.filters import BaseFilter
logging.basicConfig(level=logging.INFO)
bot = Bot(token='тут_ваш_токен')
dp = Dispatcher()
class FilterChat(BaseFilter):
"""
Фильтр, который срабатывает только в чате с названием `Test`
"""
async def __call__(self, event: UpdateUnion):
if not event.chat:
return False
return event.chat == 'Test'
@dp.message_created(CommandStart(), FilterChat())
async def custom_data(event: MessageCreated):
await event.message.answer('Привет!')
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -55,12 +55,17 @@ async def hello(event: MessageCreated):
attachments=[ attachments=[
builder.as_markup(), builder.as_markup(),
] # Для MAX клавиатура это вложение, ] # Для MAX клавиатура это вложение,
) # поэтому она в списке вложений ) # поэтому она в attachments
@dp.bot_added() @dp.bot_added()
async def bot_added(event: BotAdded): async def bot_added(event: BotAdded):
await event.bot.send_message(
if not event.chat:
logging.info('Не удалось получить chat, возможно отключен auto_requests!')
return
await bot.send_message(
chat_id=event.chat.id, chat_id=event.chat.id,
text=f'Привет чат {event.chat.title}!' text=f'Привет чат {event.chat.title}!'
) )
@@ -68,7 +73,7 @@ async def bot_added(event: BotAdded):
@dp.message_removed() @dp.message_removed()
async def message_removed(event: MessageRemoved): async def message_removed(event: MessageRemoved):
await event.bot.send_message( await bot.send_message(
chat_id=event.chat_id, chat_id=event.chat_id,
text='Я всё видел!' text='Я всё видел!'
) )
@@ -76,7 +81,7 @@ async def message_removed(event: MessageRemoved):
@dp.bot_started() @dp.bot_started()
async def bot_started(event: BotStarted): async def bot_started(event: BotStarted):
await event.bot.send_message( await bot.send_message(
chat_id=event.chat_id, chat_id=event.chat_id,
text='Привет! Отправь мне /start' text='Привет! Отправь мне /start'
) )
@@ -84,9 +89,9 @@ async def bot_started(event: BotStarted):
@dp.chat_title_changed() @dp.chat_title_changed()
async def chat_title_changed(event: ChatTitleChanged): async def chat_title_changed(event: ChatTitleChanged):
await event.bot.send_message( await bot.send_message(
chat_id=event.chat_id, chat_id=event.chat_id,
text=f'Крутое новое название "{event.chat.title}"!' text=f'Крутое новое название "{event.title}"!'
) )
@@ -106,7 +111,14 @@ async def message_edited(event: MessageEdited):
@dp.user_removed() @dp.user_removed()
async def user_removed(event: UserRemoved): async def user_removed(event: UserRemoved):
await event.bot.send_message(
if not event.from_user:
return await bot.send_message(
chat_id=event.chat_id,
text=f'Неизвестный кикнул {event.user.first_name} 😢'
)
await bot.send_message(
chat_id=event.chat_id, chat_id=event.chat_id,
text=f'{event.from_user.first_name} кикнул {event.user.first_name} 😢' text=f'{event.from_user.first_name} кикнул {event.user.first_name} 😢'
) )
@@ -114,7 +126,14 @@ async def user_removed(event: UserRemoved):
@dp.user_added() @dp.user_added()
async def user_added(event: UserAdded): async def user_added(event: UserAdded):
await event.bot.send_message(
if not event.chat:
return await bot.send_message(
chat_id=event.chat_id,
text=f'Чат приветствует вас, {event.user.first_name}!'
)
await bot.send_message(
chat_id=event.chat_id, chat_id=event.chat_id,
text=f'Чат "{event.chat.title}" приветствует вас, {event.user.first_name}!' text=f'Чат "{event.chat.title}" приветствует вас, {event.user.first_name}!'
) )
@@ -122,27 +141,32 @@ async def user_added(event: UserAdded):
@dp.bot_stopped() @dp.bot_stopped()
async def bot_stopped(event: BotStopped): async def bot_stopped(event: BotStopped):
print(event.from_user.full_name, 'остановил бота') # type: ignore logging.info(event.from_user.full_name, 'остановил бота') # type: ignore
@dp.dialog_cleared() @dp.dialog_cleared()
async def dialog_cleared(event: DialogCleared): async def dialog_cleared(event: DialogCleared):
print(event.from_user.full_name, 'очистил историю чата с ботом') # type: ignore logging.info(event.from_user.full_name, 'очистил историю чата с ботом') # type: ignore
@dp.dialog_muted() @dp.dialog_muted()
async def dialog_muted(event: DialogMuted): async def dialog_muted(event: DialogMuted):
print(event.from_user.full_name, 'отключил оповещения от чата бота до ', event.muted_until_datetime) # type: ignore logging.info(event.from_user.full_name, 'отключил оповещения от чата бота до ', event.muted_until_datetime) # type: ignore
@dp.dialog_unmuted() @dp.dialog_unmuted()
async def dialog_unmuted(event: DialogUnmuted): async def dialog_unmuted(event: DialogUnmuted):
print(event.from_user.full_name, 'включил оповещения от чата бота') # type: ignore logging.info(event.from_user.full_name, 'включил оповещения от чата бота') # type: ignore
@dp.dialog_unmuted()
async def dialog_removed(event: DialogUnmuted):
logging.info(event.from_user.full_name, 'удалил диалог с ботом') # type: ignore
@dp.message_chat_created() @dp.message_chat_created()
async def message_chat_created(event: MessageChatCreated): async def message_chat_created(event: MessageChatCreated):
await event.bot.send_message( await bot.send_message(
chat_id=event.chat.chat_id, chat_id=event.chat.chat_id,
text=f'Чат создан! Ссылка: {event.chat.link}' text=f'Чат создан! Ссылка: {event.chat.link}'
) )

View File

@@ -54,8 +54,8 @@ async def builder(event: MessageCreated):
chat_description='Test desc' chat_description='Test desc'
), ),
LinkButton( LinkButton(
text="Канал разработчика", text="Документация MAX",
url="https://t.me/loveapples_dev" url="https://dev.max.ru/docs"
), ),
) )
@@ -99,8 +99,8 @@ async def payload(event: MessageCreated):
chat_description='Test desc' chat_description='Test desc'
), ),
LinkButton( LinkButton(
text="Канал разработчика", text="Документация MAX",
url="https://t.me/loveapples_dev" url="https://dev.max.ru/docs"
), ),
], ],
[ [

View File

@@ -32,9 +32,8 @@ async def custom_data(event: MessageCreated, custom_data: str):
async def main(): async def main():
dp.middlewares = [ dp.middleware(CustomDataForRouterMiddleware())
CustomDataForRouterMiddleware()
]
await dp.start_polling(bot) await dp.start_polling(bot)

View File

@@ -50,6 +50,7 @@ from .methods.subscribe_webhook import SubscribeWebhook
from .methods.types.subscribed import Subscribed from .methods.types.subscribed import Subscribed
from .methods.types.unsubscribed import Unsubscribed from .methods.types.unsubscribed import Unsubscribed
from .methods.unsubscribe_webhook import UnsubscribeWebhook from .methods.unsubscribe_webhook import UnsubscribeWebhook
from .methods.get_message import GetMessage
if TYPE_CHECKING: if TYPE_CHECKING:
from .types.attachments.attachment import Attachment from .types.attachments.attachment import Attachment
@@ -300,7 +301,7 @@ class Bot(BaseConnection):
async def get_message( async def get_message(
self, self,
message_id: str message_id: str
) -> Messages: ) -> Message:
""" """
Получает одно сообщение по ID. Получает одно сообщение по ID.
@@ -310,13 +311,15 @@ class Bot(BaseConnection):
:return: Объект сообщения :return: Объект сообщения
""" """
return await self.get_messages( return await GetMessage(
message_ids=[message_id] bot=self,
) message_id=message_id
).fetch()
async def get_me(self) -> User: async def get_me(self) -> User:
""" """
https://dev.max.ru/docs-api/methods/GET/me\n
Получает информацию о текущем боте. Получает информацию о текущем боте.
:return: Объект пользователя бота :return: Объект пользователя бота

View File

@@ -3,11 +3,12 @@ from __future__ import annotations
import asyncio import asyncio
import functools import functools
from typing import Any, Awaitable, Callable, Dict, List, TYPE_CHECKING, Optional from typing import Any, Awaitable, Callable, Dict, List, TYPE_CHECKING, Literal, Optional
from asyncio.exceptions import TimeoutError as AsyncioTimeoutError from asyncio.exceptions import TimeoutError as AsyncioTimeoutError
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
from .filters.filter import BaseFilter
from .filters.middleware import BaseMiddleware from .filters.middleware import BaseMiddleware
from .filters.handler import Handler from .filters.handler import Handler
@@ -70,6 +71,7 @@ class Dispatcher:
self.contexts: List[MemoryContext] = [] self.contexts: List[MemoryContext] = []
self.routers: List[Router | Dispatcher] = [] self.routers: List[Router | Dispatcher] = []
self.filters: List[MagicFilter] = [] self.filters: List[MagicFilter] = []
self.base_filters: List[BaseFilter] = []
self.middlewares: List[BaseMiddleware] = [] self.middlewares: List[BaseMiddleware] = []
self.bot: Optional[Bot] = None self.bot: Optional[Bot] = None
@@ -85,6 +87,7 @@ class Dispatcher:
self.dialog_cleared = Event(update_type=UpdateType.DIALOG_CLEARED, router=self) self.dialog_cleared = Event(update_type=UpdateType.DIALOG_CLEARED, router=self)
self.dialog_muted = Event(update_type=UpdateType.DIALOG_MUTED, router=self) self.dialog_muted = Event(update_type=UpdateType.DIALOG_MUTED, router=self)
self.dialog_unmuted = Event(update_type=UpdateType.DIALOG_UNMUTED, router=self) self.dialog_unmuted = Event(update_type=UpdateType.DIALOG_UNMUTED, router=self)
self.dialog_removed = Event(update_type=UpdateType.DIALOG_REMOVED, router=self)
self.chat_title_changed = Event(update_type=UpdateType.CHAT_TITLE_CHANGED, router=self) self.chat_title_changed = Event(update_type=UpdateType.CHAT_TITLE_CHANGED, router=self)
self.message_callback = Event(update_type=UpdateType.MESSAGE_CALLBACK, router=self) self.message_callback = Event(update_type=UpdateType.MESSAGE_CALLBACK, router=self)
self.message_chat_created = Event(update_type=UpdateType.MESSAGE_CHAT_CREATED, router=self) self.message_chat_created = Event(update_type=UpdateType.MESSAGE_CHAT_CREATED, router=self)
@@ -144,6 +147,36 @@ class Dispatcher:
self.routers += [r for r in routers] self.routers += [r for r in routers]
def outer_middleware(self, middleware: BaseMiddleware) -> None:
"""
Добавляет Middleware на первое место в списке
:param: middleware: Middleware
"""
self.middlewares.insert(0, middleware)
def middleware(self, middleware: BaseMiddleware) -> None:
"""
Добавляет Middleware в список
:param middleware: Middleware
"""
self.middlewares.append(middleware)
def filter(self, base_filter: BaseFilter) -> None:
"""
Добавляет фильтр в список
:param base_filter: Фильтр
"""
self.base_filters.append(base_filter)
async def __ready(self, bot: Bot): async def __ready(self, bot: Bot):
""" """
@@ -199,8 +232,29 @@ class Dispatcher:
func_args = handler.func_event.__annotations__.keys() func_args = handler.func_event.__annotations__.keys()
kwargs_filtered = {k: v for k, v in data.items() if k in func_args} kwargs_filtered = {k: v for k, v in data.items() if k in func_args}
if kwargs_filtered:
await handler.func_event(event_object, **kwargs_filtered) await handler.func_event(event_object, **kwargs_filtered)
else:
await handler.func_event(event_object)
async def process_base_filters(
self,
event: UpdateUnion,
filters: List[BaseFilter]
) -> Optional[Dict[str, Any]] | Literal[False]:
data = {}
for _filter in filters:
result = await _filter(event)
if isinstance(result, dict):
data.update(result)
elif not result:
return result
return data
async def handle(self, event_object: UpdateUnion): async def handle(self, event_object: UpdateUnion):
@@ -232,6 +286,17 @@ class Dispatcher:
if not filter_attrs(event_object, *router.filters): if not filter_attrs(event_object, *router.filters):
continue continue
result_router_filter = await self.process_base_filters(
event=event_object,
filters=router.base_filters
)
if isinstance(result_router_filter, dict):
kwargs.update(result_router_filter)
elif not result_router_filter:
continue
for handler in router.event_handlers: for handler in router.event_handlers:
if not handler.update_type == event_object.update_type: if not handler.update_type == event_object.update_type:
@@ -247,6 +312,18 @@ class Dispatcher:
func_args = handler.func_event.__annotations__.keys() func_args = handler.func_event.__annotations__.keys()
if handler.base_filters:
result_filter = await self.process_base_filters(
event=event_object,
filters=handler.base_filters
)
if isinstance(result_filter, dict):
kwargs.update(result_filter)
elif not result_filter:
continue
if isinstance(router, Router): if isinstance(router, Router):
full_middlewares = self.middlewares + router.middlewares + handler.middlewares full_middlewares = self.middlewares + router.middlewares + handler.middlewares
elif isinstance(router, Dispatcher): elif isinstance(router, Dispatcher):

View File

@@ -24,6 +24,7 @@ class UpdateType(str, Enum):
DIALOG_CLEARED = 'dialog_cleared' DIALOG_CLEARED = 'dialog_cleared'
DIALOG_MUTED = 'dialog_muted' DIALOG_MUTED = 'dialog_muted'
DIALOG_UNMUTED = 'dialog_unmuted' DIALOG_UNMUTED = 'dialog_unmuted'
DIALOG_REMOVED = 'dialog_removed'
# Для начинки диспатчера # Для начинки диспатчера
ON_STARTED = 'on_started' ON_STARTED = 'on_started'

View File

@@ -1,7 +1,12 @@
from magic_filter import MagicFilter from magic_filter import MagicFilter
from .filter import BaseFilter
F = MagicFilter() F = MagicFilter()
__all__ = [
'BaseFilter'
]
def filter_attrs(obj: object, *filters: MagicFilter) -> bool: def filter_attrs(obj: object, *filters: MagicFilter) -> bool:
""" """

116
maxapi/filters/command.py Normal file
View File

@@ -0,0 +1,116 @@
from typing import List, Tuple
from ..types.updates import UpdateUnion
from ..filters.filter import BaseFilter
from ..types.updates.message_created import MessageCreated
class Command(BaseFilter):
"""
Фильтр сообщений на соответствие команде.
Args:
commands (str | list[str]): Ожидаемая команда или список команд без префикса.
prefix (str, optional): Префикс команды (по умолчанию '/').
check_case (bool, optional): Учитывать регистр при сравнении (по умолчанию False).
Attributes:
commands (list[str]): Список команд без префикса.
prefix (str): Префикс команды.
check_case (bool): Флаг чувствительности к регистру.
"""
def __init__(self, commands: str | List[str], prefix: str = '/', check_case: bool = False):
"""
Инициализация фильтра команд.
"""
if isinstance(commands, str):
self.commands = [commands]
else:
self.commands = commands
self.prefix = prefix
self.check_case = check_case
if not check_case:
self.commands = [cmd.lower() for cmd in self.commands]
def parse_command(self, text: str) -> Tuple[str, List[str]]:
"""
Извлекает команду из текста.
Args:
text (str): Текст сообщения.
Returns:
Optional[str]: Найденная команда с префиксом, либо None.
"""
args = text.split()
first = args[0]
if not first.startswith(self.prefix):
return '', []
return first[len(self.prefix):], args
async def __call__(self, event: UpdateUnion):
"""
Проверяет, соответствует ли сообщение заданной(ым) команде(ам).
Args:
event (MessageCreated): Событие сообщения.
Returns:
bool: True, если команда совпадает, иначе False.
"""
if not isinstance(event, MessageCreated):
return False
text = event.message.body.text
if not text:
return False
parsed_command, args = self.parse_command(text)
if not parsed_command:
return False
if not self.check_case:
if parsed_command.lower() in [commands.lower() for commands in self.commands]:
return {'args': args}
else:
return False
if parsed_command in self.commands:
return {'args': args}
return False
class CommandStart(Command):
"""
Фильтр для команды /start.
Args:
prefix (str, optional): Префикс команды (по умолчанию '/').
check_case (bool, optional): Учитывать регистр (по умолчанию False).
"""
def __init__(self, prefix = '/', check_case = False):
super().__init__(
'start',
prefix,
check_case
)
async def __call__(self, event):
return await super().__call__(event)

10
maxapi/filters/filter.py Normal file
View File

@@ -0,0 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..types.updates import UpdateUnion
class BaseFilter:
async def __call__(self, event: UpdateUnion) -> bool | dict:
return True

View File

@@ -1,11 +1,10 @@
from typing import Callable, List, Optional from typing import Callable, List, Optional
from magic_filter import F, MagicFilter from magic_filter import MagicFilter
from ..filters.filter import BaseFilter
from ..filters.middleware import BaseMiddleware from ..filters.middleware import BaseMiddleware
from ..types.command import Command, CommandStart
from ..context.state_machine import State from ..context.state_machine import State
from ..enums.update import UpdateType from ..enums.update import UpdateType
@@ -43,7 +42,8 @@ class Handler:
self.func_event: Callable = func_event self.func_event: Callable = func_event
self.update_type: UpdateType = update_type self.update_type: UpdateType = update_type
self.filters = [] self.filters: Optional[List[MagicFilter]] = []
self.base_filters: Optional[List[BaseFilter]] = []
self.states: Optional[List[State]] = [] self.states: Optional[List[State]] = []
self.middlewares: List[BaseMiddleware] = [] self.middlewares: List[BaseMiddleware] = []
@@ -52,10 +52,10 @@ class Handler:
self.filters.append(arg) self.filters.append(arg)
elif isinstance(arg, State): elif isinstance(arg, State):
self.states.append(arg) self.states.append(arg)
elif isinstance(arg, (Command, CommandStart)):
self.filters.insert(0, F.message.body.text.split()[0] == arg.command)
elif isinstance(arg, BaseMiddleware): elif isinstance(arg, BaseMiddleware):
self.middlewares.append(arg) self.middlewares.append(arg)
elif isinstance(arg, BaseFilter):
self.base_filters.append(arg)
else: else:
logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при ' logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при '
f'регистрации функции `{func_event.__name__}`') f'регистрации функции `{func_event.__name__}`')

View File

@@ -0,0 +1,49 @@
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional, Union
from ..types.message import Message
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class GetMessage(BaseConnection):
"""
Класс для получения сообщения.
Args:
bot (Bot): Экземпляр бота для выполнения запроса.
message_id (str, optional): ID сообщения (mid), чтобы получить одно сообщение в чате.
"""
def __init__(
self,
bot: 'Bot',
message_id: Optional[str] = None,
):
self.bot = bot
self.message_id = message_id
async def fetch(self) -> Message:
"""
Выполняет GET-запрос для получения сообщения.
Returns:
Message: Объект с полученным сообщением.
"""
if self.bot is None:
raise RuntimeError('Bot не инициализирован')
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.MESSAGES + '/' + self.message_id,
model=Message,
params=self.bot.params
)

View File

@@ -18,6 +18,7 @@ from ...types.updates.user_removed import UserRemoved
from ...types.updates.dialog_cleared import DialogCleared from ...types.updates.dialog_cleared import DialogCleared
from ...types.updates.dialog_muted import DialogMuted from ...types.updates.dialog_muted import DialogMuted
from ...types.updates.dialog_unmuted import DialogUnmuted from ...types.updates.dialog_unmuted import DialogUnmuted
from ...types.updates.dialog_removed import DialogRemoved
if TYPE_CHECKING: if TYPE_CHECKING:
from ...bot import Bot from ...bot import Bot
@@ -38,7 +39,8 @@ UPDATE_MODEL_MAPPING = {
UpdateType.BOT_STOPPED: BotStopped, UpdateType.BOT_STOPPED: BotStopped,
UpdateType.DIALOG_CLEARED: DialogCleared, UpdateType.DIALOG_CLEARED: DialogCleared,
UpdateType.DIALOG_MUTED: DialogMuted, UpdateType.DIALOG_MUTED: DialogMuted,
UpdateType.DIALOG_UNMUTED: DialogUnmuted UpdateType.DIALOG_UNMUTED: DialogUnmuted,
UpdateType.DIALOG_REMOVED: DialogRemoved
} }

View File

@@ -29,14 +29,16 @@ from ..types.attachments.buttons.open_app_button import OpenAppButton
from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton
from ..types.attachments.buttons.message_button import MessageButton from ..types.attachments.buttons.message_button import MessageButton
from ..types.attachments.image import PhotoAttachmentRequestPayload from ..types.attachments.image import PhotoAttachmentRequestPayload
from ..types.message import Message from ..types.message import Message, NewMessageLink
from ..types.command import Command, BotCommand, CommandStart from ..filters.command import Command, CommandStart
from ..types.command import BotCommand
from .input_media import InputMedia from .input_media import InputMedia
from .input_media import InputMediaBuffer from .input_media import InputMediaBuffer
__all__ = [ __all__ = [
'NewMessageLink',
'PhotoAttachmentRequestPayload', 'PhotoAttachmentRequestPayload',
'DialogUnmuted', 'DialogUnmuted',
'DialogMuted', 'DialogMuted',

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Literal, Optional
from ...enums.attachment import AttachmentType from ...enums.attachment import AttachmentType
@@ -15,5 +15,5 @@ class Audio(Attachment):
transcription (Optional[str]): Транскрипция аудио (если есть). transcription (Optional[str]): Транскрипция аудио (если есть).
""" """
type: AttachmentType = AttachmentType.AUDIO type: Literal[AttachmentType.AUDIO]
transcription: Optional[str] = None transcription: Optional[str] = None

View File

@@ -1,10 +1,11 @@
from typing import Literal from typing import Literal
from pydantic import BaseModel
from ..attachment import ButtonsPayload from ....enums.attachment import AttachmentType
from ..attachment import Attachment
class AttachmentButton(BaseModel): class AttachmentButton(Attachment):
""" """
Модель кнопки вложения для сообщения. Модель кнопки вложения для сообщения.
@@ -14,5 +15,4 @@ class AttachmentButton(BaseModel):
payload: Полезная нагрузка кнопки (массив рядов кнопок) payload: Полезная нагрузка кнопки (массив рядов кнопок)
""" """
type: Literal['inline_keyboard'] = 'inline_keyboard' type: Literal[AttachmentType.INLINE_KEYBOARD]
payload: ButtonsPayload

View File

@@ -1,3 +1,4 @@
from typing import Literal
from ...enums.attachment import AttachmentType from ...enums.attachment import AttachmentType
from .attachment import Attachment from .attachment import Attachment
@@ -12,4 +13,4 @@ class Contact(Attachment):
type (Literal['contact']): Тип вложения, всегда 'contact'. type (Literal['contact']): Тип вложения, всегда 'contact'.
""" """
type: AttachmentType = AttachmentType.CONTACT type: Literal[AttachmentType.CONTACT]

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Literal, Optional
from ...enums.attachment import AttachmentType from ...enums.attachment import AttachmentType
@@ -16,6 +16,6 @@ class File(Attachment):
size (Optional[int]): Размер файла в байтах. size (Optional[int]): Размер файла в байтах.
""" """
type: AttachmentType = AttachmentType.FILE type: Literal[AttachmentType.FILE]
filename: Optional[str] = None filename: Optional[str] = None
size: Optional[int] = None size: Optional[int] = None

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Literal, Optional
from pydantic import BaseModel from pydantic import BaseModel
@@ -31,4 +31,4 @@ class Image(Attachment):
type (Literal['image']): Тип вложения, всегда 'image'. type (Literal['image']): Тип вложения, всегда 'image'.
""" """
type: AttachmentType = AttachmentType.IMAGE type: Literal[AttachmentType.IMAGE]

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Literal, Optional
from ...enums.attachment import AttachmentType from ...enums.attachment import AttachmentType
@@ -16,6 +16,6 @@ class Location(Attachment):
longitude (Optional[float]): Долгота. longitude (Optional[float]): Долгота.
""" """
type: AttachmentType = AttachmentType.LOCATION type: Literal[AttachmentType.LOCATION]
latitude: Optional[float] = None latitude: Optional[float] = None
longitude: Optional[float] = None longitude: Optional[float] = None

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Literal, Optional
from ...enums.attachment import AttachmentType from ...enums.attachment import AttachmentType
@@ -17,7 +17,7 @@ class Share(Attachment):
image_url (Optional[str]): URL изображения для предпросмотра. image_url (Optional[str]): URL изображения для предпросмотра.
""" """
type: AttachmentType = AttachmentType.SHARE type: Literal[AttachmentType.SHARE]
title: Optional[str] = None title: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
image_url: Optional[str] = None image_url: Optional[str] = None

View File

@@ -1,4 +1,4 @@
from typing import Optional from typing import Literal, Optional
from ...enums.attachment import AttachmentType from ...enums.attachment import AttachmentType
@@ -16,6 +16,6 @@ class Sticker(Attachment):
height (Optional[int]): Высота стикера в пикселях. height (Optional[int]): Высота стикера в пикселях.
""" """
type: AttachmentType = AttachmentType.STICKER type: Literal[AttachmentType.STICKER]
width: Optional[int] = None width: Optional[int] = None
height: Optional[int] = None height: Optional[int] = None

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Literal, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from ...enums.attachment import AttachmentType from ...enums.attachment import AttachmentType
@@ -61,7 +61,7 @@ class Video(Attachment):
bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется. bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется.
""" """
type: AttachmentType = AttachmentType.VIDEO type: Literal[AttachmentType.VIDEO]
token: Optional[str] = None token: Optional[str] = None
urls: Optional[VideoUrl] = None urls: Optional[VideoUrl] = None
thumbnail: VideoThumbnail thumbnail: VideoThumbnail

View File

@@ -2,33 +2,6 @@ from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
class Command:
"""
Класс для представления команды бота.
Attributes:
text (str): Текст команды без префикса.
prefix (str): Префикс команды. По умолчанию '/'.
"""
def __init__(self, text: str, prefix: str = '/'):
self.text = text
self.prefix = prefix
@property
def command(self):
"""
Возвращает полную команду с префиксом.
Returns:
str: Команда, состоящая из префикса и текста.
"""
return self.prefix + self.text
class BotCommand(BaseModel): class BotCommand(BaseModel):
""" """
@@ -41,18 +14,3 @@ class BotCommand(BaseModel):
name: str name: str
description: Optional[str] = None description: Optional[str] = None
class CommandStart(Command):
"""
Класс для представления команды /start бота.
Attributes:
prefix (str): Префикс команды. По умолчанию '/'.
"""
text = 'start'
def __init__(self, prefix: str = '/'):
self.prefix = prefix

View File

@@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Any, Optional, List, Union, TYPE_CHECKING from typing import Annotated, Any, Optional, List, Union, TYPE_CHECKING
from ..types.attachments.contact import Contact
from ..enums.text_style import TextStyle from ..enums.text_style import TextStyle
from ..enums.parse_mode import ParseMode from ..enums.parse_mode import ParseMode
@@ -26,6 +28,19 @@ if TYPE_CHECKING:
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):
""" """
@@ -91,18 +106,7 @@ class MessageBody(BaseModel):
seq: int seq: int
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
markup: Optional[ markup: Optional[

View File

@@ -0,0 +1,30 @@
from typing import TYPE_CHECKING, Optional
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class DialogRemoved(Update):
"""
Обновление, сигнализирующее об удалении диалога с ботом.
Attributes:
chat_id (int): Идентификатор чата.
user (User): Пользователь (бот).
user_locale (Optional[str]): Локаль пользователя.
"""
chat_id: int
user: User
user_locale: Optional[str] = None
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "maxapi" name = "maxapi"
version = "0.9.3" version = "0.9.4"
description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX" description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -13,7 +13,7 @@ classifiers = [
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
] ]
dependencies = [ dependencies = [
"aiohttp>=3.8.0", "aiohttp>=3.12.14",
"magic_filter>=1.0.0", "magic_filter>=1.0.0",
"pydantic>=1.8.0", "pydantic>=1.8.0",
"aiofiles==24.1.0", "aiofiles==24.1.0",

View File

@@ -10,6 +10,7 @@
| `dialog_cleared` | Пользователь очистил историю диалога с ботом | | `dialog_cleared` | Пользователь очистил историю диалога с ботом |
| `dialog_muted` | Пользователь отключил оповещения от чата бота | | `dialog_muted` | Пользователь отключил оповещения от чата бота |
| `dialog_unmuted` | Пользователь включил оповещения от чата бота | | `dialog_unmuted` | Пользователь включил оповещения от чата бота |
| `dialog_removed` | Пользователь удалил диалог с ботом |
| `chat_title_changed` | Изменено название чата | | `chat_title_changed` | Изменено название чата |
| `message_callback` | Пользователь нажал на callback-кнопку (inline button) | | `message_callback` | Пользователь нажал на callback-кнопку (inline button) |
| `message_chat_created`| Срабатывает когда пользователь нажал на кнопку с действием "Создать чат" (работает некорректно со стороны API MAX, ждем исправлений) | | `message_chat_created`| Срабатывает когда пользователь нажал на кнопку с действием "Создать чат" (работает некорректно со стороны API MAX, ждем исправлений) |