From 756de23bcfc0576ab0d9f5389afcb8d5afddbb9b Mon Sep 17 00:00:00 2001 From: geekiot Date: Sun, 19 Oct 2025 18:01:25 +0500 Subject: [PATCH 1/5] Add [bot]: add dispatcher for polling the bot --- src/bot/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/bot/__init__.py b/src/bot/__init__.py index 103bccf..27793ce 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -1,6 +1,7 @@ __all__ = ["start_bot"] +from aiogram import Bot, Dispatcher from redis.asyncio.client import Redis from sqlalchemy.ext.asyncio import AsyncSession @@ -9,4 +10,8 @@ async def start_bot( bot_token: str, redis_client: Redis, database_session: AsyncSession, -) -> None: ... +) -> None: + bot = Bot(bot_token) + dp = Dispatcher() + + await dp.start_polling(bot) From e127179aa5170dfc97b819130a746b33eb0e987c Mon Sep 17 00:00:00 2001 From: geekiot Date: Mon, 20 Oct 2025 15:41:57 +0500 Subject: [PATCH 2/5] Replace [handlers]: replace handlers utils to new module --- src/bot/handlers/{filters => utils}/__init__.py | 0 src/bot/handlers/{keyboards => utils/filters}/__init__.py | 0 src/bot/handlers/{states => utils/keyboards}/__init__.py | 0 src/bot/handlers/utils/states/__init__.py | 1 + 4 files changed, 1 insertion(+) rename src/bot/handlers/{filters => utils}/__init__.py (100%) rename src/bot/handlers/{keyboards => utils/filters}/__init__.py (100%) rename src/bot/handlers/{states => utils/keyboards}/__init__.py (100%) create mode 100644 src/bot/handlers/utils/states/__init__.py diff --git a/src/bot/handlers/filters/__init__.py b/src/bot/handlers/utils/__init__.py similarity index 100% rename from src/bot/handlers/filters/__init__.py rename to src/bot/handlers/utils/__init__.py diff --git a/src/bot/handlers/keyboards/__init__.py b/src/bot/handlers/utils/filters/__init__.py similarity index 100% rename from src/bot/handlers/keyboards/__init__.py rename to src/bot/handlers/utils/filters/__init__.py diff --git a/src/bot/handlers/states/__init__.py b/src/bot/handlers/utils/keyboards/__init__.py similarity index 100% rename from src/bot/handlers/states/__init__.py rename to src/bot/handlers/utils/keyboards/__init__.py diff --git a/src/bot/handlers/utils/states/__init__.py b/src/bot/handlers/utils/states/__init__.py new file mode 100644 index 0000000..a9a2c5b --- /dev/null +++ b/src/bot/handlers/utils/states/__init__.py @@ -0,0 +1 @@ +__all__ = [] From 82339dcddfe5e2eae9f6a5c0a31593427292aee4 Mon Sep 17 00:00:00 2001 From: geekiot Date: Mon, 20 Oct 2025 15:43:47 +0500 Subject: [PATCH 3/5] Add [bot]: add handlers & middlewares connect funcs Handlers: add connect_handlers func, add RouterRegistry utile Middlewares: add connect_middlewares func Bot: update start_bot func --- src/bot/__init__.py | 16 +++++++++++++--- src/bot/handlers/__init__.py | 18 +++++++++++++++++- src/bot/handlers/utils/registry/__init__.py | 15 +++++++++++++++ src/bot/middlewares/__init__.py | 8 +++++++- 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 src/bot/handlers/utils/registry/__init__.py diff --git a/src/bot/__init__.py b/src/bot/__init__.py index 27793ce..183d764 100644 --- a/src/bot/__init__.py +++ b/src/bot/__init__.py @@ -1,10 +1,12 @@ __all__ = ["start_bot"] - from aiogram import Bot, Dispatcher from redis.asyncio.client import Redis from sqlalchemy.ext.asyncio import AsyncSession +from .handlers import connect_handlers, registry +from .middlewares import connect_middlewares + async def start_bot( bot_token: str, @@ -12,6 +14,14 @@ async def start_bot( database_session: AsyncSession, ) -> None: bot = Bot(bot_token) - dp = Dispatcher() - await dp.start_polling(bot) + dispatcher = Dispatcher() + + connect_handlers(dispatcher) + connect_middlewares(dispatcher) + + async def on_startup(bot: Bot): + await bot.delete_webhook(drop_pending_updates=True) + await registry.set_menu(bot) + + await dispatcher.start_polling(bot, on_startup=on_startup) diff --git a/src/bot/handlers/__init__.py b/src/bot/handlers/__init__.py index a9a2c5b..6e753ae 100644 --- a/src/bot/handlers/__init__.py +++ b/src/bot/handlers/__init__.py @@ -1 +1,17 @@ -__all__ = [] +__all__ = ["connect_handlers", "registry"] + + +import importlib + +from aiogram import Router + +from .utils.registry import RouterRegistry + +registry: RouterRegistry = RouterRegistry() + + +def connect_handlers(dispatcher: Router) -> None: + importlib.import_module("bot.handlers.callbacks") + importlib.import_module("bot.handlers.commands") + + dispatcher.include_router(registry.router) diff --git a/src/bot/handlers/utils/registry/__init__.py b/src/bot/handlers/utils/registry/__init__.py new file mode 100644 index 0000000..cf4f3c6 --- /dev/null +++ b/src/bot/handlers/utils/registry/__init__.py @@ -0,0 +1,15 @@ +__all__ = ["RouterRegistry"] + + +from aiogram import Bot, Router + + +class RouterRegistry: + def __init__(self) -> None: + self._router = Router() + + @property + def router(self) -> Router: + return self._router + + async def set_menu(self, bot: Bot) -> None: ... diff --git a/src/bot/middlewares/__init__.py b/src/bot/middlewares/__init__.py index a9a2c5b..a139ade 100644 --- a/src/bot/middlewares/__init__.py +++ b/src/bot/middlewares/__init__.py @@ -1 +1,7 @@ -__all__ = [] +__all__ = ["connect_middlewares"] + + +from aiogram import Router + + +def connect_middlewares(dispatcher: Router): ... From 07ac2614abc7d4e2f66c3da92c019338e6ff4cef Mon Sep 17 00:00:00 2001 From: geekiot Date: Tue, 21 Oct 2025 00:01:56 +0500 Subject: [PATCH 4/5] Add [custom types]: add ChatType, HandlerMeta, HandlerType --- src/bot/handlers/utils/types/__init__.py | 5 +++++ src/bot/handlers/utils/types/chat.py | 11 +++++++++++ src/bot/handlers/utils/types/handler.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 src/bot/handlers/utils/types/__init__.py create mode 100644 src/bot/handlers/utils/types/chat.py create mode 100644 src/bot/handlers/utils/types/handler.py diff --git a/src/bot/handlers/utils/types/__init__.py b/src/bot/handlers/utils/types/__init__.py new file mode 100644 index 0000000..4845578 --- /dev/null +++ b/src/bot/handlers/utils/types/__init__.py @@ -0,0 +1,5 @@ +__all__ = ["ChatType", "HandlerType", "HandlerMeta"] + + +from .chat import ChatType +from .handler import HandlerMeta, HandlerType diff --git a/src/bot/handlers/utils/types/chat.py b/src/bot/handlers/utils/types/chat.py new file mode 100644 index 0000000..b24f393 --- /dev/null +++ b/src/bot/handlers/utils/types/chat.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class ChatType(Enum): + PRIVATE = "private" + GROUP = "group" + SUPERGROUP = "supergroup" + CHANNEL = "channel" + + def __str__(self) -> str: + return self.value diff --git a/src/bot/handlers/utils/types/handler.py b/src/bot/handlers/utils/types/handler.py new file mode 100644 index 0000000..1304752 --- /dev/null +++ b/src/bot/handlers/utils/types/handler.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Any, Awaitable, Callable, Optional, TypeVar + +from bot.handlers.utils.types import ChatType + +HandlerType = TypeVar( + "HandlerType", bound=Callable[[Any], Awaitable[Any]] +) + + +@dataclass +class HandlerMeta: + func: Callable[[Any], Awaitable[Any]] + description: str + chat_types: list[ChatType] + command: Optional[str] = None From 800f4bb427f052d30d1df11a5397d376df23617f Mon Sep 17 00:00:00 2001 From: geekiot Date: Tue, 21 Oct 2025 00:03:43 +0500 Subject: [PATCH 5/5] Add [handlers utils]: add handlers filters & router registry --- src/bot/handlers/utils/filters/__init__.py | 37 ++++++++++- src/bot/handlers/utils/registry/__init__.py | 69 ++++++++++++++++++++- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/bot/handlers/utils/filters/__init__.py b/src/bot/handlers/utils/filters/__init__.py index a9a2c5b..4c326f2 100644 --- a/src/bot/handlers/utils/filters/__init__.py +++ b/src/bot/handlers/utils/filters/__init__.py @@ -1 +1,36 @@ -__all__ = [] +__all__ = ["ChatTypeFilter"] + + +from typing import Union + +from aiogram.filters import BaseFilter +from aiogram.types import CallbackQuery, Message + +from bot.handlers.utils.types import ChatType + + +class ChatTypeFilter(BaseFilter): + def __init__( + self, + chat_types: Union[ChatType, list[ChatType]], + ) -> None: + if isinstance(chat_types, ChatType): + self.chat_type = [chat_types.value] + else: + self.chat_type = [chat.value for chat in chat_types] + + async def __call__( + self, + event: Union[Message, CallbackQuery], + ) -> bool: + current_chat_type: str + + if isinstance(event, Message): + current_chat_type = event.chat.type + else: + if event.message is None: + return False + + current_chat_type = event.message.chat.type + + return current_chat_type in self.chat_type diff --git a/src/bot/handlers/utils/registry/__init__.py b/src/bot/handlers/utils/registry/__init__.py index cf4f3c6..81be9e3 100644 --- a/src/bot/handlers/utils/registry/__init__.py +++ b/src/bot/handlers/utils/registry/__init__.py @@ -1,15 +1,82 @@ __all__ = ["RouterRegistry"] +from typing import Any, Callable, Optional, Union + from aiogram import Bot, Router +from aiogram.filters import BaseFilter, Command +from aiogram.types.bot_command import BotCommand +from aiogram.types.bot_command_scope_all_private_chats import ( + BotCommandScopeAllPrivateChats, +) + +from bot.handlers.utils.filters import ChatTypeFilter +from bot.handlers.utils.types import ( + ChatType, + HandlerMeta, + HandlerType, +) class RouterRegistry: def __init__(self) -> None: self._router = Router() + self._handlers: list[HandlerMeta] = list() @property def router(self) -> Router: return self._router - async def set_menu(self, bot: Bot) -> None: ... + def register( + self, + *, + description: str, + chat_types: Union[ChatType, list[ChatType]], + filters: Union[BaseFilter, list[BaseFilter]] = [], + command: Optional[str] = None, + ) -> Callable[[Any], HandlerType]: + if isinstance(chat_types, ChatType): + chat_types = [chat_types] + + if isinstance(filters, BaseFilter): + filters = [filters] + + def decorator(func: HandlerType) -> HandlerType: + meta = HandlerMeta( + func=func, + description=description, + chat_types=chat_types, + command=command, + ) + self._handlers.append(meta) + + if command is None: + self._router.callback_query.register( + func, + *filters, + ChatTypeFilter(chat_types=chat_types), + ) + else: + self._router.message.register( + func, + *filters, + ChatTypeFilter(chat_types=chat_types), + Command(commands=[command]), + ) + return func + + return decorator + + async def set_menu(self, bot: Bot) -> None: + await bot.set_my_commands( + [ + BotCommand( + command=meta.command, + description=meta.description, + ) + for meta in self._handlers + if meta.command is not None + and ChatType.PRIVATE in meta.chat_types + ], + scope=BotCommandScopeAllPrivateChats(), + )