Добавлены: билдер инлайн клавиатур, FSM like aiogram, start_polling и логгирование

This commit is contained in:
Денис Семёнов 2025-06-19 02:40:38 +03:00
parent 9a39dce1a6
commit 4de32ca476
45 changed files with 711 additions and 260 deletions

View File

@ -1,16 +1,28 @@
# maxapi
#### Библиотека (like aiogram) для взаимодействия с социальной сетью MAX по Webhook (или подписке бота)
#### Библиотека (like aiogram) для взаимодействия с мессенджером MAX
Информация на данный момент:
* Проект не готов, ведется активная разработка
* Планируется:
+ Сокращение импортов в ваших хендлерах (громадные импорты в example.py)
+ Сокращение "построения" клавиатур
+ Разработка контекста бота
+ Разработка Longpoll метода
+ Доработка базовой составляющей проекта
+ и так далее...
* Проект тестируется и активно дорабатывается
* На данный момент имеется:
Роутеры
Билдер инлайн клавиатур
Этакая машина состояний и контекст к нему
Поллинг и вебхук методы запуска
Логгирование
```bash
Пример бота описан в example.py
Перед запуском примера установите зависимости:
pip install -r requirements.txt
Запуск бота:
python example.py
```
### Контакты
[Группа MAX](https://max.ru/join/IPAok63C3vFqbWTFdutMUtjmrAkGqO56YeAN7iyDfc8)

View File

@ -1,23 +1,127 @@
import asyncio
import logging
from maxapi.bot import Bot
from maxapi.dispatcher import Dispatcher
from maxapi.types.updates.message_created import MessageCreated
from maxapi.filters import F
from maxapi import Bot, Dispatcher, F
from maxapi.context import MemoryContext, State, StatesGroup
from maxapi.types import Command, MessageCreated, CallbackButton, MessageCallback
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
from for_example import router
logging.basicConfig(level=logging.INFO)
bot = Bot('токен')
dp = Dispatcher()
dp.include_routers(router)
# Отвечает на лю
# любое текстовое сообщение
@dp.message_created(F.message.body.text)
start_text = '''Мои команды:
/clear очищает ваш контекст
/state или /context показывают ваше контекстное состояние
/data показывает вашу контекстную память
'''
class Form(StatesGroup):
name = State()
age = State()
@dp.on_started()
async def _():
logging.info('Бот стартовал!')
@dp.message_created(Command('clear'))
async def hello(obj: MessageCreated, context: MemoryContext):
await context.clear()
await obj.message.answer(f"Ваш контекст был очищен!")
@dp.message_created(Command('data'))
async def hello(obj: MessageCreated, context: MemoryContext):
data = await context.get_data()
await obj.message.answer(f"Ваша контекстная память: {str(data)}")
@dp.message_created(Command('context'))
@dp.message_created(Command('state'))
async def hello(obj: MessageCreated, context: MemoryContext):
data = await context.get_state()
await obj.message.answer(f"Ваше контекстное состояние: {str(data)}")
@dp.message_created(Command('start'))
async def hello(obj: MessageCreated):
await obj.message.answer(f'Повторяю за вами: {obj.message.body.text}')
builder = InlineKeyboardBuilder()
builder.row(
CallbackButton(
text='Ввести свое имя',
payload='btn_1'
),
CallbackButton(
text='Ввести свой возраст',
payload='btn_2'
)
)
builder.row(
CallbackButton(
text='Не хочу',
payload='btn_3'
)
)
await obj.message.answer(
text=start_text,
attachments=[builder.as_markup()] # Для MAX клавиатура это вложение,
) # поэтому она в списке вложений
@dp.message_callback(F.callback.payload == 'btn_1')
async def hello(obj: MessageCallback, context: MemoryContext):
await context.set_state(Form.name)
await obj.message.delete()
await obj.message.answer(f'Отправьте свое имя:')
@dp.message_callback(F.callback.payload == 'btn_2')
async def hello(obj: MessageCallback, context: MemoryContext):
await context.set_state(Form.age)
await obj.message.delete()
await obj.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'Ну ладно 🥲')
@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)
data = await context.get_data()
await obj.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)
await obj.message.answer(f"Ого! А мне всего пару недель 😁")
async def main():
await dp.start_polling(bot)
# await dp.handle_webhook(
# bot=bot,
# host='localhost',
# port=8080
# )
asyncio.run(main())

10
for_example.py Normal file
View File

@ -0,0 +1,10 @@
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}")

10
maxapi/__init__.py Normal file
View File

@ -0,0 +1,10 @@
from .bot import Bot
from .dispatcher import Dispatcher, Router
from .filters import F
__all__ = [
Bot,
Dispatcher,
F,
Router
]

View File

@ -4,6 +4,7 @@ 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
class BaseConnection:
@ -22,8 +23,11 @@ class BaseConnection:
is_return_raw: bool = False,
**kwargs
):
s = self.bot.session
r = await s.request(
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
@ -31,7 +35,9 @@ class BaseConnection:
if not r.ok:
raw = await r.text()
return Error(code=r.status, text=raw)
error = Error(code=r.status, text=raw)
logger_bot.error(error)
return error
raw = await r.json()

View File

@ -0,0 +1,36 @@
import asyncio
from typing import Any, Dict
from ..context.state_machine import State, StatesGroup
class MemoryContext:
def __init__(self, chat_id: int, user_id: int):
self.chat_id = chat_id
self.user_id = user_id
self._context: Dict[str, Any] = {}
self._state: State | None = None
self._lock = asyncio.Lock()
async def get_data(self) -> dict[str, Any]:
async with self._lock:
return self._context
async def set_data(self, data: dict[str, Any]):
async with self._lock:
self._context = data
async def update_data(self, **kwargs):
async with self._lock:
self._context.update(kwargs)
async def set_state(self, state: State | str = None):
self._state = state
async def get_state(self):
return self._state
async def clear(self):
self._state = None
self._context = {}

View File

@ -0,0 +1,16 @@
class State:
def __init__(self):
self.name = None
def __set_name__(self, owner, attr_name):
self.name = f'{owner.__name__}:{attr_name}'
def __str__(self):
return self.name
class StatesGroup:
@classmethod
def states(cls) -> list[str]:
return [str(getattr(cls, attr)) for attr in dir(cls)
if isinstance(getattr(cls, attr), State)]

View File

@ -1,61 +1,33 @@
from typing import Callable, List
import aiohttp
from fastapi.responses import JSONResponse
import uvicorn
from fastapi import FastAPI, Request
from magic_filter import MagicFilter
from fastapi.responses import JSONResponse
from uvicorn import Config, Server
from .filters.handler import Handler
from .context import MemoryContext
from .types.updates import UpdateUnion
from .types.errors import Error
from .methods.types.getted_updates import process_update_webhook, process_update_request
from .filters import filter_m
from .types.updates import Update
from .filters import filter_attrs
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
from .loggers import logger_dp
app = FastAPI()
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.event_handlers: List[Handler] = []
self.contexts: List[MemoryContext] = []
self.bot = None
self.on_started_func = None
self.message_created = Event(update_type=UpdateType.MESSAGE_CREATED, router=self)
self.bot_added = Event(update_type=UpdateType.BOT_ADDED, router=self)
@ -68,44 +40,94 @@ class Dispatcher:
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)
self.on_started = Event(update_type=UpdateType.ON_STARTED, router=self)
def include_routers(self, *routers: 'Router'):
for router in routers:
for event in router.event_handlers:
self.event_handlers.append(event)
def get_memory_context(self, chat_id: int, user_id: int):
for ctx in self.contexts:
if ctx.chat_id == chat_id and ctx.user_id == user_id:
return ctx
new_ctx = MemoryContext(chat_id, user_id)
self.contexts.append(new_ctx)
return new_ctx
async def handle(self, event_object: UpdateUnion):
for handler in self.event_handlers:
if not handler.update_type == event_object.update_type:
continue
if handler.filters:
if not filter_attrs(event_object, *handler.filters):
continue
memory_context = self.get_memory_context(
*event_object.get_ids()
)
if not handler.state == await memory_context.get_state() \
and handler.state:
continue
func_args = handler.func_event.__annotations__.keys()
kwargs = {'context': memory_context}
for key in kwargs.copy().keys():
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)
logger_dp.info(f'Обработано: {event_object.update_type}')
break
async def start_polling(self, bot: Bot):
self.bot = bot
self.bot.session = aiohttp.ClientSession(self.bot.API_URL)
logger_dp.info(f'{len(self.event_handlers)} event handlers started')
if self.on_started_func:
await self.on_started_func()
while True:
try:
events = await self.bot.get_updates()
for event in events:
handlers: List[Handler] = self.event_handlers
for handler in handlers:
if isinstance(events, Error):
logger_dp.info(f'Ошибка при получении обновлений: {events}')
continue
if not handler.update_type == event.update_type:
continue
self.bot.marker_updates = events.get('marker')
if handler.filters:
if not filter_m(event, *handler.filters):
continue
processed_events = await process_update_request(
events=events,
bot=self.bot
)
await handler.func_event(event)
break
for event in processed_events:
try:
await self.handle(event)
except Exception as e:
logger_dp.error(f"Ошибка при обработке события: {events['update_type']}: {e}")
except Exception as e:
print(e)
...
logger_dp.error(f'Общая ошибка при обработке событий: {e}')
logger.info(f'{len(self.event_handlers)} event handlers started')
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.session = aiohttp.ClientSession(self.bot.API_URL)
@app.post("/")
if self.on_started_func:
await self.on_started_func()
@app.post('/')
async def _(request: Request):
try:
event_json = await request.json()
@ -115,26 +137,17 @@ class Dispatcher:
bot=self.bot
)
handlers: List[Handler] = self.event_handlers
for handler in handlers:
if not handler.update_type == event_object.update_type:
continue
if handler.filters:
if not filter_m(event_object, *handler.filters):
continue
await handler.func_event(event_object)
break
await self.handle(event_object)
return JSONResponse(content={'ok': True}, status_code=200)
except Exception as e:
print(e)
...
logger_dp.error(f"Ошибка при обработке события: {event_json['update_type']}: {e}")
logger.info(f'{len(self.event_handlers)} event handlers started')
uvicorn.run(app, host=host, port=port, log_level='critical')
logger_dp.info(f'{len(self.event_handlers)} событий на обработку')
config = Config(app=app, host=host, port=port, log_level="critical")
server = Server(config)
await server.serve()
class Router(Dispatcher):
@ -149,13 +162,16 @@ class Event:
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
if self.update_type == UpdateType.ON_STARTED:
self.router.on_started_func = func_event
else:
self.router.event_handlers.append(
Handler(
func_event=func_event,
update_type=self.update_type,
*args, **kwargs
)
)
)
return func_event
return decorator

View File

@ -1,7 +1,7 @@
from enum import Enum
class ButtonType(Enum):
class ButtonType(str, Enum):
REQUEST_CONTACT = 'request_contact'
CALLBACK = 'callback'
LINK = 'link'

View File

@ -12,3 +12,5 @@ class UpdateType(str, Enum):
MESSAGE_REMOVED = 'message_removed'
USER_ADDED = 'user_added'
USER_REMOVED = 'user_removed'
ON_STARTED = 'on_started'

View File

@ -6,7 +6,7 @@ from magic_filter.operations.comparator import ComparatorOperation as mf_compara
F = MagicFilter()
def filter_m(obj, *magic_args):
def filter_attrs(obj, *magic_args):
try:
for arg in magic_args:

31
maxapi/filters/handler.py Normal file
View File

@ -0,0 +1,31 @@
from typing import Callable
from magic_filter import F, MagicFilter
from ..types.command import Command
from ..context.state_machine import State
from ..enums.update import UpdateType
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 = []
self.state = None
for arg in args:
if isinstance(arg, MagicFilter):
self.filters.append(arg)
elif isinstance(arg, State):
self.state = arg
elif isinstance(arg, Command):
self.filters.insert(0, F.message.body.text == arg.command)

View File

@ -1,3 +1,4 @@
import logging
logger = logging.getLogger('bot')
logger_bot = logging.getLogger('bot')
logger_dp = logging.getLogger('dispatcher')

View File

@ -3,7 +3,7 @@
from re import findall
from typing import TYPE_CHECKING
from maxapi.methods.types.getted_list_admin_chat import GettedListAdminChat
from ..methods.types.getted_list_admin_chat import GettedListAdminChat
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -3,7 +3,7 @@
from re import findall
from typing import TYPE_CHECKING, List
from maxapi.methods.types.getted_members_chat import GettedMembersChat
from ..methods.types.getted_members_chat import GettedMembersChat
from ..enums.http_method import HTTPMethod
from ..enums.api_path import ApiPath

View File

@ -3,11 +3,10 @@
from datetime import datetime
from typing import TYPE_CHECKING, List
from ..types.updates import UpdateUnion
from ..methods.types.getted_updates import process_update_request
from ..enums.update import UpdateType
from ..types.updates import Update
from ..types.message import Messages
from ..enums.http_method import HTTPMethod
@ -28,23 +27,17 @@ class GetUpdates(BaseConnection):
self.bot = bot
self.limit = limit
async def request(self) -> Messages:
async def request(self) -> UpdateUnion:
params = self.bot.params.copy()
params['limit'] = self.limit
if self.bot.marker_updates:
params['marker'] = self.bot.marker_updates
event_json = await super().request(
method=HTTPMethod.GET,
path=ApiPath.UPDATES,
model=Messages,
model=None,
params=params,
is_return_raw=True
)
return await process_update_request(
event_json=event_json,
bot=self.bot
)
return event_json

View File

@ -2,8 +2,8 @@
from typing import List, TYPE_CHECKING
from maxapi.enums.sender_action import SenderAction
from maxapi.methods.types.sended_action import SendedAction
from ..enums.sender_action import SenderAction
from ..methods.types.sended_action import SendedAction
from .types.sended_message import SendedMessage
from ..types.message import NewMessageLink

View File

@ -1,7 +1,7 @@
from typing import Optional
from pydantic import BaseModel
from maxapi.types.message import Message
from ...types.message import Message
class GettedPin(BaseModel):

View File

@ -1,7 +1,5 @@
from typing import TYPE_CHECKING
from maxapi.enums.update import UpdateType
from ...types.updates import Update
from ...enums.update import UpdateType
from ...types.updates.bot_added import BotAdded
from ...types.updates.bot_removed import BotRemoved
@ -16,82 +14,62 @@ from ...types.updates.user_added import UserAdded
from ...types.updates.user_removed import UserRemoved
if TYPE_CHECKING:
from maxapi.bot import Bot
from ...bot import Bot
async def process_update_request(event_json: dict, bot: 'Bot'):
events = [event for event in event_json['updates']]
async def get_update_model(event: dict, bot: 'Bot'):
event_object = None
match event['update_type']:
case UpdateType.BOT_ADDED:
event_object = BotAdded(**event)
case UpdateType.BOT_REMOVED:
event_object = BotRemoved(**event)
case UpdateType.BOT_STARTED:
event_object = BotStarted(**event)
case UpdateType.CHAT_TITLE_CHANGED:
event_object = ChatTitleChanged(**event)
case UpdateType.MESSAGE_CALLBACK:
event_object = MessageCallback(**event)
case UpdateType.MESSAGE_CHAT_CREATED:
event_object = MessageChatCreated(**event)
case UpdateType.MESSAGE_CREATED:
event_object = MessageCreated(**event)
case UpdateType.MESSAGE_EDITED:
event_object = MessageEdited(**event)
case UpdateType.MESSAGE_REMOVED:
event_object = MessageRemoved(**event)
case UpdateType.USER_ADDED:
event_object = UserAdded(**event)
case UpdateType.USER_REMOVED:
event_object = UserRemoved(**event)
bot.marker_updates = event_json.get('marker')
if hasattr(event_object, 'bot'):
event_object.bot = bot
if hasattr(event_object, 'message'):
event_object.message.bot = bot
return event_object
async def process_update_request(events: dict, bot: 'Bot'):
events = [event for event in events['updates']]
objects = []
for event in events:
event_object = None
match event['update_type']:
case UpdateType.BOT_ADDED:
event_object = BotAdded(**event)
case UpdateType.BOT_REMOVED:
event_object = BotRemoved(**event)
case UpdateType.BOT_STARTED:
event_object = BotStarted(**event)
case UpdateType.CHAT_TITLE_CHANGED:
event_object = ChatTitleChanged(**event)
case UpdateType.MESSAGE_CALLBACK:
event_object = MessageCallback(**event)
event_object.message.bot = bot
event_object.bot = bot
case UpdateType.MESSAGE_CHAT_CREATED:
event_object = MessageChatCreated(**event)
case UpdateType.MESSAGE_CREATED:
event_object = MessageCreated(**event)
event_object.message.bot = bot
event_object.bot = bot
case UpdateType.MESSAGE_EDITED:
event_object = MessageEdited(**event)
case UpdateType.MESSAGE_REMOVED:
event_object = MessageRemoved(**event)
case UpdateType.USER_ADDED:
event_object = UserAdded(**event)
case UpdateType.USER_REMOVED:
event_object = UserRemoved(**event)
objects.append(event_object)
objects.append(
await get_update_model(
bot=bot,
event=event
)
)
return objects
async def process_update_webhook(event_json: dict, bot: 'Bot'):
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 = bot
event_object.bot = bot
case UpdateType.MESSAGE_CHAT_CREATED:
event_object = MessageChatCreated(**event_json)
case UpdateType.MESSAGE_CREATED:
event_object = MessageCreated(**event_json)
event_object.message.bot = bot
event_object.bot = 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)
return event_object
return await get_update_model(
bot=bot,
event=event_json
)

View File

@ -0,0 +1,49 @@
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 ..types.attachments.attachment import PhotoAttachmentPayload
from ..types.attachments.attachment import OtherAttachmentPayload
from ..types.attachments.attachment import ContactAttachmentPayload
from ..types.attachments.attachment import ButtonsPayload
from ..types.attachments.attachment import StickerAttachmentPayload
from ..types.attachments.buttons.callback_button import CallbackButton
from ..types.attachments.buttons.chat_button import ChatButton
from ..types.attachments.buttons.link_button import LinkButton
from ..types.attachments.buttons.request_contact import RequestContact
from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton
from ..types.command import Command
__all__ = [
CallbackButton,
ChatButton,
LinkButton,
RequestContact,
RequestGeoLocationButton,
Command,
PhotoAttachmentPayload,
OtherAttachmentPayload,
ContactAttachmentPayload,
ButtonsPayload,
StickerAttachmentPayload,
BotAdded,
BotRemoved,
BotStarted,
ChatTitleChanged,
MessageCallback,
MessageChatCreated,
MessageCreated,
MessageEdited,
MessageRemoved,
UserAdded,
UserRemoved
]

View File

@ -1,13 +1,9 @@
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.attachments.buttons import InlineButtonUnion
from ...types.users import User
from ...enums.attachment import AttachmentType
from .buttons.callback_button import CallbackButton
AttachmentUnion = []
@ -34,15 +30,7 @@ class ContactAttachmentPayload(BaseModel):
class ButtonsPayload(BaseModel):
buttons: List[List[
Union[
LinkButton,
CallbackButton,
RequestGeoLocationButton,
RequestContact,
ChatButton
]
]]
buttons: List[List[InlineButtonUnion]]
class Attachment(BaseModel):

View File

@ -1,12 +1,15 @@
from typing import Literal
from pydantic import BaseModel
from typing import Union
from ....enums.button_type import ButtonType
from .callback_button import CallbackButton
from .chat_button import ChatButton
from .link_button import LinkButton
from .request_contact import RequestContact
from .request_geo_location_button import RequestGeoLocationButton
class Button(BaseModel):
type: ButtonType
text: str
class Config:
use_enum_values = True
InlineButtonUnion = Union[
CallbackButton,
ChatButton,
LinkButton,
RequestContact,
RequestGeoLocationButton
]

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

@ -1,9 +1,12 @@
from typing import Optional
from maxapi.enums.button_type import ButtonType
from ....enums.intent import Intent
from . import Button
from .button import Button
class CallbackButton(Button):
type: ButtonType = ButtonType.CALLBACK
payload: Optional[str] = None
intent: Intent
intent: Intent = Intent.DEFAULT

View File

@ -1,6 +1,6 @@
from typing import Optional
from ....types.attachments.buttons import Button
from .button import Button
class ChatButton(Button):

View File

@ -1,6 +1,6 @@
from typing import Optional
from ....types.attachments.buttons import Button
from .button import Button
class LinkButton(Button):

View File

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

View File

@ -1,4 +1,4 @@
from ....types.attachments.buttons import Button
from .button import Button
class RequestGeoLocationButton(Button):

View File

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

9
maxapi/types/command.py Normal file
View File

@ -0,0 +1,9 @@
class Command:
def __init__(self, text: str, prefix: str = '/'):
self.text = text
self.prefix = prefix
@property
def command(self):
return self.prefix + self.text

View File

@ -1,11 +1,27 @@
from pydantic import BaseModel
from ...enums.update import UpdateType
from typing import Union
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
class Update(BaseModel):
update_type: UpdateType
timestamp: int
class Config:
arbitrary_types_allowed=True
UpdateUnion = Union[
BotAdded,
BotRemoved,
BotStarted,
ChatTitleChanged,
MessageCallback,
MessageChatCreated,
MessageCreated,
MessageEdited,
MessageRemoved,
UserAdded,
UserRemoved
]

View File

@ -1,8 +1,21 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from . import Update
from pydantic import Field
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class BotAdded(Update):
chat_id: Optional[int] = None
user: User
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@ -1,8 +1,21 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from . import Update
from pydantic import Field
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class BotRemoved(Update):
chat_id: Optional[int] = None
user: User
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@ -1,10 +1,23 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from . import Update
from pydantic import Field
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class BotStarted(Update):
chat_id: Optional[int] = None
user: User
user_locale: Optional[str] = None
payload: Optional[str] = None
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@ -1,9 +1,22 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from . import Update
from pydantic import Field
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class ChatTitleChanged(Update):
chat_id: Optional[int] = None
user: User
title: Optional[str] = None
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user.user_id)

View File

@ -2,7 +2,7 @@ from typing import Any, List, Optional, TYPE_CHECKING, Union
from pydantic import BaseModel, Field
from . import Update
from .update import Update
from ...types.callback import Callback
from ...types.message import Message
@ -50,6 +50,9 @@ class MessageCallback(Update):
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.message.recipient.chat_id, self.message.recipient.user_id)
async def answer(
self,
notification: str,

View File

@ -1,11 +1,24 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from pydantic import Field
from ...types.chats import Chat
from . import Update
from .update import Update
if TYPE_CHECKING:
from ...bot import Bot
class MessageChatCreated(Update):
chat: Chat
title: Optional[str] = None
message_id: Optional[str] = None
start_payload: Optional[str] = None
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, 0)

View File

@ -3,7 +3,7 @@ from typing import Any, Optional, TYPE_CHECKING, ForwardRef
from pydantic import Field
from . import Update
from .update import Update
from ...types.message import Message
if TYPE_CHECKING:
@ -17,3 +17,6 @@ class MessageCreated(Update):
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.message.recipient.chat_id, self.message.recipient.user_id)

View File

@ -1,6 +1,19 @@
from . import Update
from typing import TYPE_CHECKING, Any, Optional
from pydantic import Field
from .update import Update
from ...types.message import Message
if TYPE_CHECKING:
from ...bot import Bot
class MessageEdited(Update):
message: Message
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.message.recipient.chat_id, self.message.recipient.user_id)

View File

@ -1,9 +1,21 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from . import Update
from pydantic import Field
from .update import Update
if TYPE_CHECKING:
from ...bot import Bot
class MessageRemoved(Update):
message_id: Optional[str] = None
chat_id: Optional[int] = None
user_id: Optional[int] = None
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.user_id)

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

@ -1,10 +1,23 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from . import Update
from pydantic import Field
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class UserAdded(Update):
inviter_id: Optional[int] = None
chat_id: Optional[int] = None
user: User
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.inviter_id)

View File

@ -1,10 +1,22 @@
from typing import Optional
from typing import TYPE_CHECKING, Any, Optional
from . import Update
from pydantic import Field
from .update import Update
from ...types.users import User
if TYPE_CHECKING:
from ...bot import Bot
class UserRemoved(Update):
admin_id: Optional[int] = None
chat_id: Optional[int] = None
user: User
bot: Optional[Any] = Field(default=None, exclude=True)
if TYPE_CHECKING:
bot: Optional[Bot]
def get_ids(self):
return (self.chat_id, self.admin_id)

View File

@ -0,0 +1,18 @@
from ..enums.attachment import AttachmentType
from ..types.attachments.attachment import Attachment, ButtonsPayload
class InlineKeyboardBuilder:
def __init__(self):
self.payload = []
def row(self, *buttons):
self.payload.append([*buttons])
def as_markup(self):
return Attachment(
type=AttachmentType.INLINE_KEYBOARD,
payload=ButtonsPayload(
buttons=self.payload
)
)

5
requirements.txt Normal file
View File

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