Compare commits
4 Commits
9591780152
...
68748d0899
Author | SHA1 | Date | |
---|---|---|---|
68748d0899 | |||
6560fe011d | |||
1374d863f0 | |||
85f58913c3 |
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
97
README.md
97
README.md
@ -1,29 +1,84 @@
|
|||||||
# maxapi
|
# Асинхронный MAX API
|
||||||
|
|
||||||
#### Библиотека (like aiogram) для взаимодействия с мессенджером MAX
|
[](https://pypi.org/project/maxapi/)
|
||||||
|
[](https://pypi.org/project/maxapi/)
|
||||||
|
[](https://love-apples/maxapi/blob/main/LICENSE)
|
||||||
|
|
||||||
Информация на данный момент:
|
---
|
||||||
* Проект тестируется и активно дорабатывается
|
|
||||||
* На данный момент имеется:
|
|
||||||
|
|
||||||
* Роутеры
|
|
||||||
* Билдер инлайн клавиатур
|
|
||||||
* Этакая машина состояний и контекст к нему
|
|
||||||
* Поллинг и вебхук методы запуска
|
|
||||||
* Логгирование
|
|
||||||
|
|
||||||
|
## 📦 Установка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
Пример бота описан в example.py
|
pip install maxapi
|
||||||
Перед запуском примера установите зависимости:
|
|
||||||
|
|
||||||
pip install maxapi==0.1
|
|
||||||
|
|
||||||
Запуск бота из папки example:
|
|
||||||
|
|
||||||
python example.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Контакты
|
## 🚀 Быстрый старт
|
||||||
[Группа 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
BIN
example/audio.mp3
Normal file
Binary file not shown.
31
example/echo_example.py
Normal file
31
example/echo_example.py
Normal 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())
|
@ -3,19 +3,21 @@ import logging
|
|||||||
|
|
||||||
from maxapi import Bot, Dispatcher, F
|
from maxapi import Bot, Dispatcher, F
|
||||||
from maxapi.context import MemoryContext, State, StatesGroup
|
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 maxapi.utils.inline_keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
from example.for_example import router
|
from example.router_for_example import router
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
bot = Bot('токен')
|
bot = Bot('тут_ваш_токен')
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
dp.include_routers(router)
|
dp.include_routers(router)
|
||||||
|
|
||||||
|
|
||||||
start_text = '''Мои команды:
|
start_text = '''Пример чат-бота для MAX 💙
|
||||||
|
|
||||||
|
Мои команды:
|
||||||
|
|
||||||
/clear очищает ваш контекст
|
/clear очищает ваш контекст
|
||||||
/state или /context показывают ваше контекстное состояние
|
/state или /context показывают ваше контекстное состояние
|
||||||
@ -33,27 +35,35 @@ async def _():
|
|||||||
logging.info('Бот стартовал!')
|
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'))
|
@dp.message_created(Command('clear'))
|
||||||
async def hello(obj: MessageCreated, context: MemoryContext):
|
async def hello(event: MessageCreated, context: MemoryContext):
|
||||||
await context.clear()
|
await context.clear()
|
||||||
await obj.message.answer(f"Ваш контекст был очищен!")
|
await event.message.answer(f"Ваш контекст был очищен!")
|
||||||
|
|
||||||
|
|
||||||
@dp.message_created(Command('data'))
|
@dp.message_created(Command('data'))
|
||||||
async def hello(obj: MessageCreated, context: MemoryContext):
|
async def hello(event: MessageCreated, context: MemoryContext):
|
||||||
data = await context.get_data()
|
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('context'))
|
||||||
@dp.message_created(Command('state'))
|
@dp.message_created(Command('state'))
|
||||||
async def hello(obj: MessageCreated, context: MemoryContext):
|
async def hello(event: MessageCreated, context: MemoryContext):
|
||||||
data = await context.get_state()
|
data = await context.get_state()
|
||||||
await obj.message.answer(f"Ваше контекстное состояние: {str(data)}")
|
await event.message.answer(f"Ваше контекстное состояние: {str(data)}")
|
||||||
|
|
||||||
|
|
||||||
@dp.message_created(Command('start'))
|
@dp.message_created(Command('start'))
|
||||||
async def hello(obj: MessageCreated):
|
async def hello(event: MessageCreated):
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
builder.row(
|
builder.row(
|
||||||
@ -73,49 +83,73 @@ async def hello(obj: MessageCreated):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await obj.message.answer(
|
await event.message.answer(
|
||||||
text=start_text,
|
text=start_text,
|
||||||
attachments=[builder.as_markup()] # Для MAX клавиатура это вложение,
|
attachments=[
|
||||||
) # поэтому она в списке вложений
|
builder.as_markup(),
|
||||||
|
] # Для MAX клавиатура это вложение,
|
||||||
|
) # поэтому она в списке вложений
|
||||||
|
|
||||||
|
|
||||||
@dp.message_callback(F.callback.payload == 'btn_1')
|
@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 context.set_state(Form.name)
|
||||||
await obj.message.delete()
|
await event.message.delete()
|
||||||
await obj.message.answer(f'Отправьте свое имя:')
|
await event.message.answer(f'Отправьте свое имя:')
|
||||||
|
|
||||||
|
|
||||||
@dp.message_callback(F.callback.payload == 'btn_2')
|
@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 context.set_state(Form.age)
|
||||||
await obj.message.delete()
|
await event.message.delete()
|
||||||
await obj.message.answer(f'Отправьте ваш возраст:')
|
await event.message.answer(f'Отправьте ваш возраст:')
|
||||||
|
|
||||||
|
|
||||||
@dp.message_callback(F.callback.payload == 'btn_3')
|
@dp.message_callback(F.callback.payload == 'btn_3')
|
||||||
async def hello(obj: MessageCallback, context: MemoryContext):
|
async def hello(event: MessageCallback, context: MemoryContext):
|
||||||
await obj.message.delete()
|
await event.message.delete()
|
||||||
await obj.message.answer(f'Ну ладно 🥲')
|
await event.message.answer(f'Ну ладно 🥲')
|
||||||
|
|
||||||
|
|
||||||
@dp.message_created(F.message.body.text, Form.name)
|
@dp.message_created(F.message.body.text, Form.name)
|
||||||
async def hello(obj: MessageCreated, context: MemoryContext):
|
async def hello(event: MessageCreated, context: MemoryContext):
|
||||||
await context.update_data(name=obj.message.body.text)
|
await context.update_data(name=event.message.body.text)
|
||||||
|
|
||||||
data = await context.get_data()
|
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)
|
@dp.message_created(F.message.body.text, Form.age)
|
||||||
async def hello(obj: MessageCreated, context: MemoryContext):
|
async def hello(event: MessageCreated, context: MemoryContext):
|
||||||
await context.update_data(age=obj.message.body.text)
|
await context.update_data(age=event.message.body.text)
|
||||||
|
|
||||||
await obj.message.answer(f"Ого! А мне всего пару недель 😁")
|
await event.message.answer(f"Ого! А мне всего пару недель 😁")
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
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.start_polling(bot)
|
||||||
# await dp.handle_webhook(
|
# await dp.handle_webhook(
|
||||||
# bot=bot,
|
# bot=bot,
|
||||||
|
@ -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}")
|
|
24
example/router_for_example.py
Normal file
24
example/router_for_example.py
Normal 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 ', '')
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, TYPE_CHECKING
|
from typing import Any, Dict, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
from .methods.get_upload_url import GetUploadURL
|
||||||
from .methods.get_updates import GetUpdates
|
from .methods.get_updates import GetUpdates
|
||||||
from .methods.remove_member_chat import RemoveMemberChat
|
from .methods.remove_member_chat import RemoveMemberChat
|
||||||
from .methods.add_admin_chat import AddAdminChat
|
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.parse_mode import ParseMode
|
||||||
from .enums.sender_action import SenderAction
|
from .enums.sender_action import SenderAction
|
||||||
|
from .enums.upload_type import UploadType
|
||||||
|
|
||||||
from .types.attachments.attachment import Attachment
|
from .types.attachments.attachment import Attachment
|
||||||
from .types.attachments.image import PhotoAttachmentRequestPayload
|
from .types.attachments.image import PhotoAttachmentRequestPayload
|
||||||
from .types.message import NewMessageLink
|
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
|
from .connection.base import BaseConnection
|
||||||
|
|
||||||
@ -46,12 +49,11 @@ class Bot(BaseConnection):
|
|||||||
|
|
||||||
def __init__(self, token: str):
|
def __init__(self, token: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.bot = self
|
self.bot = self
|
||||||
|
|
||||||
self.__token = token
|
self.__token = token
|
||||||
self.params = {
|
self.params = {'access_token': self.__token}
|
||||||
'access_token': self.__token
|
|
||||||
}
|
|
||||||
self.marker_updates = None
|
self.marker_updates = None
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
@ -335,4 +337,22 @@ class Bot(BaseConnection):
|
|||||||
):
|
):
|
||||||
return await GetUpdates(
|
return await GetUpdates(
|
||||||
bot=self,
|
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()
|
).request()
|
@ -1,10 +1,17 @@
|
|||||||
|
import os
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..types.errors import Error
|
from ..types.errors import Error
|
||||||
from ..enums.http_method import HTTPMethod
|
from ..enums.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
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:
|
class BaseConnection:
|
||||||
@ -12,14 +19,14 @@ class BaseConnection:
|
|||||||
API_URL = 'https://botapi.max.ru'
|
API_URL = 'https://botapi.max.ru'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bot = None
|
self.bot: 'Bot' = None
|
||||||
self.session = None
|
self.session: aiohttp.ClientSession = None
|
||||||
|
|
||||||
async def request(
|
async def request(
|
||||||
self,
|
self,
|
||||||
method: HTTPMethod,
|
method: HTTPMethod,
|
||||||
path: ApiPath,
|
path: ApiPath,
|
||||||
model: BaseModel,
|
model: BaseModel = None,
|
||||||
is_return_raw: bool = False,
|
is_return_raw: bool = False,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
@ -27,15 +34,18 @@ class BaseConnection:
|
|||||||
if not self.bot.session:
|
if not self.bot.session:
|
||||||
self.bot.session = aiohttp.ClientSession(self.bot.API_URL)
|
self.bot.session = aiohttp.ClientSession(self.bot.API_URL)
|
||||||
|
|
||||||
r = await self.bot.session.request(
|
try:
|
||||||
method=method.value,
|
r = await self.bot.session.request(
|
||||||
url=path.value if isinstance(path, ApiPath) else path,
|
method=method.value,
|
||||||
**kwargs
|
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:
|
if not r.ok:
|
||||||
raw = await r.text()
|
raw = await r.json()
|
||||||
error = Error(code=r.status, text=raw)
|
error = Error(code=r.status, raw=raw)
|
||||||
logger_bot.error(error)
|
logger_bot.error(error)
|
||||||
return error
|
return error
|
||||||
|
|
||||||
@ -53,4 +63,32 @@ class BaseConnection:
|
|||||||
if hasattr(model, 'bot'):
|
if hasattr(model, 'bot'):
|
||||||
model.bot = self.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()
|
@ -26,11 +26,14 @@ class MemoryContext:
|
|||||||
self._context.update(kwargs)
|
self._context.update(kwargs)
|
||||||
|
|
||||||
async def set_state(self, state: State | str = None):
|
async def set_state(self, state: State | str = None):
|
||||||
self._state = state
|
async with self._lock:
|
||||||
|
self._state = state
|
||||||
|
|
||||||
async def get_state(self):
|
async def get_state(self):
|
||||||
return self._state
|
async with self._lock:
|
||||||
|
return self._state
|
||||||
|
|
||||||
async def clear(self):
|
async def clear(self):
|
||||||
self._state = None
|
async with self._lock:
|
||||||
self._context = {}
|
self._state = None
|
||||||
|
self._context = {}
|
||||||
|
@ -3,6 +3,7 @@ from typing import Callable, List
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from uvicorn import Config, Server
|
from uvicorn import Config, Server
|
||||||
|
from aiohttp import ClientConnectorDNSError
|
||||||
|
|
||||||
from .filters.handler import Handler
|
from .filters.handler import Handler
|
||||||
|
|
||||||
@ -41,6 +42,10 @@ class Dispatcher:
|
|||||||
self.user_added = Event(update_type=UpdateType.USER_ADDED, router=self)
|
self.user_added = Event(update_type=UpdateType.USER_ADDED, router=self)
|
||||||
self.user_removed = Event(update_type=UpdateType.USER_REMOVED, router=self)
|
self.user_removed = Event(update_type=UpdateType.USER_REMOVED, router=self)
|
||||||
self.on_started = Event(update_type=UpdateType.ON_STARTED, 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'):
|
def include_routers(self, *routers: 'Router'):
|
||||||
for router in routers:
|
for router in routers:
|
||||||
@ -57,6 +62,8 @@ class Dispatcher:
|
|||||||
return new_ctx
|
return new_ctx
|
||||||
|
|
||||||
async def handle(self, event_object: UpdateUnion):
|
async def handle(self, event_object: UpdateUnion):
|
||||||
|
is_handled = False
|
||||||
|
|
||||||
for handler in self.event_handlers:
|
for handler in self.event_handlers:
|
||||||
|
|
||||||
if not handler.update_type == event_object.update_type:
|
if not handler.update_type == event_object.update_type:
|
||||||
@ -66,9 +73,9 @@ class Dispatcher:
|
|||||||
if not filter_attrs(event_object, *handler.filters):
|
if not filter_attrs(event_object, *handler.filters):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
memory_context = self.get_memory_context(
|
ids = event_object.get_ids()
|
||||||
*event_object.get_ids()
|
|
||||||
)
|
memory_context = self.get_memory_context(*ids)
|
||||||
|
|
||||||
if not handler.state == await memory_context.get_state() \
|
if not handler.state == await memory_context.get_state() \
|
||||||
and handler.state:
|
and handler.state:
|
||||||
@ -82,18 +89,21 @@ class Dispatcher:
|
|||||||
if not key in func_args:
|
if not key in func_args:
|
||||||
del kwargs[key]
|
del kwargs[key]
|
||||||
|
|
||||||
if kwargs:
|
await handler.func_event(event_object, **kwargs)
|
||||||
await handler.func_event(event_object, **kwargs)
|
|
||||||
else:
|
|
||||||
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
|
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):
|
async def start_polling(self, bot: Bot):
|
||||||
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:
|
if self.on_started_func:
|
||||||
await self.on_started_func()
|
await self.on_started_func()
|
||||||
@ -117,12 +127,15 @@ class Dispatcher:
|
|||||||
try:
|
try:
|
||||||
await self.handle(event)
|
await self.handle(event)
|
||||||
except Exception as e:
|
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:
|
except Exception as e:
|
||||||
logger_dp.error(f'Общая ошибка при обработке событий: {e}')
|
logger_dp.error(f'Общая ошибка при обработке событий: {e}')
|
||||||
|
|
||||||
async def handle_webhook(self, bot: Bot, host: str = 'localhost', port: int = 8080):
|
async def handle_webhook(self, bot: Bot, host: str = 'localhost', port: int = 8080):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
await self.check_me()
|
||||||
|
|
||||||
if self.on_started_func:
|
if self.on_started_func:
|
||||||
await self.on_started_func()
|
await self.on_started_func()
|
||||||
|
@ -10,4 +10,5 @@ class ApiPath(str, Enum):
|
|||||||
ACTIONS = '/actions'
|
ACTIONS = '/actions'
|
||||||
PIN = '/pin'
|
PIN = '/pin'
|
||||||
MEMBERS = '/members'
|
MEMBERS = '/members'
|
||||||
ADMINS = '/admins'
|
ADMINS = '/admins'
|
||||||
|
UPLOADS = '/uploads'
|
9
maxapi/enums/chat_status.py
Normal file
9
maxapi/enums/chat_status.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ChatStatus(str, Enum):
|
||||||
|
ACTIVE = 'active'
|
||||||
|
REMOVED = 'removed'
|
||||||
|
LEFT = 'left'
|
||||||
|
CLOSED = 'closed'
|
||||||
|
SUSPENDED = 'suspended'
|
8
maxapi/enums/upload_type.py
Normal file
8
maxapi/enums/upload_type.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class UploadType(str, Enum):
|
||||||
|
IMAGE = 'image'
|
||||||
|
VIDEO = 'video'
|
||||||
|
AUDIO = 'audio'
|
||||||
|
FILE = 'file'
|
@ -5,6 +5,7 @@ from magic_filter import F, MagicFilter
|
|||||||
from ..types.command import Command
|
from ..types.command import Command
|
||||||
from ..context.state_machine import State
|
from ..context.state_machine import State
|
||||||
from ..enums.update import UpdateType
|
from ..enums.update import UpdateType
|
||||||
|
from ..loggers import logger_dp
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
class Handler:
|
||||||
@ -28,4 +29,7 @@ class Handler:
|
|||||||
elif isinstance(arg, State):
|
elif isinstance(arg, State):
|
||||||
self.state = arg
|
self.state = arg
|
||||||
elif isinstance(arg, Command):
|
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__}`')
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger_bot = logging.getLogger('bot')
|
logger_bot = logging.getLogger('bot')
|
||||||
|
logger_connection = logging.getLogger('connection')
|
||||||
logger_dp = logging.getLogger('dispatcher')
|
logger_dp = logging.getLogger('dispatcher')
|
@ -1,6 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
from re import findall
|
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
from .types.added_admin_chat import AddedListAdminChat
|
from .types.added_admin_chat import AddedListAdminChat
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
from re import findall
|
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
from ..methods.types.added_members_chat import AddedMembersChat
|
from ..methods.types.added_members_chat import AddedMembersChat
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from typing import Any, Dict, List, TYPE_CHECKING
|
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.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
from ..enums.api_path import ApiPath
|
||||||
|
@ -5,10 +5,8 @@ from typing import Any, Dict, List, TYPE_CHECKING
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
from ..types.attachments.image import PhotoAttachmentRequestPayload
|
from ..types.attachments.image import PhotoAttachmentRequestPayload
|
||||||
|
|
||||||
from ..types.chats import Chat
|
from ..types.chats import Chat
|
||||||
|
from ..types.command import Command
|
||||||
from ..types.users import BotCommand, User
|
|
||||||
|
|
||||||
from ..enums.http_method import HTTPMethod
|
from ..enums.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
from ..enums.api_path import ApiPath
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from re import findall
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..types.chats import Chat
|
from ..types.chats import Chat
|
||||||
|
|
||||||
from ..types.users import User
|
|
||||||
|
|
||||||
from ..enums.http_method import HTTPMethod
|
from ..enums.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
from ..enums.api_path import ApiPath
|
||||||
|
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
from re import findall
|
from re import findall
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..types.chats import Chat
|
from ..types.chats import Chat
|
||||||
|
|
||||||
from ..types.users import User
|
|
||||||
|
|
||||||
from ..enums.http_method import HTTPMethod
|
from ..enums.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
from ..enums.api_path import ApiPath
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from ..types.chats import Chats
|
from ..types.chats import Chats
|
||||||
|
|
||||||
from ..types.users import User
|
|
||||||
|
|
||||||
from ..enums.http_method import HTTPMethod
|
from ..enums.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
from ..enums.api_path import ApiPath
|
||||||
|
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
from re import findall
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..methods.types.getted_list_admin_chat import GettedListAdminChat
|
from ..methods.types.getted_list_admin_chat import GettedListAdminChat
|
||||||
|
@ -20,7 +20,7 @@ class GetMe(BaseConnection):
|
|||||||
def __init__(self, bot: 'Bot'):
|
def __init__(self, bot: 'Bot'):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
async def request(self) -> Chats:
|
async def request(self) -> User:
|
||||||
return await super().request(
|
return await super().request(
|
||||||
method=HTTPMethod.GET,
|
method=HTTPMethod.GET,
|
||||||
path=ApiPath.ME,
|
path=ApiPath.ME,
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..types.chats import ChatMember, Chats
|
from ..types.chats import ChatMember
|
||||||
|
|
||||||
from ..types.users import User
|
|
||||||
|
|
||||||
from ..enums.http_method import HTTPMethod
|
from ..enums.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
from ..enums.api_path import ApiPath
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
from re import findall
|
|
||||||
from typing import TYPE_CHECKING, List
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
from ..methods.types.getted_members_chat import GettedMembersChat
|
from ..methods.types.getted_members_chat import GettedMembersChat
|
||||||
|
35
maxapi/methods/get_upload_url.py
Normal file
35
maxapi/methods/get_upload_url.py
Normal 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,
|
||||||
|
)
|
@ -1,20 +1,33 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from typing import List, TYPE_CHECKING
|
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.sended_message import SendedMessage
|
||||||
from ..types.message import NewMessageLink
|
from ..types.message import NewMessageLink
|
||||||
|
from ..types.input_media import InputMedia
|
||||||
from ..types.attachments.attachment import Attachment
|
from ..types.attachments.attachment import Attachment
|
||||||
from ..enums.parse_mode import ParseMode
|
from ..enums.parse_mode import ParseMode
|
||||||
from ..enums.http_method import HTTPMethod
|
from ..enums.http_method import HTTPMethod
|
||||||
from ..enums.api_path import ApiPath
|
from ..enums.api_path import ApiPath
|
||||||
from ..connection.base import BaseConnection
|
from ..connection.base import BaseConnection
|
||||||
|
from ..loggers import logger_bot
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..bot import Bot
|
from ..bot import Bot
|
||||||
|
|
||||||
|
|
||||||
|
class UploadResponse:
|
||||||
|
token: str = None
|
||||||
|
|
||||||
|
|
||||||
class SendMessage(BaseConnection):
|
class SendMessage(BaseConnection):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -23,7 +36,7 @@ class SendMessage(BaseConnection):
|
|||||||
user_id: int = None,
|
user_id: int = None,
|
||||||
disable_link_preview: bool = False,
|
disable_link_preview: bool = False,
|
||||||
text: str = None,
|
text: str = None,
|
||||||
attachments: List[Attachment] = None,
|
attachments: List[Attachment | InputMedia] = None,
|
||||||
link: NewMessageLink = None,
|
link: NewMessageLink = None,
|
||||||
notify: bool = True,
|
notify: bool = True,
|
||||||
parse_mode: ParseMode = None
|
parse_mode: ParseMode = None
|
||||||
@ -38,10 +51,41 @@ class SendMessage(BaseConnection):
|
|||||||
self.notify = notify
|
self.notify = notify
|
||||||
self.parse_mode = parse_mode
|
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:
|
async def request(self) -> SendedMessage:
|
||||||
params = self.bot.params.copy()
|
params = self.bot.params.copy()
|
||||||
|
|
||||||
json = {}
|
json = {'attachments': []}
|
||||||
|
|
||||||
if self.chat_id: params['chat_id'] = self.chat_id
|
if self.chat_id: params['chat_id'] = self.chat_id
|
||||||
elif self.user_id: params['user_id'] = self.user_id
|
elif self.user_id: params['user_id'] = self.user_id
|
||||||
@ -49,17 +93,37 @@ class SendMessage(BaseConnection):
|
|||||||
json['text'] = self.text
|
json['text'] = self.text
|
||||||
json['disable_link_preview'] = str(self.disable_link_preview).lower()
|
json['disable_link_preview'] = str(self.disable_link_preview).lower()
|
||||||
|
|
||||||
if self.attachments: json['attachments'] = \
|
if self.attachments:
|
||||||
[att.model_dump() for att in 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.link is None: json['link'] = self.link.model_dump()
|
||||||
if not self.notify is None: json['notify'] = self.notify
|
if not self.notify is None: json['notify'] = self.notify
|
||||||
if not self.parse_mode is None: json['format'] = self.parse_mode.value
|
if not self.parse_mode is None: json['format'] = self.parse_mode.value
|
||||||
|
|
||||||
return await super().request(
|
response = None
|
||||||
method=HTTPMethod.POST,
|
for attempt in range(5):
|
||||||
path=ApiPath.MESSAGES,
|
response = await super().request(
|
||||||
model=SendedMessage,
|
method=HTTPMethod.POST,
|
||||||
params=params,
|
path=ApiPath.MESSAGES,
|
||||||
json=json
|
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
|
9
maxapi/methods/types/getted_upload_url.py
Normal file
9
maxapi/methods/types/getted_upload_url.py
Normal 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
|
5
maxapi/methods/types/upload_file_response.py
Normal file
5
maxapi/methods/types/upload_file_response.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class UploadFileResponse(BaseModel):
|
||||||
|
...
|
@ -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_contact import RequestContact
|
||||||
from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton
|
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__ = [
|
__all__ = [
|
||||||
|
InputMedia,
|
||||||
|
BotCommand,
|
||||||
CallbackButton,
|
CallbackButton,
|
||||||
ChatButton,
|
ChatButton,
|
||||||
LinkButton,
|
LinkButton,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from ...types.attachments.upload import AttachmentUpload
|
||||||
|
|
||||||
from ...types.attachments.buttons import InlineButtonUnion
|
from ...types.attachments.buttons import InlineButtonUnion
|
||||||
from ...types.users import User
|
from ...types.users import User
|
||||||
from ...enums.attachment import AttachmentType
|
from ...enums.attachment import AttachmentType
|
||||||
@ -36,6 +38,7 @@ class ButtonsPayload(BaseModel):
|
|||||||
class Attachment(BaseModel):
|
class Attachment(BaseModel):
|
||||||
type: AttachmentType
|
type: AttachmentType
|
||||||
payload: Optional[Union[
|
payload: Optional[Union[
|
||||||
|
AttachmentUpload,
|
||||||
PhotoAttachmentPayload,
|
PhotoAttachmentPayload,
|
||||||
OtherAttachmentPayload,
|
OtherAttachmentPayload,
|
||||||
ContactAttachmentPayload,
|
ContactAttachmentPayload,
|
||||||
|
14
maxapi/types/attachments/upload.py
Normal file
14
maxapi/types/attachments/upload.py
Normal 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
|
@ -1,10 +1,8 @@
|
|||||||
from typing import List, Optional, Union
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..types.users import User
|
from ..types.users import User
|
||||||
|
|
||||||
from ..types.users import User
|
|
||||||
|
|
||||||
|
|
||||||
class Callback(BaseModel):
|
class Callback(BaseModel):
|
||||||
timestamp: int
|
timestamp: int
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
from pydantic import BaseModel, field_validator
|
from pydantic import BaseModel, field_validator
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from enum import Enum
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ..enums.chat_status import ChatStatus
|
||||||
|
from ..enums.chat_type import ChatType
|
||||||
from ..enums.chat_permission import ChatPermission
|
from ..enums.chat_permission import ChatPermission
|
||||||
|
|
||||||
from ..types.users import User
|
from ..types.users import User
|
||||||
from ..types.message import Message
|
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):
|
class Icon(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
def __init__(self, text: str, prefix: str = '/'):
|
def __init__(self, text: str, prefix: str = '/'):
|
||||||
self.text = text
|
self.text = text
|
||||||
@ -6,4 +10,9 @@ class Command:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def command(self):
|
def command(self):
|
||||||
return self.prefix + self.text
|
return self.prefix + self.text
|
||||||
|
|
||||||
|
|
||||||
|
class BotCommand(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
@ -3,4 +3,4 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
class Error(BaseModel):
|
class Error(BaseModel):
|
||||||
code: int
|
code: int
|
||||||
text: str
|
raw: dict
|
24
maxapi/types/input_media.py
Normal file
24
maxapi/types/input_media.py
Normal 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
|
@ -51,7 +51,7 @@ class MessageCallback(Update):
|
|||||||
bot: Optional[Bot]
|
bot: Optional[Bot]
|
||||||
|
|
||||||
def get_ids(self):
|
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(
|
async def answer(
|
||||||
self,
|
self,
|
||||||
|
@ -19,4 +19,4 @@ class MessageCreated(Update):
|
|||||||
bot: Optional[Bot]
|
bot: Optional[Bot]
|
||||||
|
|
||||||
def get_ids(self):
|
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)
|
@ -3,11 +3,7 @@ from typing import List, Optional
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from ..enums.chat_permission import ChatPermission
|
from ..enums.chat_permission import ChatPermission
|
||||||
|
from ..types.command import BotCommand
|
||||||
|
|
||||||
class BotCommand(BaseModel):
|
|
||||||
name: str
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
|
@ -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
|
|
6
setup.py
6
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="maxapi",
|
name="maxapi",
|
||||||
version="0.1",
|
version="0.3",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
description="Библиотека для взаимодействия с API мессенджера MAX",
|
description="Библиотека для взаимодействия с API мессенджера MAX",
|
||||||
long_description=open("README.md", encoding='utf-8').read(),
|
long_description=open("README.md", encoding='utf-8').read(),
|
||||||
@ -16,5 +16,9 @@ setup(
|
|||||||
'pydantic==2.11.7',
|
'pydantic==2.11.7',
|
||||||
'uvicorn==0.34.3'
|
'uvicorn==0.34.3'
|
||||||
],
|
],
|
||||||
|
license='MIT',
|
||||||
|
classifiers=[
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
],
|
||||||
python_requires=">=3.10",
|
python_requires=">=3.10",
|
||||||
)
|
)
|
Loading…
x
Reference in New Issue
Block a user