diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b27b2514..bd638785 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: trailing-whitespace exclude: ^(.*\.md|.*\.yml|.*\.yaml) diff --git a/Makefile b/Makefile index 4bb4be64..5f91257c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ SHELL = /bin/bash +.PHONY: upgrade +upgrade: + uv lock --upgrade + + .PHONY: run-test run-test: @echo "Running linters and tests in parallel (uv run)..." diff --git a/README.md b/README.md index 2c450b8f..23644bd6 100644 --- a/README.md +++ b/README.md @@ -95,14 +95,15 @@ logging.basicConfig(level=logging.INFO) bot = Bot() dp = Dispatcher() + # Ответ бота при нажатии на кнопку "Начать" @dp.bot_started() async def bot_started(event: BotStarted): await bot.send_message( - chat_id=event.chat_id, - text='Привет! Отправь мне /start' + chat_id=event.chat_id, text="Привет! Отправь мне /start" ) + # Ответ бота на команду /start @dp.message_created(CommandStart()) async def hello(event: MessageCreated): @@ -118,7 +119,7 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -167,12 +168,12 @@ async def hello(event: MessageCreated): async def main(): await dp.handle_webhook( bot=bot, - host='0.0.0.0', + host="0.0.0.0", port=8080, ) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -201,11 +202,15 @@ import uvicorn from fastapi import FastAPI from maxapi.webhook.fastapi import FastAPIMaxWebhook + async def main(): webhook = FastAPIMaxWebhook(dp=dp, bot=bot) app = FastAPI(lifespan=webhook.lifespan) - webhook.setup(app, path='/webhook') - await uvicorn.Server(uvicorn.Config(app, host='0.0.0.0', port=8080)).serve() + webhook.setup(app, path="/webhook") + await uvicorn.Server( + uvicorn.Config(app, host="0.0.0.0", port=8080) + ).serve() + asyncio.run(main()) ``` @@ -217,10 +222,14 @@ import asyncio import uvicorn from maxapi.webhook.litestar import LitestarMaxWebhook + async def main(): webhook = LitestarMaxWebhook(dp=dp, bot=bot) - app = webhook.create_app(path='/webhook') - await uvicorn.Server(uvicorn.Config(app, host='0.0.0.0', port=8080)).serve() + app = webhook.create_app(path="/webhook") + await uvicorn.Server( + uvicorn.Config(app, host="0.0.0.0", port=8080) + ).serve() + asyncio.run(main()) ``` diff --git a/doc/router_inner_middleware.md b/doc/router_inner_middleware.md index 9a53cbe9..a0e48fe7 100644 --- a/doc/router_inner_middleware.md +++ b/doc/router_inner_middleware.md @@ -13,16 +13,22 @@ closes #132 ```python admin_router = Router(router_id="admin") -admin_router.middleware(BroadcastLockMiddleware()) # хочется: lock только когда broadcast реально начнётся +admin_router.middleware( + BroadcastLockMiddleware() +) # хочется: lock только когда broadcast реально начнётся + @admin_router.message_created(IsAdmin(), Command("broadcast")) async def handle_broadcast(event): ... + fallback_router = Router(router_id="fallback") + @fallback_router.message_created() async def handle_fallback(event): ... + dp.include_routers(admin_router, fallback_router) ``` @@ -183,8 +189,8 @@ handler.mw_chain = accumulated_inner_middlewares + handler.middlewares → call_ ### 1. `Dispatcher.__init__` — переименование и новый атрибут ```python -self.outer_middlewares: list[BaseMiddleware] = [] # было: self.middlewares -self.inner_middlewares: list[BaseMiddleware] = [] # новый +self.outer_middlewares: list[BaseMiddleware] = [] # было: self.middlewares +self.inner_middlewares: list[BaseMiddleware] = [] # новый ``` Атрибут `middlewares` сохраняется как `@property`-алиас с @@ -223,6 +229,7 @@ def register_outer_middleware(self, middleware: BaseMiddleware) -> None: """ self.outer_middlewares.append(middleware) + def register_inner_middleware(self, middleware: BaseMiddleware) -> None: """Inner middleware (после фильтров handler). @@ -233,6 +240,7 @@ def register_inner_middleware(self, middleware: BaseMiddleware) -> None: """ self.inner_middlewares.append(middleware) + def register_middleware(self, middleware: BaseMiddleware) -> None: """ .. deprecated:: @@ -266,7 +274,7 @@ def _iter_routers( self, routers, parent_middlewares=None, - parent_inner_middlewares=None, # новый параметр + parent_inner_middlewares=None, # новый параметр parent_filters=None, parent_base_filters=None, path=None, @@ -280,12 +288,14 @@ def _iter_routers( if router is self: accumulated_inner_middlewares = inner_middlewares else: - accumulated_inner_middlewares = inner_middlewares + router.inner_middlewares + accumulated_inner_middlewares = ( + inner_middlewares + router.inner_middlewares + ) yield ( router, accumulated_middlewares, - accumulated_inner_middlewares, # ← новый 5-й элемент + accumulated_inner_middlewares, # ← новый 5-й элемент accumulated_filters, accumulated_base_filters, ) @@ -317,17 +327,15 @@ def _prepare_handlers(self, bot: Bot) -> None: # Порядок: global inner → router inner (накопленный) → handler mw # Внешний слой — global, внутренний — handler. all_inner = ( - global_inner_mw - + accumulated_inner_mw - + handler.middlewares + global_inner_mw + accumulated_inner_mw + handler.middlewares ) handler.mw_chain = self.build_middleware_chain( all_inner, functools.partial(self.call_handler, handler), ) - router.handlers_by_type.setdefault( - handler.update_type, [] - ).append(handler) + router.handlers_by_type.setdefault(handler.update_type, []).append( + handler + ) self._cached_router_entries = list(self._iter_unique_routers(self.routers)) ``` @@ -457,9 +465,12 @@ fallback_router: ```python from maxapi import Router, Dispatcher, Bot from myapp.middleware import ( - RequestIdMiddleware, LoggingMiddleware, - DbTransactionMiddleware, HandlerLatencyMetricMiddleware, - AdminAccessLogMiddleware, BroadcastLockMiddleware, + RequestIdMiddleware, + LoggingMiddleware, + DbTransactionMiddleware, + HandlerLatencyMetricMiddleware, + AdminAccessLogMiddleware, + BroadcastLockMiddleware, ) from myapp.filters import IsAdmin from maxapi.filters import Command @@ -470,14 +481,18 @@ dp = Dispatcher() # Срабатывает для каждого события, даже для тех, что в итоге будут # проигнорированы. Подходит всё, что должно «жить» в контексте # любого входящего апдейта. -dp.register_outer_middleware(RequestIdMiddleware()) # request_id для трейсинга -dp.register_outer_middleware(LoggingMiddleware()) # лог всех updates +dp.register_outer_middleware(RequestIdMiddleware()) # request_id для трейсинга +dp.register_outer_middleware(LoggingMiddleware()) # лог всех updates # ─── Глобальный inner ───────────────────────────────────────────── # Срабатывает только если какой-то handler реально будет выполнен. # Подходит то, что бессмысленно делать «вхолостую». -dp.register_inner_middleware(DbTransactionMiddleware()) # транзакция только под handler -dp.register_inner_middleware(HandlerLatencyMetricMiddleware()) # метрика времени handler +dp.register_inner_middleware( + DbTransactionMiddleware() +) # транзакция только под handler +dp.register_inner_middleware( + HandlerLatencyMetricMiddleware() +) # метрика времени handler admin_router = Router(router_id="admin") @@ -493,9 +508,11 @@ admin_router.register_outer_middleware(AdminAccessLogMiddleware()) # исключительно перед началом тяжёлой операции. admin_router.register_inner_middleware(BroadcastLockMiddleware()) + @admin_router.message_created(IsAdmin(), Command("broadcast")) async def broadcast(event): ... + dp.include_routers(admin_router) ``` @@ -544,10 +561,14 @@ outer middleware и фильтры: ```python parent = Router(router_id="parent") -parent.register_inner_middleware(DbTransactionMiddleware()) # на любой handler в parent + детях +parent.register_inner_middleware( + DbTransactionMiddleware() # на любой handler в parent + детях +) child = Router(router_id="child") -child.register_inner_middleware(ChildAuditMiddleware()) # только на handler внутри child +child.register_inner_middleware( + ChildAuditMiddleware() # только на handler внутри child +) parent.include_routers(child) ``` @@ -600,10 +621,14 @@ handler.mw_chain = [DbTransactionMiddleware] + handler.middlewares → call_hand # aiogram router = Router() -router.message.outer_middleware(LoggingMiddleware()) # outer для message-обзёрвера -router.message.middleware(AuthMiddleware()) # inner для message-обзёрвера +router.message.outer_middleware( + LoggingMiddleware() +) # outer для message-обзёрвера +router.message.middleware(AuthMiddleware()) # inner для message-обзёрвера -router.callback_query.middleware(RateLimitMiddleware()) # только для callback_query +router.callback_query.middleware( + RateLimitMiddleware() +) # только для callback_query # Дополнительно: middleware на уровне Router.update — ловит ВСЕ типы router.update.outer_middleware(TracingMiddleware()) @@ -679,14 +704,26 @@ warning): ```python # Глобальный уровень -dp.register_outer_middleware(LoggingMW()) # ✅ канонично: outer (для каждого события) -dp.register_inner_middleware(MetricsMW()) # ✅ канонично: inner (только когда handler сработал) -dp.middleware(SomeMW()) # ⚠️ DeprecationWarning → используйте .register_outer_middleware() +dp.register_outer_middleware( + LoggingMW() +) # ✅ канонично: outer (для каждого события) +dp.register_inner_middleware( + MetricsMW() +) # ✅ канонично: inner (только когда handler сработал) +dp.middleware( + SomeMW() +) # ⚠️ DeprecationWarning → используйте .register_outer_middleware() # Роутерный уровень -router.register_outer_middleware(TracingMW()) # ✅ канонично: outer (до filters handler) -router.register_inner_middleware(AuthMW()) # ✅ канонично: inner (после filters handler) -router.middleware(AuditMW()) # ⚠️ DeprecationWarning → используйте .register_outer_middleware() +router.register_outer_middleware( + TracingMW() +) # ✅ канонично: outer (до filters handler) +router.register_inner_middleware( + AuthMW() +) # ✅ канонично: inner (после filters handler) +router.middleware( + AuditMW() +) # ⚠️ DeprecationWarning → используйте .register_outer_middleware() ``` **Почему это лучше, чем в aiogram**: в aiogram `router.middleware()` без @@ -755,6 +792,7 @@ execution order). Причины: message_router = Router() message_router.register_inner_middleware(MyMW()) + @message_router.message_created() async def handler(event): ... ``` diff --git a/docs/examples.md b/docs/examples.md index 245b7a9e..8d8b65bb 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -28,7 +28,7 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -42,23 +42,23 @@ import logging from maxapi import Bot, Dispatcher from maxapi.types import ( - BotStarted, - Command, - MessageCreated, - CallbackButton, - MessageCallback, - BotAdded, - ChatTitleChanged, - MessageEdited, - MessageRemoved, - UserAdded, + BotStarted, + Command, + MessageCreated, + CallbackButton, + MessageCallback, + BotAdded, + ChatTitleChanged, + MessageEdited, + MessageRemoved, + UserAdded, UserRemoved, BotStopped, DialogCleared, DialogMuted, DialogUnmuted, ChatButton, # deprecated: 0.9.14 - MessageChatCreated # deprecated: 0.9.14 + MessageChatCreated, # deprecated: 0.9.14 ) from maxapi.utils.inline_keyboard import InlineKeyboardBuilder @@ -68,32 +68,32 @@ bot = Bot() dp = Dispatcher() -@dp.message_created(Command('start')) +@dp.message_created(Command("start")) async def hello(event: MessageCreated): builder = InlineKeyboardBuilder() builder.row( CallbackButton( - text='Кнопка 1', - payload='btn_1' + text="Кнопка 1", + payload="btn_1", ), CallbackButton( - text='Кнопка 2', - payload='btn_2', - ) + text="Кнопка 2", + payload="btn_2", + ), ) builder.add( ChatButton( # deprecated: 0.9.14 - text='Создать чат', - chat_title='Тест чат' + text="Создать чат", + chat_title="Тест чат", ) ) await event.message.answer( - text='Привет!', + text="Привет!", attachments=[ builder.as_markup(), - ] + ], ) @@ -101,12 +101,13 @@ async def hello(event: MessageCreated): async def bot_added(event: BotAdded): chat = await event.fetch_chat() if chat is None: - logging.info('Не удалось получить chat, возможно отключен auto_requests!') + logging.info( + "Не удалось получить chat, возможно отключен auto_requests!" + ) return - + await bot.send_message( - chat_id=event.chat_id, - text=f'Привет чат {chat.title}!' + chat_id=event.chat_id, text=f"Привет чат {chat.title}!" ) @@ -114,7 +115,7 @@ async def bot_added(event: BotAdded): async def message_removed(event: MessageRemoved): await bot.send_message( chat_id=event.chat_id, - text='Я всё видел!' + text="Я всё видел!", ) @@ -122,7 +123,7 @@ async def message_removed(event: MessageRemoved): async def bot_started(event: BotStarted): await bot.send_message( chat_id=event.chat_id, - text='Привет! Отправь мне /start' + text="Привет! Отправь мне /start", ) @@ -130,22 +131,20 @@ async def bot_started(event: BotStarted): async def chat_title_changed(event: ChatTitleChanged): await bot.send_message( chat_id=event.chat_id, - text=f'Крутое новое название "{event.title}"!' + text=f'Крутое новое название "{event.title}"!', ) @dp.message_callback() async def message_callback(event: MessageCallback): await event.answer( - new_text=f'Вы нажали на кнопку {event.callback.payload}!' + new_text=f"Вы нажали на кнопку {event.callback.payload}!" ) @dp.message_edited() async def message_edited(event: MessageEdited): - await event.message.answer( - text='Вы отредактировали сообщение!' - ) + await event.message.answer(text="Вы отредактировали сообщение!") @dp.user_removed() @@ -154,12 +153,12 @@ async def user_removed(event: UserRemoved): if from_user is None: return await bot.send_message( chat_id=event.chat_id, - text=f'Неизвестный кикнул {event.user.first_name} 😢' + text=f"Неизвестный кикнул {event.user.first_name} 😢", ) - + await bot.send_message( chat_id=event.chat_id, - text=f'{from_user.first_name} кикнул {event.user.first_name} 😢' + text=f"{from_user.first_name} кикнул {event.user.first_name} 😢", ) @@ -169,12 +168,12 @@ async def user_added(event: UserAdded): if chat is None: return await bot.send_message( chat_id=event.chat_id, - text=f'Чат приветствует вас, {event.user.first_name}!' + text=f"Чат приветствует вас, {event.user.first_name}!", ) - + await bot.send_message( chat_id=event.chat_id, - text=f'Чат "{chat.title}" приветствует вас, {event.user.first_name}!' + text=f'Чат "{chat.title}" приветствует вас, {event.user.first_name}!', ) @@ -182,7 +181,7 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -222,41 +221,41 @@ bot = Bot() dp = Dispatcher() -@dp.message_created(F.message.body.text == 'привет') +@dp.message_created(F.message.body.text == "привет") async def on_hello(event: MessageCreated): - await event.message.answer('Привет!') + await event.message.answer("Привет!") -@dp.message_created(F.message.body.text.lower().contains('помощь')) +@dp.message_created(F.message.body.text.lower().contains("помощь")) async def on_help(event: MessageCreated): - await event.message.answer('Чем могу помочь?') + await event.message.answer("Чем могу помочь?") -@dp.message_created(F.message.body.text.regexp(r'^\d{4}$')) +@dp.message_created(F.message.body.text.regexp(r"^\d{4}$")) async def on_code(event: MessageCreated): - await event.message.answer('Принят 4-значный код') + await event.message.answer("Принят 4-значный код") @dp.message_created(F.message.body.attachments) async def on_attachment(event: MessageCreated): - await event.message.answer('Получено вложение') + await event.message.answer("Получено вложение") @dp.message_created(F.message.body.text.len() > 20) async def on_long_text(event: MessageCreated): - await event.message.answer('Слишком длинное сообщение') + await event.message.answer("Слишком длинное сообщение") @dp.message_created(F.message.body.text.len() > 0) async def on_non_empty(event: MessageCreated): - await event.message.answer('Вы что-то написали.') + await event.message.answer("Вы что-то написали.") async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -271,18 +270,18 @@ import logging from maxapi import Bot, Dispatcher from maxapi.types import ( ChatButton, # deprecated: 0.9.14 - LinkButton, - CallbackButton, - RequestGeoLocationButton, - MessageButton, + LinkButton, + CallbackButton, + RequestGeoLocationButton, + MessageButton, ButtonsPayload, - RequestContactButton, + RequestContactButton, OpenAppButton, - MessageCreated, - MessageCallback, - MessageChatCreated, # deprecated: 0.9.14 - CommandStart, - Command + MessageCreated, + MessageCallback, + MessageChatCreated, # deprecated: 0.9.14 + CommandStart, + Command, ) from maxapi.utils.inline_keyboard import InlineKeyboardBuilder @@ -296,70 +295,68 @@ dp = Dispatcher() async def echo(event: MessageCreated): await event.message.answer( ( - 'Привет! Мои команды:\n\n' - - '/builder - Клавиатура из InlineKeyboardBuilder\n' - '/payload - Клавиатура из pydantic моделей\n' + "Привет! Мои команды:\n\n" + "/builder - Клавиатура из InlineKeyboardBuilder\n" + "/payload - Клавиатура из pydantic моделей\n" ) ) - - -@dp.message_created(Command('builder')) + + +@dp.message_created(Command("builder")) async def builder(event: MessageCreated): builder = InlineKeyboardBuilder() - + builder.row( ChatButton( # deprecated: 0.9.14 - text="Создать чат", - chat_title='Test', - chat_description='Test desc' + text="Создать чат", + chat_title="Test", + chat_description="Test desc", ), LinkButton( - text="Документация MAX", - url="https://dev.max.ru/docs" + text="Документация MAX", + url="https://dev.max.ru/docs", ), ) - + builder.row( RequestGeoLocationButton(text="Геолокация"), MessageButton(text="Сообщение"), ) - + builder.row( RequestContactButton(text="Контакт"), OpenAppButton( - text="Приложение", - web_app=event.bot.me.username, - contact_id=event.bot.me.user_id + text="Приложение", + web_app=event.bot.me.username, + contact_id=event.bot.me.user_id, ), ) - + builder.row( CallbackButton( - text='Callback', - payload='test', + text="Callback", + payload="test", ) ) - + await event.message.answer( - text='Клавиатура из InlineKeyboardBuilder', - attachments=[ - builder.as_markup() - ]) - - -@dp.message_created(Command('payload')) + text="Клавиатура из InlineKeyboardBuilder", + attachments=[builder.as_markup()], + ) + + +@dp.message_created(Command("payload")) async def payload(event: MessageCreated): buttons = [ [ ChatButton( # deprecated: 0.9.14 - text="Создать чат", - chat_title='Test', - chat_description='Test desc' + text="Создать чат", + chat_title="Test", + chat_description="Test desc", ), LinkButton( - text="Документация MAX", - url="https://dev.max.ru/docs" + text="Документация MAX", + url="https://dev.max.ru/docs", ), ], [ @@ -369,46 +366,45 @@ async def payload(event: MessageCreated): [ RequestContactButton(text="Контакт"), OpenAppButton( - text="Приложение", - web_app=event.bot.me.username, - contact_id=event.bot.me.user_id + text="Приложение", + web_app=event.bot.me.username, + contact_id=event.bot.me.user_id, ), ], [ CallbackButton( - text='Callback', - payload='test', + text="Callback", + payload="test", ) - ] + ], ] - + buttons_payload = ButtonsPayload(buttons=buttons).pack() - + await event.message.answer( - text='Клавиатура из pydantic моделей', - attachments=[ - buttons_payload - ]) - - + text="Клавиатура из pydantic моделей", + attachments=[buttons_payload], + ) + + @dp.message_chat_created() # deprecated: 0.9.14 async def message_chat_created(obj: MessageChatCreated): await obj.bot.send_message( chat_id=obj.chat.chat_id, - text=f'Чат создан! Ссылка: {obj.chat.link}' + text=f"Чат создан! Ссылка: {obj.chat.link}", ) - + @dp.message_callback() async def message_callback(callback: MessageCallback): - await callback.message.answer('Вы нажали на Callback!') + await callback.message.answer("Вы нажали на Callback!") async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -496,16 +492,15 @@ bot = Bot() dp = Dispatcher() -@dp.message_created(F.message.link.type == 'forward') +@dp.message_created(F.message.link.type == "forward") async def get_ids_from_forward(event: MessageCreated): text = ( - 'Информация о пересланном сообщении:\n\n' - - f'Из чата: {event.message.link.chat_id}\n' - f'От пользователя: {event.message.link.sender.user_id}' + "Информация о пересланном сообщении:\n\n" + f"Из чата: {event.message.link.chat_id}\n" + f"От пользователя: {event.message.link.sender.user_id}" ) await event.message.reply(text) - + @dp.message_created() async def get_ids(event: MessageCreated): @@ -516,8 +511,8 @@ async def get_ids(event: MessageCreated): return text = ( - f'Ваш ID: {from_user.user_id}\n' - f'ID этого чата: {chat.chat_id}' + f"Ваш ID: {from_user.user_id}\n" + f"ID этого чата: {chat.chat_id}" ) await event.message.answer(text, format=Format.HTML) @@ -526,7 +521,7 @@ async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -549,30 +544,29 @@ dp = Dispatcher() class FilterChat(BaseFilter): - """ Фильтр, который срабатывает только в чате с названием `Test` """ - + async def __call__(self, event: UpdateUnion): chat = await event.fetch_chat() if chat is None: return False - return chat.title == 'Test' + return chat.title == "Test" @dp.message_created(CommandStart(), FilterChat()) async def custom_data(event: MessageCreated): - await event.message.answer('Привет!') + await event.message.answer("Привет!") async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -600,12 +594,12 @@ bot = Bot() dp = Dispatcher() -class MyPayload(CallbackPayload, prefix='mypayload'): +class MyPayload(CallbackPayload, prefix="mypayload"): foo: str action: str -class AnotherPayload(CallbackPayload, prefix='another'): +class AnotherPayload(CallbackPayload, prefix="another"): bar: str value: int @@ -614,33 +608,37 @@ class AnotherPayload(CallbackPayload, prefix='another'): async def show_keyboard(event: MessageCreated): kb = InlineKeyboardBuilder() kb.row( - CallbackButton( - text='Первая кнопка', - payload=MyPayload(foo='123', action='edit').pack(), - ), CallbackButton( - text='Вторая кнопка', - payload=AnotherPayload(bar='abc', value=42).pack(), + text="Первая кнопка", + payload=MyPayload(foo="123", action="edit").pack(), + ), + CallbackButton( + text="Вторая кнопка", + payload=AnotherPayload(bar="abc", value=42).pack(), ), ) - await event.message.answer('Нажми кнопку!', attachments=[kb.as_markup()]) + await event.message.answer("Нажми кнопку!", attachments=[kb.as_markup()]) -@dp.message_callback(MyPayload.filter(F.foo == '123')) +@dp.message_callback(MyPayload.filter(F.foo == "123")) async def on_first_callback(event: MessageCallback, payload: MyPayload): - await event.answer(new_text=f'Первая кнопка: foo={payload.foo}, action={payload.action}') + await event.answer( + new_text=f"Первая кнопка: foo={payload.foo}, action={payload.action}" + ) @dp.message_callback(AnotherPayload.filter()) async def on_second_callback(event: MessageCallback, payload: AnotherPayload): - await event.answer(new_text=f'Вторая кнопка: bar={payload.bar}, value={payload.value}') + await event.answer( + new_text=f"Вторая кнопка: bar={payload.bar}, value={payload.value}" + ) async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -671,8 +669,8 @@ class CheckChatTitleMiddleware(BaseMiddleware): event_object: UpdateUnion, data: Dict[str, Any], ) -> Any: - - if event_object.chat.title == 'MAXApi': + + if event_object.chat.title == "MAXApi": return await handler(event_object, data) @@ -683,33 +681,43 @@ class CustomDataMiddleware(BaseMiddleware): event_object: UpdateUnion, data: Dict[str, Any], ) -> Any: - - data['custom_data'] = f'Это ID того кто вызвал команду: {event_object.from_user.user_id}' - + + data["custom_data"] = ( + f"Это ID того кто вызвал команду: {event_object.from_user.user_id}" + ) + await handler(event_object, data) -@dp.message_created(Command('start'), CheckChatTitleMiddleware()) +@dp.message_created(Command("start"), CheckChatTitleMiddleware()) async def start(event: MessageCreated): - await event.message.answer('Это сообщение было отправлено, так как ваш чат называется "MAXApi"!') + await event.message.answer( + 'Это сообщение было отправлено, так как ваш чат называется "MAXApi"!' + ) - -@dp.message_created(Command('custom_data'), CustomDataMiddleware()) + +@dp.message_created(Command("custom_data"), CustomDataMiddleware()) async def custom_data(event: MessageCreated, custom_data: str): await event.message.answer(custom_data) - - -@dp.message_created(Command('many_middlewares'), CheckChatTitleMiddleware(), CustomDataMiddleware()) + + +@dp.message_created( + Command("many_middlewares"), + CheckChatTitleMiddleware(), + CustomDataMiddleware(), +) async def many_middlewares(event: MessageCreated, custom_data: str): - await event.message.answer('Это сообщение было отправлено, так как ваш чат называется "MAXApi"!') + await event.message.answer( + 'Это сообщение было отправлено, так как ваш чат называется "MAXApi"!' + ) await event.message.answer(custom_data) - + async def main(): await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -740,24 +748,26 @@ class CustomDataForRouterMiddleware(BaseMiddleware): event_object: UpdateUnion, data: Dict[str, Any], ) -> Any: - - data['custom_data'] = f'Это ID того кто вызвал команду: {event_object.from_user.user_id}' + + data["custom_data"] = ( + f"Это ID того кто вызвал команду: {event_object.from_user.user_id}" + ) result = await handler(event_object, data) return result - -@dp.message_created(Command('custom_data')) + +@dp.message_created(Command("custom_data")) async def custom_data(event: MessageCreated, custom_data: str): await event.message.answer(custom_data) - - + + async def main(): dp.register_outer_middleware(CustomDataForRouterMiddleware()) - + await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -771,7 +781,14 @@ import logging from maxapi import Bot, Dispatcher, F from maxapi.context import MemoryContext, State, StatesGroup -from maxapi.types import BotStarted, Command, MessageCreated, CallbackButton, MessageCallback, BotCommand +from maxapi.types import ( + BotStarted, + Command, + MessageCreated, + CallbackButton, + MessageCallback, + BotCommand, +) from maxapi.utils.inline_keyboard import InlineKeyboardBuilder from router import router @@ -784,14 +801,14 @@ dp = Dispatcher() dp.include_routers(router) -start_text = '''Пример чат-бота для MAX 💙 +start_text = """Пример чат-бота для MAX 💙 Мои команды: /clear очищает ваш контекст /state или /context показывают ваше контекстное состояние /data показывает вашу контекстную память -''' +""" class Form(StatesGroup): @@ -801,83 +818,72 @@ class Form(StatesGroup): @dp.on_started() async def _(): - logging.info('Бот стартовал!') + logging.info("Бот стартовал!") @dp.bot_started() async def bot_started(event: BotStarted): await event.bot.send_message( chat_id=event.chat_id, - text='Привет! Отправь мне /start' + text="Привет! Отправь мне /start", ) -@dp.message_created(Command('clear')) +@dp.message_created(Command("clear")) async def hello(event: MessageCreated, context: MemoryContext): await context.clear() await event.message.answer(f"Ваш контекст был очищен!") -@dp.message_created(Command('data')) +@dp.message_created(Command("data")) async def hello(event: MessageCreated, context: MemoryContext): data = await context.get_data() await event.message.answer(f"Ваша контекстная память: {str(data)}") -@dp.message_created(Command('context')) -@dp.message_created(Command('state')) +@dp.message_created(Command("context")) +@dp.message_created(Command("state")) async def hello(event: MessageCreated, context: MemoryContext): data = await context.get_state() await event.message.answer(f"Ваше контекстное состояние: {str(data)}") -@dp.message_created(Command('start')) +@dp.message_created(Command("start")) async def hello(event: MessageCreated): builder = InlineKeyboardBuilder() builder.row( - CallbackButton( - text='Ввести свое имя', - payload='btn_1' - ), - CallbackButton( - text='Ввести свой возраст', - payload='btn_2' - ) - ) - builder.row( - CallbackButton( - text='Не хочу', - payload='btn_3' - ) + CallbackButton(text="Ввести свое имя", payload="btn_1"), + CallbackButton(text="Ввести свой возраст", payload="btn_2"), ) + builder.row(CallbackButton(text="Не хочу", payload="btn_3")) await event.message.answer( - text=start_text, + text=start_text, attachments=[ builder.as_markup(), - ] + ], ) - -@dp.message_callback(F.callback.payload == 'btn_1') + +@dp.message_callback(F.callback.payload == "btn_1") async def hello(event: MessageCallback, context: MemoryContext): await context.set_state(Form.name) await event.message.delete() - await event.message.answer(f'Отправьте свое имя:') + await event.message.answer(f"Отправьте свое имя:") -@dp.message_callback(F.callback.payload == 'btn_2') +@dp.message_callback(F.callback.payload == "btn_2") async def hello(event: MessageCallback, context: MemoryContext): await context.set_state(Form.age) await event.message.delete() - await event.message.answer(f'Отправьте ваш возраст:') + await event.message.answer(f"Отправьте ваш возраст:") -@dp.message_callback(F.callback.payload == 'btn_3') +@dp.message_callback(F.callback.payload == "btn_3") async def hello(event: MessageCallback, context: MemoryContext): await event.message.delete() - await event.message.answer(f'Ну ладно 🥲') + await event.message.answer(f"Ну ладно 🥲") @dp.message_created(F.message.body.text, Form.name) @@ -886,8 +892,10 @@ async def hello(event: MessageCreated, context: MemoryContext): data = await context.get_data() - await event.message.answer(f"Приятно познакомиться, {data['name'].title()}!") - + await event.message.answer( + f"Приятно познакомиться, {data['name'].title()}!" + ) + @dp.message_created(F.message.body.text, Form.age) async def hello(event: MessageCreated, context: MemoryContext): @@ -898,31 +906,23 @@ async def hello(event: MessageCreated, context: MemoryContext): async def main(): await bot.set_my_commands( + BotCommand(name="/start", description="Перезапустить бота"), + BotCommand(name="/clear", description="Очищает ваш контекст"), BotCommand( - name='/start', - description='Перезапустить бота' + name="/state", description="Показывают ваше контекстное состояние" ), BotCommand( - name='/clear', - description='Очищает ваш контекст' + name="/data", description="Показывает вашу контекстную память" ), BotCommand( - name='/state', - description='Показывают ваше контекстное состояние' + name="/context", + description="Показывают ваше контекстное состояние", ), - BotCommand( - name='/data', - description='Показывает вашу контекстную память' - ), - BotCommand( - name='/context', - description='Показывают ваше контекстное состояние' - ) ) await dp.start_polling(bot) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -934,23 +934,21 @@ from maxapi.types import Command, MessageCreated from maxapi.types import InputMedia router = Router() -file = __file__.split('\\')[-1] +file = __file__.split("\\")[-1] -@router.message_created(Command('router')) +@router.message_created(Command("router")) async def hello(obj: MessageCreated): await obj.message.answer(f"Пишу тебе из роута {file}") - -# новая команда для примера, /media, + +# новая команда для примера, /media, # пример использования: /media image.png (медиафайл берется указанному пути) -@router.message_created(Command('media')) +@router.message_created(Command("media")) async def hello(event: MessageCreated): await event.message.answer( attachments=[ - InputMedia( - path=event.message.body.text.replace('/media ', '') - ) + InputMedia(path=event.message.body.text.replace("/media ", "")) ] ) ``` @@ -1014,12 +1012,12 @@ dp = Dispatcher() @dp.message_created() async def handle_message(event: MessageCreated): - await event.message.answer('Бот работает через вебхук!') + await event.message.answer("Бот работает через вебхук!") async def main(): - webhook_url = 'https://ваш-домен.рф/webhook' # <-- укажите свой - webhook_secret = 'my-secret-token' # <-- укажите свой (5–256 символов) + webhook_url = "https://ваш-домен.рф/webhook" # <-- укажите свой + webhook_secret = "my-secret-token" # <-- укажите свой (5–256 символов) # Регистрируем вебхук на стороне MAX — платформа будет отправлять # заголовок X-Max-Bot-Api-Secret с каждым запросом. @@ -1030,13 +1028,13 @@ async def main(): # или не совпадает (защита от посторонних запросов). await dp.handle_webhook( bot=bot, - host='0.0.0.0', + host="0.0.0.0", port=8080, secret=webhook_secret, ) -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -1069,12 +1067,12 @@ dp = Dispatcher() @dp.message_created() async def handle_message(event: MessageCreated): - await event.message.answer('Бот работает через вебхук!') + await event.message.answer("Бот работает через вебхук!") async def main(): - webhook_url = 'https://ваш-домен.рф/webhook' # <-- укажите свой - webhook_secret = 'my-secret-token' # <-- укажите свой (5–256 символов) + webhook_url = "https://ваш-домен.рф/webhook" # <-- укажите свой + webhook_secret = "my-secret-token" # <-- укажите свой (5–256 символов) # Передаём secret в конструктор — он сохраняется в webhook.secret. # Фреймворк будет автоматически проверять заголовок X-Max-Bot-Api-Secret @@ -1085,24 +1083,24 @@ async def main(): app = FastAPI(lifespan=webhook.lifespan) # Собственные маршруты — например, healthcheck - @app.get('/health') + @app.get("/health") async def health(): - return {'status': 'ok'} + return {"status": "ok"} # Подключаем MAX webhook-обработчик к нашему приложению - webhook.setup(app, path='/webhook') + webhook.setup(app, path="/webhook") # Подписываемся на webhook — передаём тот же secret, # чтобы платформа MAX добавляла X-Max-Bot-Api-Secret в каждый запрос. await bot.subscribe_webhook(url=webhook_url, secret=webhook_secret) # Запускаем сервер uvicorn - config = uvicorn.Config(app=app, host='0.0.0.0', port=8080) + config = uvicorn.Config(app=app, host="0.0.0.0", port=8080) server = uvicorn.Server(config) await server.serve() -if __name__ == '__main__': +if __name__ == "__main__": asyncio.run(main()) ``` @@ -1128,14 +1126,17 @@ connection_props = DefaultConnectionProperties(proxy=proxy_url) bot = Bot(default_connection=connection_props) dp = Dispatcher() -@dp.message_created(Command('start')) + +@dp.message_created(Command("start")) async def start_handler(event: MessageCreated): await event.message.answer("Привет!") + async def main(): await dp.start_polling(bot) -if __name__ == '__main__': + +if __name__ == "__main__": asyncio.run(main()) ``` @@ -1155,14 +1156,17 @@ bot = Bot( ) dp = Dispatcher() -@dp.message_created(Command('start')) + +@dp.message_created(Command("start")) async def start_handler(event: MessageCreated): await event.message.answer("Привет!") + async def main(): await dp.start_polling(bot) -if __name__ == '__main__': + +if __name__ == "__main__": asyncio.run(main()) ``` diff --git a/docs/guides/context.md b/docs/guides/context.md index dc9293cb..b650d813 100644 --- a/docs/guides/context.md +++ b/docs/guides/context.md @@ -8,21 +8,25 @@ from maxapi.context import MemoryContext, StatesGroup, State from maxapi.types import MessageCreated, Command + class Form(StatesGroup): name = State() age = State() -@dp.message_created(Command('start')) + +@dp.message_created(Command("start")) async def start_handler(event: MessageCreated, context: MemoryContext): await context.set_state(Form.name) await event.message.answer("Как вас зовут?") + @dp.message_created(Form.name) async def name_handler(event: MessageCreated, context: MemoryContext): await context.update_data(name=event.message.body.text) await context.set_state(Form.age) await event.message.answer("Сколько вам лет?") + @dp.message_created(Form.age) async def age_handler(event: MessageCreated, context: MemoryContext): data = await context.get_data() @@ -111,7 +115,7 @@ dp = Dispatcher( ```python class Form(StatesGroup): name = State() # Автоматически получит имя 'Form:name' - age = State() # Автоматически получит имя 'Form:age' + age = State() # Автоматически получит имя 'Form:age' ``` ## Фильтрация по состояниям @@ -121,18 +125,17 @@ class Form(StatesGroup): ```python # Только в состоянии Form.name @dp.message_created(Form.name) -async def name_handler(event: MessageCreated, context: MemoryContext): - ... +async def name_handler(event: MessageCreated, context: MemoryContext): ... + # Только когда НЕТ активного состояния @dp.message_created(None) -async def no_state_handler(event: MessageCreated): - ... +async def no_state_handler(event: MessageCreated): ... + # В любом из перечисленных состояний @dp.message_created(Form.name, Form.age) -async def multi_state_handler(event: MessageCreated): - ... +async def multi_state_handler(event: MessageCreated): ... ``` ## Хранение в Redis @@ -153,13 +156,13 @@ from maxapi import Dispatcher from maxapi.context import RedisContext # Инициализация клиента Redis -redis_client = redis.Redis(host='localhost', port=6379, db=0) +redis_client = redis.Redis(host="localhost", port=6379, db=0) # Передача RedisContext в Диспетчер dp = Dispatcher( storage=RedisContext, redis_client=redis_client, - key_prefix="my_bot" + key_prefix="my_bot", ) ``` @@ -223,6 +226,7 @@ bot = Bot() dp = Dispatcher() dp.register_outer_middleware(SaveMarkerMiddleware()) + async def main() -> None: marker = await load_marker() # str | None diff --git a/docs/guides/documentation_handlers.md b/docs/guides/documentation_handlers.md index 97949f85..012b9aee 100644 --- a/docs/guides/documentation_handlers.md +++ b/docs/guides/documentation_handlers.md @@ -11,11 +11,11 @@ В docstring обработчика необходимо указать маркер `commands_info:` с описанием команды: ```python -@dp.message_created(Command('start')) +@dp.message_created(Command("start")) async def start_handler(event: MessageCreated): """ Обработчик команды /start - + commands_info: Запускает бота и показывает приветственное сообщение """ await event.message.answer("Привет! Добро пожаловать!") @@ -32,24 +32,27 @@ from maxapi.types import MessageCreated, Command bot = Bot() dp = Dispatcher() -@dp.message_created(Command('help')) + +@dp.message_created(Command("help")) async def help_handler(event: MessageCreated): """ Обработчик команды помощи - + commands_info: Показывает список доступных команд и их описание """ - await event.message.answer("Доступные команды:\n/start - Начать работу\n/help - Помощь") + await event.message.answer( + "Доступные команды:\n/start - Начать работу\n/help - Помощь" + ) ``` ### Несколько команд с одним описанием ```python -@dp.message_created(Command(['start', 'begin', 'go'])) +@dp.message_created(Command(["start", "begin", "go"])) async def start_handler(event: MessageCreated): """ Обработчик команд запуска - + commands_info: Инициализирует бота и начинает диалог с пользователем """ await event.message.answer("Бот запущен!") @@ -58,11 +61,11 @@ async def start_handler(event: MessageCreated): ### Многострочное описание ```python -@dp.message_created(Command('settings')) +@dp.message_created(Command("settings")) async def settings_handler(event: MessageCreated): """ Обработчик настроек - + commands_info: Открывает меню настроек бота. Позволяет изменить язык, уведомления и другие параметры. """ @@ -98,8 +101,8 @@ from maxapi.filters.command import CommandsInfo # Пример использования cmd_info = CommandsInfo( - commands=['start', 'begin'], - info='Запускает бота' + commands=["start", "begin"], + info="Запускает бота", ) ``` @@ -112,6 +115,7 @@ import logging logger = logging.getLogger(__name__) + @dp.on_started() async def log_all_commands(): """Логирует все зарегистрированные команды""" @@ -143,47 +147,53 @@ logging.basicConfig(level=logging.INFO) bot = Bot() dp = Dispatcher() -@dp.message_created(Command('start')) + +@dp.message_created(Command("start")) async def start_handler(event: MessageCreated): """ Обработчик команды /start - + commands_info: Запускает бота и показывает приветствие """ await event.message.answer("Привет!") -@dp.message_created(Command('help')) + +@dp.message_created(Command("help")) async def help_handler(event: MessageCreated): """ Обработчик команды /help - + commands_info: Показывает справку по использованию бота """ await event.message.answer("Справка по командам...") -@dp.message_created(Command('settings')) + +@dp.message_created(Command("settings")) async def settings_handler(event: MessageCreated): """ Обработчик команды /settings - + commands_info: Открывает меню настроек """ await event.message.answer("Настройки...") + @dp.on_started() async def log_all_commands(): """Логирует все зарегистрированные команды""" logger = logging.getLogger(__name__) - + logger.info("Зарегистрированные команды:") for cmd_info in bot.handlers_commands: commands_str = ", ".join([f"/{cmd}" for cmd in cmd_info.commands]) info_str = f" - {cmd_info.info}" if cmd_info.info else "" logger.info(f" {commands_str}{info_str}") + async def main(): await dp.start_polling(bot) -if __name__ == '__main__': + +if __name__ == "__main__": asyncio.run(main()) ``` diff --git a/docs/guides/filters.md b/docs/guides/filters.md index e0ce49f9..90020a41 100644 --- a/docs/guides/filters.md +++ b/docs/guides/filters.md @@ -9,23 +9,26 @@ ```python from maxapi import F + # Только текстовые сообщения @dp.message_created(F.message.body.text) -async def text_handler(event: MessageCreated): - ... +async def text_handler(event: MessageCreated): ... + # Сообщения с вложениями @dp.message_created(F.message.body.attachments) -async def attachment_handler(event: MessageCreated): - ... +async def attachment_handler(event: MessageCreated): ... + # Комбинация условий from maxapi.enums.chat_type import ChatType + # ⚠️ Скобки обязательны: & и | имеют более высокий приоритет, чем == -@dp.message_created(F.message.body.text & (F.message.chat.type == ChatType.DIALOG)) -async def dialog_text_handler(event: MessageCreated): - ... +@dp.message_created( + F.message.body.text & (F.message.chat.type == ChatType.DIALOG) +) +async def dialog_text_handler(event: MessageCreated): ... ``` Для личных сообщений используйте `ChatType.DIALOG`. @@ -35,15 +38,15 @@ async def dialog_text_handler(event: MessageCreated): ```python from maxapi.types import Command + # Одна команда -@dp.message_created(Command('start')) -async def start_handler(event: MessageCreated): - ... +@dp.message_created(Command("start")) +async def start_handler(event: MessageCreated): ... + # Несколько команд -@dp.message_created(Command(['start', 'help', 'info'])) -async def commands_handler(event: MessageCreated): - ... +@dp.message_created(Command(["start", "help", "info"])) +async def commands_handler(event: MessageCreated): ... ``` ## Callback Payload фильтр @@ -51,23 +54,26 @@ async def commands_handler(event: MessageCreated): ```python from maxapi.filters.callback_payload import CallbackPayload + # Простой payload (строка) -@dp.message_callback(F.callback.payload == 'button_click') -async def callback_handler(event: MessageCallback): - ... +@dp.message_callback(F.callback.payload == "button_click") +async def callback_handler(event: MessageCallback): ... + # Структурированный payload (класс) -class MyPayload(CallbackPayload, prefix='mypayload'): +class MyPayload(CallbackPayload, prefix="mypayload"): action: str value: int + # Без дополнительных условий @dp.message_callback(MyPayload.filter()) async def callback_handler(event: MessageCallback, payload: MyPayload): await event.answer(f"Action: {payload.action}, Value: {payload.value}") + # С дополнительным фильтром -@dp.message_callback(MyPayload.filter(F.action == 'edit')) +@dp.message_callback(MyPayload.filter(F.action == "edit")) async def callback_handler(event: MessageCallback, payload: MyPayload): await event.answer(f"Edit action: {payload.value}") ``` @@ -140,10 +146,10 @@ F.message.body.text | F.message.body.attachments # Отрицание (NOT) ~F.message.body.text + # Несколько фильтров в декораторе (все объединяются через AND) -@dp.message_created(F.message.body.text, Command('start'), Form.name) -async def handler(event: MessageCreated): - ... +@dp.message_created(F.message.body.text, Command("start"), Form.name) +async def handler(event: MessageCreated): ... ``` ## Базовые фильтры (BaseFilter) @@ -153,6 +159,7 @@ async def handler(event: MessageCreated): ```python from maxapi.filters.filter import BaseFilter + class MyFilter(BaseFilter): async def __call__(self, event): # Возвращает True/False или dict с данными diff --git a/docs/guides/handlers.md b/docs/guides/handlers.md index 82eada3c..bf11f3f9 100644 --- a/docs/guides/handlers.md +++ b/docs/guides/handlers.md @@ -28,7 +28,8 @@ dp.message_created.register(my_handler, <фильтры>) ```python from maxapi.types import MessageCreated, Command -@dp.message_created(Command('start')) + +@dp.message_created(Command("start")) async def start_handler(event: MessageCreated): await event.message.answer("Привет!") ``` @@ -38,9 +39,11 @@ async def start_handler(event: MessageCreated): Если вы хотите, чтобы хендлер срабатывал только тогда, когда у пользователя нет активного состояния в FSM, используйте `None`: ```python -@dp.message_created(None, Command('help')) +@dp.message_created(None, Command("help")) async def help_no_state(event: MessageCreated): - await event.message.answer("Вы запросили помощь вне контекста заполнения формы.") + await event.message.answer( + "Вы запросили помощь вне контекста заполнения формы." + ) ``` ### Обработка с фильтром @@ -48,6 +51,7 @@ async def help_no_state(event: MessageCreated): ```python from maxapi import F + @dp.message_created(F.message.body.text) async def text_handler(event: MessageCreated): await event.message.answer(f"Вы написали: {event.message.body.text}") @@ -66,9 +70,11 @@ async def any_message(event: MessageCreated): ```python from maxapi.context import State, StatesGroup + class Form(StatesGroup): name = State() + @dp.message_created(F.message.body.text, Form.name) async def name_handler(event: MessageCreated, context: MemoryContext): await context.update_data(name=event.message.body.text) @@ -78,7 +84,7 @@ async def name_handler(event: MessageCreated, context: MemoryContext): ### Обработка с контекстом ```python -@dp.message_created(Command('data')) +@dp.message_created(Command("data")) async def data_handler(event: MessageCreated, context: MemoryContext): data = await context.get_data() await event.message.answer(f"Данные: {data}") @@ -92,17 +98,18 @@ async def data_handler(event: MessageCreated, context: MemoryContext): from maxapi.types.attachments.upload import AttachmentUpload, AttachmentPayload from maxapi.enums.upload_type import UploadType -@dp.message_created(Command('send_photo')) + +@dp.message_created(Command("send_photo")) async def send_photo_by_token(event: MessageCreated): # Создаем вложение, используя существующий токен attachment = AttachmentUpload( type=UploadType.IMAGE, - payload=AttachmentPayload(token="ВАШ_ТОКЕН_ЗДЕСЬ") + payload=AttachmentPayload(token="ВАШ_ТОКЕН_ЗДЕСЬ"), ) - + await event.message.answer( text="Вот ваше фото по токену", - attachments=[attachment] + attachments=[attachment], ) ``` diff --git a/docs/guides/keyboards.md b/docs/guides/keyboards.md index c49e1e3d..4b68b913 100644 --- a/docs/guides/keyboards.md +++ b/docs/guides/keyboards.md @@ -22,7 +22,7 @@ builder.row(CallbackButton(text="Ещё кнопка", payload="more")) await event.message.answer( text="Выберите действие:", - attachments=[builder.as_markup()] + attachments=[builder.as_markup()], ) ``` @@ -41,13 +41,13 @@ from maxapi.types.attachments.buttons import ( buttons = [ [LinkButton(text="Сайт", url="https://example.com")], [ClipboardButton(text="Скопировать код", payload="ABC-123")], - [CallbackButton(text="Callback", payload="data")] + [CallbackButton(text="Callback", payload="data")], ] payload = ButtonsPayload(buttons=buttons).pack() await event.message.answer( text="Клавиатура", - attachments=[payload] + attachments=[payload], ) ``` diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index e958579c..261f9f28 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -8,6 +8,7 @@ Middleware позволяет обрабатывать события до и п from maxapi.filters.middleware import BaseMiddleware from typing import Any, Awaitable, Callable, Dict + class LoggingMiddleware(BaseMiddleware): async def __call__( self, @@ -35,7 +36,7 @@ class LoggingMiddleware(BaseMiddleware): захвата распределённых блокировок и т.п. ```python -dp.register_outer_middleware(LoggingMiddleware()) # каждый update +dp.register_outer_middleware(LoggingMiddleware()) # каждый update dp.register_inner_middleware(DbTransactionMiddleware()) # только под handler ``` @@ -74,6 +75,7 @@ from maxapi.types.updates.message_created import MessageCreated # ── Фильтры ────────────────────────────────────────────────────────────────── + class IsAdmin(BaseFilter): """Пропускает только пользователей из списка администраторов.""" @@ -85,11 +87,13 @@ class IsAdmin(BaseFilter): # ── Middleware ──────────────────────────────────────────────────────────────── + class RequestIdMiddleware(BaseMiddleware): """Outer-global: проставляет уникальный request-id на каждое событие.""" async def __call__(self, handler, event, data): import uuid + data["request_id"] = str(uuid.uuid4()) return await handler(event, data) @@ -194,7 +198,7 @@ dp.register_outer_middleware → RequestIdMiddleware ## Middleware в обработчике ```python -@dp.message_created(Command('start'), LoggingMiddleware()) +@dp.message_created(Command("start"), LoggingMiddleware()) async def start_handler(event: MessageCreated): await event.message.answer("Привет!") ``` @@ -204,10 +208,11 @@ async def start_handler(event: MessageCreated): ```python class CustomDataMiddleware(BaseMiddleware): async def __call__(self, handler, event_object, data): - data['custom_data'] = f'User ID: {event_object.from_user.user_id}' + data["custom_data"] = f"User ID: {event_object.from_user.user_id}" return await handler(event_object, data) -@dp.message_created(Command('data'), CustomDataMiddleware()) + +@dp.message_created(Command("data"), CustomDataMiddleware()) async def handler(event: MessageCreated, custom_data: str): await event.message.answer(custom_data) ``` @@ -300,8 +305,7 @@ async def value_error_handler(event: ErrorEvent): @dp.errors(ExceptionTypeFilter(RuntimeError)) -async def runtime_error_handler(event: ErrorEvent): - ... +async def runtime_error_handler(event: ErrorEvent): ... ``` В декоратор можно передавать: diff --git a/docs/guides/routers.md b/docs/guides/routers.md index 0b79e51f..c8e05e7c 100644 --- a/docs/guides/routers.md +++ b/docs/guides/routers.md @@ -10,7 +10,8 @@ from maxapi.types import MessageCreated, Command router = Router(router_id="my_router") -@router.message_created(Command('help')) + +@router.message_created(Command("help")) async def help_handler(event: MessageCreated): await event.message.answer("Помощь") ``` @@ -40,10 +41,12 @@ router.filters.append(F.chat.type == ChatType.DIALOG) # Личный диало ```python from maxapi.filters.middleware import BaseMiddleware + class RouterMiddleware(BaseMiddleware): async def __call__(self, handler, event_object, data): # Логика только для этого роутера return await handler(event_object, data) + router.register_outer_middleware(RouterMiddleware()) ``` diff --git a/docs/guides/webhook_vs_polling.md b/docs/guides/webhook_vs_polling.md index 9dcc1b33..454397fe 100644 --- a/docs/guides/webhook_vs_polling.md +++ b/docs/guides/webhook_vs_polling.md @@ -42,9 +42,9 @@ async def main(): ```python async def main(): - webhook_url = 'https://ваш-домен.рф/webhook' # <-- укажите свой + webhook_url = "https://ваш-домен.рф/webhook" # <-- укажите свой await bot.subscribe_webhook(url=webhook_url) - await dp.handle_webhook(bot, host='0.0.0.0', port=8080, path='/webhook') + await dp.handle_webhook(bot, host="0.0.0.0", port=8080, path="/webhook") ``` **Параметры:** diff --git a/docs/index.md b/docs/index.md index 53740c39..930803e8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,22 +39,26 @@ logging.basicConfig(level=logging.INFO) bot = Bot() dp = Dispatcher() + # Обработчик команды /start -@dp.message_created(Command('start')) +@dp.message_created(Command("start")) async def start_handler(event: MessageCreated): await event.message.answer("Привет! 👋\nЯ простой бот на MaxAPI.") + # Обработчик всех текстовых сообщений @dp.message_created() async def echo_handler(event: MessageCreated): if event.message.body.text: await event.message.answer(f"Вы написали: {event.message.body.text}") + async def main(): # Запуск бота в режиме polling await dp.start_polling(bot) -if __name__ == '__main__': + +if __name__ == "__main__": asyncio.run(main()) ``` @@ -80,12 +84,14 @@ from maxapi.types import MessageCreated bot = Bot() dp = Dispatcher() + # Обработчик только текстовых сообщений @dp.message_created(F.message.body.text) async def text_handler(event: MessageCreated): text = event.message.body.text await event.message.answer(f"Длина вашего сообщения: {len(text)} символов") + # Обработчик сообщений с вложениями @dp.message_created(F.message.attachments) async def attachment_handler(event: MessageCreated): diff --git a/examples/README.md b/examples/README.md index bf3cf0fc..28ffa54a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -46,6 +46,7 @@ echo 'MAX_BOT_TOKEN=ваш_токен' > .env Затем в коде бота перед созданием `Bot()`: ```python from dotenv import load_dotenv + load_dotenv() # Загружает переменные из .env в os.environ bot = Bot() # Автоматически берёт токен из MAX_BOT_TOKEN @@ -380,18 +381,20 @@ python examples/01_echo_bot.py from maxapi import Bot, Dispatcher # Инициализация -bot = Bot() # Токен из MAX_BOT_TOKEN +bot = Bot() # Токен из MAX_BOT_TOKEN dp = Dispatcher() + # Обработчики @dp.message_created(...) -async def handler(event): - ... +async def handler(event): ... + # Запуск async def main(): await dp.start_polling(bot) + if __name__ == "__main__": asyncio.run(main()) ``` @@ -413,6 +416,7 @@ if __name__ == "__main__": **Как включить debug-логирование?** ```python import logging + logging.basicConfig(level=logging.DEBUG) ``` Логгеры библиотеки: `bot`, `dispatcher`, `connection`. @@ -424,6 +428,7 @@ logging.basicConfig(level=logging.DEBUG) Используйте `RedisContext` вместо `MemoryContext`: ```python import redis.asyncio as redis + dp = Dispatcher( storage=RedisContext, redis_client=redis.from_url("redis://localhost"), diff --git a/maxapi/filters/state.py b/maxapi/filters/state.py index 16ff1d61..95c11fcc 100644 --- a/maxapi/filters/state.py +++ b/maxapi/filters/state.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from ..types.updates import UpdateUnion -StateType: TypeAlias = State | str | None | StatesGroup | type[StatesGroup] +StateType: TypeAlias = State | str | StatesGroup | type[StatesGroup] | None StateInput: TypeAlias = StateType | Iterable[StateType] _UNSET = object() diff --git a/maxapi/types/fetchable.py b/maxapi/types/fetchable.py index c2bb535d..2a6ff2cd 100644 --- a/maxapi/types/fetchable.py +++ b/maxapi/types/fetchable.py @@ -43,7 +43,7 @@ def __init__( self._fetcher = fetcher self._setter = setter self._description = description - self._resolved: ResolvedValue | None | object = _UNSET + self._resolved: ResolvedValue | object | None = _UNSET self._fetch_lock = asyncio.Lock() async def fetch(self) -> ResolvedValue | None: diff --git a/maxapi/types/message.py b/maxapi/types/message.py index f35515c9..a5906364 100644 --- a/maxapi/types/message.py +++ b/maxapi/types/message.py @@ -200,7 +200,7 @@ def _utf16_to_py_index(utf16_pos: int) -> int: } char_styles: list[ - list[tuple[TextStyle, str | None | tuple[str, int]]] + list[tuple[TextStyle, str | tuple[str, int] | None]] ] = [] for i in range(len(text)): utf16_i = utf16_offsets[i] if i < len(utf16_offsets) else 0 @@ -244,7 +244,7 @@ def _utf16_to_py_index(utf16_pos: int) -> int: def wrap_chunk( chunk: str, - tags: list[tuple[TextStyle, str | None | tuple[str, int]]], + tags: list[tuple[TextStyle, str | tuple[str, int] | None]], ) -> object: node: object = chunk for style, val in reversed(tags): diff --git a/pyproject.toml b/pyproject.toml index 9bd0f296..0c7c4348 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,6 +137,7 @@ ignore = [ "ARG", "ANN", "COM812", + "CPY", "D", "DTZ", "EXE",