This commit is contained in:
Денис Семёнов 2025-06-15 21:10:31 +03:00
parent 32c6f4aaac
commit a8f60bd767
59 changed files with 1413 additions and 0 deletions

160
.gitignore vendored Normal file
View File

@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

50
example.py Normal file
View File

@ -0,0 +1,50 @@
from maxapi.bot import Bot
from maxapi.dispatcher import Dispatcher
from maxapi.types.updates.message_created import MessageCreated
from maxapi.types.updates.message_callback import MessageCallback
from maxapi.types.attachments.attachment import ButtonsPayload
from maxapi.types.attachments.buttons.callback_button import CallbackButton
from maxapi.types.attachments.attachment import Attachment
from maxapi.enums.attachment import AttachmentType
from maxapi.enums.button_type import ButtonType
from maxapi.enums.intent import Intent
from maxapi.filters import F
bot = Bot('токен')
dp = Dispatcher()
# Отвечает только на текст "Привет"
@dp.message_created(F.message.body.text == 'Привет')
async def hello(obj: MessageCreated):
await obj.message.answer('Привет 👋')
# Отвечает только на текст "Клавиатура"
@dp.message_created(F.message.body.text == 'Клавиатура')
async def hello(obj: MessageCreated):
button_1 = CallbackButton(type=ButtonType.CALLBACK, text='Кнопка 1', payload='1', intent=Intent.DEFAULT)
button_2 = CallbackButton(type=ButtonType.CALLBACK, text='Кнопка 2', payload='2', intent=Intent.DEFAULT)
keyboard = ButtonsPayload(buttons=[[button_1], [button_2]])
attachments = [Attachment(type=AttachmentType.INLINE_KEYBOARD, payload=keyboard)]
await obj.message.answer('Привет 👋', attachments=attachments)
# Ответчает на коллбек с начинкой "1"
@dp.message_callback(F.callback.payload == '1')
async def _(obj: MessageCallback):
await obj.message.answer('Вы нажали на кнопку 1 🤩')
# Ответчает на коллбек с начинкой "2"
@dp.message_callback(F.callback.payload == '2')
async def _(obj: MessageCallback):
await obj.message.answer('Вы нажали на кнопку 2 🥳')
# Отвечает на любое текстовое сообщение
@dp.message_created(F.message.body.text)
async def hello(obj: MessageCreated):
await obj.message.answer(f'Повторяю за вами: {obj.message.body.text}')
dp.handle_webhook(bot)

89
maxapi/bot.py Normal file
View File

@ -0,0 +1,89 @@
from typing import Any, Dict, List
from .methods.edit_message import EditMessage
from .enums.parse_mode import ParseMode
from .types.attachments.attachment import Attachment
from .types.message import NewMessageLink
from .types.users import BotCommand
from .methods.change_info import ChangeInfo
from .methods.get_me import GetMe
from .methods.get_messages import GetMessages
from .methods.get_chats import GetChats
from .methods.send_message import SendMessage
from .connection.base import BaseConnection
class Bot(BaseConnection):
def __init__(self, token: str):
self.__token = token
self.params = {
'access_token': self.__token
}
async def send_message(
self,
chat_id: int = None,
user_id: int = None,
disable_link_preview: bool = False,
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
):
return await SendMessage(
bot=self,
chat_id=chat_id,
user_id=user_id,
disable_link_preview=disable_link_preview,
text=text,
attachments=attachments,
link=link,
notify=notify,
parse_mode=parse_mode
).request()
async def edit_message(
self,
message_id: str,
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
):
return await EditMessage(
bot=self,
message_id=message_id,
text=text,
attachments=attachments,
link=link,
notify=notify,
parse_mode=parse_mode
).request()
async def get_messages(self, chat_id: int = None):
return await GetMessages(self, chat_id).request()
async def get_me(self):
return await GetMe(self).request()
async def change_info(
self,
name: str = None,
description: str = None,
commands: List[BotCommand] = None,
photo: Dict[str, Any] = None
):
return await ChangeInfo(
bot=self,
name=name,
description=description,
commands=commands,
photo=photo
).request()
async def get_chats(self):
return await GetChats(self).request()

36
maxapi/connection/base.py Normal file
View File

@ -0,0 +1,36 @@
import aiohttp
from pydantic import BaseModel
from ..types.errors import Error
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
class BaseConnection:
API_URL = 'https://botapi.max.ru'
async def request(
self,
method: HTTPMethod,
path: ApiPath,
model: BaseModel,
is_return_raw: bool = False,
**kwargs
):
async with aiohttp.ClientSession(self.API_URL) as s:
r = await s.request(
method=method.value,
url=path.value,
**kwargs
)
if not r.ok:
raw = await r.text()
return Error(code=r.status, text=raw)
raw = await r.json()
if is_return_raw: return raw
return model(**raw)

151
maxapi/dispatcher.py Normal file
View File

@ -0,0 +1,151 @@
from typing import Callable, List
import uvicorn
from fastapi import FastAPI, Request
from magic_filter import MagicFilter
from .filters import filter_m
from .types.updates import Update
from .bot import Bot
from .enums.update import UpdateType
from .types.updates.bot_added import BotAdded
from .types.updates.bot_removed import BotRemoved
from .types.updates.bot_started import BotStarted
from .types.updates.chat_title_changed import ChatTitleChanged
from .types.updates.message_callback import MessageCallback
from .types.updates.message_chat_created import MessageChatCreated
from .types.updates.message_created import MessageCreated
from .types.updates.message_edited import MessageEdited
from .types.updates.message_removed import MessageRemoved
from .types.updates.user_added import UserAdded
from .types.updates.user_removed import UserRemoved
from .loggers import logger
class Handler:
def __init__(
self,
*args,
func_event: Callable,
update_type: UpdateType,
**kwargs
):
self.func_event = func_event
self.update_type = update_type
self.filters = []
for arg in args:
if isinstance(arg, MagicFilter):
arg: MagicFilter = arg
self.filters.append(arg)
class Dispatcher:
def __init__(self):
self.event_handlers = []
self.bot = None
self.message_created = Event(update_type=UpdateType.MESSAGE_CREATED, router=self)
self.bot_added = Event(update_type=UpdateType.BOT_ADDED, router=self)
self.bot_removed = Event(update_type=UpdateType.BOT_REMOVED, router=self)
self.bot_started = Event(update_type=UpdateType.BOT_STARTED, router=self)
self.chat_title_changed = Event(update_type=UpdateType.CHAT_TITLE_CHANGED, router=self)
self.message_callback = Event(update_type=UpdateType.MESSAGE_CALLBACK, router=self)
self.message_chat_created = Event(update_type=UpdateType.MESSAGE_CHAT_CREATED, router=self)
self.message_edited = Event(update_type=UpdateType.MESSAGE_EDITED, router=self)
self.message_removed = Event(update_type=UpdateType.MESSAGE_REMOVED, router=self)
self.user_added = Event(update_type=UpdateType.USER_ADDED, router=self)
self.user_removed = Event(update_type=UpdateType.USER_REMOVED, router=self)
def include_routers(self, *routers: 'Router'):
for router in routers:
for event in router.event_handlers:
self.event_handlers.append(event)
def handle_webhook(self, bot: Bot, host: str = '0.0.0.0', port: int = 8080):
self.bot = bot
app = FastAPI()
@app.post("/")
async def _(request: Request):
try:
event_json = await request.json()
event = Update(**event_json)
event_object = None
match event.update_type:
case UpdateType.BOT_ADDED:
event_object = BotAdded(**event_json)
case UpdateType.BOT_REMOVED:
event_object = BotRemoved(**event_json)
case UpdateType.BOT_STARTED:
event_object = BotStarted(**event_json)
case UpdateType.CHAT_TITLE_CHANGED:
event_object = ChatTitleChanged(**event_json)
case UpdateType.MESSAGE_CALLBACK:
event_object = MessageCallback(**event_json)
event_object.message.bot = self.bot
case UpdateType.MESSAGE_CHAT_CREATED:
event_object = MessageChatCreated(**event_json)
case UpdateType.MESSAGE_CREATED:
event_object = MessageCreated(**event_json)
event_object.message.bot = self.bot
case UpdateType.MESSAGE_EDITED:
event_object = MessageEdited(**event_json)
case UpdateType.MESSAGE_REMOVED:
event_object = MessageRemoved(**event_json)
case UpdateType.USER_ADDED:
event_object = UserAdded(**event_json)
case UpdateType.USER_REMOVED:
event_object = UserRemoved(**event_json)
handlers: List[Handler] = self.event_handlers
for handler in handlers:
if not handler.update_type == event.update_type:
continue
if handler.filters:
if not filter_m(event_object, *handler.filters):
continue
await handler.func_event(event_object)
break
return True
except Exception as e:
print(e)
...
logger.info(f'{len(self.event_handlers)} event handlers started')
uvicorn.run(app, host=host, port=port, log_level='critical')
class Router(Dispatcher):
def __init__(self):
super().__init__()
class Event:
def __init__(self, update_type: UpdateType, router: Dispatcher | Router):
self.update_type = update_type
self.router = router
def __call__(self, *args, **kwargs):
def decorator(func_event: Callable):
self.router.event_handlers.append(
Handler(
func_event=func_event,
update_type=self.update_type,
*args, **kwargs
)
)
return func_event
return decorator

7
maxapi/enums/api_path.py Normal file
View File

@ -0,0 +1,7 @@
from enum import Enum
class ApiPath(str, Enum):
ME = '/me'
CHATS = '/chats'
MESSAGES = '/messages'
UPDATES = '/updates'

View File

@ -0,0 +1,11 @@
from enum import Enum
class AttachmentType(str, Enum):
IMAGE = 'image'
VIDEO = 'video'
AUDIO = 'audio'
FILE = 'file'
STICKER = 'sticker'
CONTACT = 'contact'
INLINE_KEYBOARD = 'inline_keyboard'
LOCATION = 'location'

View File

@ -0,0 +1,9 @@
from enum import Enum
class ButtonType(Enum):
REQUEST_CONTACT = 'request_contact'
CALLBACK = 'callback'
LINK = 'link'
REQUEST_GEO_LOCATION = 'request_geo_location'
CHAT = 'chat'

View File

@ -0,0 +1,6 @@
from enum import Enum
class ChatType(str, Enum):
DIALOG = 'dialog'
CHAT = 'chat'

View File

@ -0,0 +1,8 @@
from enum import Enum
class HTTPMethod(str, Enum):
POST = 'POST'
GET = 'GET'
PATCH = 'PATCH'
PUT = 'PUT'

6
maxapi/enums/intent.py Normal file
View File

@ -0,0 +1,6 @@
from enum import Enum
class Intent(str, Enum):
DEFAULT = 'default'
POSITIVE = 'positive'
NEGATIVE = 'negative'

View File

@ -0,0 +1,6 @@
from enum import Enum
class MessageLinkType(str, Enum):
FORWARD = 'forward'
REPLY = 'reply'

View File

@ -0,0 +1,5 @@
from enum import Enum
class ParseMode(str, Enum):
MARKDOWN = 'markdown'
HTML = 'html'

View File

@ -0,0 +1,13 @@
from enum import Enum
class TextStyle(Enum):
UNDERLINE = 'underline'
STRONG = 'strong'
EMPHASIZED = 'emphasized'
MONOSPACED = 'monospaced'
LINK = 'link'
STRIKETHROUGH = 'strikethrough'
USER_MENTION = 'user_mention'
HEADING = 'heading'
HIGHLIGHTED = 'highlighted'

14
maxapi/enums/update.py Normal file
View File

@ -0,0 +1,14 @@
from enum import Enum
class UpdateType(str, Enum):
MESSAGE_CREATED = 'message_created'
BOT_ADDED = 'bot_added'
BOT_REMOVED = 'bot_removed'
BOT_STARTED = 'bot_started'
CHAT_TITLE_CHANGED = 'chat_title_changed'
MESSAGE_CALLBACK = 'message_callback'
MESSAGE_CHAT_CREATED = 'message_chat_created'
MESSAGE_EDITED = 'message_edited'
MESSAGE_REMOVED = 'message_removed'
USER_ADDED = 'user_added'
USER_REMOVED = 'user_removed'

View File

@ -0,0 +1,53 @@
from magic_filter import MagicFilter
from magic_filter.operations.call import CallOperation as mf_call
from magic_filter.operations.function import FunctionOperation as mf_func
from magic_filter.operations.comparator import ComparatorOperation as mf_comparator
F = MagicFilter()
def filter_m(obj, *magic_args):
try:
for arg in magic_args:
attr_last = None
method_found = False
operations = arg._operations
if isinstance(operations[-1], mf_call):
operations = operations[:len(operations)-2]
method_found = True
elif isinstance(operations[-1], mf_func):
operations = operations[:len(operations)-1]
method_found = True
elif isinstance(operations[-1], mf_comparator):
operations = operations[:len(operations)-1]
for element in operations:
if attr_last is None:
attr_last = getattr(obj, element.name)
else:
attr_last = getattr(attr_last, element.name)
if attr_last is None:
break
if isinstance(arg._operations[-1], mf_comparator):
return attr_last == arg._operations[-1].right
if not method_found:
return bool(attr_last)
if attr_last is None:
return False
if isinstance(arg._operations[-1], mf_func):
func_operation: mf_func = arg._operations[-1]
return func_operation.resolve(attr_last, attr_last)
else:
method = getattr(attr_last, arg._operations[-2].name)
args = arg._operations[-1].args
return method(*args)
except Exception as e:
...

4
maxapi/loggers.py Normal file
View File

@ -0,0 +1,4 @@
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('bot')

View File

@ -0,0 +1,46 @@
from typing import Any, Dict, List, TYPE_CHECKING
from ..types.users import BotCommand, User
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class ChangeInfo(BaseConnection):
def __init__(
self,
bot: 'Bot',
name: str = None,
description: str = None,
commands: List[BotCommand] = None,
photo: Dict[str, Any] = None
):
self.bot = bot
self.name = name
self.description = description
self.commands = commands
self.photo = photo
async def request(self) -> User:
json = {}
if self.name: json['name'] = self.name
if self.description: json['description'] = self.description
if self.commands: json['commands'] = [command.model_dump() for command in self.commands]
if self.photo: json['photo'] = self.photo
return await super().request(
method=HTTPMethod.PATCH,
path=ApiPath.ME,
model=User,
params=self.bot.params,
json=json
)

View File

@ -0,0 +1,59 @@
from typing import List, TYPE_CHECKING
from aiomax.enums.parse_mode import ParseMode
from ..types.attachments.attachment import Attachment
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..types.message import NewMessageLink
from .types.edited_message import EditedMessage
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class EditMessage(BaseConnection):
def __init__(
self,
bot: 'Bot',
message_id: str,
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
):
self.bot = bot
self.message_id = message_id
self.text = text
self.attachments = attachments
self.link = link
self.notify = notify
self.parse_mode = parse_mode
async def request(self) -> 'EditedMessage':
params = self.bot.params.copy()
json = {}
params['message_id'] = self.message_id
if not self.text is None: json['text'] = self.text
if self.attachments: json['attachments'] = \
[att.model_dump() for att in self.attachments]
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.PUT,
path=ApiPath.MESSAGES,
model=EditedMessage,
params=params,
json=json
)

View File

@ -0,0 +1,29 @@
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
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class GetChats(BaseConnection):
def __init__(self, bot: 'Bot'):
self.bot = bot
async def request(self) -> Chats:
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.CHATS,
model=Chats,
params=self.bot.params
)

29
maxapi/methods/get_me.py Normal file
View File

@ -0,0 +1,29 @@
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
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class GetMe(BaseConnection):
def __init__(self, bot: 'Bot'):
self.bot = bot
async def request(self) -> Chats:
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.ME,
model=User,
params=self.bot.params
)

View File

@ -0,0 +1,30 @@
from typing import TYPE_CHECKING
from ..types.message import Messages
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class GetMessages(BaseConnection):
def __init__(self, bot: 'Bot', chat_id: int = None):
self.bot = bot
self.chat_id = chat_id
async def request(self) -> Messages:
params = self.bot.params.copy()
if self.chat_id: params['chat_id'] = self.chat_id
return await super().request(
method=HTTPMethod.GET,
path=ApiPath.MESSAGES,
model=Messages,
params=params
)

View File

@ -0,0 +1,68 @@
from typing import List, TYPE_CHECKING
from ..enums.parse_mode import ParseMode
from ..types.message import NewMessageLink
from ..types.attachments.attachment import Attachment
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath
from .types.sended_message import SendedMessage
from ..connection.base import BaseConnection
if TYPE_CHECKING:
from ..bot import Bot
class SendMessage(BaseConnection):
def __init__(
self,
bot: 'Bot',
chat_id: int = None,
user_id: int = None,
disable_link_preview: bool = False,
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
):
self.bot = bot
self.chat_id = chat_id
self.user_id = user_id
self.disable_link_preview = disable_link_preview
self.text = text
self.attachments = attachments
self.link = link
self.notify = notify
self.parse_mode = parse_mode
async def request(self) -> 'SendedMessage':
params = self.bot.params.copy()
json = {}
if self.chat_id: params['chat_id'] = self.chat_id
elif self.user_id: params['user_id'] = self.user_id
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 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
)

View File

@ -0,0 +1,7 @@
from pydantic import BaseModel
from ...types.message import Message
class EditedMessage(BaseModel):
message: Message

View File

@ -0,0 +1,7 @@
from pydantic import BaseModel
from ...types.message import Message
class SendedMessage(BaseModel):
message: Message

0
maxapi/types/__init__.py Normal file
View File

View File

@ -0,0 +1,59 @@
from typing import List, Optional, Union
from pydantic import BaseModel
from ...types.attachments.buttons.chat_button import ChatButton
from ...types.attachments.buttons.request_contact import RequestContact
from ...types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton
from ...types.attachments.buttons.link_button import LinkButton
from ...types.users import User
from ...enums.attachment import AttachmentType
from .buttons.callback_button import CallbackButton
AttachmentUnion = []
class StickerAttachmentPayload(BaseModel):
url: str
code: str
class PhotoAttachmentPayload(BaseModel):
photo_id: int
token: str
url: str
class OtherAttachmentPayload(BaseModel):
url: str
token: Optional[str] = None
class ContactAttachmentPayload(BaseModel):
vcf_info: Optional[str] = None
max_info: Optional[User] = None
class ButtonsPayload(BaseModel):
buttons: List[List[
Union[
LinkButton,
CallbackButton,
RequestGeoLocationButton,
RequestContact,
ChatButton
]
]]
class Attachment(BaseModel):
type: AttachmentType
payload: Optional[Union[
PhotoAttachmentPayload,
OtherAttachmentPayload,
ContactAttachmentPayload,
ButtonsPayload,
StickerAttachmentPayload
]] = None
class Config:
use_enum_values = True

View File

@ -0,0 +1,8 @@
from typing import Literal, Optional
from .attachment import Attachment
class Audio(Attachment):
type: Literal['audio'] = 'audio'
transcription: Optional[str] = None

View File

@ -0,0 +1,12 @@
from typing import Literal
from pydantic import BaseModel
from ....enums.button_type import ButtonType
class Button(BaseModel):
type: ButtonType
text: str
class Config:
use_enum_values = True

View File

@ -0,0 +1,9 @@
from typing import Literal
from pydantic import BaseModel
from ..attachment import ButtonsPayload
class AttachmentButton(BaseModel):
type: Literal['inline_keyboard'] = 'inline_keyboard'
payload: ButtonsPayload

View File

@ -0,0 +1,9 @@
from typing import Optional
from ....enums.intent import Intent
from . import Button
class CallbackButton(Button):
payload: Optional[str] = None
intent: Intent

View File

@ -0,0 +1,11 @@
from typing import Optional
from ....types.attachments.buttons import Button
class ChatButton(Button):
chat_title: Optional[str] = None
chat_description: Optional[str] = None
start_payload: Optional[str] = None
chat_title: Optional[str] = None
uuid: Optional[int] = None

View File

@ -0,0 +1,7 @@
from typing import Optional
from ....types.attachments.buttons import Button
class LinkButton(Button):
url: Optional[str] = None

View File

@ -0,0 +1,5 @@
from ....types.attachments.buttons import Button
class RequestContact(Button):
...

View File

@ -0,0 +1,5 @@
from ....types.attachments.buttons import Button
class RequestGeoLocationButton(Button):
quick: bool = False

View File

@ -0,0 +1,7 @@
from typing import Literal
from .attachment import Attachment
class Contact(Attachment):
type: Literal['contact'] = 'contact'

View File

@ -0,0 +1,9 @@
from typing import Literal, Optional
from .attachment import Attachment
class File(Attachment):
type: Literal['file'] = 'file'
filename: Optional[str] = None
size: Optional[int] = None

View File

@ -0,0 +1,6 @@
from typing import Literal
from .attachment import Attachment
class Image(Attachment):
type: Literal['image'] = 'image'

View File

@ -0,0 +1,9 @@
from typing import Literal, Optional
from .attachment import Attachment
class Location(Attachment):
type: Literal['location'] = 'location'
latitude: Optional[float] = None
longitude: Optional[float] = None

View File

@ -0,0 +1,9 @@
from typing import Optional
from .attachment import Attachment
class Share(Attachment):
title: Optional[str] = None
description: Optional[str] = None
image_url: Optional[str] = None

View File

@ -0,0 +1,9 @@
from typing import Literal, Optional
from .attachment import Attachment
class Sticker(Attachment):
type: Literal['sticker'] = 'sticker'
width: Optional[int] = None
height: Optional[int] = None

View File

@ -0,0 +1,16 @@
from typing import Literal, Optional
from pydantic import BaseModel
from .attachment import Attachment
class VideoThumbnail(BaseModel):
url: str
class Video(Attachment):
type: Literal['video'] = 'video'
thumbnail: VideoThumbnail
width: Optional[int] = None
height: Optional[int] = None
duration: Optional[int] = None

11
maxapi/types/callback.py Normal file
View File

@ -0,0 +1,11 @@
from typing import Optional
from pydantic import BaseModel
from ..types.users import User
class Callback(BaseModel):
timestamp: int
callback_id: str
payload: Optional[str] = None
user: User

46
maxapi/types/chats.py Normal file
View File

@ -0,0 +1,46 @@
from pydantic import BaseModel
from typing import List, Optional
from enum import Enum
from datetime import datetime
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
class Chat(BaseModel):
chat_id: int
type: ChatType
status: ChatStatus
title: Optional[str] = None
icon: Optional[Icon] = None
last_event_time: int
participants_count: int
owner_id: Optional[int] = None
participants: None = None
is_public: bool
link: Optional[str] = None
description: Optional[str] = None
dialog_with_user: Optional[User] = None
messages_count: Optional[int] = None
chat_message_id: Optional[str] = None
pinned_message: Optional[Message] = None
class Config:
arbitrary_types_allowed=True
class Chats(BaseModel):
chats: List[Chat] = []

6
maxapi/types/errors.py Normal file
View File

@ -0,0 +1,6 @@
from pydantic import BaseModel
class Error(BaseModel):
code: int
text: str

119
maxapi/types/message.py Normal file
View File

@ -0,0 +1,119 @@
from __future__ import annotations
from pydantic import BaseModel, Field
from typing import Any, Optional, List, Union, TYPE_CHECKING
from ..enums.parse_mode import ParseMode
from ..types.attachments.attachment import Attachment
from ..types.attachments.share import Share
from .attachments.buttons.attachment_button import AttachmentButton
from ..enums.text_style import TextStyle
from ..enums.chat_type import ChatType
from ..enums.message_link_type import MessageLinkType
from .attachments.sticker import Sticker
from .attachments.file import File
from .attachments.image import Image
from .attachments.video import Video
from .attachments.audio import Audio
from ..types.users import User
if TYPE_CHECKING:
from ..bot import Bot
class MarkupElement(BaseModel):
type: TextStyle
from_: int = Field(..., alias='from')
length: int
class Config:
populate_by_name = True
class MarkupLink(MarkupElement):
url: Optional[str] = None
class Recipient(BaseModel):
user_id: Optional[int] = None # Для пользователя
chat_id: Optional[int] = None # Для чата
chat_type: ChatType # Тип получателя (диалог или чат)
class MessageBody(BaseModel):
mid: str
seq: int
text: str = None
attachments: Optional[
List[
Union[
AttachmentButton,
Audio,
Video,
File,
Image,
Sticker,
Share
]
]
] = []
markup: Optional[
List[
Union[
MarkupLink, MarkupElement
]
]
] = []
class MessageStat(BaseModel):
views: int
class LinkedMessage(BaseModel):
type: MessageLinkType
sender: User
chat_id: Optional[int] = None
message: MessageBody
class Message(BaseModel):
sender: User
recipient: Recipient
timestamp: int
link: Optional[LinkedMessage] = None
body: Optional[MessageBody] = None
stat: Optional[MessageStat] = None
url: Optional[str] = None
bot: Optional[Any] = None
async def answer(self,
text: str = None,
disable_link_preview: bool = False,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
):
bot: Bot = self.bot
return await bot.send_message(
chat_id=self.recipient.chat_id,
user_id=self.recipient.user_id,
text=text,
disable_link_preview=disable_link_preview,
attachments=attachments,
link=link,
notify=notify,
parse_mode=parse_mode
)
class Messages(BaseModel):
messages: List[Message]
class NewMessageLink(BaseModel):
type: MessageLinkType
mid: str

View File

@ -0,0 +1,11 @@
from pydantic import BaseModel
from ...enums.update import UpdateType
class Update(BaseModel):
update_type: UpdateType
timestamp: int
class Config:
arbitrary_types_allowed=True

View File

@ -0,0 +1,8 @@
from typing import Optional
from . import Update
from ...types.users import User
class BotAdded(Update):
chat_id: Optional[int] = None
user: User

View File

@ -0,0 +1,8 @@
from typing import Optional
from . import Update
from ...types.users import User
class BotRemoved(Update):
chat_id: Optional[int] = None
user: User

View File

@ -0,0 +1,10 @@
from typing import Optional
from . import Update
from ...types.users import User
class BotStarted(Update):
chat_id: Optional[int] = None
user: User
user_locale: Optional[str] = None
payload: Optional[str] = None

View File

@ -0,0 +1,9 @@
from typing import Optional
from . import Update
from ...types.users import User
class ChatTitleChanged(Update):
chat_id: Optional[int] = None
user: User
title: Optional[str] = None

View File

@ -0,0 +1,11 @@
from typing import Optional
from . import Update
from ...types.callback import Callback
from ...types.message import Message
class MessageCallback(Update):
message: Message
user_locale: Optional[str] = None
callback: Callback

View File

@ -0,0 +1,11 @@
from typing import Optional
from ...types.chats import Chat
from . import Update
class MessageChatCreated(Update):
chat: Chat
title: Optional[str] = None
message_id: Optional[str] = None
start_payload: Optional[str] = None

View File

@ -0,0 +1,9 @@
from typing import Optional
from . import Update
from ...types.message import Message
class MessageCreated(Update):
message: Message
user_locale: Optional[str] = None

View File

@ -0,0 +1,6 @@
from . import Update
from ...types.message import Message
class MessageEdited(Update):
message: Message

View File

@ -0,0 +1,9 @@
from typing import Optional
from . import Update
class MessageRemoved(Update):
message_id: Optional[str] = None
chat_id: Optional[int] = None
user_id: Optional[int] = None

View File

@ -0,0 +1,10 @@
from typing import Optional
from . import Update
from ...types.users import User
class UserAdded(Update):
inviter_id: Optional[int] = None
chat_id: Optional[int] = None
user: User

View File

@ -0,0 +1,10 @@
from typing import Optional
from . import Update
from ...types.users import User
class UserRemoved(Update):
admin_id: Optional[int] = None
chat_id: Optional[int] = None
user: User

26
maxapi/types/users.py Normal file
View File

@ -0,0 +1,26 @@
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
class BotCommand(BaseModel):
name: str
description: Optional[str] = None
class User(BaseModel):
user_id: int
first_name: str
last_name: Optional[str] = None
username: Optional[str] = None
is_bot: bool
last_activity_time: int
description: Optional[str] = None
avatar_url: Optional[str] = None
full_avatar_url: Optional[str] = None
commands: Optional[List[BotCommand]] = None
class Config:
json_encoders = {
datetime: lambda v: int(v.timestamp() * 1000) # Конвертация datetime в Unix-время (ms)
}