Убраны подсказки лишние и добавлено пояснение для димитриев
Some checks failed
CI / Lint (ruff + mypy) (push) Failing after 36s
CI / Run tests (push) Has been skipped
CI / Docker build test (push) Successful in 18s

This commit is contained in:
2026-03-31 15:11:46 +07:00
parent 8b835ad08d
commit c10f3bf862
2 changed files with 190 additions and 93 deletions

View File

@@ -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,
(
"<b>Добавление администратора</b>\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,
(
"<b>Удаление администратора</b>\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 обновлён: <code>{escape(raw_value)}</code> мин."
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 обновлён: <code>{escape(raw_value)}</code>."
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 обновлено: <code>{escape(raw_value)}</code>."
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 обновлён: <code>{escape(raw_value)}</code>."
back_callback = "admin:menu:settings"
back_callback = "admin:menu:sync"
else:
await set_runtime_setting(setting_key, raw_value)
text = f"Настройка <code>{escape(setting_key)}</code> обновлена."
@@ -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"Администратор <code>{target_id}</code> добавлен."
if added
else (
f"Пользователь <code>{target_id}</code> уже есть среди "
"runtime-администраторов."
)
)
else:
removed = await remove_admin(target_id)
text = (
f"Runtime-администратор <code>{target_id}</code> удалён."
if removed
else f"Runtime-администратор <code>{target_id}</code> не найден."
)
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(
[
"<b>Topic и routing</b>",
"",
"Здесь можно смотреть текущую схему доставки и менять topic override без env.",
(
"Routing определяет, к какой группе относится проект, а topic override "
"определяет тему Telegram для backend, frontend и digest."
),
]
)
def _admin_sync_text() -> str:
return "\n".join(
[
"<b>Синхронизация</b>",
"",
"Синхронизация подтягивает актуальные issues из GlitchTip в локальный кэш бота.",
(
"Здесь удобно запускать ручной sync, смотреть статус и менять "
"расписание, не трогая .env."
),
]
)
def _admin_recipients_text() -> str:
return "\n".join(
[
"<b>Получатели уведомлений</b>",
"",
"Выберите группу и назначайте Telegram ID через кнопки.",
(
"Получатели — это пользователи, которым бот отправляет уведомления "
"по backend или frontend."
),
]
)
async def _runtime_settings_text() -> str:
runtime = await get_runtime_settings()
return "\n".join(
[
"<b>Настройки бота</b>",
"",
"Здесь собраны тексты интерфейса, которые можно менять без правки .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"<b>{escape(group_name.capitalize())} получатели</b>",
"",
"Здесь можно посмотреть список, добавить новый Telegram ID или удалить существующий.",
(
"Удаление отсюда прекращает доставку уведомлений этому пользователю "
"по выбранной группе."
),
]
)
async def _admins_text() -> str:
admins = await list_effective_admins()
lines = [
"<b>Администраторы</b>",
"",
"Администраторы могут менять настройки бота, получателей и расписание.",
"Добавление и удаление можно делать прямо кнопками ниже по 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"• <code>{user_id}</code> — {source_label}")
return "\n".join(lines)
@router.message(Command("subscribe"))
async def cmd_subscribe(message: Message) -> None:
await message.answer("Самоподписка отключена. Получателей настраивает администратор.")

View File

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