From 4de32ca476de00c4df9f9607597de3c37afd2cdc Mon Sep 17 00:00:00 2001 From: Denis Date: Thu, 19 Jun 2025 02:40:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B:=20=D0=B1=D0=B8=D0=BB=D0=B4=D0=B5=D1=80=20=D0=B8?= =?UTF-8?q?=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD=20=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=80,=20FSM=20like=20aiogram,=20start=5Fpol?= =?UTF-8?q?ling=20=D0=B8=20=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 30 ++- example.py | 120 +++++++++++- for_example.py | 10 + maxapi/__init__.py | 10 + maxapi/connection/base.py | 12 +- maxapi/context/__init__.py | 36 ++++ maxapi/context/state_machine.py | 16 ++ maxapi/dispatcher.py | 184 ++++++++++-------- maxapi/enums/button_type.py | 2 +- maxapi/enums/update.py | 4 +- maxapi/filters/__init__.py | 2 +- maxapi/filters/handler.py | 31 +++ maxapi/loggers.py | 3 +- maxapi/methods/get_list_admin_chat.py | 2 +- maxapi/methods/get_members_chat.py | 2 +- maxapi/methods/get_updates.py | 17 +- maxapi/methods/send_action.py | 4 +- maxapi/methods/types/getted_pineed_message.py | 2 +- maxapi/methods/types/getted_updates.py | 114 +++++------ maxapi/types/__init__.py | 49 +++++ maxapi/types/attachments/attachment.py | 16 +- maxapi/types/attachments/buttons/__init__.py | 23 ++- maxapi/types/attachments/buttons/button.py | 12 ++ .../attachments/buttons/callback_button.py | 7 +- .../types/attachments/buttons/chat_button.py | 2 +- .../types/attachments/buttons/link_button.py | 2 +- .../attachments/buttons/request_contact.py | 2 +- .../buttons/request_geo_location_button.py | 2 +- maxapi/types/attachments/share.py | 3 +- maxapi/types/command.py | 9 + maxapi/types/updates/__init__.py | 34 +++- maxapi/types/updates/bot_added.py | 19 +- maxapi/types/updates/bot_removed.py | 19 +- maxapi/types/updates/bot_started.py | 19 +- maxapi/types/updates/chat_title_changed.py | 19 +- maxapi/types/updates/message_callback.py | 5 +- maxapi/types/updates/message_chat_created.py | 19 +- maxapi/types/updates/message_created.py | 7 +- maxapi/types/updates/message_edited.py | 17 +- maxapi/types/updates/message_removed.py | 18 +- maxapi/types/updates/update.py | 11 ++ maxapi/types/updates/user_added.py | 17 +- maxapi/types/updates/user_removed.py | 16 +- maxapi/utils/inline_keyboard.py | 18 ++ requirements.txt | 5 + 45 files changed, 711 insertions(+), 260 deletions(-) create mode 100644 for_example.py create mode 100644 maxapi/__init__.py create mode 100644 maxapi/context/__init__.py create mode 100644 maxapi/context/state_machine.py create mode 100644 maxapi/filters/handler.py create mode 100644 maxapi/types/attachments/buttons/button.py create mode 100644 maxapi/types/command.py create mode 100644 maxapi/types/updates/update.py create mode 100644 maxapi/utils/inline_keyboard.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index d9f1209..70e95f1 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file diff --git a/example.py b/example.py index 7d5464a..ec22677 100644 --- a/example.py +++ b/example.py @@ -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()) \ No newline at end of file diff --git a/for_example.py b/for_example.py new file mode 100644 index 0000000..58f3d08 --- /dev/null +++ b/for_example.py @@ -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}") \ No newline at end of file diff --git a/maxapi/__init__.py b/maxapi/__init__.py new file mode 100644 index 0000000..ba7a8b8 --- /dev/null +++ b/maxapi/__init__.py @@ -0,0 +1,10 @@ +from .bot import Bot +from .dispatcher import Dispatcher, Router +from .filters import F + +__all__ = [ + Bot, + Dispatcher, + F, + Router +] \ No newline at end of file diff --git a/maxapi/connection/base.py b/maxapi/connection/base.py index cd572ef..ed31113 100644 --- a/maxapi/connection/base.py +++ b/maxapi/connection/base.py @@ -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() diff --git a/maxapi/context/__init__.py b/maxapi/context/__init__.py new file mode 100644 index 0000000..98eeb6f --- /dev/null +++ b/maxapi/context/__init__.py @@ -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 = {} diff --git a/maxapi/context/state_machine.py b/maxapi/context/state_machine.py new file mode 100644 index 0000000..a69399d --- /dev/null +++ b/maxapi/context/state_machine.py @@ -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)] \ No newline at end of file diff --git a/maxapi/dispatcher.py b/maxapi/dispatcher.py index ba89d19..fe3e9a9 100644 --- a/maxapi/dispatcher.py +++ b/maxapi/dispatcher.py @@ -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() + + if isinstance(events, Error): + logger_dp.info(f'Ошибка при получении обновлений: {events}') + continue + + self.bot.marker_updates = events.get('marker') + + processed_events = await process_update_request( + events=events, + bot=self.bot + ) - for event in events: - 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, *handler.filters): - continue - - 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 \ No newline at end of file diff --git a/maxapi/enums/button_type.py b/maxapi/enums/button_type.py index 08b5715..2b8d809 100644 --- a/maxapi/enums/button_type.py +++ b/maxapi/enums/button_type.py @@ -1,7 +1,7 @@ from enum import Enum -class ButtonType(Enum): +class ButtonType(str, Enum): REQUEST_CONTACT = 'request_contact' CALLBACK = 'callback' LINK = 'link' diff --git a/maxapi/enums/update.py b/maxapi/enums/update.py index 8220410..38755f7 100644 --- a/maxapi/enums/update.py +++ b/maxapi/enums/update.py @@ -11,4 +11,6 @@ class UpdateType(str, Enum): MESSAGE_EDITED = 'message_edited' MESSAGE_REMOVED = 'message_removed' USER_ADDED = 'user_added' - USER_REMOVED = 'user_removed' \ No newline at end of file + USER_REMOVED = 'user_removed' + + ON_STARTED = 'on_started' \ No newline at end of file diff --git a/maxapi/filters/__init__.py b/maxapi/filters/__init__.py index 9f86d7b..f194327 100644 --- a/maxapi/filters/__init__.py +++ b/maxapi/filters/__init__.py @@ -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: diff --git a/maxapi/filters/handler.py b/maxapi/filters/handler.py new file mode 100644 index 0000000..fbd9b17 --- /dev/null +++ b/maxapi/filters/handler.py @@ -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) \ No newline at end of file diff --git a/maxapi/loggers.py b/maxapi/loggers.py index c507ea6..5b3e4da 100644 --- a/maxapi/loggers.py +++ b/maxapi/loggers.py @@ -1,3 +1,4 @@ import logging -logger = logging.getLogger('bot') \ No newline at end of file +logger_bot = logging.getLogger('bot') +logger_dp = logging.getLogger('dispatcher') \ No newline at end of file diff --git a/maxapi/methods/get_list_admin_chat.py b/maxapi/methods/get_list_admin_chat.py index 8cf471d..6fc84b0 100644 --- a/maxapi/methods/get_list_admin_chat.py +++ b/maxapi/methods/get_list_admin_chat.py @@ -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 diff --git a/maxapi/methods/get_members_chat.py b/maxapi/methods/get_members_chat.py index 5bce729..6655ef5 100644 --- a/maxapi/methods/get_members_chat.py +++ b/maxapi/methods/get_members_chat.py @@ -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 diff --git a/maxapi/methods/get_updates.py b/maxapi/methods/get_updates.py index f6400fc..ffab79e 100644 --- a/maxapi/methods/get_updates.py +++ b/maxapi/methods/get_updates.py @@ -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 - ) \ No newline at end of file + return event_json \ No newline at end of file diff --git a/maxapi/methods/send_action.py b/maxapi/methods/send_action.py index 28ec38b..a8e46bb 100644 --- a/maxapi/methods/send_action.py +++ b/maxapi/methods/send_action.py @@ -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 diff --git a/maxapi/methods/types/getted_pineed_message.py b/maxapi/methods/types/getted_pineed_message.py index 1061acd..261e398 100644 --- a/maxapi/methods/types/getted_pineed_message.py +++ b/maxapi/methods/types/getted_pineed_message.py @@ -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): diff --git a/maxapi/methods/types/getted_updates.py b/maxapi/methods/types/getted_updates.py index 79162be..1cb8ad9 100644 --- a/maxapi/methods/types/getted_updates.py +++ b/maxapi/methods/types/getted_updates.py @@ -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 \ No newline at end of file + return await get_update_model( + bot=bot, + event=event_json + ) \ No newline at end of file diff --git a/maxapi/types/__init__.py b/maxapi/types/__init__.py index e69de29..cad08ad 100644 --- a/maxapi/types/__init__.py +++ b/maxapi/types/__init__.py @@ -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 +] \ No newline at end of file diff --git a/maxapi/types/attachments/attachment.py b/maxapi/types/attachments/attachment.py index 02439ed..a88750b 100644 --- a/maxapi/types/attachments/attachment.py +++ b/maxapi/types/attachments/attachment.py @@ -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): diff --git a/maxapi/types/attachments/buttons/__init__.py b/maxapi/types/attachments/buttons/__init__.py index 18732c3..e60f951 100644 --- a/maxapi/types/attachments/buttons/__init__.py +++ b/maxapi/types/attachments/buttons/__init__.py @@ -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 \ No newline at end of file +InlineButtonUnion = Union[ + CallbackButton, + ChatButton, + LinkButton, + RequestContact, + RequestGeoLocationButton +] \ No newline at end of file diff --git a/maxapi/types/attachments/buttons/button.py b/maxapi/types/attachments/buttons/button.py new file mode 100644 index 0000000..18732c3 --- /dev/null +++ b/maxapi/types/attachments/buttons/button.py @@ -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 \ No newline at end of file diff --git a/maxapi/types/attachments/buttons/callback_button.py b/maxapi/types/attachments/buttons/callback_button.py index c5e057a..3ca7d61 100644 --- a/maxapi/types/attachments/buttons/callback_button.py +++ b/maxapi/types/attachments/buttons/callback_button.py @@ -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 \ No newline at end of file + intent: Intent = Intent.DEFAULT \ No newline at end of file diff --git a/maxapi/types/attachments/buttons/chat_button.py b/maxapi/types/attachments/buttons/chat_button.py index a56eda3..82934e3 100644 --- a/maxapi/types/attachments/buttons/chat_button.py +++ b/maxapi/types/attachments/buttons/chat_button.py @@ -1,6 +1,6 @@ from typing import Optional -from ....types.attachments.buttons import Button +from .button import Button class ChatButton(Button): diff --git a/maxapi/types/attachments/buttons/link_button.py b/maxapi/types/attachments/buttons/link_button.py index 3b393e6..f364c93 100644 --- a/maxapi/types/attachments/buttons/link_button.py +++ b/maxapi/types/attachments/buttons/link_button.py @@ -1,6 +1,6 @@ from typing import Optional -from ....types.attachments.buttons import Button +from .button import Button class LinkButton(Button): diff --git a/maxapi/types/attachments/buttons/request_contact.py b/maxapi/types/attachments/buttons/request_contact.py index 1db3212..140d4ed 100644 --- a/maxapi/types/attachments/buttons/request_contact.py +++ b/maxapi/types/attachments/buttons/request_contact.py @@ -1,4 +1,4 @@ -from ....types.attachments.buttons import Button +from .button import Button class RequestContact(Button): diff --git a/maxapi/types/attachments/buttons/request_geo_location_button.py b/maxapi/types/attachments/buttons/request_geo_location_button.py index cd101bb..d63ccde 100644 --- a/maxapi/types/attachments/buttons/request_geo_location_button.py +++ b/maxapi/types/attachments/buttons/request_geo_location_button.py @@ -1,4 +1,4 @@ -from ....types.attachments.buttons import Button +from .button import Button class RequestGeoLocationButton(Button): diff --git a/maxapi/types/attachments/share.py b/maxapi/types/attachments/share.py index 3b70bd2..1b050e6 100644 --- a/maxapi/types/attachments/share.py +++ b/maxapi/types/attachments/share.py @@ -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 diff --git a/maxapi/types/command.py b/maxapi/types/command.py new file mode 100644 index 0000000..f859cfe --- /dev/null +++ b/maxapi/types/command.py @@ -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 \ No newline at end of file diff --git a/maxapi/types/updates/__init__.py b/maxapi/types/updates/__init__.py index c600837..1f983a8 100644 --- a/maxapi/types/updates/__init__.py +++ b/maxapi/types/updates/__init__.py @@ -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 \ No newline at end of file +UpdateUnion = Union[ + BotAdded, + BotRemoved, + BotStarted, + ChatTitleChanged, + MessageCallback, + MessageChatCreated, + MessageCreated, + MessageEdited, + MessageRemoved, + UserAdded, + UserRemoved +] \ No newline at end of file diff --git a/maxapi/types/updates/bot_added.py b/maxapi/types/updates/bot_added.py index 53f6518..b2cbd0e 100644 --- a/maxapi/types/updates/bot_added.py +++ b/maxapi/types/updates/bot_added.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/maxapi/types/updates/bot_removed.py b/maxapi/types/updates/bot_removed.py index 7131a35..604691f 100644 --- a/maxapi/types/updates/bot_removed.py +++ b/maxapi/types/updates/bot_removed.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/maxapi/types/updates/bot_started.py b/maxapi/types/updates/bot_started.py index d6d4dc3..85051ec 100644 --- a/maxapi/types/updates/bot_started.py +++ b/maxapi/types/updates/bot_started.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/maxapi/types/updates/chat_title_changed.py b/maxapi/types/updates/chat_title_changed.py index 2f88f79..9fe8baf 100644 --- a/maxapi/types/updates/chat_title_changed.py +++ b/maxapi/types/updates/chat_title_changed.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/maxapi/types/updates/message_callback.py b/maxapi/types/updates/message_callback.py index 9c1939c..c69427c 100644 --- a/maxapi/types/updates/message_callback.py +++ b/maxapi/types/updates/message_callback.py @@ -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 @@ -49,6 +49,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, diff --git a/maxapi/types/updates/message_chat_created.py b/maxapi/types/updates/message_chat_created.py index 9f990dc..0180ab4 100644 --- a/maxapi/types/updates/message_chat_created.py +++ b/maxapi/types/updates/message_chat_created.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/maxapi/types/updates/message_created.py b/maxapi/types/updates/message_created.py index d0e1952..dd82b23 100644 --- a/maxapi/types/updates/message_created.py +++ b/maxapi/types/updates/message_created.py @@ -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: @@ -16,4 +16,7 @@ class MessageCreated(Update): bot: Optional[Any] = Field(default=None, exclude=True) if TYPE_CHECKING: - bot: Optional[Bot] \ No newline at end of file + bot: Optional[Bot] + + def get_ids(self): + return (self.message.recipient.chat_id, self.message.recipient.user_id) \ No newline at end of file diff --git a/maxapi/types/updates/message_edited.py b/maxapi/types/updates/message_edited.py index 2ae865c..137f9d1 100644 --- a/maxapi/types/updates/message_edited.py +++ b/maxapi/types/updates/message_edited.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/maxapi/types/updates/message_removed.py b/maxapi/types/updates/message_removed.py index c04cd55..6c822a9 100644 --- a/maxapi/types/updates/message_removed.py +++ b/maxapi/types/updates/message_removed.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/maxapi/types/updates/update.py b/maxapi/types/updates/update.py new file mode 100644 index 0000000..c600837 --- /dev/null +++ b/maxapi/types/updates/update.py @@ -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 \ No newline at end of file diff --git a/maxapi/types/updates/user_added.py b/maxapi/types/updates/user_added.py index 7ac9303..887d755 100644 --- a/maxapi/types/updates/user_added.py +++ b/maxapi/types/updates/user_added.py @@ -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) \ No newline at end of file diff --git a/maxapi/types/updates/user_removed.py b/maxapi/types/updates/user_removed.py index f7157c6..c201acf 100644 --- a/maxapi/types/updates/user_removed.py +++ b/maxapi/types/updates/user_removed.py @@ -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) \ No newline at end of file diff --git a/maxapi/utils/inline_keyboard.py b/maxapi/utils/inline_keyboard.py new file mode 100644 index 0000000..aeee4a1 --- /dev/null +++ b/maxapi/utils/inline_keyboard.py @@ -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 + ) + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bfee7d7 --- /dev/null +++ b/requirements.txt @@ -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