From 8b835ad08d8ce4b6681fcc2372c1d1f2cfea6d06 Mon Sep 17 00:00:00 2001 From: Verum Date: Tue, 31 Mar 2026 14:47:21 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/glitchup_bot/bot/handlers/commands.py | 144 +++++++++++++++++++++- src/glitchup_bot/bot/keyboards.py | 21 ++-- 2 files changed, 157 insertions(+), 8 deletions(-) diff --git a/src/glitchup_bot/bot/handlers/commands.py b/src/glitchup_bot/bot/handlers/commands.py index 2e90e2b..8ff4bc3 100644 --- a/src/glitchup_bot/bot/handlers/commands.py +++ b/src/glitchup_bot/bot/handlers/commands.py @@ -6,7 +6,7 @@ from html import escape from uuid import uuid4 from aiogram import F, Router -from aiogram.exceptions import TelegramBadRequest +from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter from aiogram.filters import Command from aiogram.types import CallbackQuery, Message @@ -281,6 +281,72 @@ def _admin_guide_text() -> str: ) +def _admin_hint_text(section: str, target: str | None = None) -> str: + hints = { + "sync": "\n".join( + [ + "Что такое синхронизация", + "", + "Синхронизация подтягивает актуальные issues из GlitchTip в локальный кэш бота.", + "Ручной sync нужен, когда вы хотите обновить данные " + "прямо сейчас, не дожидаясь расписания.", + ] + ), + "recipients": "\n".join( + [ + "Что такое получатели", + "", + "Получатели — это Telegram ID пользователей, которым бот отправляет уведомления.", + "Backend и frontend настраиваются отдельно.", + ] + ), + "routing": "\n".join( + [ + "Что такое Topic и routing", + "", + "Routing определяет, в какую группу попадает проект.", + "Topic override определяет, в какую тему Telegram будут " + "приходить сообщения для backend, frontend и digest.", + ] + ), + "settings": "\n".join( + [ + "Что такое настройки бота", + "", + "Здесь собраны runtime-параметры, которые можно менять " + "без правки `.env` и без пересборки контейнера.", + "Сюда входят автосинк, расписание digest и тексты интерфейса.", + ] + ), + "mute": "\n".join( + [ + "Что такое mute rules", + "", + "Mute rule — это шаблон, по которому бот скрывает шумные или неважные события.", + "Если событие совпало с правилом, оно не будет мешать в уведомлениях.", + ] + ), + "admins": "\n".join( + [ + "Что такое администраторы", + "", + "Администраторы могут менять настройки бота, получателей и расписание.", + "Часть админов может приходить из `.env`, а часть хранится в runtime-базе.", + ] + ), + "recipient_group": "\n".join( + [ + f"{escape((target or '').capitalize())} получатели", + "", + "Добавляйте сюда Telegram ID тех, кто должен получать " + "уведомления по выбранной группе.", + "Удаление отсюда прекращает доставку уведомлений этому пользователю.", + ] + ), + } + return hints[section] + + def _sync_summary_text(summary) -> str: return "\n".join( [ @@ -405,6 +471,18 @@ async def _show_callback_screen( reply_markup=reply_markup, disable_web_page_preview=disable_web_page_preview, ) + except TelegramRetryAfter as exc: + logger.warning( + "Telegram edit flood control for chat %s; " + "falling back to sending a new message after %s seconds", + callback.message.chat.id, + exc.retry_after, + ) + await callback.message.answer( + text, + reply_markup=reply_markup, + disable_web_page_preview=disable_web_page_preview, + ) except TelegramBadRequest as exc: if "message is not modified" in str(exc).lower(): return @@ -720,6 +798,70 @@ async def cb_admin_actions(callback: CallbackQuery) -> None: reply_markup=admin_routing_keyboard(), ) return + if action == "hint:sync": + await _deliver_result( + 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, + ) + return if action == "menu:settings": await _show_callback_screen( callback, diff --git a/src/glitchup_bot/bot/keyboards.py b/src/glitchup_bot/bot/keyboards.py index ca8075f..7d00e3f 100644 --- a/src/glitchup_bot/bot/keyboards.py +++ b/src/glitchup_bot/bot/keyboards.py @@ -72,9 +72,12 @@ def admin_home_keyboard() -> InlineKeyboardMarkup: 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="Mute rules", callback_data="admin:mute_list") + builder.button(text="Что такое mute?", callback_data="admin:hint:mute") builder.button(text="Инструкция", callback_data="admin:guide") - builder.adjust(2, 2, 2, 1) + builder.button(text="Назад", callback_data="help:open") + builder.adjust(2, 2, 2, 2, 1) return builder.as_markup() @@ -82,9 +85,9 @@ 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:open") - builder.button(text="Пользовательское меню", callback_data="help:open") - builder.adjust(2, 2) + builder.adjust(2, 1, 1) return builder.as_markup() @@ -94,8 +97,9 @@ 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) + builder.adjust(2, 2, 1, 1) return builder.as_markup() @@ -109,8 +113,9 @@ def admin_settings_keyboard() -> InlineKeyboardMarkup: 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) + builder.adjust(2, 2, 2, 2, 1, 1) return builder.as_markup() @@ -118,8 +123,9 @@ 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) + builder.adjust(2, 1, 1) return builder.as_markup() @@ -128,8 +134,9 @@ 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) + builder.adjust(2, 1, 1, 1) return builder.as_markup()