diff --git a/src/bot.py b/src/bot.py
index 0495ec1..c0ac228 100644
--- a/src/bot.py
+++ b/src/bot.py
@@ -25,6 +25,7 @@
)
from .jobs.utils import get_job_runnable
from .tg import handlers, sender
+from .tg.handler_registry import HANDLER_REGISTRY
from .tg.handlers.utils import admin_only, direct_message_only, manager_only
logging.addLevelName(USAGE_LOG_LEVEL, "NOTICE")
@@ -39,6 +40,17 @@ def usage(self, message, *args, **kws):
class SysBlokBot:
+ """
+ Main Bot class responsible for initialization and handler registration.
+
+ Attributes:
+ config_manager: Manages configuration settings.
+ application: Telegram Application instance.
+ telegram_sender: wrapper for sending messages.
+ app_context: Application context state.
+ handlers_info: Dictionary to store handler descriptions for /help command.
+ """
+
def __init__(
self,
config_manager: ConfigManager,
@@ -92,351 +104,52 @@ def __init__(
logger.info("SysBlokBot successfully initialized")
def init_handlers(self):
- # all command handlers defined here
- # business logic cmds
- self.add_admin_handler(
- "send_trello_board_state",
- CommandCategories.BROADCAST,
- self.admin_broadcast_handler("trello_board_state_job"),
- "рассылка сводки о состоянии доски",
- )
- self.add_manager_handler(
- "get_trello_board_state",
- CommandCategories.SUMMARY,
- self.manager_reply_handler("trello_board_state_job"),
- "получить сводку о состоянии доски",
- )
- self.add_manager_handler(
- "get_publication_plans",
- CommandCategories.SUMMARY,
- self.manager_reply_handler("publication_plans_job"),
- "получить сводку о публикуемыми на неделе постами",
- )
- self.add_admin_handler(
- "send_publication_plans",
- CommandCategories.BROADCAST,
- self.admin_broadcast_handler("publication_plans_job"),
- "рассылка сводки о публикуемых на неделе постах",
- )
- self.add_manager_handler(
- "get_manager_status",
- CommandCategories.SUMMARY,
- direct_message_only(
- self.manager_reply_handler("board_my_cards_razvitie_job")
- ),
- "получить мои карточки из доски Развитие",
- )
- self.add_manager_handler(
- "fill_posts_list",
- CommandCategories.DEBUG,
- direct_message_only(self.manager_reply_handler("fill_posts_list_job")),
- "заполнить реестр постов (пока не работает)",
- )
- self.add_manager_handler(
- "fill_posts_list_focalboard",
- CommandCategories.DEBUG,
- direct_message_only(
- self.manager_reply_handler("fill_posts_list_focalboard_job")
- ),
- "заполнить реестр постов из Focalboard (пока не работает)",
- )
- self.add_admin_handler(
- "hr_acquisition",
- CommandCategories.HR,
- self.manager_reply_handler("hr_acquisition_job"),
- "обработать новые анкеты",
- )
- self.add_admin_handler(
- "hr_acquisition_pt",
- CommandCategories.HR,
- self.manager_reply_handler("hr_acquisition_pt_job"),
- "обработать новые анкеты Пишу Тебе",
- )
- self.add_manager_handler(
- "get_hr_status",
- CommandCategories.HR,
- self.manager_reply_handler("hr_status_job"),
- "получить статус по работе hr (по новичкам и участникам на испытательном)",
- )
- self.add_admin_handler(
- "send_hr_status",
- CommandCategories.BROADCAST,
- self.admin_broadcast_handler("hr_status_job"),
- "разослать статус по работе hr (по новичкам и участинкам на испытательном)",
- )
- self.add_manager_handler(
- "create_folders_for_illustrators",
- CommandCategories.REGISTRY,
- self.manager_reply_handler("create_folders_for_illustrators_job"),
- "создать папки для иллюстраторов",
- )
- self.add_manager_handler(
- "get_tasks_report_focalboard",
- CommandCategories.MOST_USED,
- # CommandCategories.SUMMARY,
- direct_message_only(handlers.get_tasks_report_focalboard),
- "получить список задач из Focalboard",
- )
-
- self.add_manager_handler(
- "get_rubrics",
- CommandCategories.MOST_USED,
- direct_message_only(handlers.get_rubrics),
- "получить рубрики из доски Редакция",
- )
-
- self.add_manager_handler(
- "get_articles_rubric",
- CommandCategories.DEBUG, # used to be SUMMARY but hiding for now
- self.manager_reply_handler("trello_get_articles_rubric_job"),
- "получить карточки по названию рубрики в трелло",
- )
- self.add_manager_handler(
- "get_chat_id",
- CommandCategories.MOST_USED,
- handlers.get_chat_id,
- "получить chat_id (свой или группы)",
- )
- self.add_manager_handler(
- "manage_reminders",
- CommandCategories.MOST_USED,
- handlers.manage_reminders,
- "настроить напоминания",
- )
- self.add_manager_handler(
- "get_fb_analytics_report",
- CommandCategories.STATS,
- self.manager_reply_handler("fb_analytics_report_job"),
- "получить статистику facebook страницы за неделю",
- )
- self.add_manager_handler(
- "get_ig_analytics_report",
- CommandCategories.STATS,
- self.manager_reply_handler("ig_analytics_report_job"),
- "получить статистику instagram страницы за неделю",
- )
- self.add_manager_handler(
- "get_tg_analytics_report",
- CommandCategories.STATS,
- self.manager_reply_handler("tg_analytics_report_job"),
- "получить статистику telegram канала за неделю",
- )
- self.add_manager_handler(
- "get_report_from_sheet",
- CommandCategories.SUMMARY,
- self.manager_reply_handler("sheet_report_job"),
- "получить статистику по табличке (например, оцифровка открыток)",
- )
- # hidden from /help command for curator enrollment
- self.add_manager_handler(
- "enroll_curator", CommandCategories.HR, handlers.enroll_curator
- )
-
- # admin-only technical cmds
- self.add_admin_handler(
- "update_config",
- CommandCategories.CONFIG,
- self.admin_reply_handler("config_updater_job"),
- "обновить конфиг вне расписания",
- )
- self.add_admin_handler(
- "list_jobs",
- CommandCategories.CONFIG,
- handlers.list_jobs,
- "показать статус асинхронных задач",
- )
- self.add_admin_handler(
- "get_usage_list",
- CommandCategories.CONFIG,
- handlers.list_chats,
- "показать места использование бота: пользователи и чаты",
- )
- self.add_admin_handler(
- "set_log_level",
- CommandCategories.LOGGING,
- handlers.set_log_level,
- "изменить уровень логирования (info / debug)",
- )
- self.add_admin_handler(
- "mute_errors",
- CommandCategories.LOGGING,
- handlers.mute_errors,
- "отключить логирование ошибок в телеграм",
- )
- self.add_admin_handler(
- "unmute_errors",
- CommandCategories.LOGGING,
- handlers.unmute_errors,
- "включить логирование ошибок в телеграм",
- )
- self.add_admin_handler(
- "get_config",
- CommandCategories.CONFIG,
- handlers.get_config,
- "получить текущий конфиг (частично или полностью)",
- )
- self.add_admin_handler(
- "get_config_jobs",
- CommandCategories.CONFIG,
- handlers.get_config_jobs,
- "получить текущий конфиг джобов (частично или полностью)",
- )
- self.add_admin_handler(
- "reload_config_jobs",
- CommandCategories.CONFIG,
- handlers.reload_config_jobs,
- "обновить конфиг джобов с Google-диска",
- )
- self.add_admin_handler(
- "set_config",
- CommandCategories.CONFIG,
- handlers.set_config,
- "установить новое значение в конфиге",
- )
- self.add_admin_handler(
- "add_manager",
- CommandCategories.MOST_USED,
- handlers.add_manager,
- "добавить менеджера в список",
- )
- self.add_admin_handler(
- "change_board",
- CommandCategories.CONFIG,
- handlers.change_board,
- "изменить Trello board_id",
- )
- self.add_admin_handler(
- "send_reminders",
- CommandCategories.BROADCAST,
- self.admin_reply_handler("send_reminders_job"),
- "отослать напоминания вне расписания",
- )
- self.add_admin_handler(
- "send_trello_curator_notification",
- CommandCategories.BROADCAST,
- self.admin_reply_handler("trello_board_state_notifications_job"),
- "разослать кураторам состояние их карточек вне расписания",
- )
- self.add_admin_handler(
- "manage_all_reminders",
- CommandCategories.MOST_USED,
- handlers.manage_all_reminders,
- "настроить все напоминания",
- )
- self.add_admin_handler(
- "get_roles_for_member",
- # CommandCategories.HR,
- CommandCategories.DEBUG,
- handlers.get_roles_for_member,
- "показать роли для участника",
- )
- self.add_admin_handler(
- "get_members_for_role",
- # CommandCategories.HR,
- CommandCategories.DEBUG,
- handlers.get_members_for_role,
- "показать участников для роли",
- )
- self.add_admin_handler(
- "check_chat_consistency",
- CommandCategories.HR,
- self.admin_reply_handler("hr_check_chat_consistency_job"),
- "консистентность чата редакции",
- )
- self.add_admin_handler(
- "check_chat_consistency_frozen",
- CommandCategories.HR,
- self.admin_reply_handler("hr_check_chat_consistency_frozen_job"),
- "консистентность чата редакции (замороженные участники)",
- )
- self.add_admin_handler(
- "get_members_without_telegram",
- CommandCategories.HR,
- self.admin_reply_handler("hr_get_members_without_telegram_job"),
- (
- "активные участники без указанного телеграма"
- "(телефон это 10+ цифр+-(), отсутствие включает #N/A и кириллицу)"
- ),
- )
- self.add_admin_handler(
- "check_site_health",
- CommandCategories.DATA_SYNC,
- self.admin_reply_handler("site_health_check_job"),
- "проверка статуса сайта",
- )
- self.add_admin_handler(
- "get_chat_data",
- CommandCategories.DEBUG,
- handlers.get_chat_data,
- "get_chat_data",
- )
- self.add_admin_handler(
- "clean_chat_data",
- CommandCategories.DEBUG,
- handlers.clean_chat_data,
- "clean_chat_data",
- )
- self.add_admin_handler(
- "get_managers",
- CommandCategories.MOST_USED,
- handlers.get_managers,
- "get_managers",
- )
-
- # sample handler
- self.add_handler(
- "sample_handler",
- self.admin_reply_handler("sample_job"),
- )
-
- # db commands hidden from /help command
- self.add_handler(
- "db_fetch_authors_sheet",
- self.admin_reply_handler("db_fetch_authors_sheet_job"),
- )
- self.add_handler(
- "db_fetch_curators_sheet",
- self.admin_reply_handler("db_fetch_curators_sheet_job"),
- )
- self.add_handler(
- "db_fetch_team_sheet",
- self.admin_reply_handler("db_fetch_team_sheet_job"),
- )
- self.add_handler(
- "db_fetch_strings_sheet",
- self.admin_reply_handler("db_fetch_strings_sheet_job"),
- )
- self.add_admin_handler(
- "db_fetch_all_team_members",
- CommandCategories.MOST_USED,
- self.admin_reply_handler("db_fetch_all_team_members_job"),
- "db_fetch_all_team_members",
- )
- self.add_admin_handler(
- "backfill_telegram_user_ids",
- CommandCategories.DATA_SYNC,
- self.admin_reply_handler("backfill_telegram_user_ids_job"),
- "backfill Telegram user IDs from team member usernames",
- )
+ """
+ Initializes Telegram handlers based on the configuration in `HANDLER_REGISTRY`.
+ Iterates over the registry, resolves handlers (direct or job-based),
+ applies wrappers (e.g. direct_only), and registers them with the application.
+ """
+ # 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)
+
+ # Apply modifiers
+ if config.direct_only:
+ handler = direct_message_only(handler)
+
+ # Register
+ if config.access_level == "admin":
+ self.add_admin_handler(
+ config.command, config.category, handler, config.description
+ )
+ elif config.access_level == "manager":
+ self.add_manager_handler(
+ config.command, config.category, handler, config.description
+ )
+ elif config.access_level == "user":
+ self.add_user_handler(
+ config.command, config.category, handler, config.description
+ )
+ else: # hidden
+ self.add_handler(config.command, handler)
- # general purpose cmds
- self.add_admin_handler(
- "start", CommandCategories.DEBUG, handlers.start, "начать чат с ботом"
- )
+ # Special case: Help handler (requires dynamic self.handlers_info)
self.add_admin_handler(
"help",
CommandCategories.DEBUG,
- # CommandCategories.MOST_USED,
lambda update, context: handlers.help(update, context, self.handlers_info),
"получить список доступных команд",
)
- self.add_admin_handler(
- "shrug",
- CommandCategories.DEBUG,
- # CommandCategories.MOST_USED,
- self.admin_reply_handler("shrug_job"),
- "¯\\_(ツ)_/¯",
- )
# on non-command user message
diff --git a/src/facebook/facebook_client.py b/src/facebook/facebook_client.py
index ab265bf..cf43038 100644
--- a/src/facebook/facebook_client.py
+++ b/src/facebook/facebook_client.py
@@ -70,10 +70,12 @@ def get_total_reach(
) -> List[Tuple[datetime, int]]:
"""
Get statistics on the total reach of new posts.
+ NOTE: 'page_posts_impressions_unique' is deprecated in v19.0.
+ Using 'page_impressions_unique' (Total Page Reach) as replacement.
"""
batches = self._get_all_batches(
connection_name="insights",
- metric="page_posts_impressions_unique",
+ metric="page_impressions_unique",
period=period.value,
since=since,
until=until,
@@ -91,7 +93,7 @@ def get_organic_reach(
"""
batches = self._get_all_batches(
connection_name="insights",
- metric="page_posts_impressions_organic_unique",
+ metric="page_impressions_organic_unique",
period=period.value,
since=since,
until=until,
@@ -127,7 +129,7 @@ def get_new_fan_count(
"""
batches = self._get_all_batches(
connection_name="insights",
- metric="page_fan_adds_unique",
+ metric="page_daily_follows_unique",
period=period.value,
since=since,
until=until,
@@ -147,7 +149,12 @@ def _get_all_batches(
}
params.update(kwargs)
page = self._make_graph_api_call(f"{self._page_id}/{connection_name}", params)
- result += page["data"]
+
+ if "error" in page:
+ logger.error(f"Error fetching {connection_name}: {page['error']}")
+ return result
+
+ result += page.get("data", [])
# process next
result += self._iterate_over_pages(connection_name, since, until, page, True)
# process previous
@@ -192,10 +199,16 @@ def _iterate_over_pages(
current_page = self._make_graph_api_call(
f"{self._page_id}/{connection_name}", args
)
+ if "error" in current_page:
+ logger.error(
+ f"Error in pagination for {connection_name}: {current_page['error']}"
+ )
+ break
+
if "data" in current_page:
result += current_page["data"]
else:
- logger.error(f"Error in pagination: {current_page}")
+ logger.warning(f"No data found in pagination response: {current_page}")
break
return result
diff --git a/src/tg/handler_registry.py b/src/tg/handler_registry.py
new file mode 100644
index 0000000..6a8955a
--- /dev/null
+++ b/src/tg/handler_registry.py
@@ -0,0 +1,444 @@
+from dataclasses import dataclass
+from typing import Callable, Optional, Literal
+
+from ..consts import CommandCategories
+from . import handlers
+
+
+@dataclass
+class HandlerConfig:
+ command: str
+ description: str = ""
+ category: Optional[CommandCategories] = None
+ access_level: Literal["admin", "manager", "user", "hidden"] = "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"
+
+ # Modifiers
+ direct_only: bool = False
+
+
+HANDLER_REGISTRY = [
+ # Business logic cmds
+ HandlerConfig(
+ command="send_trello_board_state",
+ category=CommandCategories.BROADCAST,
+ access_level="admin",
+ job_name="trello_board_state_job",
+ job_type="admin_broadcast",
+ description="рассылка сводки о состоянии доски",
+ ),
+ HandlerConfig(
+ command="get_trello_board_state",
+ category=CommandCategories.SUMMARY,
+ access_level="manager",
+ job_name="trello_board_state_job",
+ job_type="manager_reply",
+ description="получить сводку о состоянии доски",
+ ),
+ HandlerConfig(
+ command="get_publication_plans",
+ category=CommandCategories.SUMMARY,
+ access_level="manager",
+ job_name="publication_plans_job",
+ job_type="manager_reply",
+ description="получить сводку о публикуемыми на неделе постами",
+ ),
+ HandlerConfig(
+ command="send_publication_plans",
+ category=CommandCategories.BROADCAST,
+ access_level="admin",
+ job_name="publication_plans_job",
+ job_type="admin_broadcast",
+ description="рассылка сводки о публикуемых на неделе постах",
+ ),
+ HandlerConfig(
+ command="get_manager_status",
+ category=CommandCategories.SUMMARY,
+ access_level="manager",
+ job_name="board_my_cards_razvitie_job",
+ job_type="manager_reply",
+ direct_only=True,
+ description="получить мои карточки из доски Развитие",
+ ),
+ HandlerConfig(
+ command="fill_posts_list",
+ category=CommandCategories.DEBUG,
+ access_level="manager",
+ job_name="fill_posts_list_job",
+ job_type="manager_reply",
+ direct_only=True,
+ description="заполнить реестр постов (пока не работает)",
+ ),
+ HandlerConfig(
+ command="fill_posts_list_focalboard",
+ category=CommandCategories.DEBUG,
+ access_level="manager",
+ job_name="fill_posts_list_focalboard_job",
+ job_type="manager_reply",
+ direct_only=True,
+ description="заполнить реестр постов из Focalboard (пока не работает)",
+ ),
+ HandlerConfig(
+ command="hr_acquisition",
+ category=CommandCategories.HR,
+ access_level="admin",
+ job_name="hr_acquisition_job",
+ job_type="manager_reply",
+ description="обработать новые анкеты",
+ ),
+ HandlerConfig(
+ command="hr_acquisition_pt",
+ category=CommandCategories.HR,
+ access_level="admin",
+ job_name="hr_acquisition_pt_job",
+ job_type="manager_reply",
+ description="обработать новые анкеты Пишу Тебе",
+ ),
+ HandlerConfig(
+ command="get_hr_status",
+ category=CommandCategories.HR,
+ access_level="manager",
+ job_name="hr_status_job",
+ job_type="manager_reply",
+ description="получить статус по работе hr (по новичкам и участникам на испытательном)",
+ ),
+ HandlerConfig(
+ command="send_hr_status",
+ category=CommandCategories.BROADCAST,
+ access_level="admin",
+ job_name="hr_status_job",
+ job_type="admin_broadcast",
+ description="разослать статус по работе hr (по новичкам и участинкам на испытательном)",
+ ),
+ HandlerConfig(
+ command="create_folders_for_illustrators",
+ category=CommandCategories.REGISTRY,
+ access_level="manager",
+ job_name="create_folders_for_illustrators_job",
+ job_type="manager_reply",
+ description="создать папки для иллюстраторов",
+ ),
+ HandlerConfig(
+ command="get_tasks_report_focalboard",
+ category=CommandCategories.MOST_USED,
+ access_level="manager",
+ handler_func=handlers.get_tasks_report_focalboard,
+ direct_only=True,
+ description="получить список задач из Focalboard",
+ ),
+ HandlerConfig(
+ command="get_rubrics",
+ category=CommandCategories.MOST_USED,
+ access_level="manager",
+ handler_func=handlers.get_rubrics,
+ direct_only=True,
+ description="получить рубрики из доски Редакция",
+ ),
+ HandlerConfig(
+ command="get_articles_rubric",
+ category=CommandCategories.DEBUG,
+ access_level="manager",
+ job_name="trello_get_articles_rubric_job",
+ job_type="manager_reply",
+ description="получить карточки по названию рубрики в трелло",
+ ),
+ HandlerConfig(
+ command="get_chat_id",
+ category=CommandCategories.MOST_USED,
+ access_level="manager",
+ handler_func=handlers.get_chat_id,
+ description="получить chat_id (свой или группы)",
+ ),
+ HandlerConfig(
+ command="manage_reminders",
+ category=CommandCategories.MOST_USED,
+ access_level="manager",
+ handler_func=handlers.manage_reminders,
+ description="настроить напоминания",
+ ),
+ HandlerConfig(
+ command="get_fb_analytics_report",
+ category=CommandCategories.STATS,
+ access_level="manager",
+ job_name="fb_analytics_report_job",
+ job_type="manager_reply",
+ description="получить статистику facebook страницы за неделю",
+ ),
+ HandlerConfig(
+ command="get_ig_analytics_report",
+ category=CommandCategories.STATS,
+ access_level="manager",
+ job_name="ig_analytics_report_job",
+ job_type="manager_reply",
+ description="получить статистику instagram страницы за неделю",
+ ),
+ HandlerConfig(
+ command="get_tg_analytics_report",
+ category=CommandCategories.STATS,
+ access_level="manager",
+ job_name="tg_analytics_report_job",
+ job_type="manager_reply",
+ description="получить статистику telegram канала за неделю",
+ ),
+ HandlerConfig(
+ command="get_report_from_sheet",
+ category=CommandCategories.SUMMARY,
+ access_level="manager",
+ job_name="sheet_report_job",
+ job_type="manager_reply",
+ description="получить статистику по табличке (например, оцифровка открыток)",
+ ),
+ HandlerConfig(
+ command="enroll_curator",
+ category=CommandCategories.HR,
+ access_level="manager",
+ handler_func=handlers.enroll_curator,
+ description="", # hidden from help
+ ),
+ # Admin-only technical cmds
+ HandlerConfig(
+ command="update_config",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ job_name="config_updater_job",
+ job_type="admin_reply",
+ description="обновить конфиг вне расписания",
+ ),
+ HandlerConfig(
+ command="list_jobs",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ handler_func=handlers.list_jobs,
+ description="показать статус асинхронных задач",
+ ),
+ HandlerConfig(
+ command="get_usage_list",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ handler_func=handlers.list_chats,
+ description="показать места использование бота: пользователи и чаты",
+ ),
+ HandlerConfig(
+ command="set_log_level",
+ category=CommandCategories.LOGGING,
+ access_level="admin",
+ handler_func=handlers.set_log_level,
+ description="изменить уровень логирования (info / debug)",
+ ),
+ HandlerConfig(
+ command="mute_errors",
+ category=CommandCategories.LOGGING,
+ access_level="admin",
+ handler_func=handlers.mute_errors,
+ description="отключить логирование ошибок в телеграм",
+ ),
+ HandlerConfig(
+ command="unmute_errors",
+ category=CommandCategories.LOGGING,
+ access_level="admin",
+ handler_func=handlers.unmute_errors,
+ description="включить логирование ошибок в телеграм",
+ ),
+ HandlerConfig(
+ command="get_config",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ handler_func=handlers.get_config,
+ description="получить текущий конфиг (частично или полностью)",
+ ),
+ HandlerConfig(
+ command="get_config_jobs",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ handler_func=handlers.get_config_jobs,
+ description="получить текущий конфиг джобов (частично или полностью)",
+ ),
+ HandlerConfig(
+ command="reload_config_jobs",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ handler_func=handlers.reload_config_jobs,
+ description="обновить конфиг джобов с Google-диска",
+ ),
+ HandlerConfig(
+ command="set_config",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ handler_func=handlers.set_config,
+ description="установить новое значение в конфиге",
+ ),
+ HandlerConfig(
+ command="add_manager",
+ category=CommandCategories.MOST_USED,
+ access_level="admin",
+ handler_func=handlers.add_manager,
+ description="добавить менеджера в список",
+ ),
+ HandlerConfig(
+ command="change_board",
+ category=CommandCategories.CONFIG,
+ access_level="admin",
+ handler_func=handlers.change_board,
+ description="изменить Trello board_id",
+ ),
+ HandlerConfig(
+ command="send_reminders",
+ category=CommandCategories.BROADCAST,
+ access_level="admin",
+ job_name="send_reminders_job",
+ job_type="admin_reply",
+ description="отослать напоминания вне расписания",
+ ),
+ HandlerConfig(
+ command="send_trello_curator_notification",
+ category=CommandCategories.BROADCAST,
+ access_level="admin",
+ job_name="trello_board_state_notifications_job",
+ job_type="admin_reply",
+ description="разослать кураторам состояние их карточек вне расписания",
+ ),
+ HandlerConfig(
+ command="manage_all_reminders",
+ category=CommandCategories.MOST_USED,
+ access_level="admin",
+ handler_func=handlers.manage_all_reminders,
+ description="настроить все напоминания",
+ ),
+ HandlerConfig(
+ command="get_roles_for_member",
+ category=CommandCategories.DEBUG,
+ access_level="admin",
+ handler_func=handlers.get_roles_for_member,
+ description="показать роли для участника",
+ ),
+ HandlerConfig(
+ command="get_members_for_role",
+ category=CommandCategories.DEBUG,
+ access_level="admin",
+ handler_func=handlers.get_members_for_role,
+ description="показать участников для роли",
+ ),
+ HandlerConfig(
+ command="check_chat_consistency",
+ category=CommandCategories.HR,
+ access_level="admin",
+ job_name="hr_check_chat_consistency_job",
+ job_type="admin_reply",
+ description="консистентность чата редакции",
+ ),
+ HandlerConfig(
+ command="check_chat_consistency_frozen",
+ category=CommandCategories.HR,
+ access_level="admin",
+ job_name="hr_check_chat_consistency_frozen_job",
+ job_type="admin_reply",
+ description="консистентность чата редакции (замороженные участники)",
+ ),
+ HandlerConfig(
+ command="get_members_without_telegram",
+ category=CommandCategories.HR,
+ access_level="admin",
+ job_name="hr_get_members_without_telegram_job",
+ job_type="admin_reply",
+ description="активные участники без указанного телеграма(телефон это 10+ цифр+-(), отсутствие включает #N/A и кириллицу)",
+ ),
+ HandlerConfig(
+ command="check_site_health",
+ category=CommandCategories.DATA_SYNC,
+ access_level="admin",
+ job_name="site_health_check_job",
+ job_type="admin_reply",
+ description="проверка статуса сайта",
+ ),
+ HandlerConfig(
+ command="get_chat_data",
+ category=CommandCategories.DEBUG,
+ access_level="admin",
+ handler_func=handlers.get_chat_data,
+ description="get_chat_data",
+ ),
+ HandlerConfig(
+ command="clean_chat_data",
+ category=CommandCategories.DEBUG,
+ access_level="admin",
+ handler_func=handlers.clean_chat_data,
+ description="clean_chat_data",
+ ),
+ HandlerConfig(
+ command="get_managers",
+ category=CommandCategories.MOST_USED,
+ access_level="admin",
+ handler_func=handlers.get_managers,
+ description="get_managers",
+ ),
+ # Sample and DB commands
+ HandlerConfig(
+ command="sample_handler",
+ access_level="hidden",
+ job_name="sample_job",
+ job_type="admin_reply",
+ ),
+ HandlerConfig(
+ command="db_fetch_authors_sheet",
+ access_level="hidden",
+ job_name="db_fetch_authors_sheet_job",
+ job_type="admin_reply",
+ ),
+ HandlerConfig(
+ command="db_fetch_curators_sheet",
+ access_level="hidden",
+ job_name="db_fetch_curators_sheet_job",
+ job_type="admin_reply",
+ ),
+ HandlerConfig(
+ command="db_fetch_team_sheet",
+ access_level="hidden",
+ job_name="db_fetch_team_sheet_job",
+ job_type="admin_reply",
+ ),
+ HandlerConfig(
+ command="db_fetch_strings_sheet",
+ access_level="hidden",
+ job_name="db_fetch_strings_sheet_job",
+ job_type="admin_reply",
+ ),
+ HandlerConfig(
+ command="db_fetch_all_team_members",
+ category=CommandCategories.MOST_USED,
+ access_level="admin",
+ job_name="db_fetch_all_team_members_job",
+ job_type="admin_reply",
+ description="db_fetch_all_team_members",
+ ),
+ HandlerConfig(
+ command="backfill_telegram_user_ids",
+ category=CommandCategories.DATA_SYNC,
+ access_level="admin",
+ job_name="backfill_telegram_user_ids_job",
+ job_type="admin_reply",
+ description="backfill Telegram user IDs from team member usernames",
+ ),
+ # General purpose
+ HandlerConfig(
+ command="start",
+ category=CommandCategories.DEBUG,
+ access_level="admin",
+ handler_func=handlers.start,
+ description="начать чат с ботом",
+ ),
+ HandlerConfig(
+ command="shrug",
+ category=CommandCategories.DEBUG,
+ access_level="admin",
+ job_name="shrug_job",
+ job_type="admin_reply",
+ description="¯\\_(ツ)_/¯",
+ ),
+ # HELP is handled specially or requires lambda
+]
diff --git a/src/tg/handlers/__init__.py b/src/tg/handlers/__init__.py
index 3ab3803..380b372 100644
--- a/src/tg/handlers/__init__.py
+++ b/src/tg/handlers/__init__.py
@@ -44,6 +44,6 @@
# Plain text message handler
from .user_message_handler import (
handle_callback_query,
- handle_new_members,
handle_user_message,
)
+from .flow_handlers import handle_new_members
diff --git a/src/tg/handlers/flow_handlers.py b/src/tg/handlers/flow_handlers.py
new file mode 100644
index 0000000..b9e16b6
--- /dev/null
+++ b/src/tg/handlers/flow_handlers.py
@@ -0,0 +1,971 @@
+import calendar
+import logging
+import telegram
+from datetime import datetime
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from ... import consts
+from ...app_context import AppContext
+from ...consts import (
+ ButtonValues,
+ GetTasksReportData,
+ PlainTextUserAction,
+ BoardListAlias,
+)
+from ...db.db_client import DBClient
+from ...db.db_objects import Reminder
+from ...focalboard.focalboard_client import FocalboardClient
+from ...strings import load
+from ...tg.handlers import get_tasks_report_handler
+from ...trello.trello_client import TrelloClient
+from .utils import get_sender_id, reply, get_sender_username, get_chat_id, get_chat_name
+
+logger = logging.getLogger(__name__)
+
+SECTIONS = [
+ ("Идеи для статей", BoardListAlias.TOPIC_SUGGESTION_1),
+ ("Готовая тема", BoardListAlias.TOPIC_READY_2),
+ ("Уже пишу", BoardListAlias.DRAFT_N_PROGRESS_3, True),
+ ("Передано на редактуру", BoardListAlias.DRAFT_COMPLETED_4),
+ ("На редактуре", BoardListAlias.PENDING_EDITOR_5),
+ ("Проверка качества SEO", BoardListAlias.PENDING_SEO_EDITOR_6),
+ ("Отредактировано", BoardListAlias.APPROVED_EDITOR_7),
+ ("Отобрано на финальную проверку", BoardListAlias.PENDING_CHIEF_EDITOR_8),
+ ("Отобрано для публикации", BoardListAlias.PUBLISH_BACKLOG_9),
+ ("Готово для вёрстки", BoardListAlias.PUBLISH_IN_PROGRESS_10),
+]
+
+
+def handle_stateless_message(update, tg_context):
+ """
+ Handles messages that don't have an active command state (stateless).
+ Forwards message to n8n and upserts user.
+ """
+ if update.message and update.message.text:
+ try:
+ app_context = AppContext()
+ user_id = get_sender_id(update)
+ username = (
+ get_sender_username(update)
+ if update.message.from_user.username
+ else None
+ )
+
+ # Auto-create/update User record
+ team_member_id = None
+ if username:
+ # Normalize username (remove @ if present)
+ normalized_username = username.lstrip("@")
+ # Find TeamMember with matching telegram username
+ team_members = app_context.db_client.get_all_members()
+ matching_member = next(
+ (
+ m
+ for m in team_members
+ if m.telegram
+ and m.telegram.strip().lstrip("@").lower()
+ == normalized_username.lower()
+ ),
+ None,
+ )
+ if matching_member:
+ team_member_id = matching_member.id
+
+ # Upsert User record
+ app_context.db_client.upsert_user_from_telegram(
+ telegram_user_id=user_id,
+ telegram_username=username,
+ team_member_id=team_member_id,
+ )
+
+ query = update.message.text.strip()
+ app_context.n8n_client.send_webhook(user_id, query)
+ except Exception as e:
+ logger.error(
+ f"Failed to send message to n8n: {e}",
+ exc_info=True,
+ )
+
+
+def handle_new_members(
+ update: telegram.Update, tg_context: telegram.ext.CallbackContext
+):
+ # writes chat_id and chat name to db when anybody (including the bot) is added to a new chat
+ # very heuristic solution
+ DBClient().set_chat_name(get_chat_id(update), get_chat_name(update))
+
+
+class BaseUserMessageHandler(ABC):
+ def __init__(self, update, tg_context, command_data, user_input, button):
+ self.update = update
+ self.tg_context = tg_context
+ self.command_data = command_data
+ self.user_input = user_input
+ self.button = button
+
+ @abstractmethod
+ def handle(self) -> Optional[PlainTextUserAction]:
+ raise NotImplementedError
+
+
+def _generate_rubric_summary(update, rubric_name: str) -> None:
+ try:
+ app_context = AppContext()
+ fc = app_context.focalboard_client
+ labels = fc._get_labels()
+ rubric_label = next(
+ (
+ lbl
+ for lbl in labels
+ if lbl.name.strip().lower() == rubric_name.strip().lower()
+ ),
+ None,
+ )
+ if not rubric_label:
+ logger.warning(
+ f"_generate_rubric_summary: Рубрика не найдена: {rubric_name}"
+ )
+ reply(
+ load(
+ "rubric_not_found",
+ rubric_name=rubric_name,
+ ),
+ update,
+ )
+ return
+
+ # Get all lists
+ try:
+ lists = fc.get_lists(board_id=fc.board_id, sorted=False)
+ except Exception as e:
+ logger.error(
+ f"_generate_rubric_summary: не удалось получить lists: {e}",
+ exc_info=True,
+ )
+ reply(load("failed_get_board_lists"), update)
+ return
+
+ message_parts = [
+ load(
+ "rubric_report_job__intro",
+ rubric=rubric_name,
+ )
+ ]
+
+ had_errors = False
+
+ for column_name, alias, *meta_flag in SECTIONS:
+ need_meta = bool(meta_flag and meta_flag[0])
+ heading = load(alias.value)
+ # Find column
+ target_list = next(
+ (lst for lst in lists if lst.name.strip().startswith(column_name)), None
+ )
+
+ if not target_list:
+ message_parts.append(f"{heading} (0)")
+ message_parts.append("")
+ continue
+
+ try:
+ cards = fc.get_cards(list_ids=[target_list.id], board_id=fc.board_id)
+ except Exception:
+ had_errors = True
+ message_parts.append(f"{heading} (0)")
+ message_parts.append("")
+ continue
+
+ filtered = [
+ card
+ for card in cards
+ if any(lbl.id == rubric_label.id for lbl in card.labels)
+ ]
+
+ filtered.sort(key=lambda c: c.due or datetime.max)
+
+ count = len(filtered)
+ message_parts.append(f"{heading} ({count})")
+
+ if not filtered:
+ message_parts.append("(пусто)")
+ else:
+ for card in filtered:
+ link = f'{card.name}'
+ if need_meta:
+ due_str = (
+ card.due.strftime("%d.%m.%Y") if card.due else "без срока"
+ )
+ try:
+ fields = fc.get_custom_fields(card.id)
+ authors = (
+ ", ".join(fields.authors)
+ if fields.authors
+ else "неизвестно"
+ )
+ except Exception:
+ authors = "неизвестно"
+ message_parts.append(f"- {link}")
+ message_parts.append(f" • Дедлайн: {due_str}")
+ message_parts.append(f" • Автор: {authors}")
+ else:
+ message_parts.append(f"- {link}")
+
+ message_parts.append("")
+
+ if had_errors:
+ message_parts.append(load("partial_data_error "))
+
+ reply("\n".join(message_parts), update, parse_mode="HTML")
+
+ except Exception:
+ reply(load("failed_try_later"), update)
+
+
+def _show_reminder_edit_options(
+ reminder: Reminder, update: telegram.Update, command_data: dict
+):
+ """
+ Shows the menu with options to edit a specific reminder.
+ """
+ # keyboard for edit
+ button_list = [
+ [
+ telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_text_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__TEXT.value,
+ )
+ ],
+ [
+ telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_datetime_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__DATETIME.value,
+ )
+ ],
+ [
+ telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_title_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__TITLE.value,
+ )
+ ],
+ [
+ telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_chat_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__CHAT.value,
+ )
+ ],
+ [
+ (
+ telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_suspend_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__SUSPEND.value,
+ )
+ if reminder.is_active
+ else telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_resume_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__RESUME.value,
+ )
+ )
+ ],
+ [
+ (
+ telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_poll_active_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__DISABLE_POLL.value,
+ )
+ if reminder.send_poll
+ else telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__edit_poll_inactive_btn"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__ENABLE_POLL.value,
+ )
+ )
+ ],
+ ]
+ reply_markup = telegram.InlineKeyboardMarkup(button_list)
+ weekday_str = (
+ calendar.TextCalendar().formatweekday(int(reminder.weekday), 15).strip()
+ )
+ reply(
+ load(
+ "manage_reminders_handler__weekly_reminder",
+ weekday=weekday_str,
+ time=reminder.time,
+ text=reminder.text,
+ ),
+ update,
+ reply_markup=reply_markup,
+ )
+ # The caller is expected to return the next action transition to CHOOSE_EDIT_ACTION
+
+
+def _handle_task_report_helper(command_data, add_labels, update):
+ board_id = command_data[consts.GetTasksReportData.BOARD_ID]
+ list_id = command_data[consts.GetTasksReportData.LIST_ID]
+ introduction = command_data[consts.GetTasksReportData.INTRO_TEXT]
+ use_focalboard = command_data[consts.GetTasksReportData.USE_FOCALBOARD]
+ messages = get_tasks_report_handler.generate_report_messages(
+ board_id, list_id, introduction, add_labels, use_focalboard=use_focalboard
+ )
+ for message in messages:
+ reply(message, update)
+ # finished with last action for /trello_client_get_lists
+ return None
+
+
+class GetRubricsChooseRubricHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ try:
+ idx = int(self.user_input) - 1
+ rubrics = self.command_data.get(
+ GetTasksReportData.LISTS
+ ) or self.tg_context.chat_data.get("available_rubrics", [])
+ if not (0 <= idx < len(rubrics)):
+ raise ValueError
+ except Exception:
+ reply(
+ load(
+ "invalid_rubric_number",
+ max=len(rubrics),
+ ),
+ self.update,
+ )
+ return PlainTextUserAction.GET_RUBRICS__CHOOSE_RUBRIC
+
+ selected = rubrics[idx]
+ _generate_rubric_summary(self.update, selected)
+
+ self.tg_context.chat_data.pop(consts.LAST_ACTIONABLE_COMMAND, None)
+ return None
+
+
+class GetTasksReportEnterBoardUrlHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ trello_client = TrelloClient()
+ try:
+ board = trello_client.get_board_by_url(self.user_input)
+ trello_lists = trello_client.get_lists(board.id)
+ except Exception:
+ reply(load("get_tasks_report_handler__board_not_found"), self.update)
+ return PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_URL
+
+ self.command_data[consts.GetTasksReportData.BOARD_ID] = board.id
+ self.command_data[consts.GetTasksReportData.LISTS] = [
+ lst.to_dict() for lst in trello_lists
+ ]
+
+ trello_lists_formatted = "\n".join(
+ [f"{i + 1}) {lst.name}" for i, lst in enumerate(trello_lists)]
+ )
+ reply(
+ load(
+ "get_tasks_report_handler__choose_trello_list",
+ lists=trello_lists_formatted,
+ ),
+ self.update,
+ )
+ return PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER
+
+
+class GetTasksReportEnterBoardNumberHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ trello_client = TrelloClient()
+ focalboard_client = FocalboardClient()
+ try:
+ board_list = self.tg_context.chat_data[consts.GetTasksReportData.LISTS]
+ use_focalboard = self.tg_context.chat_data[
+ consts.GetTasksReportData.USE_FOCALBOARD
+ ]
+ list_idx = int(self.user_input) - 1
+ assert 0 <= list_idx < len(board_list)
+ board_id = board_list[list_idx]["id"]
+ if use_focalboard:
+ trello_lists = focalboard_client.get_lists(board_id, sorted=True)
+ trello_lists = trello_lists[::-1]
+ else:
+ trello_lists = trello_client.get_lists(board_id)
+ trello_lists = trello_lists[::-1]
+ except Exception as e:
+ logger.warning("Failed to parse board number", exc_info=e)
+ reply(
+ load(
+ "get_tasks_report_handler__enter_the_number",
+ max_val=len(board_list),
+ ),
+ self.update,
+ )
+ return PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER
+
+ self.command_data[consts.GetTasksReportData.BOARD_ID] = board_id
+ self.command_data[consts.GetTasksReportData.USE_FOCALBOARD] = use_focalboard
+ self.command_data[consts.GetTasksReportData.LISTS] = [
+ lst.to_dict() for lst in trello_lists
+ ]
+
+ trello_lists_formatted = "\n".join(
+ [
+ f"{len(trello_lists) - i}) {lst.name}"
+ for i, lst in enumerate(trello_lists)
+ ]
+ )
+ reply(
+ load(
+ "get_tasks_report_handler__choose_trello_list",
+ lists=trello_lists_formatted,
+ ),
+ self.update,
+ )
+ return PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER
+
+
+class GetTasksReportEnterListNumberHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ try:
+ trello_lists = self.command_data.get(consts.GetTasksReportData.LISTS, [])
+ list_idx = -int(self.user_input)
+ assert 0 > list_idx >= -len(trello_lists)
+ list_id = trello_lists[list_idx]["id"]
+ except Exception as e:
+ logger.warning("Failed to parse list number", exc_info=e)
+ reply(
+ load(
+ "get_tasks_report_handler__enter_the_number",
+ max_val=len(trello_lists),
+ ),
+ self.update,
+ )
+ return PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER
+
+ self.command_data[consts.GetTasksReportData.LIST_ID] = list_id
+
+ reply_markup = telegram.InlineKeyboardMarkup(
+ [
+ [
+ telegram.InlineKeyboardButton(
+ load("get_tasks_report_handler__no_text_btn"),
+ callback_data=ButtonValues.GET_TASKS_REPORT__NO_INTRO.value,
+ )
+ ]
+ ]
+ )
+ if not self.tg_context.chat_data.get("advanced"):
+ add_labels = self.button == ButtonValues.GET_TASKS_REPORT__LABELS__NO
+ self.command_data[consts.GetTasksReportData.INTRO_TEXT] = None
+ return _handle_task_report_helper(
+ self.command_data, add_labels, self.update
+ )
+
+ reply(
+ load("get_tasks_report_handler__enter_intro"),
+ self.update,
+ reply_markup=reply_markup,
+ )
+ return PlainTextUserAction.GET_TASKS_REPORT__ENTER_INTRO
+
+
+class GetTasksReportEnterIntroHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ if (
+ self.button is not None
+ and self.button == ButtonValues.GET_TASKS_REPORT__NO_INTRO
+ ):
+ self.command_data[consts.GetTasksReportData.INTRO_TEXT] = None
+ else:
+ self.command_data[consts.GetTasksReportData.INTRO_TEXT] = self.user_input
+
+ button_list = [
+ [
+ telegram.InlineKeyboardButton(
+ load("common__yes"),
+ callback_data=ButtonValues.GET_TASKS_REPORT__LABELS__YES.value,
+ ),
+ telegram.InlineKeyboardButton(
+ load("common__no"),
+ callback_data=ButtonValues.GET_TASKS_REPORT__LABELS__NO.value,
+ ),
+ ]
+ ]
+ reply_markup = telegram.InlineKeyboardMarkup(button_list)
+ reply(
+ load("get_tasks_report_handler__choose_if_fill_labels"),
+ self.update,
+ reply_markup=reply_markup,
+ )
+ return PlainTextUserAction.GET_TASKS_REPORT__CHOOSE_IF_FILL_LABELS
+
+
+class GetTasksReportChooseIfFillLabelsHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ if self.button is None:
+ reply(load("user_message_handler__press_button_please"), self.update)
+ return PlainTextUserAction.GET_TASKS_REPORT__CHOOSE_IF_FILL_LABELS
+
+ add_labels = self.button == ButtonValues.GET_TASKS_REPORT__LABELS__YES
+ return _handle_task_report_helper(self.command_data, add_labels, self.update)
+
+
+class ManageRemindersChooseActionHandler(BaseUserMessageHandler):
+ def _handle_direct_reminder_edit(self):
+ reminder_ids = self.command_data.get(
+ consts.ManageRemindersData.EXISTING_REMINDERS, []
+ )
+ try:
+ assert 0 < int(self.user_input) <= len(reminder_ids)
+ reminder_id, _, _ = reminder_ids[int(self.user_input) - 1]
+ except Exception:
+ reply(load("manage_reminders_handler__reminder_number_bad"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_ACTION
+
+ self.command_data[consts.ManageRemindersData.ACTION_TYPE] = (
+ ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT
+ )
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID] = reminder_id
+ reminder = DBClient().get_reminder_by_id(reminder_id)
+ _show_reminder_edit_options(reminder, self.update, self.command_data)
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_EDIT_ACTION
+
+ def _handle_new_action(self):
+ reply(load("manager_reminders_handler__enter_chat_id"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID
+
+ def _handle_delete_action(self):
+ reply(
+ load("manage_reminders_handler__enter_reminder_number_to_delete"),
+ self.update,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_REMINDER_NUMBER
+
+ def _handle_edit_action(self):
+ reply(
+ load("manage_reminders_handler__enter_reminder_number_to_edit"), self.update
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_REMINDER_NUMBER
+
+ def handle(self) -> Optional[PlainTextUserAction]:
+ # If user sends a number, interpret it as a shortcut to edit that reminder
+ if self.user_input and self.user_input.isdigit():
+ return self._handle_direct_reminder_edit()
+
+ if self.button is None:
+ reply(load("user_message_handler__press_button_please"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_ACTION
+
+ action_map = {
+ ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW: self._handle_new_action,
+ ButtonValues.MANAGE_REMINDERS__ACTIONS__DELETE: self._handle_delete_action,
+ ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT: self._handle_edit_action,
+ }
+
+ handler_method = action_map.get(self.button)
+ if handler_method:
+ self.command_data[consts.ManageRemindersData.ACTION_TYPE] = self.button
+ return handler_method()
+
+ # Fallback if unknown button
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_ACTION
+
+
+class ManageRemindersEnterReminderNumberHandler(BaseUserMessageHandler):
+ def _handle_delete_action(self, chat_title, reminder_name):
+ button_list = [
+ [
+ telegram.InlineKeyboardButton(
+ load("common__yes"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__DELETE__YES.value,
+ ),
+ telegram.InlineKeyboardButton(
+ load("common__no"),
+ callback_data=ButtonValues.MANAGE_REMINDERS__DELETE__NO.value,
+ ),
+ ]
+ ]
+ reply_markup = telegram.InlineKeyboardMarkup(button_list)
+ reply(
+ load(
+ "manage_reminders_handler__confirm_delete",
+ chat=chat_title,
+ reminder=reminder_name,
+ ),
+ self.update,
+ reply_markup=reply_markup,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__DELETE_REQUEST
+
+ def _handle_edit_action(self, reminder_id):
+ reminder = DBClient().get_reminder_by_id(reminder_id)
+ _show_reminder_edit_options(reminder, self.update, self.command_data)
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_EDIT_ACTION
+
+ def handle(self) -> Optional[PlainTextUserAction]:
+ reminder_ids = self.command_data[consts.ManageRemindersData.EXISTING_REMINDERS]
+ try:
+ assert 0 < int(self.user_input) <= len(reminder_ids)
+ reminder_id, chat_title, reminder_name = reminder_ids[
+ int(self.user_input) - 1
+ ]
+ except Exception:
+ reply(load("manage_reminders_handler__reminder_number_bad"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_REMINDER_NUMBER
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID] = reminder_id
+
+ action = self.command_data[consts.ManageRemindersData.ACTION_TYPE]
+ if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__DELETE:
+ return self._handle_delete_action(chat_title, reminder_name)
+ elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
+ return self._handle_edit_action(reminder_id)
+ else:
+ logger.error(f'Bad reminder action "{action}"')
+ return None
+
+
+class ManageRemindersChooseEditActionHandler(BaseUserMessageHandler):
+ def _handle_edit_text(self):
+ reply(load("manage_reminders_handler__enter_text"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_TEXT
+
+ def _handle_edit_title(self, reminder):
+ reply(
+ load("manage_reminders_handler__enter_new_name", old_name=reminder.name),
+ self.update,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_NAME
+
+ def _handle_edit_chat(self, reminder):
+ reply(
+ load("manage_reminders_handler__enter_chat_id", name=reminder.name),
+ self.update,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID
+
+ def _handle_edit_datetime(self):
+ reply_markup = telegram.InlineKeyboardMarkup(consts.WEEKDAY_BUTTONS)
+ reply(
+ load("manage_reminders_handler__choose_weekday"),
+ self.update,
+ reply_markup=reply_markup,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_WEEKDAY
+
+ def _handle_suspend(self, reminder_id, reminder):
+ DBClient().update_reminder(reminder_id, is_active=False)
+ reply(
+ load(
+ "manage_reminders_handler__reminder_was_suspended",
+ name=reminder.name,
+ ),
+ self.update,
+ )
+ return None
+
+ def _handle_resume(self, reminder_id, reminder):
+ DBClient().update_reminder(reminder_id, is_active=True)
+ reply(
+ load(
+ "manage_reminders_handler__reminder_was_resumed",
+ next_reminder_datetime=reminder.next_reminder_datetime,
+ ),
+ self.update,
+ )
+ return None
+
+ def _handle_disable_poll_request(self):
+ button_no = telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__disable_poll_btn"),
+ callback_data=consts.ButtonValues.MANAGE_REMINDERS__DISABLE_POLL__YES.value,
+ )
+ keyboard = [[button_no]]
+ reply(
+ load("manage_reminders_handler__disable_poll_question"),
+ self.update,
+ reply_markup=telegram.InlineKeyboardMarkup(keyboard),
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__DISABLE_POLL
+
+ def _handle_enable_poll_request(self):
+ button_yes = telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__enable_poll_btn"),
+ callback_data=consts.ButtonValues.MANAGE_REMINDERS__ENABLE_POLL__YES.value,
+ )
+ keyboard = [[button_yes]]
+ reply(
+ load("manage_reminders_handler__enable_poll_question"),
+ self.update,
+ reply_markup=telegram.InlineKeyboardMarkup(keyboard),
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__ENABLE_POLL
+
+ def handle(self) -> Optional[PlainTextUserAction]:
+ if self.button is None:
+ reply(load("manage_reminders_handler__press_button_please"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_EDIT_ACTION
+
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ reminder = DBClient().get_reminder_by_id(reminder_id)
+
+ action_map = {
+ ButtonValues.MANAGE_REMINDERS__EDIT__TEXT: self._handle_edit_text,
+ ButtonValues.MANAGE_REMINDERS__EDIT__TITLE: lambda: self._handle_edit_title(
+ reminder
+ ),
+ ButtonValues.MANAGE_REMINDERS__EDIT__CHAT: lambda: self._handle_edit_chat(
+ reminder
+ ),
+ ButtonValues.MANAGE_REMINDERS__EDIT__DATETIME: self._handle_edit_datetime,
+ ButtonValues.MANAGE_REMINDERS__EDIT__SUSPEND: lambda: self._handle_suspend(
+ reminder_id, reminder
+ ),
+ ButtonValues.MANAGE_REMINDERS__EDIT__RESUME: lambda: self._handle_resume(
+ reminder_id, reminder
+ ),
+ ButtonValues.MANAGE_REMINDERS__DISABLE_POLL: self._handle_disable_poll_request,
+ ButtonValues.MANAGE_REMINDERS__ENABLE_POLL: self._handle_enable_poll_request,
+ }
+
+ handler = action_map.get(self.button)
+ if handler:
+ return handler()
+
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_EDIT_ACTION
+
+
+class ManageRemindersEnablePollHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ if self.button == consts.ButtonValues.MANAGE_REMINDERS__ENABLE_POLL__YES:
+ db_client = DBClient()
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ db_client.update_reminder(reminder_id, send_poll=True)
+ reply(
+ load("manage_reminders_handler__poll_was_enabled"),
+ self.update,
+ )
+ return None
+ return PlainTextUserAction.MANAGE_REMINDERS__ENABLE_POLL
+
+
+class ManageRemindersDisablePollHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ if self.button == consts.ButtonValues.MANAGE_REMINDERS__DISABLE_POLL__YES:
+ db_client = DBClient()
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ db_client.update_reminder(reminder_id, send_poll=False)
+ reply(
+ load("manage_reminders_handler__poll_was_disabled"),
+ self.update,
+ )
+ return None
+ return PlainTextUserAction.MANAGE_REMINDERS__DISABLE_POLL
+
+
+class ManageRemindersDeleteRequestHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ if self.button is None:
+ reply(load("user_message_handler__press_button_please"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__DELETE_REQUEST
+
+ if self.button == ButtonValues.MANAGE_REMINDERS__DELETE__YES:
+ DBClient().delete_reminder(
+ self.command_data.get(consts.ManageRemindersData.CHOSEN_REMINDER_ID)
+ )
+ reply(load("manage_reminders_handler__reminder_was_deleted"), self.update)
+ return None
+ elif self.button == ButtonValues.MANAGE_REMINDERS__DELETE__NO:
+ reply(
+ load("manage_reminders_handler__reminder_was_not_deleted"), self.update
+ )
+ return None
+ return PlainTextUserAction.MANAGE_REMINDERS__DELETE_REQUEST
+
+
+class ManageRemindersEnterChatIdHandler(BaseUserMessageHandler):
+ def _handle_new_action(self, chat_title):
+ reply(
+ load("manage_reminders_handler__enter_name", chat_title=chat_title),
+ self.update,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_NAME
+
+ def _handle_edit_action(self, chat_title, chat_id):
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ reminder = DBClient().get_reminder_by_id(reminder_id)
+ DBClient().update_reminder(reminder_id, group_chat_id=chat_id)
+ reply(
+ load(
+ "manage_reminders_handler__reminder_set",
+ name=reminder.name,
+ chat_title=chat_title,
+ ),
+ self.update,
+ )
+ return None
+
+ def handle(self) -> Optional[PlainTextUserAction]:
+ try:
+ chat_id = int(self.user_input)
+ chat_title = DBClient().get_chat_name(chat_id)
+ except Exception as e:
+ reply(load("manage_reminders_handler__bad_chat_id"), self.update)
+ logger.info("Failed to parse chat id", exc_info=e)
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID
+
+ self.command_data[consts.ManageRemindersData.GROUP_CHAT_ID] = chat_id
+ action = self.command_data[consts.ManageRemindersData.ACTION_TYPE]
+
+ if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW:
+ return self._handle_new_action(chat_title)
+ elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
+ return self._handle_edit_action(chat_title, chat_id)
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID
+
+
+class ManageRemindersEnterNameHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ self.command_data[consts.ManageRemindersData.REMINDER_NAME] = self.user_input
+ action = self.command_data[consts.ManageRemindersData.ACTION_TYPE]
+
+ if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW:
+ reply(load("manage_reminders_handler__enter_text"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_TEXT
+ elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ DBClient().update_reminder(reminder_id, name=self.user_input)
+ reply(
+ load("manage_reminders_handler__reminder_name_was_changed"), self.update
+ )
+ return None
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_NAME
+
+
+class ManageRemindersEnterTextHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ # keeps original formatting, e.g. hyperlinks
+ text = self.update.message.text_html.strip()
+ self.command_data[consts.ManageRemindersData.REMINDER_TEXT] = text
+ action = self.command_data[consts.ManageRemindersData.ACTION_TYPE]
+
+ if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW:
+ reply_markup = telegram.InlineKeyboardMarkup(consts.WEEKDAY_BUTTONS)
+ reply(
+ load("manage_reminders_handler__choose_weekday"),
+ self.update,
+ reply_markup=reply_markup,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_WEEKDAY
+ elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ DBClient().update_reminder(reminder_id, text=text)
+ reply(
+ load("manage_reminders_handler__reminder_text_was_changed"), self.update
+ )
+ return None
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_TEXT
+
+
+class ManageRemindersChooseWeekdayHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ if self.button is None:
+ reply(load("user_message_handler__press_button_please"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_WEEKDAY
+
+ weekday_num, weekday_name = self.button.value.split(":")
+ self.command_data[consts.ManageRemindersData.WEEKDAY_NUM] = int(weekday_num)
+ self.command_data[consts.ManageRemindersData.WEEKDAY_NAME] = weekday_name
+ reply(
+ load("manage_reminders_handler__enter_time", weekday_name=weekday_name),
+ self.update,
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_TIME
+
+
+class ManageRemindersEnterTimeHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ try:
+ datetime.strptime(self.user_input or "", "%H:%M")
+ except ValueError:
+ reply(load("manage_reminders_handler__time_bad"), self.update)
+ return PlainTextUserAction.MANAGE_REMINDERS__ENTER_TIME
+ self.command_data[consts.ManageRemindersData.TIME] = self.user_input
+ action = self.command_data[consts.ManageRemindersData.ACTION_TYPE]
+ weekday_num = self.command_data[consts.ManageRemindersData.WEEKDAY_NUM]
+ time = self.command_data[consts.ManageRemindersData.TIME]
+
+ if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ DBClient().update_reminder(reminder_id, weekday=weekday_num, time=time)
+ return None
+
+ button_yes = telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__poll_yes_btn"),
+ callback_data=consts.ButtonValues.MANAGE_REMINDERS__POLL__YES.value,
+ )
+ button_no = telegram.InlineKeyboardButton(
+ load("manage_reminders_handler__poll_no_btn"),
+ callback_data=consts.ButtonValues.MANAGE_REMINDERS__POLL__NO.value,
+ )
+ buttons = [button_yes, button_no]
+ reply(
+ load("manage_reminders_handler__poll_question"),
+ self.update,
+ reply_markup=telegram.InlineKeyboardMarkup([buttons]),
+ )
+ return PlainTextUserAction.MANAGE_REMINDERS__SUCCESS
+
+
+class ManageRemindersSuccessHandler(BaseUserMessageHandler):
+ def handle(self) -> Optional[PlainTextUserAction]:
+ text = self.command_data.get(consts.ManageRemindersData.REMINDER_TEXT)
+ group_chat_id = self.command_data.get(consts.ManageRemindersData.GROUP_CHAT_ID)
+ name = self.command_data.get(consts.ManageRemindersData.REMINDER_NAME)
+ weekday_num = self.command_data[consts.ManageRemindersData.WEEKDAY_NUM]
+ weekday_name = self.command_data[consts.ManageRemindersData.WEEKDAY_NAME]
+ time = self.command_data[consts.ManageRemindersData.TIME]
+
+ if self.button == consts.ButtonValues.MANAGE_REMINDERS__TOGGLE_POLL__YES:
+ if text is None:
+ reminder_id = int(
+ self.command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
+ )
+ DBClient().update_reminder(reminder_id, weekday=weekday_num, time=time)
+ else:
+ DBClient().add_reminder(
+ creator_chat_id=get_sender_id(self.update),
+ group_chat_id=group_chat_id,
+ name=name,
+ text=text,
+ weekday_num=weekday_num,
+ time=time,
+ send_poll=True,
+ )
+ else:
+ DBClient().add_reminder(
+ creator_chat_id=get_sender_id(self.update),
+ group_chat_id=group_chat_id,
+ name=name,
+ text=text,
+ weekday_num=weekday_num,
+ time=time,
+ send_poll=False,
+ )
+ weekday_name = self.command_data[consts.ManageRemindersData.WEEKDAY_NAME]
+ time = self.command_data[consts.ManageRemindersData.TIME]
+ reply(
+ load(
+ "manage_reminders_handler__success_time",
+ weekday_name=weekday_name,
+ time=time,
+ ),
+ self.update,
+ )
+ return None
diff --git a/src/tg/handlers/user_message_handler.py b/src/tg/handlers/user_message_handler.py
index 374e899..d4dd006 100644
--- a/src/tg/handlers/user_message_handler.py
+++ b/src/tg/handlers/user_message_handler.py
@@ -1,149 +1,15 @@
-import calendar
import logging
import telegram
-from datetime import datetime
from ... import consts
-from ...app_context import AppContext
from ...consts import (
ButtonValues,
- GetTasksReportData,
PlainTextUserAction,
- BoardListAlias,
)
-from ...db.db_client import DBClient
-from ...db.db_objects import Reminder
-from ...focalboard.focalboard_client import FocalboardClient
-from ...strings import load
-from ...tg.handlers import get_tasks_report_handler
-from ...trello.trello_client import TrelloClient
-from .utils import get_chat_id, get_chat_name, get_sender_id, get_sender_username, reply
-logger = logging.getLogger(__name__)
-
-SECTIONS = [
- ("Идеи для статей", BoardListAlias.TOPIC_SUGGESTION_1),
- ("Готовая тема", BoardListAlias.TOPIC_READY_2),
- ("Уже пишу", BoardListAlias.DRAFT_N_PROGRESS_3, True),
- ("Передано на редактуру", BoardListAlias.DRAFT_COMPLETED_4),
- ("На редактуре", BoardListAlias.PENDING_EDITOR_5),
- ("Проверка качества SEO", BoardListAlias.PENDING_SEO_EDITOR_6),
- ("Отредактировано", BoardListAlias.APPROVED_EDITOR_7),
- ("Отобрано на финальную проверку", BoardListAlias.PENDING_CHIEF_EDITOR_8),
- ("Отобрано для публикации", BoardListAlias.PUBLISH_BACKLOG_9),
- ("Готово для вёрстки", BoardListAlias.PUBLISH_IN_PROGRESS_10),
-]
-
-
-def _generate_rubric_summary(update, rubric_name: str) -> None:
- try:
- app_context = AppContext()
- fc = app_context.focalboard_client
- labels = fc._get_labels()
- rubric_label = next(
- (
- lbl
- for lbl in labels
- if lbl.name.strip().lower() == rubric_name.strip().lower()
- ),
- None,
- )
- if not rubric_label:
- logger.warning(
- f"_generate_rubric_summary: Рубрика не найдена: {rubric_name}"
- )
- reply(
- load(
- "rubric_not_found",
- rubric_name=rubric_name,
- ),
- )
- return
-
- # Get all lists
- try:
- lists = fc.get_lists(board_id=fc.board_id, sorted=False)
- except Exception as e:
- logger.error(
- f"_generate_rubric_summary: не удалось получить lists: {e}",
- exc_info=True,
- )
- reply(load("failed_get_board_lists"), update)
- return
-
- message_parts = [
- load(
- "rubric_report_job__intro",
- rubric=rubric_name,
- )
- ]
-
- had_errors = False
-
- for column_name, alias, *meta_flag in SECTIONS:
- need_meta = bool(meta_flag and meta_flag[0])
- heading = load(alias.value)
- # Find column
- target_list = next(
- (lst for lst in lists if lst.name.strip().startswith(column_name)), None
- )
-
- if not target_list:
- message_parts.append(f"{heading} (0)")
- message_parts.append("")
- continue
- try:
- cards = fc.get_cards(list_ids=[target_list.id], board_id=fc.board_id)
- except Exception:
- had_errors = True
- message_parts.append(f"{heading} (0)")
- message_parts.append("")
- continue
+from . import flow_handlers
- filtered = [
- card
- for card in cards
- if any(lbl.id == rubric_label.id for lbl in card.labels)
- ]
-
- filtered.sort(key=lambda c: c.due or datetime.max)
-
- count = len(filtered)
- message_parts.append(f"{heading} ({count})")
-
- if not filtered:
- message_parts.append("(пусто)")
- else:
- for card in filtered:
- link = f'{card.name}'
- if need_meta:
- due_str = (
- card.due.strftime("%d.%m.%Y") if card.due else "без срока"
- )
- try:
- fields = fc.get_custom_fields(card.id)
- authors = (
- ", ".join(fields.authors)
- if fields.authors
- else "неизвестно"
- )
- except Exception:
- authors = "неизвестно"
- message_parts.append(f"- {link}")
- message_parts.append(f" • Дедлайн: {due_str}")
- message_parts.append(f" • Автор: {authors}")
- else:
- message_parts.append(f"- {link}")
-
- message_parts.append("")
-
- if had_errors:
- message_parts.append(load("partial_data_error "))
-
- reply("\n".join(message_parts), update, parse_mode="HTML")
-
- except Exception:
- reply(load("failed_try_later"), update)
+logger = logging.getLogger(__name__)
def handle_callback_query(
@@ -156,83 +22,30 @@ def handle_callback_query(
handle_user_message(update, tg_context, ButtonValues(update.callback_query.data))
-# helper to avoid code duplication
-def _show_reminder_edit_options(
- reminder: Reminder, update: telegram.Update, command_data: dict
-):
- """
- Shows the menu with options to edit a specific reminder.
- """
- # keyboard for edit
- button_list = [
- [
- telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_text_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__TEXT.value,
- )
- ],
- [
- telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_datetime_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__DATETIME.value,
- )
- ],
- [
- telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_title_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__TITLE.value,
- )
- ],
- [
- telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_chat_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__CHAT.value,
- )
- ],
- [
- (
- telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_suspend_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__SUSPEND.value,
- )
- if reminder.is_active
- else telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_resume_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__EDIT__RESUME.value,
- )
- )
- ],
- [
- (
- telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_poll_active_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__DISABLE_POLL.value,
- )
- if reminder.send_poll
- else telegram.InlineKeyboardButton(
- load("manage_reminders_handler__edit_poll_inactive_btn"),
- callback_data=ButtonValues.MANAGE_REMINDERS__ENABLE_POLL.value,
- )
- )
- ],
- ]
- reply_markup = telegram.InlineKeyboardMarkup(button_list)
- weekday_str = (
- calendar.TextCalendar().formatweekday(int(reminder.weekday), 15).strip()
- )
- reply(
- load(
- "manage_reminders_handler__weekly_reminder",
- weekday=weekday_str,
- time=reminder.time,
- text=reminder.text,
- ),
- update,
- reply_markup=reply_markup,
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_EDIT_ACTION
- )
+ACTION_HANDLERS = {
+ PlainTextUserAction.GET_RUBRICS__CHOOSE_RUBRIC: flow_handlers.GetRubricsChooseRubricHandler,
+ PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_URL: flow_handlers.GetTasksReportEnterBoardUrlHandler,
+ PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER: flow_handlers.GetTasksReportEnterBoardNumberHandler,
+ PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER: flow_handlers.GetTasksReportEnterListNumberHandler,
+ PlainTextUserAction.GET_TASKS_REPORT__ENTER_INTRO: flow_handlers.GetTasksReportEnterIntroHandler,
+ PlainTextUserAction.GET_TASKS_REPORT__CHOOSE_IF_FILL_LABELS: flow_handlers.GetTasksReportChooseIfFillLabelsHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_ACTION: flow_handlers.ManageRemindersChooseActionHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__ENTER_REMINDER_NUMBER: flow_handlers.ManageRemindersEnterReminderNumberHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_EDIT_ACTION: flow_handlers.ManageRemindersChooseEditActionHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__ENABLE_POLL: flow_handlers.ManageRemindersEnablePollHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__DISABLE_POLL: flow_handlers.ManageRemindersDisablePollHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__DELETE_REQUEST: flow_handlers.ManageRemindersDeleteRequestHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID: flow_handlers.ManageRemindersEnterChatIdHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__ENTER_NAME: flow_handlers.ManageRemindersEnterNameHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__ENTER_TEXT: flow_handlers.ManageRemindersEnterTextHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_WEEKDAY: flow_handlers.ManageRemindersChooseWeekdayHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__ENTER_TIME: flow_handlers.ManageRemindersEnterTimeHandler,
+ PlainTextUserAction.MANAGE_REMINDERS__SUCCESS: flow_handlers.ManageRemindersSuccessHandler,
+}
+
+
+def set_next_action(command_data: dict, next_action: PlainTextUserAction):
+ command_data[consts.NEXT_ACTION] = next_action.value if next_action else next_action
def handle_user_message(
@@ -247,50 +60,7 @@ def handle_user_message(
# to understand what kind of data currently expected from user
if not command_id:
# No active command - forward to n8n if message exists
- if update.message and update.message.text:
- try:
- app_context = AppContext()
- user_id = get_sender_id(update)
- username = (
- get_sender_username(update)
- if update.message.from_user.username
- else None
- )
-
- # Auto-create/update User record
- team_member_id = None
- if username:
- # Normalize username (remove @ if present)
- normalized_username = username.lstrip("@")
- # Find TeamMember with matching telegram username
- team_members = app_context.db_client.get_all_members()
- matching_member = next(
- (
- m
- for m in team_members
- if m.telegram
- and m.telegram.strip().lstrip("@").lower()
- == normalized_username.lower()
- ),
- None,
- )
- if matching_member:
- team_member_id = matching_member.id
-
- # Upsert User record
- app_context.db_client.upsert_user_from_telegram(
- telegram_user_id=user_id,
- telegram_username=username,
- team_member_id=team_member_id,
- )
-
- query = update.message.text.strip()
- app_context.n8n_client.send_webhook(user_id, query)
- except Exception as e:
- logger.error(
- f"Failed to send message to n8n: {e}",
- exc_info=True,
- )
+ flow_handlers.handle_stateless_message(update, tg_context)
return
command_data = tg_context.chat_data.get(command_id, {})
tg_context.chat_data[command_id] = command_data
@@ -298,616 +68,14 @@ def handle_user_message(
if not next_action:
# last action for a command was successfully executed and nothing left to do
return
- next_action = PlainTextUserAction(next_action)
+ next_action_enum = PlainTextUserAction(next_action)
user_input = update.message.text.strip() if update.message is not None else None
- if next_action == PlainTextUserAction.GET_RUBRICS__CHOOSE_RUBRIC:
- try:
- idx = int(user_input) - 1
- rubrics = command_data.get(
- GetTasksReportData.LISTS
- ) or tg_context.chat_data.get("available_rubrics", [])
- if not (0 <= idx < len(rubrics)):
- raise ValueError
- except Exception:
- reply(
- load(
- "invalid_rubric_number",
- max=len(rubrics),
- ),
- update,
- )
- return
-
- selected = rubrics[idx]
- _generate_rubric_summary(update, selected)
-
- tg_context.chat_data.pop(consts.LAST_ACTIONABLE_COMMAND, None)
- tg_context.chat_data.pop(command_id, None)
+ if next_action_enum in ACTION_HANDLERS:
+ handler_cls = ACTION_HANDLERS[next_action_enum]
+ handler = handler_cls(update, tg_context, command_data, user_input, button)
+ next_state = handler.handle()
+ set_next_action(command_data, next_state)
return
- # Below comes a long switch of possible next_action.
- # Following conventions are used:
- # - If you got an exception or `user_input` is invalid, call `reply('...', update)` explaining
- # what's wrong and what user can do. We'll ask user for the same data until we get it.
- # - If data from user is good, after processing it call `set_next_action(command_data, ...)`
- # You probably also want to call `reply` to guide user to the next piece of data.
- # - If it's the last piece of info expected from user, you should call
- # `set_next_action(command_data, None)` so that we won't talk to user anymore,
- # until they start a new command.
- if next_action == PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_URL:
- trello_client = TrelloClient()
- try:
- board = trello_client.get_board_by_url(user_input)
- trello_lists = trello_client.get_lists(board.id)
- except Exception:
- reply(load("get_tasks_report_handler__board_not_found"), update)
- return
- command_data[consts.GetTasksReportData.BOARD_ID] = board.id
- command_data[consts.GetTasksReportData.LISTS] = [
- lst.to_dict() for lst in trello_lists
- ]
-
- trello_lists_formatted = "\n".join(
- [f"{i + 1}) {lst.name}" for i, lst in enumerate(trello_lists)]
- )
- reply(
- load(
- "get_tasks_report_handler__choose_trello_list",
- lists=trello_lists_formatted,
- ),
- update,
- )
- set_next_action(
- command_data, PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER
- )
- return
- elif next_action == PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER:
- trello_client = TrelloClient()
- focalboard_client = FocalboardClient()
- try:
- board_list = tg_context.chat_data[consts.GetTasksReportData.LISTS]
- use_focalboard = tg_context.chat_data[
- consts.GetTasksReportData.USE_FOCALBOARD
- ]
- list_idx = int(user_input) - 1
- assert 0 <= list_idx < len(board_list)
- board_id = board_list[list_idx]["id"]
- if use_focalboard:
- trello_lists = focalboard_client.get_lists(board_id, sorted=True)
- trello_lists = trello_lists[::-1]
- else:
- trello_lists = trello_client.get_lists(board_id)
- trello_lists = trello_lists[::-1]
- except Exception as e:
- logger.warning("Failed to parse board number", exc_info=e)
- reply(
- load(
- "get_tasks_report_handler__enter_the_number",
- max_val=len(board_list),
- ),
- update,
- )
- return
-
- command_data[consts.GetTasksReportData.BOARD_ID] = board_id
- command_data[consts.GetTasksReportData.USE_FOCALBOARD] = use_focalboard
- command_data[consts.GetTasksReportData.LISTS] = [
- lst.to_dict() for lst in trello_lists
- ]
-
- trello_lists_formatted = "\n".join(
- [
- f"{len(trello_lists) - i}) {lst.name}"
- for i, lst in enumerate(trello_lists)
- ]
- )
- reply(
- load(
- "get_tasks_report_handler__choose_trello_list",
- lists=trello_lists_formatted,
- ),
- update,
- )
- set_next_action(
- command_data, PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER
- )
- return
- elif next_action == PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER:
- try:
- trello_lists = command_data.get(consts.GetTasksReportData.LISTS, [])
- list_idx = -int(user_input)
- assert 0 > list_idx >= -len(trello_lists)
- list_id = trello_lists[list_idx]["id"]
- except Exception as e:
- logger.warning("Failed to parse list number", exc_info=e)
- reply(
- load(
- "get_tasks_report_handler__enter_the_number",
- max_val=len(trello_lists),
- ),
- update,
- )
- return
- command_data[consts.GetTasksReportData.LIST_ID] = list_id
-
- reply_markup = telegram.InlineKeyboardMarkup(
- [
- [
- telegram.InlineKeyboardButton(
- load("get_tasks_report_handler__no_text_btn"),
- callback_data=ButtonValues.GET_TASKS_REPORT__NO_INTRO.value,
- )
- ]
- ]
- )
- if not tg_context.chat_data.get("advanced"):
- add_labels = button == ButtonValues.GET_TASKS_REPORT__LABELS__NO
- command_data[consts.GetTasksReportData.INTRO_TEXT] = None
- handle_task_report(command_data, add_labels, update)
- return
-
- reply(
- load("get_tasks_report_handler__enter_intro"),
- update,
- reply_markup=reply_markup,
- )
- set_next_action(command_data, PlainTextUserAction.GET_TASKS_REPORT__ENTER_INTRO)
- return
- elif next_action == PlainTextUserAction.GET_TASKS_REPORT__ENTER_INTRO:
- if button is not None and button == ButtonValues.GET_TASKS_REPORT__NO_INTRO:
- command_data[consts.GetTasksReportData.INTRO_TEXT] = None
- else:
- command_data[consts.GetTasksReportData.INTRO_TEXT] = user_input
-
- button_list = [
- [
- telegram.InlineKeyboardButton(
- load("common__yes"),
- callback_data=ButtonValues.GET_TASKS_REPORT__LABELS__YES.value,
- ),
- telegram.InlineKeyboardButton(
- load("common__no"),
- callback_data=ButtonValues.GET_TASKS_REPORT__LABELS__NO.value,
- ),
- ]
- ]
- reply_markup = telegram.InlineKeyboardMarkup(button_list)
- reply(
- load("get_tasks_report_handler__choose_if_fill_labels"),
- update,
- reply_markup=reply_markup,
- )
- set_next_action(
- command_data, PlainTextUserAction.GET_TASKS_REPORT__CHOOSE_IF_FILL_LABELS
- )
- return
- elif next_action == PlainTextUserAction.GET_TASKS_REPORT__CHOOSE_IF_FILL_LABELS:
- if button is None:
- reply(load("user_message_handler__press_button_please"), update)
- return
- add_labels = button == ButtonValues.GET_TASKS_REPORT__LABELS__YES
- handle_task_report(command_data, add_labels, update)
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_ACTION:
- # If user sends a number, interpret it as a shortcut to edit that reminder
- if user_input and user_input.isdigit():
- reminder_ids = command_data.get(
- consts.ManageRemindersData.EXISTING_REMINDERS, []
- )
- try:
- assert 0 < int(user_input) <= len(reminder_ids)
- reminder_id, _, _ = reminder_ids[int(user_input) - 1]
- except Exception:
- reply(load("manage_reminders_handler__reminder_number_bad"), update)
- return
-
- command_data[consts.ManageRemindersData.ACTION_TYPE] = (
- ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT
- )
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID] = reminder_id
- reminder = DBClient().get_reminder_by_id(reminder_id)
- _show_reminder_edit_options(reminder, update, command_data)
- return
-
- if button is None:
- reply(load("user_message_handler__press_button_please"), update)
- return
-
- if button == ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW:
- reply(load("manager_reminders_handler__enter_chat_id"), update)
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID
- )
- elif button == ButtonValues.MANAGE_REMINDERS__ACTIONS__DELETE:
- reply(
- load("manage_reminders_handler__enter_reminder_number_to_delete"),
- update,
- )
- set_next_action(
- command_data,
- PlainTextUserAction.MANAGE_REMINDERS__ENTER_REMINDER_NUMBER,
- )
- elif button == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
- reply(
- load("manage_reminders_handler__enter_reminder_number_to_edit"), update
- )
- set_next_action(
- command_data,
- PlainTextUserAction.MANAGE_REMINDERS__ENTER_REMINDER_NUMBER,
- )
- command_data[consts.ManageRemindersData.ACTION_TYPE] = button
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__ENTER_REMINDER_NUMBER:
- reminder_ids = command_data[consts.ManageRemindersData.EXISTING_REMINDERS]
- try:
- assert 0 < int(user_input) <= len(reminder_ids)
- reminder_id, chat_title, reminder_name = reminder_ids[int(user_input) - 1]
- except Exception:
- reply(load("manage_reminders_handler__reminder_number_bad"), update)
- return
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID] = reminder_id
-
- action = command_data[consts.ManageRemindersData.ACTION_TYPE]
- if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__DELETE:
- # keyboard for delete
- button_list = [
- [
- telegram.InlineKeyboardButton(
- load("common__yes"),
- callback_data=ButtonValues.MANAGE_REMINDERS__DELETE__YES.value,
- ),
- telegram.InlineKeyboardButton(
- load("common__no"),
- callback_data=ButtonValues.MANAGE_REMINDERS__DELETE__NO.value,
- ),
- ]
- ]
- reply_markup = telegram.InlineKeyboardMarkup(button_list)
- reply(
- load(
- "manage_reminders_handler__confirm_delete",
- chat=chat_title,
- reminder=reminder_name,
- ),
- update,
- reply_markup=reply_markup,
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__DELETE_REQUEST
- )
- elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
- reminder = DBClient().get_reminder_by_id(reminder_id)
- _show_reminder_edit_options(reminder, update, command_data)
- else:
- logger.error(f'Bad reminder action "{action}"')
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_EDIT_ACTION:
- if button is None:
- reply(load("manage_reminders_handler__press_button_please"), update)
- return
- db_client = DBClient()
- reminder_id = int(command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID])
- reminder = db_client.get_reminder_by_id(reminder_id)
- if button == ButtonValues.MANAGE_REMINDERS__EDIT__TEXT:
- reply(load("manage_reminders_handler__enter_text"), update)
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__ENTER_TEXT
- )
- return
- elif button == ButtonValues.MANAGE_REMINDERS__EDIT__TITLE:
- reply(
- load(
- "manage_reminders_handler__enter_new_name", old_name=reminder.name
- ),
- update,
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__ENTER_NAME
- )
- return
- elif button == ButtonValues.MANAGE_REMINDERS__EDIT__CHAT:
- reply(
- load("manage_reminders_handler__enter_chat_id", name=reminder.name),
- update,
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID
- )
- return
- elif button == ButtonValues.MANAGE_REMINDERS__EDIT__DATETIME:
- reply_markup = telegram.InlineKeyboardMarkup(consts.WEEKDAY_BUTTONS)
- reply(
- load("manage_reminders_handler__choose_weekday"),
- update,
- reply_markup=reply_markup,
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_WEEKDAY
- )
- return
- elif button == ButtonValues.MANAGE_REMINDERS__EDIT__SUSPEND:
- db_client.update_reminder(reminder_id, is_active=False)
- reply(
- load(
- "manage_reminders_handler__reminder_was_suspended",
- name=reminder.name,
- ),
- update,
- )
- set_next_action(command_data, None)
- return
- elif button == ButtonValues.MANAGE_REMINDERS__EDIT__RESUME:
- db_client.update_reminder(reminder_id, is_active=True)
- reply(
- load(
- "manage_reminders_handler__reminder_was_resumed",
- next_reminder_datetime=reminder.next_reminder_datetime,
- ),
- update,
- )
- set_next_action(command_data, None)
- elif button == ButtonValues.MANAGE_REMINDERS__DISABLE_POLL:
- button_no = telegram.InlineKeyboardButton(
- load("manage_reminders_handler__disable_poll_btn"),
- callback_data=consts.ButtonValues.MANAGE_REMINDERS__DISABLE_POLL__YES.value,
- )
- keyboard = [[button_no]]
- reply(
- load("manage_reminders_handler__disable_poll_question"),
- update,
- reply_markup=telegram.InlineKeyboardMarkup(keyboard),
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__DISABLE_POLL
- )
- return
- elif button == ButtonValues.MANAGE_REMINDERS__ENABLE_POLL:
- button_yes = telegram.InlineKeyboardButton(
- load("manage_reminders_handler__enable_poll_btn"),
- callback_data=consts.ButtonValues.MANAGE_REMINDERS__ENABLE_POLL__YES.value,
- )
- keyboard = [[button_yes]]
- reply(
- load("manage_reminders_handler__enable_poll_question"),
- update,
- reply_markup=telegram.InlineKeyboardMarkup(keyboard),
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__ENABLE_POLL
- )
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__ENABLE_POLL:
- if button == consts.ButtonValues.MANAGE_REMINDERS__ENABLE_POLL__YES:
- db_client = DBClient()
- reminder_id = int(
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
- )
- reminder = db_client.get_reminder_by_id(reminder_id)
- db_client.update_reminder(reminder_id, send_poll=True)
- reply(
- load("manage_reminders_handler__poll_was_enabled"),
- update,
- )
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__DISABLE_POLL:
- if button == consts.ButtonValues.MANAGE_REMINDERS__DISABLE_POLL__YES:
- db_client = DBClient()
- reminder_id = int(
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
- )
- reminder = db_client.get_reminder_by_id(reminder_id)
- db_client.update_reminder(reminder_id, send_poll=False)
- reply(
- load("manage_reminders_handler__poll_was_disabled"),
- update,
- )
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__DELETE_REQUEST:
- if button is None:
- reply(load("user_message_handler__press_button_please"), update)
- return
- if button == ButtonValues.MANAGE_REMINDERS__DELETE__YES:
- DBClient().delete_reminder(
- command_data.get(consts.ManageRemindersData.CHOSEN_REMINDER_ID)
- )
- reply(load("manage_reminders_handler__reminder_was_deleted"), update)
- set_next_action(command_data, None)
- elif button == ButtonValues.MANAGE_REMINDERS__DELETE__NO:
- reply(load("manage_reminders_handler__reminder_was_not_deleted"), update)
- set_next_action(command_data, None)
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__ENTER_CHAT_ID:
- try:
- chat_id = int(user_input)
- chat_title = DBClient().get_chat_name(chat_id)
- except Exception as e:
- reply(load("manage_reminders_handler__bad_chat_id"), update)
- logger.info("Failed to parse chat id", exc_info=e)
- return
- command_data[consts.ManageRemindersData.GROUP_CHAT_ID] = chat_id
- action = command_data[consts.ManageRemindersData.ACTION_TYPE]
-
- if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW:
- reply(
- load("manage_reminders_handler__enter_name", chat_title=chat_title),
- update,
- )
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__ENTER_NAME
- )
- elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
- reminder_id = int(
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
- )
- reminder = DBClient().get_reminder_by_id(reminder_id)
- DBClient().update_reminder(reminder_id, group_chat_id=chat_id)
- reply(
- load(
- "manage_reminders_handler__reminder_set",
- name=reminder.name,
- chat_title=chat_title,
- ),
- update,
- )
- set_next_action(command_data, None)
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__ENTER_NAME:
- command_data[consts.ManageRemindersData.REMINDER_NAME] = user_input
- action = command_data[consts.ManageRemindersData.ACTION_TYPE]
-
- if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW:
- reply(load("manage_reminders_handler__enter_text"), update)
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__ENTER_TEXT
- )
- elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
- reminder_id = int(
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
- )
- DBClient().update_reminder(reminder_id, name=user_input)
- reply(load("manage_reminders_handler__reminder_name_was_changed"), update)
- set_next_action(command_data, None)
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__ENTER_TEXT:
- # keeps original formatting, e.g. hyperlinks
- text = update.message.text_html.strip()
- command_data[consts.ManageRemindersData.REMINDER_TEXT] = text
- action = command_data[consts.ManageRemindersData.ACTION_TYPE]
-
- if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__NEW:
- reply_markup = telegram.InlineKeyboardMarkup(consts.WEEKDAY_BUTTONS)
- set_next_action(
- command_data, PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_WEEKDAY
- )
- reply(
- load("manage_reminders_handler__choose_weekday"),
- update,
- reply_markup=reply_markup,
- )
- elif action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
- reminder_id = int(
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
- )
- DBClient().update_reminder(reminder_id, text=text)
- reply(load("manage_reminders_handler__reminder_text_was_changed"), update)
- set_next_action(command_data, None)
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_WEEKDAY:
- if button is None:
- reply(load("user_message_handler__press_button_please"), update)
- return
- weekday_num, weekday_name = button.value.split(":")
- command_data[consts.ManageRemindersData.WEEKDAY_NUM] = int(weekday_num)
- command_data[consts.ManageRemindersData.WEEKDAY_NAME] = weekday_name
- set_next_action(command_data, PlainTextUserAction.MANAGE_REMINDERS__ENTER_TIME)
- reply(
- load("manage_reminders_handler__enter_time", weekday_name=weekday_name),
- update,
- )
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__ENTER_TIME:
- try:
- datetime.strptime(user_input or "", "%H:%M")
- except ValueError:
- reply(load("manage_reminders_handler__time_bad"), update)
- return
- command_data[consts.ManageRemindersData.TIME] = user_input
- action = command_data[consts.ManageRemindersData.ACTION_TYPE]
- weekday_num = command_data[consts.ManageRemindersData.WEEKDAY_NUM]
- time = command_data[consts.ManageRemindersData.TIME]
-
- if action == ButtonValues.MANAGE_REMINDERS__ACTIONS__EDIT:
- reminder_id = int(
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
- )
- DBClient().update_reminder(reminder_id, weekday=weekday_num, time=time)
- set_next_action(command_data, None)
- button_yes = telegram.InlineKeyboardButton(
- load("manage_reminders_handler__poll_yes_btn"),
- callback_data=consts.ButtonValues.MANAGE_REMINDERS__POLL__YES.value,
- )
- button_no = telegram.InlineKeyboardButton(
- load("manage_reminders_handler__poll_no_btn"),
- callback_data=consts.ButtonValues.MANAGE_REMINDERS__POLL__NO.value,
- )
- buttons = [button_yes, button_no]
- reply(
- load("manage_reminders_handler__poll_question"),
- update,
- reply_markup=telegram.InlineKeyboardMarkup([buttons]),
- )
- set_next_action(command_data, PlainTextUserAction.MANAGE_REMINDERS__SUCCESS)
- return
- elif next_action == PlainTextUserAction.MANAGE_REMINDERS__SUCCESS:
- text = command_data.get(consts.ManageRemindersData.REMINDER_TEXT)
- group_chat_id = command_data.get(consts.ManageRemindersData.GROUP_CHAT_ID)
- name = command_data.get(consts.ManageRemindersData.REMINDER_NAME)
- weekday_num = command_data[consts.ManageRemindersData.WEEKDAY_NUM]
- weekday_name = command_data[consts.ManageRemindersData.WEEKDAY_NAME]
- time = command_data[consts.ManageRemindersData.TIME]
- if button == consts.ButtonValues.MANAGE_REMINDERS__TOGGLE_POLL__YES:
- if text is None:
- reminder_id = int(
- command_data[consts.ManageRemindersData.CHOSEN_REMINDER_ID]
- )
- DBClient().update_reminder(reminder_id, weekday=weekday_num, time=time)
- else:
- DBClient().add_reminder(
- creator_chat_id=get_sender_id(update),
- group_chat_id=group_chat_id,
- name=name,
- text=text,
- weekday_num=weekday_num,
- time=time,
- send_poll=True,
- )
- else:
- DBClient().add_reminder(
- creator_chat_id=get_sender_id(update),
- group_chat_id=group_chat_id,
- name=name,
- text=text,
- weekday_num=weekday_num,
- time=time,
- send_poll=False,
- )
- weekday_name = command_data[consts.ManageRemindersData.WEEKDAY_NAME]
- time = command_data[consts.ManageRemindersData.TIME]
- reply(
- load(
- "manage_reminders_handler__success_time",
- weekday_name=weekday_name,
- time=time,
- ),
- update,
- )
- set_next_action(command_data, None)
- return
- else:
- logger.error(f"Unknown user action: {next_action}")
-
-
-def set_next_action(command_data: dict, next_action: PlainTextUserAction):
- command_data[consts.NEXT_ACTION] = next_action.value if next_action else next_action
-
-
-def handle_new_members(
- update: telegram.Update, tg_context: telegram.ext.CallbackContext
-):
- # writes chat_id and chat name to db when anybody (including the bot) is added to a new chat
- # very heuristic solution
- DBClient().set_chat_name(get_chat_id(update), get_chat_name(update))
-
-
-def handle_task_report(command_data, add_labels, update):
- board_id = command_data[consts.GetTasksReportData.BOARD_ID]
- list_id = command_data[consts.GetTasksReportData.LIST_ID]
- introduction = command_data[consts.GetTasksReportData.INTRO_TEXT]
- use_focalboard = command_data[consts.GetTasksReportData.USE_FOCALBOARD]
- messages = get_tasks_report_handler.generate_report_messages(
- board_id, list_id, introduction, add_labels, use_focalboard=use_focalboard
- )
- for message in messages:
- reply(message, update)
- # finished with last action for /trello_client_get_lists
- set_next_action(command_data, None)
- return
+ logger.error(f"Unknown user action: {next_action}")