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

This commit is contained in:
2026-03-31 14:24:50 +07:00
parent ea4a6fbe38
commit e811b259fc
11 changed files with 436 additions and 92 deletions

View File

@@ -12,10 +12,11 @@ from aiogram.types import CallbackQuery, Message
from glitchup_bot.bot.keyboards import (
admin_home_keyboard,
admin_overview_keyboard,
admin_recipient_group_keyboard,
admin_recipients_keyboard,
admin_result_keyboard,
admin_routing_keyboard,
admin_settings_keyboard,
admin_sync_keyboard,
help_home_keyboard,
help_result_keyboard,
@@ -49,6 +50,7 @@ from glitchup_bot.services.routing import (
set_project_group,
set_topic_override,
)
from glitchup_bot.services.runtime_settings import get_runtime_settings, set_runtime_setting
from glitchup_bot.services.sync_service import get_last_sync_state
router = Router()
@@ -57,6 +59,7 @@ MAX_PAGE_CHARS = 3000
MAX_PAGE_LINES = 18
MAX_PAGINATION_SESSIONS = 200
PENDING_RECIPIENT_ACTIONS: dict[int, tuple[str, str]] = {}
PENDING_SETTING_ACTIONS: dict[int, tuple[str, str]] = {}
@dataclass(slots=True)
@@ -164,6 +167,7 @@ def _help_text(is_admin: bool) -> str:
async def _start_text(is_admin: bool) -> str:
runtime = await get_runtime_settings()
state = await get_last_sync_state("api_sync")
sync_value = (
state.last_successful_at.astimezone().strftime("%Y-%m-%d %H:%M")
@@ -171,22 +175,22 @@ async def _start_text(is_admin: bool) -> str:
else "ещё не было"
)
lines = [
"<b>GlitchUp Bot</b>",
f"<b>{escape(runtime.bot_title)}</b>",
f"Синхронизация: {escape(sync_value)}",
"",
"Откройте нужную сводку кнопками ниже.",
escape(runtime.bot_purpose),
]
if is_admin:
lines.extend(["", "Администрирование доступно через кнопку ниже."])
lines.extend(["", escape(runtime.bot_admin_hint)])
return "\n".join(lines)
def _admin_overview_text() -> str:
def _admin_routing_text() -> str:
return "\n".join(
[
"<b>Сводки</b>",
"<b>Topic и routing</b>",
"",
"Быстрый доступ к основным экранам без лишних разделов.",
"Здесь можно смотреть текущую схему и менять topic override без env.",
]
)
@@ -198,10 +202,11 @@ def _admin_text() -> str:
"",
"Здесь только практичные разделы:",
"• синхронизация",
"• основные сводки",
"• получатели по backend/frontend",
"• topic и routing",
"• runtime-настройки бота",
"• администраторы",
" routing и mute rules",
"• mute rules",
]
)
@@ -228,6 +233,26 @@ def _admin_recipients_text() -> str:
)
async def _runtime_settings_text() -> str:
runtime = await get_runtime_settings()
lines = [
"<b>Настройки бота</b>",
"",
f"• автосинк: {'включён' if runtime.sync_enabled else 'выключен'}",
f"• интервал sync: {runtime.sync_interval_minutes} мин",
(
"• отчёт: "
f"{runtime.digest_cron_day} "
f"{runtime.digest_cron_hour:02d}:{runtime.digest_cron_minute:02d} "
f"{runtime.digest_timezone}"
),
f"• название: {escape(runtime.bot_title)}",
f"• описание: {escape(runtime.bot_purpose)}",
f"• подсказка админу: {escape(runtime.bot_admin_hint)}",
]
return "\n".join(lines)
def _recipient_group_text(group_name: str) -> str:
return "\n".join(
[
@@ -313,6 +338,20 @@ async def _recipients_text(group_name: str) -> str:
return "\n".join(lines)
def _setting_prompt_text(setting_key: str, target: str) -> str:
prompts = {
"topic": f"Отправьте новым сообщением числовой topic ID для <b>{escape(target)}</b>.",
"sync_interval_minutes": "Отправьте интервал синхронизации в минутах.",
"digest_cron_day": "Отправьте день отчёта, например <code>mon</code>.",
"digest_time": "Отправьте время отчёта в формате <code>HH:MM</code>.",
"digest_timezone": "Отправьте timezone, например <code>Asia/Krasnoyarsk</code>.",
"bot_title": "Отправьте новое название бота.",
"bot_purpose": "Отправьте короткое описание, для чего нужен бот.",
"bot_admin_hint": "Отправьте текст подсказки для администратора на /start.",
}
return prompts[setting_key]
async def _require_admin(message: Message) -> bool:
if await _is_admin_user(_sender_id(message)):
return True
@@ -667,13 +706,6 @@ async def cb_admin_actions(callback: CallbackQuery) -> None:
reply_markup=admin_sync_keyboard(),
)
return
if action == "menu:overview":
await _show_callback_screen(
callback,
_admin_overview_text(),
reply_markup=admin_overview_keyboard(),
)
return
if action == "menu:recipients":
await _show_callback_screen(
callback,
@@ -681,6 +713,20 @@ async def cb_admin_actions(callback: CallbackQuery) -> None:
reply_markup=admin_recipients_keyboard(),
)
return
if action == "menu:routing":
await _show_callback_screen(
callback,
_admin_routing_text(),
reply_markup=admin_routing_keyboard(),
)
return
if action == "menu:settings":
await _show_callback_screen(
callback,
await _runtime_settings_text(),
reply_markup=admin_settings_keyboard(),
)
return
if action in {"recipients:backend", "recipients:frontend"}:
group_name = action.split(":", 1)[1]
await _show_callback_screen(
@@ -742,6 +788,99 @@ async def cb_admin_actions(callback: CallbackQuery) -> None:
admin_mode=True,
)
return
if action == "settings:sync_enabled":
runtime = await get_runtime_settings()
await set_runtime_setting("sync_enabled", "false" if runtime.sync_enabled else "true")
from glitchup_bot.tasks.scheduler import reload_scheduler
await reload_scheduler()
await _show_callback_screen(
callback,
await _runtime_settings_text(),
reply_markup=admin_settings_keyboard(),
)
return
if action == "settings:sync_interval":
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("sync_interval_minutes", "")
await _show_callback_screen(
callback,
_setting_prompt_text("sync_interval_minutes", ""),
reply_markup=admin_result_keyboard("admin:menu:settings"),
)
return
if action == "settings:digest_day":
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("digest_cron_day", "")
await _show_callback_screen(
callback,
_setting_prompt_text("digest_cron_day", ""),
reply_markup=admin_result_keyboard("admin:menu:settings"),
)
return
if action == "settings:digest_time":
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("digest_time", "")
await _show_callback_screen(
callback,
_setting_prompt_text("digest_time", ""),
reply_markup=admin_result_keyboard("admin:menu:settings"),
)
return
if action == "settings:digest_timezone":
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("digest_timezone", "")
await _show_callback_screen(
callback,
_setting_prompt_text("digest_timezone", ""),
reply_markup=admin_result_keyboard("admin:menu:settings"),
)
return
if action == "settings:bot_title":
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("bot_title", "")
await _show_callback_screen(
callback,
_setting_prompt_text("bot_title", ""),
reply_markup=admin_result_keyboard("admin:menu:settings"),
)
return
if action == "settings:bot_purpose":
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("bot_purpose", "")
await _show_callback_screen(
callback,
_setting_prompt_text("bot_purpose", ""),
reply_markup=admin_result_keyboard("admin:menu:settings"),
)
return
if action == "settings:bot_admin_hint":
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("bot_admin_hint", "")
await _show_callback_screen(
callback,
_setting_prompt_text("bot_admin_hint", ""),
reply_markup=admin_result_keyboard("admin:menu:settings"),
)
return
if action.startswith("settings:topic:"):
group_name = action.rsplit(":", 1)[1]
admin_id = _callback_sender_id(callback)
if admin_id is not None:
PENDING_SETTING_ACTIONS[admin_id] = ("topic", group_name)
await _show_callback_screen(
callback,
_setting_prompt_text("topic", group_name),
reply_markup=admin_result_keyboard("admin:menu:routing"),
)
return
if action == "sync":
summary = await run_manual_sync()
await _deliver_result(
@@ -779,42 +918,6 @@ async def cb_admin_actions(callback: CallbackQuery) -> None:
admin_mode=True,
)
return
if action == "today":
await _run_summary_action(
callback,
lambda: build_today_summary(refresh=False),
back_callback="admin:menu:overview",
is_admin=True,
admin_mode=True,
)
return
if action == "week":
await _run_summary_action(
callback,
lambda: build_digest(refresh=False),
back_callback="admin:menu:overview",
is_admin=True,
admin_mode=True,
)
return
if action == "top":
await _run_summary_action(
callback,
lambda: build_top_issues(refresh=False),
back_callback="admin:menu:overview",
is_admin=True,
admin_mode=True,
)
return
if action == "stale":
await _run_summary_action(
callback,
lambda: build_stale_issues(refresh=False),
back_callback="admin:menu:overview",
is_admin=True,
admin_mode=True,
)
return
if action == "guide":
await _deliver_result(
callback,
@@ -1033,7 +1136,7 @@ async def cmd_admin_del(message: Message) -> None:
@router.message(F.text)
async def cmd_pending_recipient_input(message: Message) -> None:
user_id = _sender_id(message)
if user_id is None or user_id not in PENDING_RECIPIENT_ACTIONS:
if user_id is None:
return
if not await _require_admin(message):
return
@@ -1041,6 +1144,73 @@ async def cmd_pending_recipient_input(message: Message) -> None:
raw_value = (message.text or "").strip()
if raw_value.startswith("/"):
return
if user_id in PENDING_SETTING_ACTIONS:
setting_key, target = PENDING_SETTING_ACTIONS.pop(user_id)
if setting_key == "topic":
if not raw_value.lstrip("-").isdigit():
await message.answer("Нужен числовой topic ID.")
return
await set_topic_override(target, int(raw_value))
text = f"Topic для <b>{escape(target)}</b> обновлён: <code>{escape(raw_value)}</code>."
back_callback = "admin:menu:routing"
elif setting_key == "sync_interval_minutes":
if not raw_value.isdigit():
await message.answer("Нужно указать интервал в минутах числом.")
return
await set_runtime_setting("sync_interval_minutes", raw_value)
from glitchup_bot.tasks.scheduler import reload_scheduler
await reload_scheduler()
text = f"Интервал sync обновлён: <code>{escape(raw_value)}</code> мин."
back_callback = "admin:menu:settings"
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"
elif setting_key == "digest_time":
if ":" not in raw_value:
await message.answer("Нужно указать время в формате HH:MM.")
return
hour, minute = raw_value.split(":", 1)
if not (hour.isdigit() and minute.isdigit()):
await message.answer("Нужно указать время в формате HH:MM.")
return
await set_runtime_setting("digest_cron_hour", hour)
await set_runtime_setting("digest_cron_minute", minute)
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"
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"
else:
await set_runtime_setting(setting_key, raw_value)
text = f"Настройка <code>{escape(setting_key)}</code> обновлена."
back_callback = "admin:menu:settings"
await _deliver_result(
message,
text,
back_callback=back_callback,
is_admin=True,
admin_mode=True,
)
return
if user_id not in PENDING_RECIPIENT_ACTIONS:
return
if not raw_value.lstrip("-").isdigit():
await message.answer("Нужен числовой Telegram ID.")
return