Compare commits

...

4 Commits

44 changed files with 541 additions and 166 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Твоё Имя
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,29 +1,84 @@
# maxapi
# Асинхронный MAX API
#### Библиотека (like aiogram) для взаимодействия с мессенджером MAX
[![PyPI version](https://img.shields.io/pypi/v/maxapi.svg)](https://pypi.org/project/maxapi/)
[![Python Version](https://img.shields.io/pypi/pyversions/maxapi.svg)](https://pypi.org/project/maxapi/)
[![License](https://img.shields.io/github/license/love-apples/maxapi.svg)](https://love-apples/maxapi/blob/main/LICENSE)
Информация на данный момент:
* Проект тестируется и активно дорабатывается
* На данный момент имеется:
* Роутеры
* Билдер инлайн клавиатур
* Этакая машина состояний и контекст к нему
* Поллинг и вебхук методы запуска
* Логгирование
---
## 📦 Установка
```bash
Пример бота описан в example.py
Перед запуском примера установите зависимости:
pip install maxapi==0.1
Запуск бота из папки example:
python example.py
pip install maxapi
```
---
### Контакты
[Группа MAX](https://max.ru/join/IPAok63C3vFqbWTFdutMUtjmrAkGqO56YeAN7iyDfc8)
## 🚀 Быстрый старт
```python
import asyncio
import logging
from maxapi import Bot, Dispatcher
from maxapi.types import BotStarted, Command, MessageCreated
logging.basicConfig(level=logging.INFO)
bot = Bot('f9LHodD0cOL5NY7All_9xJRh5ZhPw6bRvq_0Adm8-1bZZEHdRy6_ZHDMNVPejUYNZg7Zhty-wKHNv2X2WJBQ')
dp = Dispatcher()
@dp.bot_started()
async def bot_started(event: BotStarted):
await event.bot.send_message(
chat_id=event.chat_id,
text='Привет! Отправь мне /start'
)
@dp.message_created(Command('start'))
async def hello(event: MessageCreated):
await event.message.answer(f"Пример чат-бота для MAX 💙")
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())
```
---
## 📚 Документация
В разработке...
---
## 🧩 Возможности
- ✅ Роутеры
- ✅ Билдер инлайн клавиатур
- ✅ Простая загрузка медиафайлов
- ✅ MagicFilter
- ✅ Внутренние функции моделей
- ✅ Контекстный менеджер
- ✅ Поллинг
- ✅ Вебхук
- ✅ Логгирование
---
## 💬 Обратная связь и поддержка
- MAX: [Чат](https://max.ru/join/IPAok63C3vFqbWTFdutMUtjmrAkGqO56YeAN7iyDfc8)
- Telegram: [@loveappless](https://t.me/loveappless)
---
## 📄 Лицензия
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для подробностей.

BIN
example/audio.mp3 Normal file

Binary file not shown.

31
example/echo_example.py Normal file
View File

@ -0,0 +1,31 @@
import asyncio
import logging
from maxapi import Bot, Dispatcher
from maxapi.types import BotStarted, Command, MessageCreated
logging.basicConfig(level=logging.INFO)
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
@dp.bot_started()
async def bot_started(event: BotStarted):
await event.bot.send_message(
chat_id=event.chat_id,
text='Привет! Отправь мне /start'
)
@dp.message_created(Command('start'))
async def hello(event: MessageCreated):
await event.message.answer(f"Пример чат-бота для MAX 💙")
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@ -3,19 +3,21 @@ import logging
from maxapi import Bot, Dispatcher, F
from maxapi.context import MemoryContext, State, StatesGroup
from maxapi.types import Command, MessageCreated, CallbackButton, MessageCallback
from maxapi.types import BotStarted, Command, MessageCreated, CallbackButton, MessageCallback, BotCommand
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
from example.for_example import router
from example.router_for_example import router
logging.basicConfig(level=logging.INFO)
bot = Bot('токен')
bot = Bot('тут_ваш_токен')
dp = Dispatcher()
dp.include_routers(router)
start_text = '''Мои команды:
start_text = '''Пример чат-бота для MAX 💙
Мои команды:
/clear очищает ваш контекст
/state или /context показывают ваше контекстное состояние
@ -33,27 +35,35 @@ async def _():
logging.info('Бот стартовал!')
@dp.bot_started()
async def bot_started(event: BotStarted):
await event.bot.send_message(
chat_id=event.chat_id,
text='Привет! Отправь мне /start'
)
@dp.message_created(Command('clear'))
async def hello(obj: MessageCreated, context: MemoryContext):
async def hello(event: MessageCreated, context: MemoryContext):
await context.clear()
await obj.message.answer(f"Ваш контекст был очищен!")
await event.message.answer(f"Ваш контекст был очищен!")
@dp.message_created(Command('data'))
async def hello(obj: MessageCreated, context: MemoryContext):
async def hello(event: MessageCreated, context: MemoryContext):
data = await context.get_data()
await obj.message.answer(f"Ваша контекстная память: {str(data)}")
await event.message.answer(f"Ваша контекстная память: {str(data)}")
@dp.message_created(Command('context'))
@dp.message_created(Command('state'))
async def hello(obj: MessageCreated, context: MemoryContext):
async def hello(event: MessageCreated, context: MemoryContext):
data = await context.get_state()
await obj.message.answer(f"Ваше контекстное состояние: {str(data)}")
await event.message.answer(f"Ваше контекстное состояние: {str(data)}")
@dp.message_created(Command('start'))
async def hello(obj: MessageCreated):
async def hello(event: MessageCreated):
builder = InlineKeyboardBuilder()
builder.row(
@ -73,49 +83,73 @@ async def hello(obj: MessageCreated):
)
)
await obj.message.answer(
await event.message.answer(
text=start_text,
attachments=[builder.as_markup()] # Для MAX клавиатура это вложение,
) # поэтому она в списке вложений
attachments=[
builder.as_markup(),
] # Для MAX клавиатура это вложение,
) # поэтому она в списке вложений
@dp.message_callback(F.callback.payload == 'btn_1')
async def hello(obj: MessageCallback, context: MemoryContext):
async def hello(event: MessageCallback, context: MemoryContext):
await context.set_state(Form.name)
await obj.message.delete()
await obj.message.answer(f'Отправьте свое имя:')
await event.message.delete()
await event.message.answer(f'Отправьте свое имя:')
@dp.message_callback(F.callback.payload == 'btn_2')
async def hello(obj: MessageCallback, context: MemoryContext):
async def hello(event: MessageCallback, context: MemoryContext):
await context.set_state(Form.age)
await obj.message.delete()
await obj.message.answer(f'Отправьте ваш возраст:')
await event.message.delete()
await event.message.answer(f'Отправьте ваш возраст:')
@dp.message_callback(F.callback.payload == 'btn_3')
async def hello(obj: MessageCallback, context: MemoryContext):
await obj.message.delete()
await obj.message.answer(f'Ну ладно 🥲')
async def hello(event: MessageCallback, context: MemoryContext):
await event.message.delete()
await event.message.answer(f'Ну ладно 🥲')
@dp.message_created(F.message.body.text, Form.name)
async def hello(obj: MessageCreated, context: MemoryContext):
await context.update_data(name=obj.message.body.text)
async def hello(event: MessageCreated, context: MemoryContext):
await context.update_data(name=event.message.body.text)
data = await context.get_data()
await obj.message.answer(f"Приятно познакомиться, {data['name'].title()}!")
await event.message.answer(f"Приятно познакомиться, {data['name'].title()}!")
@dp.message_created(F.message.body.text, Form.age)
async def hello(obj: MessageCreated, context: MemoryContext):
await context.update_data(age=obj.message.body.text)
async def hello(event: MessageCreated, context: MemoryContext):
await context.update_data(age=event.message.body.text)
await obj.message.answer(f"Ого! А мне всего пару недель 😁")
await event.message.answer(f"Ого! А мне всего пару недель 😁")
async def main():
await bot.set_my_commands(
BotCommand(
name='/start',
description='Перезапустить бота'
),
BotCommand(
name='/clear',
description='Очищает ваш контекст'
),
BotCommand(
name='/state',
description='Показывают ваше контекстное состояние'
),
BotCommand(
name='/data',
description='Показывает вашу контекстную память'
),
BotCommand(
name='/context',
description='Показывают ваше контекстное состояние'
)
)
await dp.start_polling(bot)
# await dp.handle_webhook(
# bot=bot,

View File

@ -1,10 +0,0 @@
from maxapi import F, Router
from maxapi.types import Command, MessageCreated
router = Router()
@router.message_created(Command('router'))
async def hello(obj: MessageCreated):
file = __file__.split('\\')[-1]
await obj.message.answer(f"Пишу тебе из роута {file}")

View File

@ -0,0 +1,24 @@
from maxapi import F, Router
from maxapi.types import Command, MessageCreated
from maxapi.types import InputMedia
router = Router()
file = __file__.split('\\')[-1]
@router.message_created(Command('router'))
async def hello(obj: MessageCreated):
await obj.message.answer(f"Пишу тебе из роута {file}")
# новая команда для примера, /media,
# пример использования: /media image.png (медиафайл берется указанному пути)
@router.message_created(Command('media'))
async def hello(event: MessageCreated):
await event.message.answer(
attachments=[
InputMedia(
path=event.message.body.text.replace('/media ', '')
)
]
)

View File

@ -1,6 +1,7 @@
from datetime import datetime
from typing import Any, Dict, List, TYPE_CHECKING
from .methods.get_upload_url import GetUploadURL
from .methods.get_updates import GetUpdates
from .methods.remove_member_chat import RemoveMemberChat
from .methods.add_admin_chat import AddAdminChat
@ -30,11 +31,13 @@ from .methods.send_message import SendMessage
from .enums.parse_mode import ParseMode
from .enums.sender_action import SenderAction
from .enums.upload_type import UploadType
from .types.attachments.attachment import Attachment
from .types.attachments.image import PhotoAttachmentRequestPayload
from .types.message import NewMessageLink
from .types.users import BotCommand, ChatAdmin
from .types.users import ChatAdmin
from .types.command import BotCommand
from .connection.base import BaseConnection
@ -46,12 +49,11 @@ class Bot(BaseConnection):
def __init__(self, token: str):
super().__init__()
self.bot = self
self.__token = token
self.params = {
'access_token': self.__token
}
self.params = {'access_token': self.__token}
self.marker_updates = None
async def send_message(
@ -335,4 +337,22 @@ class Bot(BaseConnection):
):
return await GetUpdates(
bot=self,
).request()
async def get_upload_url(
self,
type: UploadType
):
return await GetUploadURL(
bot=self,
type=type
).request()
async def set_my_commands(
self,
*commands: BotCommand
):
return await ChangeInfo(
bot=self,
commands=list(commands)
).request()

View File

@ -1,10 +1,17 @@
import os
from typing import TYPE_CHECKING
import aiohttp
from pydantic import BaseModel
from ..types.errors import Error
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..loggers import logger_bot
from ..enums.upload_type import UploadType
from ..loggers import logger_bot, logger_connection
if TYPE_CHECKING:
from ..bot import Bot
class BaseConnection:
@ -12,14 +19,14 @@ class BaseConnection:
API_URL = 'https://botapi.max.ru'
def __init__(self):
self.bot = None
self.session = None
self.bot: 'Bot' = None
self.session: aiohttp.ClientSession = None
async def request(
self,
method: HTTPMethod,
path: ApiPath,
model: BaseModel,
model: BaseModel = None,
is_return_raw: bool = False,
**kwargs
):
@ -27,15 +34,18 @@ class BaseConnection:
if not self.bot.session:
self.bot.session = aiohttp.ClientSession(self.bot.API_URL)
r = await self.bot.session.request(
method=method.value,
url=path.value if isinstance(path, ApiPath) else path,
**kwargs
)
try:
r = await self.bot.session.request(
method=method.value,
url=path.value if isinstance(path, ApiPath) else path,
**kwargs
)
except aiohttp.ClientConnectorDNSError as e:
return logger_connection.error(f'Ошибка при отправке запроса: {e}')
if not r.ok:
raw = await r.text()
error = Error(code=r.status, text=raw)
raw = await r.json()
error = Error(code=r.status, raw=raw)
logger_bot.error(error)
return error
@ -53,4 +63,32 @@ class BaseConnection:
if hasattr(model, 'bot'):
model.bot = self.bot
return model
return model
async def upload_file(
self,
url: str,
path: str,
type: UploadType
):
with open(path, 'rb') as f:
file_data = f.read()
basename = os.path.basename(path)
name, ext = os.path.splitext(basename)
form = aiohttp.FormData()
form.add_field(
name='data',
value=file_data,
filename=basename,
content_type=f"{type.value}/{ext.lstrip('.')}"
)
async with aiohttp.ClientSession() as session:
response = await session.post(
url=url,
data=form
)
return await response.text()

View File

@ -26,11 +26,14 @@ class MemoryContext:
self._context.update(kwargs)
async def set_state(self, state: State | str = None):
self._state = state
async with self._lock:
self._state = state
async def get_state(self):
return self._state
async with self._lock:
return self._state
async def clear(self):
self._state = None
self._context = {}
async with self._lock:
self._state = None
self._context = {}

View File

@ -3,6 +3,7 @@ from typing import Callable, List
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from uvicorn import Config, Server
from aiohttp import ClientConnectorDNSError
from .filters.handler import Handler
@ -41,6 +42,10 @@ class Dispatcher:
self.user_added = Event(update_type=UpdateType.USER_ADDED, router=self)
self.user_removed = Event(update_type=UpdateType.USER_REMOVED, router=self)
self.on_started = Event(update_type=UpdateType.ON_STARTED, router=self)
async def check_me(self):
me = await self.bot.get_me()
logger_dp.info(f'Бот: @{me.username} id={me.user_id}')
def include_routers(self, *routers: 'Router'):
for router in routers:
@ -57,6 +62,8 @@ class Dispatcher:
return new_ctx
async def handle(self, event_object: UpdateUnion):
is_handled = False
for handler in self.event_handlers:
if not handler.update_type == event_object.update_type:
@ -66,9 +73,9 @@ class Dispatcher:
if not filter_attrs(event_object, *handler.filters):
continue
memory_context = self.get_memory_context(
*event_object.get_ids()
)
ids = event_object.get_ids()
memory_context = self.get_memory_context(*ids)
if not handler.state == await memory_context.get_state() \
and handler.state:
@ -82,18 +89,21 @@ class Dispatcher:
if not key in func_args:
del kwargs[key]
if kwargs:
await handler.func_event(event_object, **kwargs)
else:
await handler.func_event(event_object, **kwargs)
await handler.func_event(event_object, **kwargs)
logger_dp.info(f'Обработано: {event_object.update_type}')
logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}')
is_handled = True
break
if not is_handled:
logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}')
async def start_polling(self, bot: Bot):
self.bot = bot
await self.check_me()
logger_dp.info(f'{len(self.event_handlers)} event handlers started')
logger_dp.info(f'{len(self.event_handlers)} событий на обработку')
if self.on_started_func:
await self.on_started_func()
@ -117,12 +127,15 @@ class Dispatcher:
try:
await self.handle(event)
except Exception as e:
logger_dp.error(f"Ошибка при обработке события: {events['update_type']}: {e}")
logger_dp.error(f"Ошибка при обработке события: {event.update_type}: {e}")
except ClientConnectorDNSError:
logger_dp.error(f'Ошибка подключения: {e}')
except Exception as e:
logger_dp.error(f'Общая ошибка при обработке событий: {e}')
async def handle_webhook(self, bot: Bot, host: str = 'localhost', port: int = 8080):
self.bot = bot
await self.check_me()
if self.on_started_func:
await self.on_started_func()

View File

@ -10,4 +10,5 @@ class ApiPath(str, Enum):
ACTIONS = '/actions'
PIN = '/pin'
MEMBERS = '/members'
ADMINS = '/admins'
ADMINS = '/admins'
UPLOADS = '/uploads'

View File

@ -0,0 +1,9 @@
from enum import Enum
class ChatStatus(str, Enum):
ACTIVE = 'active'
REMOVED = 'removed'
LEFT = 'left'
CLOSED = 'closed'
SUSPENDED = 'suspended'

View File

@ -0,0 +1,8 @@
from enum import Enum
class UploadType(str, Enum):
IMAGE = 'image'
VIDEO = 'video'
AUDIO = 'audio'
FILE = 'file'

View File

@ -5,6 +5,7 @@ from magic_filter import F, MagicFilter
from ..types.command import Command
from ..context.state_machine import State
from ..enums.update import UpdateType
from ..loggers import logger_dp
class Handler:
@ -28,4 +29,7 @@ class Handler:
elif isinstance(arg, State):
self.state = arg
elif isinstance(arg, Command):
self.filters.insert(0, F.message.body.text == arg.command)
self.filters.insert(0, F.message.body.text.startswith(arg.command))
else:
logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при '
f'регистрации функции `{func_event.__name__}`')

View File

@ -1,4 +1,5 @@
import logging
logger_bot = logging.getLogger('bot')
logger_connection = logging.getLogger('connection')
logger_dp = logging.getLogger('dispatcher')

View File

@ -1,6 +1,3 @@
from re import findall
from typing import TYPE_CHECKING, List
from .types.added_admin_chat import AddedListAdminChat

View File

@ -1,6 +1,3 @@
from re import findall
from typing import TYPE_CHECKING, List
from ..methods.types.added_members_chat import AddedMembersChat

View File

@ -1,8 +1,7 @@
from typing import Any, Dict, List, TYPE_CHECKING
from ..types.users import BotCommand, User
from ..types.users import User
from ..types.command import BotCommand
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -5,10 +5,8 @@ from typing import Any, Dict, List, TYPE_CHECKING
from collections import Counter
from ..types.attachments.image import PhotoAttachmentRequestPayload
from ..types.chats import Chat
from ..types.users import BotCommand, User
from ..types.command import Command
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -1,12 +1,7 @@
from re import findall
from typing import TYPE_CHECKING
from ..types.chats import Chat
from ..types.users import User
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -1,12 +1,8 @@
from re import findall
from typing import TYPE_CHECKING
from ..types.chats import Chat
from ..types.users import User
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -4,8 +4,6 @@ from typing import TYPE_CHECKING
from ..types.chats import Chats
from ..types.users import User
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -1,6 +1,3 @@
from re import findall
from typing import TYPE_CHECKING
from ..methods.types.getted_list_admin_chat import GettedListAdminChat

View File

@ -20,7 +20,7 @@ class GetMe(BaseConnection):
def __init__(self, bot: 'Bot'):
self.bot = bot
async def request(self) -> Chats:
async def request(self) -> User:
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.ME,

View File

@ -2,9 +2,7 @@
from typing import TYPE_CHECKING
from ..types.chats import ChatMember, Chats
from ..types.users import User
from ..types.chats import ChatMember
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -1,6 +1,3 @@
from re import findall
from typing import TYPE_CHECKING, List
from ..methods.types.getted_members_chat import GettedMembersChat

View File

@ -0,0 +1,35 @@
from typing import TYPE_CHECKING
from ..methods.types.getted_upload_url import GettedUploadUrl
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..enums.upload_type import UploadType
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class GetUploadURL(BaseConnection):
def __init__(
self,
bot: 'Bot',
type: UploadType
):
self.bot = bot
self.type = type
async def request(self) -> GettedUploadUrl:
params = self.bot.params.copy()
params['type'] = self.type.value
return await super().request(
method=HTTPMethod.POST,
path=ApiPath.UPLOADS,
model=GettedUploadUrl,
params=params,
)

View File

@ -1,20 +1,33 @@
import asyncio
from typing import List, TYPE_CHECKING
from json import loads as json_loads
from ..enums.upload_type import UploadType
from ..types.attachments.upload import AttachmentPayload, AttachmentUpload
from ..types.errors import Error
from .types.sended_message import SendedMessage
from ..types.message import NewMessageLink
from ..types.input_media import InputMedia
from ..types.attachments.attachment import Attachment
from ..enums.parse_mode import ParseMode
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
from ..loggers import logger_bot
if TYPE_CHECKING:
from ..bot import Bot
class UploadResponse:
token: str = None
class SendMessage(BaseConnection):
def __init__(
self,
@ -23,7 +36,7 @@ class SendMessage(BaseConnection):
user_id: int = None,
disable_link_preview: bool = False,
text: str = None,
attachments: List[Attachment] = None,
attachments: List[Attachment | InputMedia] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
@ -38,10 +51,41 @@ class SendMessage(BaseConnection):
self.notify = notify
self.parse_mode = parse_mode
async def __process_input_media(
self,
att: InputMedia
):
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 att.type in (UploadType.VIDEO, UploadType.AUDIO):
token = upload.token
elif att.type == UploadType.FILE:
json_r = json_loads(upload_file_response)
token = json_r['token']
elif att.type == UploadType.IMAGE:
json_r = json_loads(upload_file_response)
json_r_keys = list(json_r['photos'].keys())
token = json_r['photos'][json_r_keys[0]]['token']
return AttachmentUpload(
type=att.type,
payload=AttachmentPayload(
token=token
)
)
async def request(self) -> SendedMessage:
params = self.bot.params.copy()
json = {}
json = {'attachments': []}
if self.chat_id: params['chat_id'] = self.chat_id
elif self.user_id: params['user_id'] = self.user_id
@ -49,17 +93,37 @@ class SendMessage(BaseConnection):
json['text'] = self.text
json['disable_link_preview'] = str(self.disable_link_preview).lower()
if self.attachments: json['attachments'] = \
[att.model_dump() for att in self.attachments]
if self.attachments:
for att in self.attachments:
if isinstance(att, InputMedia):
input_media = await self.__process_input_media(att)
json['attachments'].append(
input_media.model_dump()
)
else:
json['attachments'].append(att.model_dump())
if not self.link is None: json['link'] = self.link.model_dump()
if not self.notify is None: json['notify'] = self.notify
if not self.parse_mode is None: json['format'] = self.parse_mode.value
return await super().request(
method=HTTPMethod.POST,
path=ApiPath.MESSAGES,
model=SendedMessage,
params=params,
json=json
)
response = None
for attempt in range(5):
response = await super().request(
method=HTTPMethod.POST,
path=ApiPath.MESSAGES,
model=SendedMessage,
params=params,
json=json
)
if isinstance(response, Error):
if response.raw.get('code') == 'attachment.not.ready':
logger_bot.info(f'Ошибка при отправке загруженного медиа, попытка {attempt+1}, жду 2 секунды')
await asyncio.sleep(2)
continue
return response
return response

View File

@ -0,0 +1,9 @@
from typing import Any, Optional
from pydantic import BaseModel
from ...types.message import Message
class GettedUploadUrl(BaseModel):
url: Optional[str] = None
token: Optional[str] = None

View File

@ -0,0 +1,5 @@
from pydantic import BaseModel
class UploadFileResponse(BaseModel):
...

View File

@ -21,9 +21,13 @@ from ..types.attachments.buttons.link_button import LinkButton
from ..types.attachments.buttons.request_contact import RequestContact
from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton
from ..types.command import Command
from ..types.command import Command, BotCommand
from .input_media import InputMedia
__all__ = [
InputMedia,
BotCommand,
CallbackButton,
ChatButton,
LinkButton,

View File

@ -1,6 +1,8 @@
from typing import List, Optional, Union
from pydantic import BaseModel
from ...types.attachments.upload import AttachmentUpload
from ...types.attachments.buttons import InlineButtonUnion
from ...types.users import User
from ...enums.attachment import AttachmentType
@ -36,6 +38,7 @@ class ButtonsPayload(BaseModel):
class Attachment(BaseModel):
type: AttachmentType
payload: Optional[Union[
AttachmentUpload,
PhotoAttachmentPayload,
OtherAttachmentPayload,
ContactAttachmentPayload,

View File

@ -0,0 +1,14 @@
from pydantic import BaseModel
from ...enums.upload_type import UploadType
class AttachmentPayload(BaseModel):
token: str
class AttachmentUpload(BaseModel):
type: UploadType
payload: AttachmentPayload

View File

@ -1,10 +1,8 @@
from typing import List, Optional, Union
from typing import Optional
from pydantic import BaseModel
from ..types.users import User
from ..types.users import User
class Callback(BaseModel):
timestamp: int

View File

@ -1,23 +1,14 @@
from pydantic import BaseModel, field_validator
from typing import Dict, List, Optional
from enum import Enum
from datetime import datetime
from ..enums.chat_status import ChatStatus
from ..enums.chat_type import ChatType
from ..enums.chat_permission import ChatPermission
from ..types.users import User
from ..types.message import Message
class ChatType(str, Enum):
DIALOG = "dialog"
CHAT = "chat"
class ChatStatus(str, Enum):
ACTIVE = "active"
REMOVED = "removed"
LEFT = "left"
CLOSED = "closed"
SUSPENDED = "suspended"
class Icon(BaseModel):
url: str

View File

@ -1,4 +1,8 @@
from typing import Optional
from pydantic import BaseModel
class Command:
def __init__(self, text: str, prefix: str = '/'):
self.text = text
@ -6,4 +10,9 @@ class Command:
@property
def command(self):
return self.prefix + self.text
return self.prefix + self.text
class BotCommand(BaseModel):
name: str
description: Optional[str] = None

View File

@ -3,4 +3,4 @@ from pydantic import BaseModel
class Error(BaseModel):
code: int
text: str
raw: dict

View File

@ -0,0 +1,24 @@
import mimetypes
from ..enums.upload_type import UploadType
class InputMedia:
def __init__(self, path: str):
self.path = path
self.type = self.__detect_file_type(path)
def __detect_file_type(self, path: str) -> UploadType:
mime_type, _ = mimetypes.guess_type(path)
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

@ -51,7 +51,7 @@ class MessageCallback(Update):
bot: Optional[Bot]
def get_ids(self):
return (self.message.recipient.chat_id, self.message.recipient.user_id)
return (self.message.recipient.chat_id, self.callback.user.user_id)
async def answer(
self,

View File

@ -19,4 +19,4 @@ class MessageCreated(Update):
bot: Optional[Bot]
def get_ids(self):
return (self.message.recipient.chat_id, self.message.recipient.user_id)
return (self.message.recipient.chat_id, self.message.sender.user_id)

View File

@ -3,11 +3,7 @@ from typing import List, Optional
from datetime import datetime
from ..enums.chat_permission import ChatPermission
class BotCommand(BaseModel):
name: str
description: Optional[str] = None
from ..types.command import BotCommand
class User(BaseModel):

View File

@ -1,5 +0,0 @@
aiohttp==3.11.16
fastapi==0.115.13
magic_filter==1.0.12
pydantic==2.11.7
uvicorn==0.34.3

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="maxapi",
version="0.1",
version="0.3",
packages=find_packages(),
description="Библиотека для взаимодействия с API мессенджера MAX",
long_description=open("README.md", encoding='utf-8').read(),
@ -16,5 +16,9 @@ setup(
'pydantic==2.11.7',
'uvicorn==0.34.3'
],
license='MIT',
classifiers=[
'License :: OSI Approved :: MIT License',
],
python_requires=">=3.10",
)