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

View File

@@ -1,10 +1,13 @@
import os
import mimetypes
from typing import TYPE_CHECKING
from uuid import uuid4
import aiofiles
import aiohttp
import puremagic
from pydantic import BaseModel
from ..exceptions.invalid_token import InvalidToken
@@ -135,6 +138,51 @@ class BaseConnection:
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(
self,
path: str,

View File

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

View File

@@ -14,3 +14,5 @@ class ButtonType(str, Enum):
LINK = 'link'
REQUEST_GEO_LOCATION = 'request_geo_location'
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 ..types.command import Command
from ..types.command import Command, CommandStart
from ..context.state_machine import State
@@ -52,7 +52,7 @@ class Handler:
self.filters.append(arg)
elif isinstance(arg, State):
self.state = arg
elif isinstance(arg, Command):
elif isinstance(arg, (Command, CommandStart)):
self.filters.insert(0, F.message.body.text.startswith(arg.command))
elif isinstance(arg, BaseMiddleware):
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.errors import Error
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 ..enums.upload_type import UploadType
@@ -67,7 +67,7 @@ class SendMessage(BaseConnection):
async def __process_input_media(
self,
att: InputMedia
att: InputMedia | InputMediaBuffer
):
# очень нестабильный метод независящий от модуля
@@ -85,11 +85,18 @@ class SendMessage(BaseConnection):
upload = await self.bot.get_upload_url(att.type)
upload_file_response = await self.upload_file(
url=upload.url,
path=att.path,
type=att.type
)
if isinstance(att, InputMedia):
upload_file_response = await self.upload_file(
url=upload.url,
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):
token = upload.token
@@ -134,7 +141,7 @@ class SendMessage(BaseConnection):
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)
json['attachments'].append(
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 import UpdateUnion
from ..types.attachments.attachment import Attachment
from ..types.attachments.attachment import PhotoAttachmentPayload
from ..types.attachments.attachment import OtherAttachmentPayload
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.chat_button import ChatButton
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.message_button import MessageButton
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 InputMediaBuffer
__all__ = [
CommandStart,
OpenAppButton,
Message,
Attachment,
InputMediaBuffer,
MessageButton,
UpdateUnion,
InputMedia,
BotCommand,
CallbackButton,
ChatButton,
LinkButton,
RequestContact,
RequestContactButton,
RequestGeoLocationButton,
Command,
PhotoAttachmentPayload,

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
from typing import Optional
from ....enums.button_type import ButtonType
from .button import Button
@@ -12,4 +14,5 @@ class LinkButton(Button):
url: Ссылка для перехода (должна содержать http/https)
"""
type: ButtonType = ButtonType.LINK
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
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
@@ -10,4 +12,5 @@ class RequestGeoLocationButton(Button):
подтверждения пользователя (по умолчанию False)
"""
type: ButtonType = ButtonType.REQUEST_GEO_LOCATION
quick: bool = False

View File

@@ -40,4 +40,19 @@ class BotCommand(BaseModel):
"""
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
if TYPE_CHECKING:
from io import BytesIO
class InputMedia:
"""
Класс для представления медиафайла.
Attributes:
path (str): Путь к файлу.
type (UploadType): Тип файла, определенный на основе MIME-типа.
type (UploadType): Тип файла, определенный на основе содержимого (MIME-типа).
"""
def __init__(self, path: str):
"""
Инициализирует объект медиафайла.
Args:
path (str): Путь к файлу.
"""
self.path = path
self.type = self.__detect_file_type(path)
def __detect_file_type(self, path: str) -> UploadType:
"""
Определяет тип файла на основе его MIME-типа.
Определяет тип файла на основе его содержимого (MIME-типа).
Args:
path (str): Путь к файлу.
@@ -36,12 +40,62 @@ class InputMedia:
Returns:
UploadType: Тип файла (VIDEO, IMAGE, AUDIO или FILE).
"""
mime_type, _ = mimetypes.guess_type(path)
with open(path, 'rb') as f:
sample = f.read(4096)
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:
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
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/'):

View File

@@ -1,6 +1,6 @@
[project]
name = "maxapi"
version = "0.8.7"
version = "0.8.9"
description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX"
readme = "README.md"
requires-python = ">=3.10"
@@ -19,6 +19,7 @@ dependencies = [
"pydantic>=1.8.0",
"uvicorn>=0.15.0",
"aiofiles==24.1.0",
"puremagic==1.30"
]
[project.urls]
@@ -29,4 +30,7 @@ license-files = []
[build-system]
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)}")
```