Compare commits

...

14 Commits

29 changed files with 438 additions and 117 deletions

View File

@@ -8,4 +8,5 @@
- [Миддлварь в хендлерах](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)
- [Миддлварь в роутерах](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=[
builder.as_markup(),
] # Для MAX клавиатура это вложение,
) # поэтому она в списке вложений
) # поэтому она в attachments
@dp.bot_added()
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,
text=f'Привет чат {event.chat.title}!'
)
@@ -68,7 +73,7 @@ async def bot_added(event: BotAdded):
@dp.message_removed()
async def message_removed(event: MessageRemoved):
await event.bot.send_message(
await bot.send_message(
chat_id=event.chat_id,
text='Я всё видел!'
)
@@ -76,7 +81,7 @@ async def message_removed(event: MessageRemoved):
@dp.bot_started()
async def bot_started(event: BotStarted):
await event.bot.send_message(
await bot.send_message(
chat_id=event.chat_id,
text='Привет! Отправь мне /start'
)
@@ -84,9 +89,9 @@ async def bot_started(event: BotStarted):
@dp.chat_title_changed()
async def chat_title_changed(event: ChatTitleChanged):
await event.bot.send_message(
await bot.send_message(
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()
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,
text=f'{event.from_user.first_name} кикнул {event.user.first_name} 😢'
)
@@ -114,7 +126,14 @@ async def user_removed(event: UserRemoved):
@dp.user_added()
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,
text=f'Чат "{event.chat.title}" приветствует вас, {event.user.first_name}!'
)
@@ -122,27 +141,32 @@ async def user_added(event: UserAdded):
@dp.bot_stopped()
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()
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()
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()
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()
async def message_chat_created(event: MessageChatCreated):
await event.bot.send_message(
await bot.send_message(
chat_id=event.chat.chat_id,
text=f'Чат создан! Ссылка: {event.chat.link}'
)

View File

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

View File

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

View File

@@ -3,11 +3,12 @@ from __future__ import annotations
import asyncio
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 aiohttp import ClientConnectorError
from .filters.filter import BaseFilter
from .filters.middleware import BaseMiddleware
from .filters.handler import Handler
@@ -70,6 +71,7 @@ class Dispatcher:
self.contexts: List[MemoryContext] = []
self.routers: List[Router | Dispatcher] = []
self.filters: List[MagicFilter] = []
self.base_filters: List[BaseFilter] = []
self.middlewares: List[BaseMiddleware] = []
self.bot: Optional[Bot] = None
@@ -85,6 +87,7 @@ class Dispatcher:
self.dialog_cleared = Event(update_type=UpdateType.DIALOG_CLEARED, 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_removed = Event(update_type=UpdateType.DIALOG_REMOVED, 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_chat_created = Event(update_type=UpdateType.MESSAGE_CHAT_CREATED, router=self)
@@ -143,6 +146,36 @@ class Dispatcher:
"""
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):
@@ -199,8 +232,29 @@ class Dispatcher:
func_args = handler.func_event.__annotations__.keys()
kwargs_filtered = {k: v for k, v in data.items() if k in func_args}
await handler.func_event(event_object, **kwargs_filtered)
if 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):
@@ -232,6 +286,17 @@ class Dispatcher:
if not filter_attrs(event_object, *router.filters):
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:
if not handler.update_type == event_object.update_type:
@@ -244,9 +309,21 @@ class Dispatcher:
if handler.states:
if current_state not in handler.states:
continue
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):
full_middlewares = self.middlewares + router.middlewares + handler.middlewares
elif isinstance(router, Dispatcher):
@@ -256,7 +333,7 @@ class Dispatcher:
full_middlewares,
functools.partial(self.call_handler, handler)
)
kwargs_filtered = {k: v for k, v in kwargs.items() if k in func_args}
await handler_chain(event_object, kwargs_filtered)

View File

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

View File

@@ -1,7 +1,12 @@
from magic_filter import MagicFilter
from .filter import BaseFilter
F = MagicFilter()
__all__ = [
'BaseFilter'
]
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 magic_filter import F, MagicFilter
from magic_filter import MagicFilter
from ..filters.filter import BaseFilter
from ..filters.middleware import BaseMiddleware
from ..types.command import Command, CommandStart
from ..context.state_machine import State
from ..enums.update import UpdateType
@@ -43,7 +42,8 @@ class Handler:
self.func_event: Callable = func_event
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.middlewares: List[BaseMiddleware] = []
@@ -52,10 +52,10 @@ class Handler:
self.filters.append(arg)
elif isinstance(arg, State):
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):
self.middlewares.append(arg)
elif isinstance(arg, BaseFilter):
self.base_filters.append(arg)
else:
logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при '
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_muted import DialogMuted
from ...types.updates.dialog_unmuted import DialogUnmuted
from ...types.updates.dialog_removed import DialogRemoved
if TYPE_CHECKING:
from ...bot import Bot
@@ -38,7 +39,8 @@ UPDATE_MODEL_MAPPING = {
UpdateType.BOT_STOPPED: BotStopped,
UpdateType.DIALOG_CLEARED: DialogCleared,
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.message_button import MessageButton
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 InputMediaBuffer
__all__ = [
'NewMessageLink',
'PhotoAttachmentRequestPayload',
'DialogUnmuted',
'DialogMuted',

View File

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

View File

@@ -1,10 +1,11 @@
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: Полезная нагрузка кнопки (массив рядов кнопок)
"""
type: Literal['inline_keyboard'] = 'inline_keyboard'
payload: ButtonsPayload
type: Literal[AttachmentType.INLINE_KEYBOARD]

View File

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

View File

@@ -1,4 +1,4 @@
from typing import Optional
from typing import Literal, Optional
from pydantic import BaseModel
@@ -31,4 +31,4 @@ class Image(Attachment):
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
@@ -16,6 +16,6 @@ class Location(Attachment):
longitude (Optional[float]): Долгота.
"""
type: AttachmentType = AttachmentType.LOCATION
type: Literal[AttachmentType.LOCATION]
latitude: 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
@@ -17,7 +17,7 @@ class Share(Attachment):
image_url (Optional[str]): URL изображения для предпросмотра.
"""
type: AttachmentType = AttachmentType.SHARE
type: Literal[AttachmentType.SHARE]
title: Optional[str] = None
description: 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
@@ -16,6 +16,6 @@ class Sticker(Attachment):
height (Optional[int]): Высота стикера в пикселях.
"""
type: AttachmentType = AttachmentType.STICKER
type: Literal[AttachmentType.STICKER]
width: 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 ...enums.attachment import AttachmentType
@@ -61,7 +61,7 @@ class Video(Attachment):
bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется.
"""
type: AttachmentType = AttachmentType.VIDEO
type: Literal[AttachmentType.VIDEO]
token: Optional[str] = None
urls: Optional[VideoUrl] = None
thumbnail: VideoThumbnail

View File

@@ -1,32 +1,5 @@
from typing import Optional
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):
@@ -40,19 +13,4 @@ class BotCommand(BaseModel):
"""
name: str
description: Optional[str] = None
class CommandStart(Command):
"""
Класс для представления команды /start бота.
Attributes:
prefix (str): Префикс команды. По умолчанию '/'.
"""
text = 'start'
def __init__(self, prefix: str = '/'):
self.prefix = prefix
description: Optional[str] = None

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
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.parse_mode import ParseMode
@@ -24,6 +26,19 @@ 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):
@@ -91,18 +106,7 @@ class MessageBody(BaseModel):
seq: int
text: Optional[str] = None
attachments: Optional[
List[
Union[
AttachmentButton,
Audio,
Video,
File,
Image,
Sticker,
Share,
Location
]
]
List[Attachments]
] = Field(default_factory=list) # type: ignore
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]
name = "maxapi"
version = "0.9.3"
version = "0.9.4"
description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX"
readme = "README.md"
requires-python = ">=3.10"
@@ -13,7 +13,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
]
dependencies = [
"aiohttp>=3.8.0",
"aiohttp>=3.12.14",
"magic_filter>=1.0.0",
"pydantic>=1.8.0",
"aiofiles==24.1.0",

View File

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