From b7f61a7a23ae82b111bb8c4202551a3a5d95d2be Mon Sep 17 00:00:00 2001 From: Lev Chechulin Date: Wed, 28 Jan 2026 14:03:25 +0100 Subject: [PATCH] feat: init handlers refactoring --- src/bot.py | 95 ++++++------------ src/consts.py | 14 +++ src/tg/handler_registry.py | 184 +++++++++++++++++------------------ src/tg/handler_strategies.py | 96 ++++++++++++++++++ 4 files changed, 229 insertions(+), 160 deletions(-) create mode 100644 src/tg/handler_strategies.py diff --git a/src/bot.py b/src/bot.py index c0ac228..7645874 100644 --- a/src/bot.py +++ b/src/bot.py @@ -22,11 +22,12 @@ COMMIT_URL, USAGE_LOG_LEVEL, CommandCategories, + JobType, + AccessLevel, ) -from .jobs.utils import get_job_runnable -from .tg import handlers, sender +from .tg import handlers, sender, handler_strategies from .tg.handler_registry import HANDLER_REGISTRY -from .tg.handlers.utils import admin_only, direct_message_only, manager_only +from .tg.handlers.utils import direct_message_only logging.addLevelName(USAGE_LOG_LEVEL, "NOTICE") @@ -109,34 +110,47 @@ def init_handlers(self): Iterates over the registry, resolves handlers (direct or job-based), applies wrappers (e.g. direct_only), and registers them with the application. """ + # Strategy mapping + strategies = { + JobType.ADMIN_BROADCAST: handler_strategies.AdminBroadcastFactory(), + JobType.ADMIN_REPLY: handler_strategies.AdminReplyFactory(), + JobType.MANAGER_REPLY: handler_strategies.ManagerReplyFactory(), + JobType.USER_REPLY: handler_strategies.UserReplyFactory(), + } + # Register handlers from registry for config in HANDLER_REGISTRY: # Resolve handler handler = config.handler_func if config.job_name: - if config.job_type == "admin_broadcast": - handler = self.admin_broadcast_handler(config.job_name) - elif config.job_type == "admin_reply": - handler = self.admin_reply_handler(config.job_name) - elif config.job_type == "manager_reply": - handler = self.manager_reply_handler(config.job_name) - elif config.job_type == "user_reply": - handler = self.user_handler(config.job_name) + strategy = strategies.get(config.job_type) + if strategy: + handler = strategy.create( + config.job_name, + self.app_context, + self.telegram_sender, + self.config_manager, + ) + else: + logger.error( + f"Unknown job type {config.job_type} for job {config.job_name}" + ) + continue # Apply modifiers if config.direct_only: handler = direct_message_only(handler) # Register - if config.access_level == "admin": + if config.access_level == AccessLevel.ADMIN: self.add_admin_handler( config.command, config.category, handler, config.description ) - elif config.access_level == "manager": + elif config.access_level == AccessLevel.MANAGER: self.add_manager_handler( config.command, config.category, handler, config.description ) - elif config.access_level == "user": + elif config.access_level == AccessLevel.USER: self.add_user_handler( config.command, config.category, handler, config.description ) @@ -251,56 +265,3 @@ def add_user_handler( """Adds handler. It will be listed in /help for everybody""" self.add_handler(handler_cmd, handler_func) self.handlers_info[handler_category]["user"][f"/{handler_cmd}"] = description - - # Methods, creating handlers from jobs with proper invocation restrictions - def admin_broadcast_handler(self, job_name: str) -> Callable: - """ - Handler that invokes the job as configured in settings, if called by admin. - Can possibly send message to multiple chat ids, if configured in settings. - """ - return admin_only(self._create_broadcast_handler(job_name)) - - def admin_reply_handler(self, job_name: str) -> Callable: - """ - Handler that invokes the job as configured in settings, if called by admin. - Replies to the admin that invoked it. - """ - return admin_only(self._create_reply_handler(job_name)) - - def manager_reply_handler(self, job_name: str) -> Callable: - """ - Handler that replies if manager invokes it (DM or chat). - """ - return manager_only( - self._create_reply_handler( - job_name, - ) - ) - - def user_handler(self, job_name: str) -> Callable: - """ - Handler that replies to any user. - """ - return self._create_reply_handler(job_name) - - def _create_reply_handler(self, job_name: str) -> Callable: - """ - Creates a handler that replies to a message of given user. - """ - return lambda update, tg_context: get_job_runnable(job_name)( - app_context=self.app_context, - send=self.telegram_sender.create_reply_send(update), - called_from_handler=True, - args=update.message.text.split()[1:], - ) - - def _create_broadcast_handler(self, job_name: str) -> Callable: - """ - Creates a handler that sends message to list of chat ids. - """ - chat_ids = self.config_manager.get_job_send_to(job_name) - return lambda update, tg_context: get_job_runnable(job_name)( - app_context=self.app_context, - send=self.telegram_sender.create_chat_ids_send(chat_ids), - called_from_handler=True, - ) diff --git a/src/consts.py b/src/consts.py index 637e14b..30e3e44 100644 --- a/src/consts.py +++ b/src/consts.py @@ -286,3 +286,17 @@ class CommandCategories(Enum): REMINDERS = "help__08_reminders" HR = "help__09_hr" DEBUG = "help__10_debug" + + +class AccessLevel(Enum): + ADMIN = "admin" + MANAGER = "manager" + USER = "user" + HIDDEN = "hidden" + + +class JobType(Enum): + ADMIN_BROADCAST = "admin_broadcast" + ADMIN_REPLY = "admin_reply" + MANAGER_REPLY = "manager_reply" + USER_REPLY = "user_reply" diff --git a/src/tg/handler_registry.py b/src/tg/handler_registry.py index dfd92a2..7bb8aff 100644 --- a/src/tg/handler_registry.py +++ b/src/tg/handler_registry.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from typing import Callable, Optional, Literal +from typing import Callable, Optional -from ...consts import CommandCategories +from ...consts import CommandCategories, JobType, AccessLevel from . import handlers @@ -10,14 +10,12 @@ class HandlerConfig: command: str description: str = "" category: Optional[CommandCategories] = None - access_level: Literal["admin", "manager", "user", "hidden"] = "hidden" + access_level: AccessLevel = AccessLevel.HIDDEN # Logic configuration handler_func: Optional[Callable] = None job_name: Optional[str] = None - job_type: Literal[ - "admin_broadcast", "admin_reply", "manager_reply", "user_reply" - ] = "manager_reply" + job_type: JobType = JobType.MANAGER_REPLY # Modifiers direct_only: bool = False @@ -28,106 +26,106 @@ class HandlerConfig: HandlerConfig( command="send_trello_board_state", category=CommandCategories.BROADCAST, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="trello_board_state_job", - job_type="admin_broadcast", + job_type=JobType.ADMIN_BROADCAST, description="рассылка сводки о состоянии доски", ), HandlerConfig( command="get_trello_board_state", category=CommandCategories.SUMMARY, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="trello_board_state_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить сводку о состоянии доски", ), HandlerConfig( command="get_publication_plans", category=CommandCategories.SUMMARY, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="publication_plans_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить сводку о публикуемыми на неделе постами", ), HandlerConfig( command="send_publication_plans", category=CommandCategories.BROADCAST, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="publication_plans_job", - job_type="admin_broadcast", + job_type=JobType.ADMIN_BROADCAST, description="рассылка сводки о публикуемых на неделе постах", ), HandlerConfig( command="get_manager_status", category=CommandCategories.SUMMARY, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="board_my_cards_razvitie_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, direct_only=True, description="получить мои карточки из доски Развитие", ), HandlerConfig( command="fill_posts_list", category=CommandCategories.DEBUG, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="fill_posts_list_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, direct_only=True, description="заполнить реестр постов (пока не работает)", ), HandlerConfig( command="fill_posts_list_focalboard", category=CommandCategories.DEBUG, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="fill_posts_list_focalboard_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, direct_only=True, description="заполнить реестр постов из Focalboard (пока не работает)", ), HandlerConfig( command="hr_acquisition", category=CommandCategories.HR, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="hr_acquisition_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="обработать новые анкеты", ), HandlerConfig( command="hr_acquisition_pt", category=CommandCategories.HR, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="hr_acquisition_pt_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="обработать новые анкеты Пишу Тебе", ), HandlerConfig( command="get_hr_status", category=CommandCategories.HR, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="hr_status_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить статус по работе hr (по новичкам и участникам на испытательном)", ), HandlerConfig( command="send_hr_status", category=CommandCategories.BROADCAST, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="hr_status_job", - job_type="admin_broadcast", + job_type=JobType.ADMIN_BROADCAST, description="разослать статус по работе hr (по новичкам и участинкам на испытательном)", ), HandlerConfig( command="create_folders_for_illustrators", category=CommandCategories.REGISTRY, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="create_folders_for_illustrators_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="создать папки для иллюстраторов", ), HandlerConfig( command="get_tasks_report_focalboard", category=CommandCategories.MOST_USED, - access_level="manager", + access_level=AccessLevel.MANAGER, handler_func=handlers.get_tasks_report_focalboard, direct_only=True, description="получить список задач из Focalboard", @@ -135,7 +133,7 @@ class HandlerConfig: HandlerConfig( command="get_rubrics", category=CommandCategories.MOST_USED, - access_level="manager", + access_level=AccessLevel.MANAGER, handler_func=handlers.get_rubrics, direct_only=True, description="получить рубрики из доски Редакция", @@ -143,61 +141,61 @@ class HandlerConfig: HandlerConfig( command="get_articles_rubric", category=CommandCategories.DEBUG, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="trello_get_articles_rubric_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить карточки по названию рубрики в трелло", ), HandlerConfig( command="get_chat_id", category=CommandCategories.MOST_USED, - access_level="manager", + access_level=AccessLevel.MANAGER, handler_func=handlers.get_chat_id, description="получить chat_id (свой или группы)", ), HandlerConfig( command="manage_reminders", category=CommandCategories.MOST_USED, - access_level="manager", + access_level=AccessLevel.MANAGER, handler_func=handlers.manage_reminders, description="настроить напоминания", ), HandlerConfig( command="get_fb_analytics_report", category=CommandCategories.STATS, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="fb_analytics_report_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить статистику facebook страницы за неделю", ), HandlerConfig( command="get_ig_analytics_report", category=CommandCategories.STATS, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="ig_analytics_report_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить статистику instagram страницы за неделю", ), HandlerConfig( command="get_tg_analytics_report", category=CommandCategories.STATS, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="tg_analytics_report_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить статистику telegram канала за неделю", ), HandlerConfig( command="get_report_from_sheet", category=CommandCategories.SUMMARY, - access_level="manager", + access_level=AccessLevel.MANAGER, job_name="sheet_report_job", - job_type="manager_reply", + job_type=JobType.MANAGER_REPLY, description="получить статистику по табличке (например, оцифровка открыток)", ), HandlerConfig( command="enroll_curator", category=CommandCategories.HR, - access_level="manager", + access_level=AccessLevel.MANAGER, handler_func=handlers.enroll_curator, description="", # hidden from help ), @@ -205,239 +203,239 @@ class HandlerConfig: HandlerConfig( command="update_config", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="config_updater_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="обновить конфиг вне расписания", ), HandlerConfig( command="list_jobs", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.list_jobs, description="показать статус асинхронных задач", ), HandlerConfig( command="get_usage_list", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.list_chats, description="показать места использование бота: пользователи и чаты", ), HandlerConfig( command="set_log_level", category=CommandCategories.LOGGING, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.set_log_level, description="изменить уровень логирования (info / debug)", ), HandlerConfig( command="mute_errors", category=CommandCategories.LOGGING, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.mute_errors, description="отключить логирование ошибок в телеграм", ), HandlerConfig( command="unmute_errors", category=CommandCategories.LOGGING, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.unmute_errors, description="включить логирование ошибок в телеграм", ), HandlerConfig( command="get_config", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.get_config, description="получить текущий конфиг (частично или полностью)", ), HandlerConfig( command="get_config_jobs", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.get_config_jobs, description="получить текущий конфиг джобов (частично или полностью)", ), HandlerConfig( command="reload_config_jobs", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.reload_config_jobs, description="обновить конфиг джобов с Google-диска", ), HandlerConfig( command="set_config", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.set_config, description="установить новое значение в конфиге", ), HandlerConfig( command="add_manager", category=CommandCategories.MOST_USED, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.add_manager, description="добавить менеджера в список", ), HandlerConfig( command="change_board", category=CommandCategories.CONFIG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.change_board, description="изменить Trello board_id", ), HandlerConfig( command="send_reminders", category=CommandCategories.BROADCAST, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="send_reminders_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="отослать напоминания вне расписания", ), HandlerConfig( command="send_trello_curator_notification", category=CommandCategories.BROADCAST, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="trello_board_state_notifications_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="разослать кураторам состояние их карточек вне расписания", ), HandlerConfig( command="manage_all_reminders", category=CommandCategories.MOST_USED, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.manage_all_reminders, description="настроить все напоминания", ), HandlerConfig( command="get_roles_for_member", category=CommandCategories.DEBUG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.get_roles_for_member, description="показать роли для участника", ), HandlerConfig( command="get_members_for_role", category=CommandCategories.DEBUG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.get_members_for_role, description="показать участников для роли", ), HandlerConfig( command="check_chat_consistency", category=CommandCategories.HR, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="hr_check_chat_consistency_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="консистентность чата редакции", ), HandlerConfig( command="check_chat_consistency_frozen", category=CommandCategories.HR, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="hr_check_chat_consistency_frozen_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="консистентность чата редакции (замороженные участники)", ), HandlerConfig( command="get_members_without_telegram", category=CommandCategories.HR, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="hr_get_members_without_telegram_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="активные участники без указанного телеграма(телефон это 10+ цифр+-(), отсутствие включает #N/A и кириллицу)", ), HandlerConfig( command="check_site_health", category=CommandCategories.DATA_SYNC, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="site_health_check_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="проверка статуса сайта", ), HandlerConfig( command="get_chat_data", category=CommandCategories.DEBUG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.get_chat_data, description="get_chat_data", ), HandlerConfig( command="clean_chat_data", category=CommandCategories.DEBUG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.clean_chat_data, description="clean_chat_data", ), HandlerConfig( command="get_managers", category=CommandCategories.MOST_USED, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.get_managers, description="get_managers", ), # Sample and DB commands HandlerConfig( command="sample_handler", - access_level="hidden", + access_level=AccessLevel.HIDDEN, job_name="sample_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, ), HandlerConfig( command="db_fetch_authors_sheet", - access_level="hidden", + access_level=AccessLevel.HIDDEN, job_name="db_fetch_authors_sheet_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, ), HandlerConfig( command="db_fetch_curators_sheet", - access_level="hidden", + access_level=AccessLevel.HIDDEN, job_name="db_fetch_curators_sheet_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, ), HandlerConfig( command="db_fetch_team_sheet", - access_level="hidden", + access_level=AccessLevel.HIDDEN, job_name="db_fetch_team_sheet_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, ), HandlerConfig( command="db_fetch_strings_sheet", - access_level="hidden", + access_level=AccessLevel.HIDDEN, job_name="db_fetch_strings_sheet_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, ), HandlerConfig( command="db_fetch_all_team_members", category=CommandCategories.MOST_USED, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="db_fetch_all_team_members_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="db_fetch_all_team_members", ), HandlerConfig( command="backfill_telegram_user_ids", category=CommandCategories.DATA_SYNC, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="backfill_telegram_user_ids_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="backfill Telegram user IDs from team member usernames", ), # General purpose HandlerConfig( command="start", category=CommandCategories.DEBUG, - access_level="admin", + access_level=AccessLevel.ADMIN, handler_func=handlers.start, description="начать чат с ботом", ), HandlerConfig( command="shrug", category=CommandCategories.DEBUG, - access_level="admin", + access_level=AccessLevel.ADMIN, job_name="shrug_job", - job_type="admin_reply", + job_type=JobType.ADMIN_REPLY, description="¯\\_(ツ)_/¯", ), # HELP is handled specially or requires lambda diff --git a/src/tg/handler_strategies.py b/src/tg/handler_strategies.py new file mode 100644 index 0000000..0eedc26 --- /dev/null +++ b/src/tg/handler_strategies.py @@ -0,0 +1,96 @@ +from abc import ABC, abstractmethod +from typing import Callable + +from .handlers.utils import admin_only, manager_only +from ..jobs.utils import get_job_runnable +from ..app_context import AppContext +from ..tg.sender import TelegramSender + + +class HandlerFactory(ABC): + @abstractmethod + def create( + self, + job_name: str, + app_context: AppContext, + telegram_sender: TelegramSender, + config_manager, + ) -> Callable: + pass + + +class AdminBroadcastFactory(HandlerFactory): + def create( + self, + job_name: str, + app_context: AppContext, + telegram_sender: TelegramSender, + config_manager, + ) -> Callable: + chat_ids = config_manager.get_job_send_to(job_name) + + def handler(update, tg_context): + return get_job_runnable(job_name)( + app_context=app_context, + send=telegram_sender.create_chat_ids_send(chat_ids), + called_from_handler=True, + ) + + return admin_only(handler) + + +class AdminReplyFactory(HandlerFactory): + def create( + self, + job_name: str, + app_context: AppContext, + telegram_sender: TelegramSender, + config_manager, + ) -> Callable: + def handler(update, tg_context): + return get_job_runnable(job_name)( + app_context=app_context, + send=telegram_sender.create_reply_send(update), + called_from_handler=True, + args=update.message.text.split()[1:], + ) + + return admin_only(handler) + + +class ManagerReplyFactory(HandlerFactory): + def create( + self, + job_name: str, + app_context: AppContext, + telegram_sender: TelegramSender, + config_manager, + ) -> Callable: + def handler(update, tg_context): + return get_job_runnable(job_name)( + app_context=app_context, + send=telegram_sender.create_reply_send(update), + called_from_handler=True, + args=update.message.text.split()[1:], + ) + + return manager_only(handler) + + +class UserReplyFactory(HandlerFactory): + def create( + self, + job_name: str, + app_context: AppContext, + telegram_sender: TelegramSender, + config_manager, + ) -> Callable: + def handler(update, tg_context): + return get_job_runnable(job_name)( + app_context=app_context, + send=telegram_sender.create_reply_send(update), + called_from_handler=True, + args=update.message.text.split()[1:], + ) + + return handler