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()