From ee58238261bca4a0b8ea8ddc589ef538e1c460b3 Mon Sep 17 00:00:00 2001 From: Denis Date: Sat, 21 Jun 2025 02:05:33 +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=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20Update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 84 ------------ example/echo_example.py | 31 ----- examples/echo/main.py | 24 ++++ examples/events/main.py | 122 ++++++++++++++++++ examples/magic_filters/magic_filters.py | 48 +++++++ .../router_with_input_media}/audio.mp3 | Bin .../router_with_input_media/main.py | 5 +- .../router_with_input_media/router.py | 0 maxapi/bot.py | 50 ++++++- maxapi/connection/base.py | 40 +++++- maxapi/dispatcher.py | 97 ++++++++------ maxapi/exceptions/download_file.py | 4 + maxapi/exceptions/invalid_token.py | 4 + maxapi/filters/__init__.py | 55 ++------ maxapi/filters/handler.py | 14 +- maxapi/filters/middleware.py | 6 + maxapi/methods/download_media.py | 52 ++++++++ maxapi/methods/get_members_chat.py | 4 +- maxapi/methods/types/getted_updates.py | 43 ++++++ maxapi/types/attachments/attachment.py | 45 ++++++- maxapi/types/updates/bot_added.py | 4 - maxapi/types/updates/bot_removed.py | 4 - maxapi/types/updates/bot_started.py | 5 +- maxapi/types/updates/chat_title_changed.py | 4 - maxapi/types/updates/message_callback.py | 13 +- maxapi/types/updates/message_chat_created.py | 13 +- maxapi/types/updates/message_created.py | 12 +- maxapi/types/updates/message_edited.py | 12 -- maxapi/types/updates/message_removed.py | 12 +- maxapi/types/updates/update.py | 18 ++- maxapi/types/updates/user_added.py | 13 +- maxapi/types/updates/user_removed.py | 12 +- 32 files changed, 546 insertions(+), 304 deletions(-) delete mode 100644 README.md delete mode 100644 example/echo_example.py create mode 100644 examples/echo/main.py create mode 100644 examples/events/main.py create mode 100644 examples/magic_filters/magic_filters.py rename {example => examples/router_with_input_media}/audio.mp3 (100%) rename example/example.py => examples/router_with_input_media/main.py (98%) rename example/router_for_example.py => examples/router_with_input_media/router.py (100%) create mode 100644 maxapi/exceptions/download_file.py create mode 100644 maxapi/exceptions/invalid_token.py create mode 100644 maxapi/filters/middleware.py create mode 100644 maxapi/methods/download_media.py diff --git a/README.md b/README.md deleted file mode 100644 index 8c4af6b..0000000 --- a/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Асинхронный MAX API - -[![PyPI version](https://img.shields.io/pypi/v/maxapi.svg)](https://pypi.org/project/maxapi/) -[![Python Version](https://img.shields.io/pypi/pyversions/maxapi.svg)](https://pypi.org/project/maxapi/) -[![License](https://img.shields.io/github/license/love-apples/maxapi.svg)](https://love-apples/maxapi/blob/main/LICENSE) - ---- - -## 📦 Установка - -```bash -pip install maxapi -``` - ---- - -## 🚀 Быстрый старт - -```python -import asyncio -import logging - -from maxapi import Bot, Dispatcher -from maxapi.types import BotStarted, Command, MessageCreated - -logging.basicConfig(level=logging.INFO) - -bot = Bot('тут_ваш_токен') -dp = Dispatcher() - - -@dp.bot_started() -async def bot_started(event: BotStarted): - await event.bot.send_message( - chat_id=event.chat_id, - text='Привет! Отправь мне /start' - ) - - -@dp.message_created(Command('start')) -async def hello(event: MessageCreated): - await event.message.answer(f"Пример чат-бота для MAX 💙") - - -async def main(): - await dp.start_polling(bot) - - -if __name__ == '__main__': - asyncio.run(main()) -``` - ---- - -## 📚 Документация - -В разработке... - ---- - -## 🧩 Возможности - -- ✅ Роутеры -- ✅ Билдер инлайн клавиатур -- ✅ Простая загрузка медиафайлов -- ✅ MagicFilter -- ✅ Внутренние функции моделей -- ✅ Контекстный менеджер -- ✅ Поллинг -- ✅ Вебхук -- ✅ Логгирование - ---- - - -## 💬 Обратная связь и поддержка - -- MAX: [Чат](https://max.ru/join/IPAok63C3vFqbWTFdutMUtjmrAkGqO56YeAN7iyDfc8) -- Telegram: [@loveappless](https://t.me/loveappless) ---- - -## 📄 Лицензия - -Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для подробностей. diff --git a/example/echo_example.py b/example/echo_example.py deleted file mode 100644 index e60f98f..0000000 --- a/example/echo_example.py +++ /dev/null @@ -1,31 +0,0 @@ -import asyncio -import logging - -from maxapi import Bot, Dispatcher -from maxapi.types import BotStarted, Command, MessageCreated - -logging.basicConfig(level=logging.INFO) - -bot = Bot('тут_ваш_токен') -dp = Dispatcher() - - -@dp.bot_started() -async def bot_started(event: BotStarted): - await event.bot.send_message( - chat_id=event.chat_id, - text='Привет! Отправь мне /start' - ) - - -@dp.message_created(Command('start')) -async def hello(event: MessageCreated): - await event.message.answer(f"Пример чат-бота для MAX 💙") - - -async def main(): - await dp.start_polling(bot) - - -if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file diff --git a/examples/echo/main.py b/examples/echo/main.py new file mode 100644 index 0000000..779937e --- /dev/null +++ b/examples/echo/main.py @@ -0,0 +1,24 @@ +import asyncio +import logging + +from maxapi import Bot, Dispatcher +from maxapi.filters import F +from maxapi.types import MessageCreated + +logging.basicConfig(level=logging.INFO) + +bot = Bot('тут_ваш_токен') +dp = Dispatcher() + + +@dp.message_created(F.message.body.text) +async def echo(event: MessageCreated): + await event.message.answer(f"Повторяю за вами: {event.message.body.text}") + + +async def main(): + await dp.start_polling(bot) + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/examples/events/main.py b/examples/events/main.py new file mode 100644 index 0000000..f49a230 --- /dev/null +++ b/examples/events/main.py @@ -0,0 +1,122 @@ +import asyncio +import logging + +from maxapi import Bot, Dispatcher +from maxapi.types import ( + BotStarted, + Command, + MessageCreated, + CallbackButton, + MessageCallback, + BotAdded, + ChatTitleChanged, + MessageEdited, + MessageRemoved, + UserAdded, + UserRemoved +) +from maxapi.utils.inline_keyboard import InlineKeyboardBuilder + +logging.basicConfig(level=logging.INFO) + +bot = Bot('тут_ваш_токен') +dp = Dispatcher() + + +@dp.message_created(Command('start')) +async def hello(event: MessageCreated): + builder = InlineKeyboardBuilder() + + builder.row( + CallbackButton( + text='Кнопка 1', + payload='btn_1' + ), + CallbackButton( + text='Кнопка 2', + payload='btn_2', + ) + ) + builder.add( + CallbackButton( + text='Кнопка 3', + payload='btn_3', + ) + ) + + await event.message.answer( + text='Привет!', + attachments=[ + builder.as_markup(), + ] # Для MAX клавиатура это вложение, + ) # поэтому она в списке вложений + + +@dp.bot_added() +async def bot_added(event: BotAdded): + await event.bot.send_message( + chat_id=event.chat.id, + text=f'Привет чат {event.chat.title}!' + ) + + +@dp.message_removed() +async def message_removed(event: MessageRemoved): + await event.bot.send_message( + chat_id=event.chat_id, + text='Я всё видел!' + ) + + +@dp.bot_started() +async def bot_started(event: BotStarted): + await event.bot.send_message( + chat_id=event.chat_id, + text='Привет! Отправь мне /start' + ) + + +@dp.chat_title_changed() +async def chat_title_changed(event: ChatTitleChanged): + await event.bot.send_message( + chat_id=event.chat_id, + text=f'Крутое новое название "{event.chat.title}!"' + ) + + +@dp.message_callback() +async def message_callback(event: MessageCallback): + await event.answer( + new_text=f'Вы нажали на кнопку {event.callback.payload}!' + ) + + +@dp.message_edited() +async def message_edited(event: MessageEdited): + await event.message.answer( + text='Вы отредактировали сообщение!' + ) + + +@dp.user_removed() +async def user_removed(event: UserRemoved): + await event.bot.send_message( + chat_id=event.chat_id, + text=f'{event.from_user.first_name} кикнул {event.user.first_name} 😢' + ) + + +@dp.user_added() +async def user_added(event: UserAdded): + await event.bot.send_message( + chat_id=event.chat_id, + text=f'Чат "{event.chat.title}" приветствует вас, {event.user.first_name}!' + ) + + +async def main(): + await dp.start_polling(bot) + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/examples/magic_filters/magic_filters.py b/examples/magic_filters/magic_filters.py new file mode 100644 index 0000000..0097d88 --- /dev/null +++ b/examples/magic_filters/magic_filters.py @@ -0,0 +1,48 @@ +import asyncio +import logging + +from maxapi import Bot, Dispatcher, F +from maxapi.types import MessageCreated + +logging.basicConfig(level=logging.INFO) + +bot = Bot('тут_ваш_токен') +dp = Dispatcher() + + +@dp.message_created(F.message.body.text == 'привет') +async def on_hello(event: MessageCreated): + await event.message.answer('Привет!') + + +@dp.message_created(F.message.body.text.lower().contains('помощь')) +async def on_help(event: MessageCreated): + await event.message.answer('Чем могу помочь?') + + +@dp.message_created(F.message.body.text.regexp(r'^\d{4}$')) +async def on_code(event: MessageCreated): + await event.message.answer('Принят 4-значный код') + + +@dp.message_created(F.message.body.attachments) +async def on_attachment(event: MessageCreated): + await event.message.answer('Получено вложение') + + +@dp.message_created(F.message.body.text.len() > 20) +async def on_long_text(event: MessageCreated): + await event.message.answer('Слишком длинное сообщение') + + +@dp.message_created(F.message.body.text.len() > 0) +async def on_non_empty(event: MessageCreated): + await event.message.answer('Вы что-то написали.') + + +async def main(): + await dp.start_polling(bot) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/example/audio.mp3 b/examples/router_with_input_media/audio.mp3 similarity index 100% rename from example/audio.mp3 rename to examples/router_with_input_media/audio.mp3 diff --git a/example/example.py b/examples/router_with_input_media/main.py similarity index 98% rename from example/example.py rename to examples/router_with_input_media/main.py index 569946a..9e515ca 100644 --- a/example/example.py +++ b/examples/router_with_input_media/main.py @@ -6,7 +6,7 @@ from maxapi.context import MemoryContext, State, StatesGroup from maxapi.types import BotStarted, Command, MessageCreated, CallbackButton, MessageCallback, BotCommand from maxapi.utils.inline_keyboard import InlineKeyboardBuilder -from example.router_for_example import router +from router import router logging.basicConfig(level=logging.INFO) @@ -158,4 +158,5 @@ async def main(): # ) -asyncio.run(main()) \ No newline at end of file +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/example/router_for_example.py b/examples/router_with_input_media/router.py similarity index 100% rename from example/router_for_example.py rename to examples/router_with_input_media/router.py diff --git a/maxapi/bot.py b/maxapi/bot.py index e806d06..99c4e78 100644 --- a/maxapi/bot.py +++ b/maxapi/bot.py @@ -1,6 +1,8 @@ from datetime import datetime from typing import Any, Dict, List, TYPE_CHECKING +from maxapi.methods.download_media import DownloadMedia + from .methods.get_upload_url import GetUploadURL from .methods.get_updates import GetUpdates from .methods.remove_member_chat import RemoveMemberChat @@ -570,7 +572,7 @@ class Bot(BaseConnection): """Получает участников чата. :param chat_id: ID чата - :param user_ids: Фильтр по ID пользователей + :param user_ids: Список ID участников :param marker: Маркер для пагинации :param count: Количество участников @@ -584,6 +586,28 @@ class Bot(BaseConnection): marker=marker, count=count, ).request() + + async def get_chat_member( + self, + chat_id: int, + user_id: int, + ) -> GettedMembersChat: + + """Получает участника чата. + + :param chat_id: ID чата + :param user_id: ID участника + + :return: Участник + """ + + members = await self.get_chat_members( + chat_id=chat_id, + user_ids=[user_id] + ) + + if members.members: + return members.members[0] async def add_chat_members( self, @@ -673,4 +697,28 @@ class Bot(BaseConnection): return await ChangeInfo( bot=self, commands=list(commands) + ).request() + + async def download_file( + self, + path: str, + url: str, + token: str + ): + + """ + Скачивает медиа с указанной ссылки по токену, сохраняя по определенному пути + + :param path: Путь сохранения медиа + :param url: Ссылка на медиа + :param token: Токен медиа + + :return: Числовой статус + """ + + return await DownloadMedia( + bot=self, + path=path, + media_url=url, + media_token=token ).request() \ No newline at end of file diff --git a/maxapi/connection/base.py b/maxapi/connection/base.py index 7b09db7..2179570 100644 --- a/maxapi/connection/base.py +++ b/maxapi/connection/base.py @@ -1,13 +1,19 @@ import os + from typing import TYPE_CHECKING +import aiofiles import aiohttp + from pydantic import BaseModel +from ..exceptions.invalid_token import InvalidToken + from ..types.errors import Error from ..enums.http_method import HTTPMethod from ..enums.api_path import ApiPath from ..enums.upload_type import UploadType + from ..loggers import logger_bot, logger_connection if TYPE_CHECKING: @@ -65,6 +71,9 @@ class BaseConnection: ) except aiohttp.ClientConnectorDNSError as e: return logger_connection.error(f'Ошибка при отправке запроса: {e}') + + if r.status == 401: + raise InvalidToken('Неверный токен!') if not r.ok: raw = await r.json() @@ -124,4 +133,33 @@ class BaseConnection: data=form ) - return await response.text() \ No newline at end of file + return await response.text() + + async def download_file( + self, + path: str, + url: str, + token: str, + ): + """ + Скачивает медиа с указанной ссылки по токену, сохраняя по определенному пути + + :param path: Путь сохранения медиа + :param url: Ссылка на медиа + :param token: Токен медиа + + :return: Числовой статус + """ + + headers = { + 'Authorization': f'Bearer {token}' + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + + if response.status == 200: + async with aiofiles.open(path, 'wb') as f: + await f.write(await response.read()) + + return response.status \ No newline at end of file diff --git a/maxapi/dispatcher.py b/maxapi/dispatcher.py index f79c2db..b1ab861 100644 --- a/maxapi/dispatcher.py +++ b/maxapi/dispatcher.py @@ -2,6 +2,7 @@ from typing import Callable, List from fastapi import FastAPI, Request from fastapi.responses import JSONResponse +from magic_filter import MagicFilter from uvicorn import Config, Server from aiohttp import ClientConnectorError @@ -33,6 +34,8 @@ class Dispatcher: def __init__(self): self.event_handlers: List[Handler] = [] self.contexts: List[MemoryContext] = [] + self.routers: List[Router] = [] + self.filters: List[MagicFilter] = [] self.bot = None self.on_started_func = None @@ -65,9 +68,24 @@ class Dispatcher: """ for router in routers: - for event in router.event_handlers: - self.event_handlers.append(event) + self.routers.append(router) + + async def __ready(self, bot: Bot): + self.bot = bot + await self.check_me() + + self.routers += [self] + + handlers_count = 0 + for router in self.routers: + for handler in router.event_handlers: + handlers_count += 1 + logger_dp.info(f'{handlers_count} событий на обработку') + + if self.on_started_func: + await self.on_started_func() + def __get_memory_context(self, chat_id: int, user_id: int): """Возвращает или создает контекст для чата и пользователя. @@ -95,40 +113,51 @@ class Dispatcher: Args: event_object: Объект события для обработки """ + ids = event_object.get_ids() is_handled = False + + for router in self.routers: + + if is_handled: + break + + if router.filters: + if not filter_attrs(event_object, *router.filters): + continue + + for handler in router.event_handlers: - 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): + if not handler.update_type == event_object.update_type: continue - ids = event_object.get_ids() + if handler.filters: + if not filter_attrs(event_object, *handler.filters): + continue - memory_context = self.__get_memory_context(*ids) - - if not handler.state == await memory_context.get_state() \ - and handler.state: - continue - - func_args = handler.func_event.__annotations__.keys() + memory_context = self.__get_memory_context(*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} + kwargs = {'context': memory_context} - for key in kwargs.copy().keys(): - if not key in func_args: - del kwargs[key] + for key in kwargs.copy().keys(): + if not key in func_args: + del kwargs[key] + + if handler.middleware: + await handler.middleware() - await handler.func_event(event_object, **kwargs) + await handler.func_event(event_object, **kwargs) - logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') + logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') - is_handled = True - break + is_handled = True + break if not is_handled: logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') @@ -140,14 +169,7 @@ class Dispatcher: Args: bot: Экземпляр бота """ - - self.bot = bot - await self.check_me() - - logger_dp.info(f'{len(self.event_handlers)} событий на обработку') - - if self.on_started_func: - await self.on_started_func() + await self.__ready(bot) while True: try: @@ -184,11 +206,7 @@ class Dispatcher: port: Порт для сервера """ - self.bot = bot - await self.check_me() - - if self.on_started_func: - await self.on_started_func() + await self.__ready(bot) @app.post('/') async def _(request: Request): @@ -206,7 +224,6 @@ class Dispatcher: except Exception as e: logger_dp.error(f"Ошибка при обработке события: {event_json['update_type']}: {e}") - logger_dp.info(f'{len(self.event_handlers)} событий на обработку') config = Config(app=app, host=host, port=port, log_level="critical") server = Server(config) @@ -231,8 +248,10 @@ class Event: def __call__(self, *args, **kwargs): def decorator(func_event: Callable): + if self.update_type == UpdateType.ON_STARTED: self.router.on_started_func = func_event + else: self.router.event_handlers.append( Handler( diff --git a/maxapi/exceptions/download_file.py b/maxapi/exceptions/download_file.py new file mode 100644 index 0000000..8dec223 --- /dev/null +++ b/maxapi/exceptions/download_file.py @@ -0,0 +1,4 @@ + + +class NotAvailableForDownload(BaseException): + ... \ No newline at end of file diff --git a/maxapi/exceptions/invalid_token.py b/maxapi/exceptions/invalid_token.py new file mode 100644 index 0000000..6ef3586 --- /dev/null +++ b/maxapi/exceptions/invalid_token.py @@ -0,0 +1,4 @@ + + +class InvalidToken(BaseException): + ... \ No newline at end of file diff --git a/maxapi/filters/__init__.py b/maxapi/filters/__init__.py index f194327..53e6547 100644 --- a/maxapi/filters/__init__.py +++ b/maxapi/filters/__init__.py @@ -6,48 +6,15 @@ from magic_filter.operations.comparator import ComparatorOperation as mf_compara F = MagicFilter() -def filter_attrs(obj, *magic_args): +def filter_attrs(obj: object, *filters: MagicFilter) -> bool: + """ + Применяет один или несколько фильтров MagicFilter к объекту. + + :param obj: Любой объект с атрибутами (например, event/message) + :param filters: Один или несколько MagicFilter выражений + :return: True, если все фильтры возвращают True, иначе False + """ 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: - ... \ No newline at end of file + return all(f.resolve(obj) for f in filters) + except Exception: + return False \ No newline at end of file diff --git a/maxapi/filters/handler.py b/maxapi/filters/handler.py index ab0533b..16f246d 100644 --- a/maxapi/filters/handler.py +++ b/maxapi/filters/handler.py @@ -2,9 +2,14 @@ from typing import Callable from magic_filter import F, MagicFilter +from ..filters.middleware import BaseMiddleware + from ..types.command import Command + from ..context.state_machine import State + from ..enums.update import UpdateType + from ..loggers import logger_dp @@ -36,10 +41,11 @@ class Handler: :param kwargs: Дополнительные параметры (не используются) """ - self.func_event = func_event - self.update_type = update_type + self.func_event: Callable = func_event + self.update_type: UpdateType = update_type self.filters = [] - self.state = None + self.state: State = None + self.middleware: BaseMiddleware = None for arg in args: if isinstance(arg, MagicFilter): @@ -48,6 +54,8 @@ class Handler: self.state = arg elif isinstance(arg, Command): self.filters.insert(0, F.message.body.text.startswith(arg.command)) + elif isinstance(arg, BaseMiddleware): + self.middleware = arg else: logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при ' f'регистрации функции `{func_event.__name__}`') \ No newline at end of file diff --git a/maxapi/filters/middleware.py b/maxapi/filters/middleware.py new file mode 100644 index 0000000..a50196f --- /dev/null +++ b/maxapi/filters/middleware.py @@ -0,0 +1,6 @@ +from ..types.updates import UpdateUnion + + +class BaseMiddleware: + def __init__(self): + ... \ No newline at end of file diff --git a/maxapi/methods/download_media.py b/maxapi/methods/download_media.py new file mode 100644 index 0000000..3d45dc7 --- /dev/null +++ b/maxapi/methods/download_media.py @@ -0,0 +1,52 @@ +from typing import TYPE_CHECKING + +from ..methods.types.deleted_pin_message import DeletedPinMessage + +from ..enums.http_method import HTTPMethod +from ..enums.api_path import ApiPath +from ..enums.upload_type import UploadType + +from ..connection.base import BaseConnection + + +if TYPE_CHECKING: + from ..bot import Bot + + +class DownloadMedia(BaseConnection): + + """ + Класс для скачивания медиафайлов. + + Args: + bot (Bot): Экземпляр бота для выполнения запроса. + media_url (str): Ссылка на медиа. + media_token (str): Токен медиа. + """ + + def __init__( + self, + bot: 'Bot', + path: str, + media_url: str, + media_token: str + ): + self.bot = bot + self.path = path + self.media_url = media_url + self.media_token = media_token + + async def request(self) -> int: + + """ + Выполняет GET-запрос для скачивания медиафайла + + Returns: + int: Код операции. + """ + + return await super().download_file( + path=self.path, + url=self.media_url, + token=self.media_token + ) \ No newline at end of file diff --git a/maxapi/methods/get_members_chat.py b/maxapi/methods/get_members_chat.py index 9ebb21f..ffe0207 100644 --- a/maxapi/methods/get_members_chat.py +++ b/maxapi/methods/get_members_chat.py @@ -60,7 +60,9 @@ class GetMembersChat(BaseConnection): params = self.bot.params.copy() - if self.user_ids: params['user_ids'] = ','.join(self.user_ids) + if self.user_ids: + self.user_ids = [str(user_id) for user_id in self.user_ids] + params['user_ids'] = ','.join(self.user_ids) if self.marker: params['marker'] = self.marker if self.count: params['marker'] = self.count diff --git a/maxapi/methods/types/getted_updates.py b/maxapi/methods/types/getted_updates.py index 1cb8ad9..ceaafb9 100644 --- a/maxapi/methods/types/getted_updates.py +++ b/maxapi/methods/types/getted_updates.py @@ -19,34 +19,77 @@ if TYPE_CHECKING: 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) + event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) + event_object.from_user = event_object.callback.user + case UpdateType.MESSAGE_CHAT_CREATED: event_object = MessageChatCreated(**event) + event_object.chat = event_object.chat + case UpdateType.MESSAGE_CREATED: event_object = MessageCreated(**event) + event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) + event_object.from_user = event_object.message.sender + case UpdateType.MESSAGE_EDITED: event_object = MessageEdited(**event) + event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) + event_object.from_user = event_object.message.sender + case UpdateType.MESSAGE_REMOVED: event_object = MessageRemoved(**event) + event_object.chat = await bot.get_chat_by_id(event_object.chat_id) + event_object.from_user = await bot.get_chat_member( + chat_id=event_object.chat_id, + user_id=event_object.user_id + ) + case UpdateType.USER_ADDED: event_object = UserAdded(**event) + event_object.chat = await bot.get_chat_by_id(event_object.chat_id) + event_object.from_user = event_object.user + case UpdateType.USER_REMOVED: event_object = UserRemoved(**event) + event_object.chat = await bot.get_chat_by_id(event_object.chat_id) + event_object.from_user = await bot.get_chat_member( + chat_id=event_object.chat_id, + user_id=event_object.admin_id + ) if event_object.admin_id else None + + if event['update_type'] in (UpdateType.BOT_ADDED, + UpdateType.BOT_REMOVED, + UpdateType.BOT_STARTED, + UpdateType.CHAT_TITLE_CHANGED): + event_object.chat = await bot.get_chat_by_id(event_object.chat_id) + event_object.from_user = event_object.user if hasattr(event_object, 'bot'): event_object.bot = bot + if hasattr(event_object, 'message'): event_object.message.bot = bot + + for attachment in event_object.message.body.attachments: + if hasattr(attachment, 'bot'): + attachment.bot = bot return event_object diff --git a/maxapi/types/attachments/attachment.py b/maxapi/types/attachments/attachment.py index 879cf8f..7012915 100644 --- a/maxapi/types/attachments/attachment.py +++ b/maxapi/types/attachments/attachment.py @@ -1,14 +1,19 @@ -from typing import List, Optional, Union -from pydantic import BaseModel +from typing import TYPE_CHECKING, Any, List, Optional, Union +from pydantic import BaseModel, Field + +from ...exceptions.download_file import NotAvailableForDownload from ...types.attachments.upload import AttachmentUpload - from ...types.attachments.buttons import InlineButtonUnion from ...types.users import User from ...enums.attachment import AttachmentType +if TYPE_CHECKING: + from ...bot import Bot + + class StickerAttachmentPayload(BaseModel): """ @@ -98,6 +103,36 @@ class Attachment(BaseModel): ButtonsPayload, StickerAttachmentPayload ]] = None - + bot: Optional[Any] = Field(default=None, exclude=True) + + if TYPE_CHECKING: + bot: Optional[Bot] + class Config: - use_enum_values = True \ No newline at end of file + use_enum_values = True + + async def download( + self, + path: str + ): + + """ + Скачивает медиа, сохраняя по определенному пути + + :param path: Путь сохранения медиа + + :return: Числовой статус + """ + + if not hasattr(self.payload, 'token') or \ + not hasattr(self.payload, 'url'): + raise NotAvailableForDownload() + + elif not self.payload.token or not self.payload.url: + raise NotAvailableForDownload(f'Медиа типа `{self.type}` недоступно для скачивания') + + return await self.bot.download_file( + path=path, + url=self.payload.url, + token=self.payload.token, + ) \ No newline at end of file diff --git a/maxapi/types/updates/bot_added.py b/maxapi/types/updates/bot_added.py index ab44185..ad79f2f 100644 --- a/maxapi/types/updates/bot_added.py +++ b/maxapi/types/updates/bot_added.py @@ -1,7 +1,5 @@ from typing import TYPE_CHECKING, Any, Optional -from pydantic import Field - from .update import Update from ...types.users import User @@ -18,12 +16,10 @@ class BotAdded(Update): Attributes: chat_id (Optional[int]): Идентификатор чата, куда добавлен бот. user (User): Объект пользователя-бота. - bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется. """ chat_id: Optional[int] = None user: User - bot: Optional[Any] = Field(default=None, exclude=True) if TYPE_CHECKING: bot: Optional[Bot] diff --git a/maxapi/types/updates/bot_removed.py b/maxapi/types/updates/bot_removed.py index c25ca3c..2bb0811 100644 --- a/maxapi/types/updates/bot_removed.py +++ b/maxapi/types/updates/bot_removed.py @@ -1,7 +1,5 @@ from typing import TYPE_CHECKING, Any, Optional -from pydantic import Field - from .update import Update from ...types.users import User @@ -18,12 +16,10 @@ class BotRemoved(Update): Attributes: chat_id (Optional[int]): Идентификатор чата, из которого удалён бот. user (User): Объект пользователя-бота. - bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется. """ chat_id: Optional[int] = None user: User - bot: Optional[Any] = Field(default=None, exclude=True) if TYPE_CHECKING: bot: Optional[Bot] diff --git a/maxapi/types/updates/bot_started.py b/maxapi/types/updates/bot_started.py index f72ebbb..fde7b37 100644 --- a/maxapi/types/updates/bot_started.py +++ b/maxapi/types/updates/bot_started.py @@ -1,8 +1,7 @@ from typing import TYPE_CHECKING, Any, Optional -from pydantic import Field - from .update import Update + from ...types.users import User if TYPE_CHECKING: @@ -19,14 +18,12 @@ class BotStarted(Update): user (User): Пользователь (бот). user_locale (Optional[str]): Локаль пользователя. payload (Optional[str]): Дополнительные данные. - bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется. """ 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] diff --git a/maxapi/types/updates/chat_title_changed.py b/maxapi/types/updates/chat_title_changed.py index 6ecb864..341c256 100644 --- a/maxapi/types/updates/chat_title_changed.py +++ b/maxapi/types/updates/chat_title_changed.py @@ -1,7 +1,5 @@ from typing import TYPE_CHECKING, Any, Optional -from pydantic import Field - from .update import Update from ...types.users import User @@ -19,13 +17,11 @@ class ChatTitleChanged(Update): chat_id (Optional[int]): Идентификатор чата. user (User): Пользователь, совершивший изменение. title (Optional[str]): Новое название чата. - bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется. """ 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] diff --git a/maxapi/types/updates/message_callback.py b/maxapi/types/updates/message_callback.py index e0ad559..dcf3322 100644 --- a/maxapi/types/updates/message_callback.py +++ b/maxapi/types/updates/message_callback.py @@ -1,6 +1,6 @@ -from typing import Any, List, Optional, TYPE_CHECKING, Union +from typing import List, Optional, TYPE_CHECKING, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel from .update import Update @@ -21,6 +21,8 @@ from ..attachments.audio import Audio if TYPE_CHECKING: from ...bot import Bot + from ...types.chats import Chat + from ...types.users import User class MessageForCallback(BaseModel): @@ -65,16 +67,11 @@ class MessageCallback(Update): message (Message): Сообщение, на которое пришёл callback. user_locale (Optional[str]): Локаль пользователя. callback (Callback): Объект callback. - bot (Optional[Any]): Экземпляр бота, не сериализуется. """ message: Message user_locale: Optional[str] = None callback: Callback - bot: Optional[Any] = Field(default=None, exclude=True) - - if TYPE_CHECKING: - bot: Optional[Bot] def get_ids(self): @@ -89,7 +86,7 @@ class MessageCallback(Update): async def answer( self, - notification: str, + notification: str = None, new_text: str = None, link: NewMessageLink = None, notify: bool = True, diff --git a/maxapi/types/updates/message_chat_created.py b/maxapi/types/updates/message_chat_created.py index 0180ab4..96de461 100644 --- a/maxapi/types/updates/message_chat_created.py +++ b/maxapi/types/updates/message_chat_created.py @@ -1,24 +1,15 @@ -from typing import TYPE_CHECKING, Any, Optional - -from pydantic import Field +from typing import Optional from ...types.chats import Chat 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) \ No newline at end of file + return (self.chat.chat_id, self.chat.owner_id) \ No newline at end of file diff --git a/maxapi/types/updates/message_created.py b/maxapi/types/updates/message_created.py index f94cc6d..64a77ee 100644 --- a/maxapi/types/updates/message_created.py +++ b/maxapi/types/updates/message_created.py @@ -1,15 +1,10 @@ from __future__ import annotations -from typing import Any, Optional, TYPE_CHECKING - -from pydantic import Field +from typing import Optional, TYPE_CHECKING from .update import Update from ...types.message import Message -if TYPE_CHECKING: - from ...bot import Bot - class MessageCreated(Update): @@ -19,16 +14,11 @@ class MessageCreated(Update): Attributes: message (Message): Объект сообщения. user_locale (Optional[str]): Локаль пользователя. - bot (Optional[Any]): Экземпляр бота, не сериализуется. """ message: Message user_locale: Optional[str] = None - bot: Optional[Any] = Field(default=None, exclude=True) - if TYPE_CHECKING: - bot: Optional[Bot] - def get_ids(self): """ diff --git a/maxapi/types/updates/message_edited.py b/maxapi/types/updates/message_edited.py index 0a728f1..0b79984 100644 --- a/maxapi/types/updates/message_edited.py +++ b/maxapi/types/updates/message_edited.py @@ -1,14 +1,7 @@ -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): @@ -17,15 +10,10 @@ class MessageEdited(Update): Attributes: message (Message): Объект измененного сообщения. - bot (Optional[Any]): Экземпляр бота, не сериализуется. """ message: Message - bot: Optional[Any] = Field(default=None, exclude=True) - if TYPE_CHECKING: - bot: Optional[Bot] - def get_ids(self): """ diff --git a/maxapi/types/updates/message_removed.py b/maxapi/types/updates/message_removed.py index 83f32b3..bc7e4ab 100644 --- a/maxapi/types/updates/message_removed.py +++ b/maxapi/types/updates/message_removed.py @@ -1,12 +1,7 @@ -from typing import TYPE_CHECKING, Any, Optional - -from pydantic import Field +from typing import Optional from .update import Update -if TYPE_CHECKING: - from ...bot import Bot - class MessageRemoved(Update): @@ -17,16 +12,11 @@ class MessageRemoved(Update): message_id (Optional[str]): Идентификатор удаленного сообщения. Может быть None. chat_id (Optional[int]): Идентификатор чата. Может быть None. user_id (Optional[int]): Идентификатор пользователя. Может быть None. - bot (Optional[Bot]): Объект бота, исключается из сериализации. """ 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): diff --git a/maxapi/types/updates/update.py b/maxapi/types/updates/update.py index caa986b..1952252 100644 --- a/maxapi/types/updates/update.py +++ b/maxapi/types/updates/update.py @@ -1,7 +1,14 @@ -from pydantic import BaseModel +from __future__ import annotations +from typing import TYPE_CHECKING, Any, Optional +from pydantic import BaseModel, Field from ...enums.update import UpdateType +if TYPE_CHECKING: + from ...bot import Bot + from ...types.chats import Chat + from ...types.users import User + class Update(BaseModel): @@ -15,6 +22,15 @@ class Update(BaseModel): update_type: UpdateType timestamp: int + + bot: Optional[Any] = Field(default=None, exclude=True) + from_user: Optional[Any] = Field(default=None, exclude=True) + chat: Optional[Any] = Field(default=None, exclude=True) + + if TYPE_CHECKING: + bot: Optional[Bot] + from_user: Optional[User] + chat: Optional[Chat] 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 d5ae547..c9742dd 100644 --- a/maxapi/types/updates/user_added.py +++ b/maxapi/types/updates/user_added.py @@ -1,16 +1,10 @@ -from typing import TYPE_CHECKING, Any, Optional - -from pydantic import Field +from typing import Optional from .update import Update from ...types.users import User -if TYPE_CHECKING: - from ...bot import Bot - - class UserAdded(Update): """ @@ -20,17 +14,12 @@ class UserAdded(Update): inviter_id (Optional[int]): Идентификатор пользователя, добавившего нового участника. Может быть None. chat_id (Optional[int]): Идентификатор чата. Может быть None. user (User): Объект пользователя, добавленного в чат. - bot (Optional[Bot]): Объект бота, исключается из сериализации. """ 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): """ diff --git a/maxapi/types/updates/user_removed.py b/maxapi/types/updates/user_removed.py index 6a218d2..19f642c 100644 --- a/maxapi/types/updates/user_removed.py +++ b/maxapi/types/updates/user_removed.py @@ -1,14 +1,9 @@ -from typing import TYPE_CHECKING, Any, Optional - -from pydantic import Field +from typing import Optional from .update import Update from ...types.users import User -if TYPE_CHECKING: - from ...bot import Bot - class UserRemoved(Update): @@ -19,17 +14,12 @@ class UserRemoved(Update): admin_id (Optional[int]): Идентификатор администратора, удалившего пользователя. Может быть None. chat_id (Optional[int]): Идентификатор чата. Может быть None. user (User): Объект пользователя, удаленного из чата. - bot (Optional[Bot]): Объект бота, исключается из сериализации. """ 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): """