diff --git a/src/glitchup_bot/bot/handlers/commands.py b/src/glitchup_bot/bot/handlers/commands.py index 8ff4bc3..d56eb25 100644 --- a/src/glitchup_bot/bot/handlers/commands.py +++ b/src/glitchup_bot/bot/handlers/commands.py @@ -11,6 +11,7 @@ from aiogram.filters import Command from aiogram.types import CallbackQuery, Message from glitchup_bot.bot.keyboards import ( + admin_admins_keyboard, admin_home_keyboard, admin_recipient_group_keyboard, admin_recipients_keyboard, @@ -58,6 +59,7 @@ logger = logging.getLogger(__name__) MAX_PAGE_CHARS = 3000 MAX_PAGE_LINES = 18 MAX_PAGINATION_SESSIONS = 200 +PENDING_ADMIN_ACTIONS: dict[int, str] = {} PENDING_RECIPIENT_ACTIONS: dict[int, tuple[str, str]] = {} PENDING_SETTING_ACTIONS: dict[int, tuple[str, str]] = {} @@ -798,68 +800,11 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: reply_markup=admin_routing_keyboard(), ) return - if action == "hint:sync": - await _deliver_result( + if action == "menu:admins": + await _show_callback_screen( callback, - _admin_hint_text("sync"), - back_callback="admin:menu:sync", - is_admin=True, - admin_mode=True, - ) - return - if action == "hint:recipients": - await _deliver_result( - callback, - _admin_hint_text("recipients"), - back_callback="admin:menu:recipients", - is_admin=True, - admin_mode=True, - ) - return - if action == "hint:routing": - await _deliver_result( - callback, - _admin_hint_text("routing"), - back_callback="admin:menu:routing", - is_admin=True, - admin_mode=True, - ) - return - if action == "hint:settings": - await _deliver_result( - callback, - _admin_hint_text("settings"), - back_callback="admin:menu:settings", - is_admin=True, - admin_mode=True, - ) - return - if action == "hint:mute": - await _deliver_result( - callback, - _admin_hint_text("mute"), - back_callback="admin:open", - is_admin=True, - admin_mode=True, - ) - return - if action == "hint:admins": - await _deliver_result( - callback, - _admin_hint_text("admins"), - back_callback="admin:open", - is_admin=True, - admin_mode=True, - ) - return - if action.startswith("hint:recipient_group:"): - group_name = action.rsplit(":", 1)[1] - await _deliver_result( - callback, - _admin_hint_text("recipient_group", group_name), - back_callback=f"admin:recipients:{group_name}", - is_admin=True, - admin_mode=True, + await _admins_text(), + reply_markup=admin_admins_keyboard(), ) return if action == "menu:settings": @@ -925,11 +870,37 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: await _deliver_result( callback, await _admins_text(), - back_callback="admin:open", + back_callback="admin:menu:admins", is_admin=True, admin_mode=True, ) return + if action == "admins:add": + admin_id = _callback_sender_id(callback) + if admin_id is not None: + PENDING_ADMIN_ACTIONS[admin_id] = "add" + await _show_callback_screen( + callback, + ( + "Добавление администратора\n\n" + "Отправьте следующим сообщением Telegram ID, который нужно добавить." + ), + reply_markup=admin_result_keyboard("admin:menu:admins"), + ) + return + if action == "admins:del": + admin_id = _callback_sender_id(callback) + if admin_id is not None: + PENDING_ADMIN_ACTIONS[admin_id] = "del" + await _show_callback_screen( + callback, + ( + "Удаление администратора\n\n" + "Отправьте следующим сообщением Telegram ID, который нужно удалить." + ), + reply_markup=admin_result_keyboard("admin:menu:admins"), + ) + return if action == "settings:sync_enabled": runtime = await get_runtime_settings() await set_runtime_setting("sync_enabled", "false" if runtime.sync_enabled else "true") @@ -938,8 +909,8 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: await reload_scheduler() await _show_callback_screen( callback, - await _runtime_settings_text(), - reply_markup=admin_settings_keyboard(), + _admin_sync_text(), + reply_markup=admin_sync_keyboard(), ) return if action == "settings:sync_interval": @@ -949,7 +920,7 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: await _show_callback_screen( callback, _setting_prompt_text("sync_interval_minutes", ""), - reply_markup=admin_result_keyboard("admin:menu:settings"), + reply_markup=admin_result_keyboard("admin:menu:sync"), ) return if action == "settings:digest_day": @@ -959,7 +930,7 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: await _show_callback_screen( callback, _setting_prompt_text("digest_cron_day", ""), - reply_markup=admin_result_keyboard("admin:menu:settings"), + reply_markup=admin_result_keyboard("admin:menu:sync"), ) return if action == "settings:digest_time": @@ -969,7 +940,7 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: await _show_callback_screen( callback, _setting_prompt_text("digest_time", ""), - reply_markup=admin_result_keyboard("admin:menu:settings"), + reply_markup=admin_result_keyboard("admin:menu:sync"), ) return if action == "settings:digest_timezone": @@ -979,7 +950,7 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: await _show_callback_screen( callback, _setting_prompt_text("digest_timezone", ""), - reply_markup=admin_result_keyboard("admin:menu:settings"), + reply_markup=admin_result_keyboard("admin:menu:sync"), ) return if action == "settings:bot_title": @@ -1217,7 +1188,7 @@ async def cmd_admins(message: Message) -> None: await _deliver_result( message, await _admins_text(), - back_callback="admin:open", + back_callback="admin:menu:admins", is_admin=True, admin_mode=True, ) @@ -1244,7 +1215,7 @@ async def cmd_admin_add(message: Message) -> None: await _deliver_result( message, text, - back_callback="admin:open", + back_callback="admin:menu:admins", is_admin=True, admin_mode=True, ) @@ -1269,7 +1240,7 @@ async def cmd_admin_del(message: Message) -> None: await _deliver_result( message, text, - back_callback="admin:open", + back_callback="admin:menu:admins", is_admin=True, admin_mode=True, ) @@ -1306,14 +1277,14 @@ async def cmd_pending_recipient_input(message: Message) -> None: await reload_scheduler() text = f"Интервал sync обновлён: {escape(raw_value)} мин." - back_callback = "admin:menu:settings" + back_callback = "admin:menu:sync" elif setting_key == "digest_cron_day": await set_runtime_setting("digest_cron_day", raw_value) from glitchup_bot.tasks.scheduler import reload_scheduler await reload_scheduler() text = f"День weekly digest обновлён: {escape(raw_value)}." - back_callback = "admin:menu:settings" + back_callback = "admin:menu:sync" elif setting_key == "digest_time": if ":" not in raw_value: await message.answer("Нужно указать время в формате HH:MM.") @@ -1328,14 +1299,14 @@ async def cmd_pending_recipient_input(message: Message) -> None: await reload_scheduler() text = f"Время weekly digest обновлено: {escape(raw_value)}." - back_callback = "admin:menu:settings" + back_callback = "admin:menu:sync" elif setting_key == "digest_timezone": await set_runtime_setting("digest_timezone", raw_value) from glitchup_bot.tasks.scheduler import reload_scheduler await reload_scheduler() text = f"Timezone обновлён: {escape(raw_value)}." - back_callback = "admin:menu:settings" + back_callback = "admin:menu:sync" else: await set_runtime_setting(setting_key, raw_value) text = f"Настройка {escape(setting_key)} обновлена." @@ -1350,6 +1321,40 @@ async def cmd_pending_recipient_input(message: Message) -> None: ) return + if user_id in PENDING_ADMIN_ACTIONS: + if not raw_value.lstrip("-").isdigit(): + await message.answer("Нужен числовой Telegram ID.") + return + + action = PENDING_ADMIN_ACTIONS.pop(user_id) + target_id = int(raw_value) + if action == "add": + added = await add_admin(target_id) + text = ( + f"Администратор {target_id} добавлен." + if added + else ( + f"Пользователь {target_id} уже есть среди " + "runtime-администраторов." + ) + ) + else: + removed = await remove_admin(target_id) + text = ( + f"Runtime-администратор {target_id} удалён." + if removed + else f"Runtime-администратор {target_id} не найден." + ) + + await _deliver_result( + message, + text, + back_callback="admin:menu:admins", + is_admin=True, + admin_mode=True, + ) + return + if user_id not in PENDING_RECIPIENT_ACTIONS: return @@ -1385,6 +1390,95 @@ async def cmd_pending_recipient_input(message: Message) -> None: ) +def _admin_routing_text() -> str: + return "\n".join( + [ + "Topic и routing", + "", + "Здесь можно смотреть текущую схему доставки и менять topic override без env.", + ( + "Routing определяет, к какой группе относится проект, а topic override " + "определяет тему Telegram для backend, frontend и digest." + ), + ] + ) + + +def _admin_sync_text() -> str: + return "\n".join( + [ + "Синхронизация", + "", + "Синхронизация подтягивает актуальные issues из GlitchTip в локальный кэш бота.", + ( + "Здесь удобно запускать ручной sync, смотреть статус и менять " + "расписание, не трогая .env." + ), + ] + ) + + +def _admin_recipients_text() -> str: + return "\n".join( + [ + "Получатели уведомлений", + "", + "Выберите группу и назначайте Telegram ID через кнопки.", + ( + "Получатели — это пользователи, которым бот отправляет уведомления " + "по backend или frontend." + ), + ] + ) + + +async def _runtime_settings_text() -> str: + runtime = await get_runtime_settings() + return "\n".join( + [ + "Настройки бота", + "", + "Здесь собраны тексты интерфейса, которые можно менять без правки .env.", + f"• название: {escape(runtime.bot_title)}", + f"• описание: {escape(runtime.bot_purpose)}", + f"• подсказка админу: {escape(runtime.bot_admin_hint)}", + ] + ) + + +def _recipient_group_text(group_name: str) -> str: + return "\n".join( + [ + f"{escape(group_name.capitalize())} получатели", + "", + "Здесь можно посмотреть список, добавить новый Telegram ID или удалить существующий.", + ( + "Удаление отсюда прекращает доставку уведомлений этому пользователю " + "по выбранной группе." + ), + ] + ) + + +async def _admins_text() -> str: + admins = await list_effective_admins() + lines = [ + "Администраторы", + "", + "Администраторы могут менять настройки бота, получателей и расписание.", + "Добавление и удаление можно делать прямо кнопками ниже по Telegram ID.", + ] + if not admins: + lines.extend(["", "Список пока пуст."]) + return "\n".join(lines) + + lines.append("") + for user_id, source in admins: + source_label = source.replace("env", "из .env") + lines.append(f"• {user_id} — {source_label}") + return "\n".join(lines) + + @router.message(Command("subscribe")) async def cmd_subscribe(message: Message) -> None: await message.answer("Самоподписка отключена. Получателей настраивает администратор.") diff --git a/src/glitchup_bot/bot/keyboards.py b/src/glitchup_bot/bot/keyboards.py index 7d00e3f..9478b23 100644 --- a/src/glitchup_bot/bot/keyboards.py +++ b/src/glitchup_bot/bot/keyboards.py @@ -71,13 +71,11 @@ def admin_home_keyboard() -> InlineKeyboardMarkup: builder.button(text="Получатели", callback_data="admin:menu:recipients") builder.button(text="Topic и routing", callback_data="admin:menu:routing") builder.button(text="Настройки бота", callback_data="admin:menu:settings") - builder.button(text="Администраторы", callback_data="admin:admins") - builder.button(text="Что такое админы?", callback_data="admin:hint:admins") + builder.button(text="Администраторы", callback_data="admin:menu:admins") builder.button(text="Mute rules", callback_data="admin:mute_list") - builder.button(text="Что такое mute?", callback_data="admin:hint:mute") builder.button(text="Инструкция", callback_data="admin:guide") builder.button(text="Назад", callback_data="help:open") - builder.adjust(2, 2, 2, 2, 1) + builder.adjust(2, 2, 2, 2) return builder.as_markup() @@ -85,9 +83,13 @@ def admin_sync_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="Запустить sync", callback_data="admin:sync") builder.button(text="Статус sync", callback_data="admin:sync_status") - builder.button(text="Что это?", callback_data="admin:hint:sync") + builder.button(text="Автосинк: вкл/выкл", callback_data="admin:settings:sync_enabled") + builder.button(text="Интервал sync", callback_data="admin:settings:sync_interval") + builder.button(text="День отчёта", callback_data="admin:settings:digest_day") + builder.button(text="Время отчёта", callback_data="admin:settings:digest_time") + builder.button(text="Часовой пояс", callback_data="admin:settings:digest_timezone") builder.button(text="Назад", callback_data="admin:open") - builder.adjust(2, 1, 1) + builder.adjust(2, 2, 2, 1, 1) return builder.as_markup() @@ -97,7 +99,6 @@ def admin_routing_keyboard() -> InlineKeyboardMarkup: builder.button(text="Topic backend", callback_data="admin:settings:topic:backend") builder.button(text="Topic frontend", callback_data="admin:settings:topic:frontend") builder.button(text="Topic digest", callback_data="admin:settings:topic:digest") - builder.button(text="Что это?", callback_data="admin:hint:routing") builder.button(text="Назад", callback_data="admin:open") builder.adjust(2, 2, 1, 1) return builder.as_markup() @@ -105,17 +106,11 @@ def admin_routing_keyboard() -> InlineKeyboardMarkup: def admin_settings_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="Автосинк: вкл/выкл", callback_data="admin:settings:sync_enabled") - builder.button(text="Интервал sync", callback_data="admin:settings:sync_interval") - builder.button(text="День отчёта", callback_data="admin:settings:digest_day") - builder.button(text="Время отчёта", callback_data="admin:settings:digest_time") - builder.button(text="Часовой пояс", callback_data="admin:settings:digest_timezone") builder.button(text="Название бота", callback_data="admin:settings:bot_title") builder.button(text="Описание бота", callback_data="admin:settings:bot_purpose") builder.button(text="Подсказка админу", callback_data="admin:settings:bot_admin_hint") - builder.button(text="Что это?", callback_data="admin:hint:settings") builder.button(text="Назад", callback_data="admin:open") - builder.adjust(2, 2, 2, 2, 1, 1) + builder.adjust(2, 1, 1) return builder.as_markup() @@ -123,9 +118,8 @@ def admin_recipients_keyboard() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="Backend", callback_data="admin:recipients:backend") builder.button(text="Frontend", callback_data="admin:recipients:frontend") - builder.button(text="Что это?", callback_data="admin:hint:recipients") builder.button(text="Назад", callback_data="admin:open") - builder.adjust(2, 1, 1) + builder.adjust(2, 1) return builder.as_markup() @@ -134,9 +128,18 @@ def admin_recipient_group_keyboard(group_name: str) -> InlineKeyboardMarkup: builder.button(text="Список", callback_data=f"admin:recipients:list:{group_name}") builder.button(text="Добавить ID", callback_data=f"admin:recipients:add:{group_name}") builder.button(text="Удалить ID", callback_data=f"admin:recipients:del:{group_name}") - builder.button(text="Что это?", callback_data=f"admin:hint:recipient_group:{group_name}") builder.button(text="Назад", callback_data="admin:menu:recipients") - builder.adjust(2, 1, 1, 1) + builder.adjust(2, 1, 1) + return builder.as_markup() + + +def admin_admins_keyboard() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button(text="Список", callback_data="admin:admins") + builder.button(text="Добавить ID", callback_data="admin:admins:add") + builder.button(text="Удалить ID", callback_data="admin:admins:del") + builder.button(text="Назад", callback_data="admin:open") + builder.adjust(2, 1, 1) return builder.as_markup()