Правки проблем
Some checks failed
CI / Lint (ruff + mypy) (push) Failing after 35s
CI / Run tests (push) Has been skipped
CI / Docker build test (push) Successful in 18s

This commit is contained in:
2026-03-31 14:47:21 +07:00
parent e811b259fc
commit 8b835ad08d
2 changed files with 157 additions and 8 deletions

View File

@@ -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(
[
"<b>Что такое синхронизация</b>",
"",
"Синхронизация подтягивает актуальные issues из GlitchTip в локальный кэш бота.",
"Ручной sync нужен, когда вы хотите обновить данные "
"прямо сейчас, не дожидаясь расписания.",
]
),
"recipients": "\n".join(
[
"<b>Что такое получатели</b>",
"",
"Получатели — это Telegram ID пользователей, которым бот отправляет уведомления.",
"Backend и frontend настраиваются отдельно.",
]
),
"routing": "\n".join(
[
"<b>Что такое Topic и routing</b>",
"",
"Routing определяет, в какую группу попадает проект.",
"Topic override определяет, в какую тему Telegram будут "
"приходить сообщения для backend, frontend и digest.",
]
),
"settings": "\n".join(
[
"<b>Что такое настройки бота</b>",
"",
"Здесь собраны runtime-параметры, которые можно менять "
"без правки `.env` и без пересборки контейнера.",
"Сюда входят автосинк, расписание digest и тексты интерфейса.",
]
),
"mute": "\n".join(
[
"<b>Что такое mute rules</b>",
"",
"Mute rule — это шаблон, по которому бот скрывает шумные или неважные события.",
"Если событие совпало с правилом, оно не будет мешать в уведомлениях.",
]
),
"admins": "\n".join(
[
"<b>Что такое администраторы</b>",
"",
"Администраторы могут менять настройки бота, получателей и расписание.",
"Часть админов может приходить из `.env`, а часть хранится в runtime-базе.",
]
),
"recipient_group": "\n".join(
[
f"<b>{escape((target or '').capitalize())} получатели</b>",
"",
"Добавляйте сюда 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,

View File

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