Compare commits

...

23 Commits

Author SHA1 Message Date
e8b7c71d25 Поправлены Wiki 2025-07-19 17:03:14 +03:00
aab87e16b2 Поправлен RequestGeoLocationButton 2025-07-19 17:02:59 +03:00
df383665dc Доработан RequestContact 2025-07-19 17:02:47 +03:00
bd06b33343 Добавлен OpenAppButton 2025-07-19 17:02:37 +03:00
7b70d1de18 Добавлен MessageButton 2025-07-19 17:02:21 +03:00
5f2c908da4 Поправлен LinkButton 2025-07-19 17:02:05 +03:00
1abbc16cc8 Изменены импорты 2025-07-19 17:01:50 +03:00
7b61ceaa58 Добавлен .pack() в ButtonsPayload для удобства 2025-07-19 17:01:42 +03:00
37f7907398 Добавлен CommandStart 2025-07-19 17:01:08 +03:00
9dab5f97fb Изменены импорты 2025-07-19 17:00:45 +03:00
0a3d1ca327 Добавлен CommandStart 2025-07-19 17:00:29 +03:00
93043835d1 Добавлены типы кнопок: message, open_app 2025-07-19 17:00:19 +03:00
3548d0558f Добавлено присваивание @property .me для bot 2025-07-19 16:59:57 +03:00
5ae4de6816 Добавлен @property .me (присваивается при запуске бота) 2025-07-19 16:59:25 +03:00
d77288ea07 Пример со всеми кнопками для клавиатуры 2025-07-19 16:58:42 +03:00
30cf778504 0.8.9 2025-07-19 13:43:34 +03:00
dd1bdb5e37 Добавлена загрузка файлов из буфера, InputMediaBuffer 2025-07-19 13:41:03 +03:00
b20a46de24 Добавлена загрузка файлов из буфера, InputMediaBuffer 2025-07-19 13:39:07 +03:00
de05e7931a 0.8.8 2025-07-17 02:05:48 +03:00
a8727c71e9 Обновлен блок "Документация" 2025-07-15 18:32:54 +03:00
b6c11cd28a Добавлена вики 2025-07-15 18:29:54 +03:00
12f64f0805 Поправлены докстринги 2025-07-15 12:11:00 +03:00
3df4dd21b4 Поправлены докстринги 2025-07-15 12:09:31 +03:00
23 changed files with 1022 additions and 95 deletions

View File

@@ -56,7 +56,7 @@ if __name__ == '__main__':
## 📚 Документация ## 📚 Документация
В разработке... [Тут](https://github.com/love-apples/maxapi/wiki)
--- ---

153
examples/keyboard/main.py Normal file
View File

@@ -0,0 +1,153 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher
# Кнопки
from maxapi.types import (
ChatButton,
LinkButton,
CallbackButton,
RequestGeoLocationButton,
MessageButton,
ButtonsPayload, # Для постройки клавиатуры без InlineKeyboardBuilder
RequestContactButton,
OpenAppButton,
)
from maxapi.types import (
MessageCreated,
MessageCallback,
MessageChatCreated,
CommandStart,
Command
)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
logging.basicConfig(level=logging.INFO)
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
@dp.message_created(CommandStart())
async def echo(event: MessageCreated):
await event.message.answer(
(
'Привет! Мои команды:\n\n'
'/builder - Клавиатура из InlineKeyboardBuilder\n'
'/pyaload - Клавиатура из pydantic моделей\n'
)
)
@dp.message_created(Command('builder'))
async def echo(event: MessageCreated):
builder = InlineKeyboardBuilder()
builder.row(
ChatButton(
text="Создать чат",
chat_title='Test',
chat_description='Test desc'
),
LinkButton(
text="Канал разработчика",
url="https://t.me/loveapples_dev"
),
)
builder.row(
RequestGeoLocationButton(text="Геолокация"),
MessageButton(text="Сообщение"),
)
builder.row(
RequestContactButton(text="Контакт"),
OpenAppButton(
text="Приложение",
web_app=event.bot.me.username,
contact_id=event.bot.me.user_id
),
)
builder.row(
CallbackButton(
text='Callback',
payload='test',
)
)
await event.message.answer(
text='Клавиатура из InlineKeyboardBuilder',
attachments=[
builder.as_markup()
])
@dp.message_created(Command('payload'))
async def echo(event: MessageCreated):
buttons = [
[
# кнопку типа "chat" убрали из документации,
# возможны баги
ChatButton(
text="Создать чат",
chat_title='Test',
chat_description='Test desc'
),
LinkButton(
text="Канал разработчика",
url="https://t.me/loveapples_dev"
),
],
[
RequestGeoLocationButton(text="Геолокация"),
MessageButton(text="Сообщение"),
],
[
RequestContactButton(text="Контакт"),
OpenAppButton(
text="Приложение",
web_app=event.bot.me.username,
contact_id=event.bot.me.user_id
),
],
[
CallbackButton(
text='Callback',
payload='test',
)
]
]
buttons_payload = ButtonsPayload(buttons=buttons).pack()
await event.message.answer(
text='Клавиатура из pydantic моделей',
attachments=[
buttons_payload
])
@dp.message_chat_created()
async def callback(obj: MessageChatCreated):
await obj.bot.send_message(
chat_id=obj.chat.chat_id,
text=f'Чат создан! Ссылка: {obj.chat.link}'
)
@dp.message_callback()
async def callback(callback: MessageCallback):
await callback.message.answer('Вы нажали на Callback!')
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -82,7 +82,8 @@ class Bot(BaseConnection):
auto_requests: bool = True, auto_requests: bool = True,
): ):
"""Инициализирует экземпляр бота с указанным токеном. """
Инициализирует экземпляр бота с указанным токеном.
:param token: Токен доступа к API бота :param token: Токен доступа к API бота
:param parse_mode: Форматирование по умолчанию :param parse_mode: Форматирование по умолчанию
@@ -103,6 +104,12 @@ class Bot(BaseConnection):
self.notify = notify self.notify = notify
self.auto_requests = auto_requests self.auto_requests = auto_requests
self._me: User = None
@property
def me(self):
return self._me
def _resolve_notify(self, notify: Optional[bool]) -> Optional[bool]: def _resolve_notify(self, notify: Optional[bool]) -> Optional[bool]:
return notify if notify is not None else self.notify return notify if notify is not None else self.notify
@@ -120,7 +127,8 @@ class Bot(BaseConnection):
parse_mode: Optional[ParseMode] = None parse_mode: Optional[ParseMode] = None
) -> SendedMessage: ) -> SendedMessage:
"""Отправляет сообщение в чат или пользователю. """
Отправляет сообщение в чат или пользователю.
:param chat_id: ID чата для отправки (обязателен, если не указан user_id) :param chat_id: ID чата для отправки (обязателен, если не указан user_id)
:param user_id: ID пользователя для отправки (обязателен, если не указан chat_id) :param user_id: ID пользователя для отправки (обязателен, если не указан chat_id)
@@ -150,7 +158,8 @@ class Bot(BaseConnection):
action: SenderAction = SenderAction.TYPING_ON action: SenderAction = SenderAction.TYPING_ON
) -> SendedAction: ) -> SendedAction:
"""Отправляет действие в чат (например, "печатает"). """
Отправляет действие в чат (например, "печатает").
:param chat_id: ID чата для отправки действия :param chat_id: ID чата для отправки действия
:param action: Тип действия (по умолчанию SenderAction.TYPING_ON) :param action: Тип действия (по умолчанию SenderAction.TYPING_ON)
@@ -174,7 +183,8 @@ class Bot(BaseConnection):
parse_mode: Optional[ParseMode] = None parse_mode: Optional[ParseMode] = None
) -> EditedMessage: ) -> EditedMessage:
"""Редактирует существующее сообщение. """
Редактирует существующее сообщение.
:param message_id: ID сообщения для редактирования :param message_id: ID сообщения для редактирования
:param text: Новый текст сообщения :param text: Новый текст сообщения
@@ -201,7 +211,8 @@ class Bot(BaseConnection):
message_id: str message_id: str
) -> DeletedMessage: ) -> DeletedMessage:
"""Удаляет сообщение. """
Удаляет сообщение.
:param message_id: ID сообщения для удаления :param message_id: ID сообщения для удаления
@@ -218,7 +229,8 @@ class Bot(BaseConnection):
chat_id: int chat_id: int
) -> DeletedChat: ) -> DeletedChat:
"""Удаляет чат. """
Удаляет чат.
:param chat_id: ID чата для удаления :param chat_id: ID чата для удаления
@@ -239,7 +251,8 @@ class Bot(BaseConnection):
count: int = 50, count: int = 50,
) -> Messages: ) -> Messages:
"""Получает сообщения из чата. """
Получает сообщения из чата.
:param chat_id: ID чата (обязателен, если не указаны message_ids) :param chat_id: ID чата (обязателен, если не указаны message_ids)
:param message_ids: Список ID сообщений для получения :param message_ids: Список ID сообщений для получения
@@ -264,7 +277,8 @@ class Bot(BaseConnection):
message_id: str message_id: str
) -> Messages: ) -> Messages:
"""Получает одно сообщение по ID. """
Получает одно сообщение по ID.
:param message_id: ID сообщения :param message_id: ID сообщения
@@ -277,7 +291,8 @@ class Bot(BaseConnection):
async def get_me(self) -> User: async def get_me(self) -> User:
"""Получает информацию о текущем боте. """
Получает информацию о текущем боте.
:return: Объект пользователя бота :return: Объект пользователя бота
""" """
@@ -289,7 +304,8 @@ class Bot(BaseConnection):
chat_id: int chat_id: int
) -> GettedPin: ) -> GettedPin:
"""Получает закрепленное сообщение в чате. """
Получает закрепленное сообщение в чате.
:param chat_id: ID чата :param chat_id: ID чата
@@ -309,7 +325,8 @@ class Bot(BaseConnection):
photo: Dict[str, Any] = None photo: Dict[str, Any] = None
) -> User: ) -> User:
"""Изменяет информацию о боте. """
Изменяет информацию о боте.
:param name: Новое имя бота :param name: Новое имя бота
:param description: Новое описание бота :param description: Новое описание бота
@@ -333,7 +350,8 @@ class Bot(BaseConnection):
marker: int = None marker: int = None
) -> Chats: ) -> Chats:
"""Получает список чатов бота. """
Получает список чатов бота.
:param count: Количество чатов (по умолчанию 50) :param count: Количество чатов (по умолчанию 50)
:param marker: Маркер для пагинации :param marker: Маркер для пагинации
@@ -352,7 +370,8 @@ class Bot(BaseConnection):
link: str link: str
) -> Chat: ) -> Chat:
"""Получает чат по ссылке. """
Получает чат по ссылке.
:param link: Ссылка на чат :param link: Ссылка на чат
@@ -366,7 +385,8 @@ class Bot(BaseConnection):
id: int id: int
) -> Chat: ) -> Chat:
"""Получает чат по ID. """
Получает чат по ID.
:param id: ID чата :param id: ID чата
@@ -384,7 +404,8 @@ class Bot(BaseConnection):
notify: Optional[bool] = None, notify: Optional[bool] = None,
) -> Chat: ) -> Chat:
"""Редактирует параметры чата. """
Редактирует параметры чата.
:param chat_id: ID чата :param chat_id: ID чата
:param icon: Данные иконки чата :param icon: Данные иконки чата
@@ -409,7 +430,8 @@ class Bot(BaseConnection):
video_token: str video_token: str
) -> Video: ) -> Video:
"""Получает видео по токену. """
Получает видео по токену.
:param video_token: Токен видео :param video_token: Токен видео
@@ -428,7 +450,8 @@ class Bot(BaseConnection):
notification: str = None notification: str = None
) -> SendedCallback: ) -> SendedCallback:
"""Отправляет callback ответ. """
Отправляет callback ответ.
:param callback_id: ID callback :param callback_id: ID callback
:param message: Сообщение для отправки :param message: Сообщение для отправки
@@ -451,7 +474,8 @@ class Bot(BaseConnection):
notify: Optional[bool] = None notify: Optional[bool] = None
) -> PinnedMessage: ) -> PinnedMessage:
"""Закрепляет сообщение в чате. """
Закрепляет сообщение в чате.
:param chat_id: ID чата :param chat_id: ID чата
:param message_id: ID сообщения :param message_id: ID сообщения
@@ -472,7 +496,8 @@ class Bot(BaseConnection):
chat_id: int, chat_id: int,
) -> DeletedPinMessage: ) -> DeletedPinMessage:
"""Удаляет закрепленное сообщение в чате. """
Удаляет закрепленное сообщение в чате.
:param chat_id: ID чата :param chat_id: ID чата
@@ -489,7 +514,8 @@ class Bot(BaseConnection):
chat_id: int, chat_id: int,
) -> ChatMember: ) -> ChatMember:
"""Получает информацию о боте в конкретном чате. """
Получает информацию о боте в конкретном чате.
:param chat_id: ID чата :param chat_id: ID чата
@@ -506,7 +532,8 @@ class Bot(BaseConnection):
chat_id: int, chat_id: int,
) -> DeletedBotFromChat: ) -> DeletedBotFromChat:
"""Удаляет бота из чата. """
Удаляет бота из чата.
:param chat_id: ID чата :param chat_id: ID чата
@@ -523,7 +550,8 @@ class Bot(BaseConnection):
chat_id: int, chat_id: int,
) -> GettedListAdminChat: ) -> GettedListAdminChat:
"""Получает список администраторов чата. """
Получает список администраторов чата.
:param chat_id: ID чата :param chat_id: ID чата
@@ -542,7 +570,8 @@ class Bot(BaseConnection):
marker: int = None marker: int = None
) -> AddedListAdminChat: ) -> AddedListAdminChat:
"""Добавляет администраторов в чат. """
Добавляет администраторов в чат.
:param chat_id: ID чата :param chat_id: ID чата
:param admins: Список администраторов :param admins: Список администраторов
@@ -564,7 +593,8 @@ class Bot(BaseConnection):
user_id: int user_id: int
) -> RemovedAdmin: ) -> RemovedAdmin:
"""Удаляет администратора из чата. """
Удаляет администратора из чата.
:param chat_id: ID чата :param chat_id: ID чата
:param user_id: ID пользователя :param user_id: ID пользователя
@@ -586,7 +616,8 @@ class Bot(BaseConnection):
count: int = None, count: int = None,
) -> GettedMembersChat: ) -> GettedMembersChat:
"""Получает участников чата. """
Получает участников чата.
:param chat_id: ID чата :param chat_id: ID чата
:param user_ids: Список ID участников :param user_ids: Список ID участников
@@ -610,7 +641,8 @@ class Bot(BaseConnection):
user_id: int, user_id: int,
) -> GettedMembersChat: ) -> GettedMembersChat:
"""Получает участника чата. """
Получает участника чата.
:param chat_id: ID чата :param chat_id: ID чата
:param user_id: ID участника :param user_id: ID участника
@@ -632,7 +664,8 @@ class Bot(BaseConnection):
user_ids: List[str], user_ids: List[str],
) -> AddedMembersChat: ) -> AddedMembersChat:
"""Добавляет участников в чат. """
Добавляет участников в чат.
:param chat_id: ID чата :param chat_id: ID чата
:param user_ids: Список ID пользователей :param user_ids: Список ID пользователей
@@ -653,7 +686,8 @@ class Bot(BaseConnection):
block: bool = False, block: bool = False,
) -> RemovedMemberChat: ) -> RemovedMemberChat:
"""Исключает участника из чата. """
Исключает участника из чата.
:param chat_id: ID чата :param chat_id: ID чата
:param user_id: ID пользователя :param user_id: ID пользователя
@@ -673,7 +707,8 @@ class Bot(BaseConnection):
self, self,
) -> UpdateUnion: ) -> UpdateUnion:
"""Получает обновления для бота. """
Получает обновления для бота.
:return: Список обновлений :return: Список обновлений
""" """
@@ -687,7 +722,8 @@ class Bot(BaseConnection):
type: UploadType type: UploadType
) -> GettedUploadUrl: ) -> GettedUploadUrl:
"""Получает URL для загрузки файлов. """
Получает URL для загрузки файлов.
:param type: Тип загружаемого файла :param type: Тип загружаемого файла
@@ -704,7 +740,8 @@ class Bot(BaseConnection):
*commands: BotCommand *commands: BotCommand
) -> User: ) -> User:
"""Устанавливает список команд бота. """
Устанавливает список команд бота.
:param commands: Список команд :param commands: Список команд

View File

@@ -1,10 +1,13 @@
import os import os
import mimetypes
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from uuid import uuid4
import aiofiles import aiofiles
import aiohttp import aiohttp
import puremagic
from pydantic import BaseModel from pydantic import BaseModel
from ..exceptions.invalid_token import InvalidToken from ..exceptions.invalid_token import InvalidToken
@@ -135,6 +138,51 @@ class BaseConnection:
return await response.text() return await response.text()
async def upload_file_buffer(
self,
url: str,
buffer: bytes,
type: UploadType
):
"""
Загружает файл из буфера.
:param url: Конечная точка загрузки файла
:param buffer: Буфер (bytes)
:param type: Тип файла (video, image, audio, file)
:return: Сырой .text() ответ от сервера после загрузки файла
"""
try:
matches = puremagic.magic_string(buffer[:4096])
if matches:
mime_type = matches[0][1]
ext = mimetypes.guess_extension(mime_type) or ''
else:
mime_type = f"{type.value}/*"
ext = ''
except Exception:
mime_type = f"{type.value}/*"
ext = ''
basename = f'{uuid4()}{ext}'
form = aiohttp.FormData()
form.add_field(
name='data',
value=buffer,
filename=basename,
content_type=mime_type
)
async with aiohttp.ClientSession() as session:
response = await session.post(
url=url,
data=form
)
return await response.text()
async def download_file( async def download_file(
self, self,
path: str, path: str,

View File

@@ -36,12 +36,19 @@ GET_UPDATES_RETRY_DELAY = 5
class Dispatcher: class Dispatcher:
"""Основной класс для обработки событий бота. """
Основной класс для обработки событий бота.
Обеспечивает работу с вебхуком и поллингом, управляет обработчиками событий. Обеспечивает запуск поллинга и вебхука, маршрутизацию событий,
применение middleware, фильтров и вызов соответствующих обработчиков.
""" """
def __init__(self): def __init__(self):
"""
Инициализация диспетчера.
"""
self.event_handlers: List[Handler] = [] self.event_handlers: List[Handler] = []
self.contexts: List[MemoryContext] = [] self.contexts: List[MemoryContext] = []
self.routers: List[Router] = [] self.routers: List[Router] = []
@@ -66,22 +73,34 @@ class Dispatcher:
async def check_me(self): async def check_me(self):
"""Проверяет и логирует информацию о боте.""" """
Проверяет и логирует информацию о боте.
"""
me = await self.bot.get_me() me = await self.bot.get_me()
self.bot._me = me
logger_dp.info(f'Бот: @{me.username} first_name={me.first_name} id={me.user_id}') logger_dp.info(f'Бот: @{me.username} first_name={me.first_name} id={me.user_id}')
def include_routers(self, *routers: 'Router'): def include_routers(self, *routers: 'Router'):
"""Добавляет обработчики из роутеров. """
Добавляет указанные роутеры в диспетчер.
Args: :param routers: Роутеры для добавления.
*routers: Роутеры для включения
""" """
self.routers += [r for r in routers] self.routers += [r for r in routers]
async def __ready(self, bot: Bot): async def __ready(self, bot: Bot):
"""
Подготавливает диспетчер: сохраняет бота, регистрирует обработчики, вызывает on_started.
:param bot: Экземпляр бота.
"""
self.bot = bot self.bot = bot
await self.check_me() await self.check_me()
@@ -96,14 +115,12 @@ class Dispatcher:
def __get_memory_context(self, chat_id: int, user_id: int): def __get_memory_context(self, chat_id: int, user_id: int):
"""Возвращает или создает контекст для чата и пользователя. """
Возвращает существующий или создает новый контекст по chat_id и user_id.
Args: :param chat_id: Идентификатор чата.
chat_id: ID чата :param user_id: Идентификатор пользователя.
user_id: ID пользователя :return: Объект MemoryContext.
Returns:
Существующий или новый контекст
""" """
for ctx in self.contexts: for ctx in self.contexts:
@@ -121,6 +138,15 @@ class Dispatcher:
result_data_kwargs: Dict[str, Any] result_data_kwargs: Dict[str, Any]
): ):
"""
Последовательно обрабатывает middleware цепочку.
:param middlewares: Список middleware.
:param event_object: Объект события.
:param result_data_kwargs: Аргументы, передаваемые обработчику.
:return: Изменённые аргументы или None.
"""
for middleware in middlewares: for middleware in middlewares:
result = await middleware.process_middleware( result = await middleware.process_middleware(
event_object=event_object, event_object=event_object,
@@ -139,11 +165,12 @@ class Dispatcher:
async def handle(self, event_object: UpdateUnion): async def handle(self, event_object: UpdateUnion):
"""Обрабатывает событие.
Args:
event_object: Объект события для обработки
""" """
Основной обработчик события. Применяет фильтры, middleware и вызывает подходящий handler.
:param event_object: Событие, пришедшее в бот.
"""
try: try:
ids = event_object.get_ids() ids = event_object.get_ids()
memory_context = self.__get_memory_context(*ids) memory_context = self.__get_memory_context(*ids)
@@ -209,11 +236,12 @@ class Dispatcher:
async def start_polling(self, bot: Bot): async def start_polling(self, bot: Bot):
"""Запускает поллинг обновлений.
Args:
bot: Экземпляр бота
""" """
Запускает цикл получения обновлений с сервера (long polling).
:param bot: Экземпляр бота.
"""
await self.__ready(bot) await self.__ready(bot)
while True: while True:
@@ -243,12 +271,12 @@ class Dispatcher:
async def handle_webhook(self, bot: Bot, host: str = '0.0.0.0', port: int = 8080): async def handle_webhook(self, bot: Bot, host: str = '0.0.0.0', port: int = 8080):
"""Запускает вебхук сервер. """
Запускает FastAPI-приложение для приёма обновлений через вебхук.
Args: :param bot: Экземпляр бота.
bot: Экземпляр бота :param host: Хост, на котором запускается сервер.
host: Хост для сервера :param port: Порт сервера.
port: Порт для сервера
""" """
await self.__ready(bot) await self.__ready(bot)
@@ -277,7 +305,9 @@ class Dispatcher:
class Router(Dispatcher): class Router(Dispatcher):
"""Роутер для группировки обработчиков событий.""" """
Роутер для группировки обработчиков событий.
"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -285,13 +315,30 @@ class Router(Dispatcher):
class Event: class Event:
"""Декоратор для регистрации обработчиков событий.""" """
Декоратор для регистрации обработчиков событий.
"""
def __init__(self, update_type: UpdateType, router: Dispatcher | Router): def __init__(self, update_type: UpdateType, router: Dispatcher | Router):
"""
Инициализирует событие-декоратор.
:param update_type: Тип события (UpdateType).
:param router: Роутер или диспетчер, в который регистрируется обработчик.
"""
self.update_type = update_type self.update_type = update_type
self.router = router self.router = router
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""
Регистрирует функцию как обработчик события.
:return: Исходная функция.
"""
def decorator(func_event: Callable): def decorator(func_event: Callable):
if self.update_type == UpdateType.ON_STARTED: if self.update_type == UpdateType.ON_STARTED:

View File

@@ -14,3 +14,5 @@ class ButtonType(str, Enum):
LINK = 'link' LINK = 'link'
REQUEST_GEO_LOCATION = 'request_geo_location' REQUEST_GEO_LOCATION = 'request_geo_location'
CHAT = 'chat' CHAT = 'chat'
MESSAGE = 'message'
OPEN_APP = 'open_app'

View File

@@ -4,7 +4,7 @@ from magic_filter import F, MagicFilter
from ..filters.middleware import BaseMiddleware from ..filters.middleware import BaseMiddleware
from ..types.command import Command from ..types.command import Command, CommandStart
from ..context.state_machine import State from ..context.state_machine import State
@@ -52,7 +52,7 @@ class Handler:
self.filters.append(arg) self.filters.append(arg)
elif isinstance(arg, State): elif isinstance(arg, State):
self.state = arg self.state = arg
elif isinstance(arg, Command): elif isinstance(arg, (Command, CommandStart)):
self.filters.insert(0, F.message.body.text.startswith(arg.command)) self.filters.insert(0, F.message.body.text.startswith(arg.command))
elif isinstance(arg, BaseMiddleware): elif isinstance(arg, BaseMiddleware):
self.middlewares.append(arg) self.middlewares.append(arg)

View File

@@ -9,7 +9,7 @@ from .types.sended_message import SendedMessage
from ..types.attachments.upload import AttachmentPayload, AttachmentUpload from ..types.attachments.upload import AttachmentPayload, AttachmentUpload
from ..types.errors import Error from ..types.errors import Error
from ..types.message import NewMessageLink from ..types.message import NewMessageLink
from ..types.input_media import InputMedia from ..types.input_media import InputMedia, InputMediaBuffer
from ..types.attachments.attachment import Attachment from ..types.attachments.attachment import Attachment
from ..enums.upload_type import UploadType from ..enums.upload_type import UploadType
@@ -67,7 +67,7 @@ class SendMessage(BaseConnection):
async def __process_input_media( async def __process_input_media(
self, self,
att: InputMedia att: InputMedia | InputMediaBuffer
): ):
# очень нестабильный метод независящий от модуля # очень нестабильный метод независящий от модуля
@@ -85,11 +85,18 @@ class SendMessage(BaseConnection):
upload = await self.bot.get_upload_url(att.type) upload = await self.bot.get_upload_url(att.type)
upload_file_response = await self.upload_file( if isinstance(att, InputMedia):
url=upload.url, upload_file_response = await self.upload_file(
path=att.path, url=upload.url,
type=att.type path=att.path,
) type=att.type,
)
elif isinstance(att, InputMediaBuffer):
upload_file_response = await self.upload_file_buffer(
url=upload.url,
buffer=att.buffer,
type=att.type,
)
if att.type in (UploadType.VIDEO, UploadType.AUDIO): if att.type in (UploadType.VIDEO, UploadType.AUDIO):
token = upload.token token = upload.token
@@ -134,7 +141,7 @@ class SendMessage(BaseConnection):
for att in self.attachments: for att in self.attachments:
if isinstance(att, InputMedia): if isinstance(att, InputMedia) or isinstance(att, InputMediaBuffer):
input_media = await self.__process_input_media(att) input_media = await self.__process_input_media(att)
json['attachments'].append( json['attachments'].append(
input_media.model_dump() input_media.model_dump()

View File

@@ -11,6 +11,7 @@ from ..types.updates.user_added import UserAdded
from ..types.updates.user_removed import UserRemoved from ..types.updates.user_removed import UserRemoved
from ..types.updates import UpdateUnion from ..types.updates import UpdateUnion
from ..types.attachments.attachment import Attachment
from ..types.attachments.attachment import PhotoAttachmentPayload from ..types.attachments.attachment import PhotoAttachmentPayload
from ..types.attachments.attachment import OtherAttachmentPayload from ..types.attachments.attachment import OtherAttachmentPayload
from ..types.attachments.attachment import ContactAttachmentPayload from ..types.attachments.attachment import ContactAttachmentPayload
@@ -19,22 +20,31 @@ from ..types.attachments.attachment import StickerAttachmentPayload
from ..types.attachments.buttons.callback_button import CallbackButton from ..types.attachments.buttons.callback_button import CallbackButton
from ..types.attachments.buttons.chat_button import ChatButton from ..types.attachments.buttons.chat_button import ChatButton
from ..types.attachments.buttons.link_button import LinkButton from ..types.attachments.buttons.link_button import LinkButton
from ..types.attachments.buttons.request_contact import RequestContact from ..types.attachments.buttons.request_contact import RequestContactButton
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.message import Message from ..types.message import Message
from ..types.command import Command, BotCommand from ..types.command import Command, BotCommand, CommandStart
from .input_media import InputMedia from .input_media import InputMedia
from .input_media import InputMediaBuffer
__all__ = [ __all__ = [
CommandStart,
OpenAppButton,
Message,
Attachment,
InputMediaBuffer,
MessageButton,
UpdateUnion, UpdateUnion,
InputMedia, InputMedia,
BotCommand, BotCommand,
CallbackButton, CallbackButton,
ChatButton, ChatButton,
LinkButton, LinkButton,
RequestContact, RequestContactButton,
RequestGeoLocationButton, RequestGeoLocationButton,
Command, Command,
PhotoAttachmentPayload, PhotoAttachmentPayload,

View File

@@ -83,6 +83,12 @@ class ButtonsPayload(BaseModel):
buttons: List[List[InlineButtonUnion]] buttons: List[List[InlineButtonUnion]]
def pack(self):
return Attachment(
type=AttachmentType.INLINE_KEYBOARD,
payload=self
)
class Attachment(BaseModel): class Attachment(BaseModel):

View File

@@ -3,13 +3,17 @@ from typing import Union
from .callback_button import CallbackButton from .callback_button import CallbackButton
from .chat_button import ChatButton from .chat_button import ChatButton
from .link_button import LinkButton from .link_button import LinkButton
from .request_contact import RequestContact from .request_contact import RequestContactButton
from .request_geo_location_button import RequestGeoLocationButton from .request_geo_location_button import RequestGeoLocationButton
from .message_button import MessageButton
from .open_app_button import OpenAppButton
InlineButtonUnion = Union[ InlineButtonUnion = Union[
CallbackButton, CallbackButton,
ChatButton, ChatButton,
LinkButton, LinkButton,
RequestContact, RequestContactButton,
RequestGeoLocationButton RequestGeoLocationButton,
MessageButton,
OpenAppButton
] ]

View File

@@ -1,5 +1,7 @@
from typing import Optional from typing import Optional
from ....enums.button_type import ButtonType
from .button import Button from .button import Button
@@ -12,4 +14,5 @@ class LinkButton(Button):
url: Ссылка для перехода (должна содержать http/https) url: Ссылка для перехода (должна содержать http/https)
""" """
type: ButtonType = ButtonType.LINK
url: Optional[str] = None url: Optional[str] = None

View File

@@ -0,0 +1,19 @@
from pydantic import BaseModel
from ....enums.button_type import ButtonType
from .button import Button
class MessageButton(Button):
"""
Кнопка для отправки текста
Attributes:
type: Тип кнопки (определяет её поведение и функционал)
text: Отправляемый текст
"""
type: ButtonType = ButtonType.MESSAGE
text: str

View File

@@ -0,0 +1,22 @@
from typing import Optional
from ....enums.button_type import ButtonType
from .button import Button
class OpenAppButton(Button):
"""
Кнопка для открытия приложения
Attributes:
text: Видимый текст кнопки
web_app: Публичное имя (username) бота или ссылка на него, чьё мини-приложение надо запустить
contact_id: Идентификатор бота, чьё мини-приложение надо запустить
"""
type: ButtonType = ButtonType.OPEN_APP
text: str
web_app: Optional[str] = None
contact_id: Optional[int] = None

View File

@@ -1,8 +1,18 @@
from typing import Optional
from ....enums.button_type import ButtonType
from .button import Button from .button import Button
class RequestContact(Button): class RequestContactButton(Button):
""" """
Кнопка с контактом. Кнопка с контактом
Args:
text: Текст кнопки
""" """
...
type: ButtonType = ButtonType.REQUEST_CONTACT
text: str

View File

@@ -1,3 +1,5 @@
from ....enums.button_type import ButtonType
from .button import Button from .button import Button
@@ -10,4 +12,5 @@ class RequestGeoLocationButton(Button):
подтверждения пользователя (по умолчанию False) подтверждения пользователя (по умолчанию False)
""" """
type: ButtonType = ButtonType.REQUEST_GEO_LOCATION
quick: bool = False quick: bool = False

View File

@@ -41,3 +41,18 @@ 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,34 +1,38 @@
import mimetypes from __future__ import annotations
from typing import TYPE_CHECKING
import puremagic
from ..enums.upload_type import UploadType from ..enums.upload_type import UploadType
class InputMedia: if TYPE_CHECKING:
from io import BytesIO
class InputMedia:
""" """
Класс для представления медиафайла. Класс для представления медиафайла.
Attributes: Attributes:
path (str): Путь к файлу. path (str): Путь к файлу.
type (UploadType): Тип файла, определенный на основе MIME-типа. type (UploadType): Тип файла, определенный на основе содержимого (MIME-типа).
""" """
def __init__(self, path: str): def __init__(self, path: str):
""" """
Инициализирует объект медиафайла. Инициализирует объект медиафайла.
Args: Args:
path (str): Путь к файлу. path (str): Путь к файлу.
""" """
self.path = path self.path = path
self.type = self.__detect_file_type(path) self.type = self.__detect_file_type(path)
def __detect_file_type(self, path: str) -> UploadType: def __detect_file_type(self, path: str) -> UploadType:
""" """
Определяет тип файла на основе его MIME-типа. Определяет тип файла на основе его содержимого (MIME-типа).
Args: Args:
path (str): Путь к файлу. path (str): Путь к файлу.
@@ -36,8 +40,17 @@ class InputMedia:
Returns: Returns:
UploadType: Тип файла (VIDEO, IMAGE, AUDIO или FILE). UploadType: Тип файла (VIDEO, IMAGE, AUDIO или FILE).
""" """
with open(path, 'rb') as f:
sample = f.read(4096)
mime_type, _ = mimetypes.guess_type(path) try:
matches = puremagic.magic_string(sample)
if matches:
mime_type = matches[0].mime_type
else:
mime_type = None
except Exception:
mime_type = None
if mime_type is None: if mime_type is None:
return UploadType.FILE return UploadType.FILE
@@ -50,3 +63,44 @@ class InputMedia:
return UploadType.AUDIO return UploadType.AUDIO
else: else:
return UploadType.FILE return UploadType.FILE
class InputMediaBuffer:
"""
Класс для представления медиафайла из буфера.
Attributes:
buffer (BytesIO): Буфер с содержимым файла.
type (UploadType): Тип файла, определенный по содержимому.
"""
def __init__(self, buffer: BytesIO):
"""
Инициализирует объект медиафайла из буфера.
Args:
buffer (IO): Буфер с содержимым файла.
"""
self.buffer = buffer
self.type = self.__detect_file_type(buffer)
def __detect_file_type(self, buffer: BytesIO) -> UploadType:
try:
matches = puremagic.magic_string(buffer)
if matches:
mime_type = matches[0].mime_type
else:
mime_type = None
except Exception:
mime_type = None
if mime_type is None:
return UploadType.FILE
if mime_type.startswith('video/'):
return UploadType.VIDEO
elif mime_type.startswith('image/'):
return UploadType.IMAGE
elif mime_type.startswith('audio/'):
return UploadType.AUDIO
else:
return UploadType.FILE

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "maxapi" name = "maxapi"
version = "0.8.7" version = "0.8.9"
description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX" description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -19,6 +19,7 @@ dependencies = [
"pydantic>=1.8.0", "pydantic>=1.8.0",
"uvicorn>=0.15.0", "uvicorn>=0.15.0",
"aiofiles==24.1.0", "aiofiles==24.1.0",
"puremagic==1.30"
] ]
[project.urls] [project.urls]
@@ -30,3 +31,6 @@ license-files = []
[build-system] [build-system]
requires = ["setuptools>=68.0.0", "wheel"] requires = ["setuptools>=68.0.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["maxapi*", "wiki*", "examples*"]

284
wiki/bot_methods.md Normal file
View File

@@ -0,0 +1,284 @@
# Методы класса Bot
## 💬 Работа с сообщениями
### `send_message(...)`
**Описание:** Отправить сообщение в чат или пользователю.
**Аргументы:**
* `chat_id` *(int)* — ID чата. Обязателен, если не указан `user_id`.
* `user_id` *(int)* — ID пользователя. Обязателен, если не указан `chat_id`.
* `text` *(str)* — текст сообщения.
* `attachments` *(List\[Attachment])* — вложения (фото, видео и т.д.).
* `link` *(NewMessageLink)* — объект для создания ссылочного сообщения.
* `notify` *(bool)* — отправлять ли уведомление (по умолчанию берётся из настроек бота).
* `parse_mode` *(ParseMode)* — форматирование текста (например, `ParseMode.HTML`).
**Возвращает:** `SendedMessage` — объект отправленного сообщения.
---
### `edit_message(...)`
**Описание:** Редактировать существующее сообщение.
**Аргументы:**
* `message_id` *(str)* — ID сообщения, полученное ранее в `SendedMessage.id`.
* `text`, `attachments`, `link`, `notify`, `parse_mode` — см. `send_message`.
**Возвращает:** `EditedMessage` — объект изменённого сообщения.
---
### `delete_message(message_id)`
**Описание:** Удалить сообщение по его ID.
**Аргументы:**
* `message_id` *(str)* — ID сообщения.
**Возвращает:** `DeletedMessage` — результат удаления.
---
### `get_messages(...)`
**Описание:** Получить список сообщений.
**Аргументы:**
* `chat_id` *(int)* — ID чата.
* `message_ids` *(List\[str])* — список ID сообщений.
* `from_time` / `to_time` *(datetime | int)* — диапазон по времени.
* `count` *(int)* — сколько сообщений вернуть (по умолчанию 50).
**Возвращает:** `Messages` — список объектов сообщений.
---
### `get_message(message_id)`
**Описание:** Получить одно сообщение по ID.
**Аргументы:**
* `message_id` *(str)* — ID сообщения.
**Возвращает:** `Messages` — содержит одно сообщение в списке.
---
### `pin_message(...)`
**Описание:** Закрепить сообщение в чате.
**Аргументы:**
* `chat_id` *(int)* — ID чата.
* `message_id` *(str)* — ID сообщения.
* `notify` *(bool)* — уведомление.
**Возвращает:** `PinnedMessage`
---
### `delete_pin_message(chat_id)`
**Описание:** Удалить закреплённое сообщение.
**Аргументы:**
* `chat_id` *(int)* — ID чата.
**Возвращает:** `DeletedPinMessage`
---
## 🤖 Информация о боте
### `get_me()`
**Описание:** Получить объект бота.
**Возвращает:** `User` — текущий бот.
---
### `change_info(...)`
**Описание:** Изменить профиль бота.
**Аргументы:**
* `name` *(str)* — новое имя.
* `description` *(str)* — описание.
* `commands` *(List\[BotCommand])* — команды (name + description).
* `photo` *(Dict)*`{ "url": ..., "token": ... }` — загруженное изображение. URL можно получить через `get_upload_url(...)`.
**Возвращает:** `User`
---
### `set_my_commands(*commands)`
**Описание:** Установить команды бота.
**Аргументы:**
* `commands` *(BotCommand)* — команды, например `BotCommand(name="help", description="Справка")`
**Возвращает:** `User`
---
## 👥 Работа с чатами
### `get_chats(...)`
**Описание:** Получить список чатов.
**Аргументы:**
* `count` *(int)* — количество (по умолчанию 50).
* `marker` *(int)* — маркер страницы.
**Возвращает:** `Chats`
---
### `get_chat_by_id(id)` / `get_chat_by_link(link)`
**Описание:** Получить объект чата по ID или публичной ссылке.
**Возвращает:** `Chat`
---
### `edit_chat(...)`
**Описание:** Изменить чат.
**Аргументы:**
* `chat_id`, `title`, `pin`, `notify` — как выше.
* `icon` *(PhotoAttachmentRequestPayload)* — вложение фото, загруженное через `get_upload_url(...)` и `download_file(...)`.
**Возвращает:** `Chat`
---
### `delete_chat(chat_id)`
Удаляет чат.
**Возвращает:** `DeletedChat`
---
## 👤 Работа с участниками чатов
### `get_chat_members(...)` / `get_chat_member(...)`
**Описание:** Получить одного или нескольких участников.
**Возвращает:** `GettedMembersChat` (у него есть `.members`)
---
### `add_chat_members(...)`
**Описание:** Добавить участников.
**Аргументы:**
* `chat_id`, `user_ids` *(List\[str])* — список строковых ID.
**Возвращает:** `AddedMembersChat`
---
### `kick_chat_member(...)`
**Описание:** Исключить и опционально заблокировать.
**Возвращает:** `RemovedMemberChat`
---
### `get_list_admin_chat(...)` / `add_list_admin_chat(...)` / `remove_admin(...)`
**Описание:** Управление администраторами.
**Возвращают:** `GettedListAdminChat`, `AddedListAdminChat`, `RemovedAdmin`
---
### `get_me_from_chat(...)`
**Описание:** Получить, кем является бот в чате.
**Возвращает:** `ChatMember`
### `delete_me_from_chat(...)`
**Удаляет бота из чата.**
**Возвращает:** `DeletedBotFromChat`
---
## 🔄 Обновления и действия
### `get_updates()`
**Описание:** Получить события (новости, сообщения и т.д.).
**Возвращает:** `UpdateUnion`
---
### `send_action(...)`
**Описание:** Отправить "печатает..." и т.д.
**Аргументы:**
* `chat_id`, `action` *(SenderAction)* — например, `SenderAction.TYPING_ON`
**Возвращает:** `SendedAction`
---
### `send_callback(...)`
**Описание:** Ответ на callback-кнопку.
**Аргументы:**
* `callback_id`, `message`, `notification`
**Возвращает:** `SendedCallback`
---
## 📎 Медиа и файлы
### `get_video(video_token)`
**Возвращает:** `Video`
### `get_upload_url(type)`
**Аргументы:** `type` *(UploadType)* — например, `UploadType.IMAGE`
**Возвращает:** `GettedUploadUrl` (у него есть `.url`)
### `download_file(path, url, token)` (НЕАКТУАЛЬНО)
**Описание:** Скачивает файл, используя URL и токен.
**Возвращает:** статус загрузки

35
wiki/events.md Normal file
View File

@@ -0,0 +1,35 @@
# События
Для обработки разных типов обновлений используются события (Event). Ниже перечислены основные события и их назначение.
| Событие | Описание |
|-----------------------|----------------------------------------------------------------------------------------------|
| `message_created` | Создание нового сообщения (пользователь отправил сообщение) |
| `bot_added` | Бот добавлен в чат |
| `bot_removed` | Бот удалён из чата |
| `bot_started` | Пользователь запустил бота |
| `chat_title_changed` | Изменено название чата |
| `message_callback` | Пользователь нажал на callback-кнопку (inline button) |
| `message_chat_created`| Срабатывает когда пользователь нажал на кнопку с действием "Создать чат" (работает некорректно со стороны API MAX, ждем исправлений) |
| `message_edited` | Сообщение было отредактировано |
| `message_removed` | Сообщение было удалено |
| `user_added` | Пользователь добавлен в чат |
| `user_removed` | Пользователь удалён из чата |
| `on_started` | Бот запущен (**внутреннее** событие библиотеки) |
---
## Пример использования
```python
@dp.message_created()
async def on_message(event: MessageCreated):
... # обработка нового сообщения
@dp.bot_added()
async def on_bot_added(event: BotAdded):
... # логика при добавлении бота
@dp.message_callback()
async def on_callback(event: MessageCallback):
... # обработка нажатия на callback-кнопку

65
wiki/handlers.md Normal file
View File

@@ -0,0 +1,65 @@
# Философия хендлеров или как задается хендлер в maxapi
Для регистрации хендлера в maxapi используется объект `Dispatcher` или `Router` и декоратор с указанием типа события и фильтра.
## Общий синтаксис
```python
@dp.<тип_события>(<фильтры>)
async def <имя_функции>(event: <тип_события>):
...
```
* `dp` — экземпляр `Dispatcher`
* `<тип_события>` — тип события (например, `message_created`)
* `<фильтр>` — условие `MagicFilter`, по которому срабатывает хендлер (например, наличие текста в сообщении)
* `event` — объект события с данными (например, `MessageCreated`)
## Пример
```python
@dp.message_created(F.message.body.text)
async def echo(event: MessageCreated):
await event.message.answer(f"Повторяю за вами: {event.message.body.text}")
```
* `@dp.message_created` — хендлер на событие создания сообщения
* `F.message.body.text` — фильтр: сработает только если в сообщении есть текст
* `echo` — асинхронная функция-обработчик, которая принимает событие `MessageCreated`
* В теле функции вызывается метод `answer` для отправки ответа с повтором текста
## Полный код
```python
import asyncio
import logging
from maxapi import Bot, Dispatcher
from maxapi.filters import F
from maxapi.types import MessageCreated
logging.basicConfig(level=logging.INFO)
bot = Bot(ут_ваш_токен')
dp = Dispatcher()
@dp.message_created(F.message.body.text)
async def echo(event: MessageCreated):
await event.message.answer(f"Повторяю за вами: {event.message.body.text}")
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())
```

99
wiki/memory_context.md Normal file
View File

@@ -0,0 +1,99 @@
# MemoryContext
Контекст данных пользователя с поддержкой асинхронных блокировок. Используется для хранения и управления состоянием пользователя в рамках сессии.
При передаче в хендлер события `message_chat_created` в качестве атрибута `chat_id` идёт идентификатор созданного чата, а `user_id` - идентификатора бота!
## Класс: `MemoryContext`
```python
MemoryContext(chat_id: int, user_id: int)
````
### Аргументы:
* `chat_id` (`int`): Идентификатор чата.
* `user_id` (`int`): Идентификатор пользователя.
## Методы
### `async def get_data() -> dict[str, Any]`
Возвращает текущие данные контекста.
#### Возвращает:
* `dict[str, Any]`: Словарь с текущими данными пользователя.
---
### `async def set_data(data: dict[str, Any])`
Полностью заменяет контекст данных.
#### Аргументы:
* `data` (`dict[str, Any]`): Новый словарь данных, заменяющий текущий.
---
### `async def update_data(**kwargs)`
Обновляет текущий контекст, добавляя или изменяя переданные пары ключ-значение.
#### Аргументы:
* `**kwargs`: Ключи и значения для обновления контекста.
---
### `async def set_state(state: State | str = None)`
Устанавливает новое состояние пользователя или сбрасывает его.
#### Аргументы:
* `state` (`State | str | None`): Новое состояние. Если `None` состояние будет сброшено.
---
### `async def get_state() -> State | None`
Возвращает текущее состояние пользователя.
#### Возвращает:
* `State | None`: Текущее состояние или `None`, если не установлено.
---
### `async def clear()`
Очищает все данные контекста и сбрасывает состояние.
---
## Пример использования
[Полный пример](https://github.com/love-apples/maxapi/tree/main/examples/router_with_input_media)
```python
@dp.message_created(Command('clear'))
async def hello(event: MessageCreated, context: MemoryContext):
await context.clear()
await event.message.answer(f"Ваш контекст был очищен!")
@dp.message_created(Command('data'))
async def hello(event: MessageCreated, context: MemoryContext):
data = await context.get_data()
await event.message.answer(f"Ваша контекстная память: {str(data)}")
@dp.message_created(Command('context'))
@dp.message_created(Command('state'))
async def hello(event: MessageCreated, context: MemoryContext):
data = await context.get_state()
await event.message.answer(f"Ваше контекстное состояние: {str(data)}")
```