тут всякие фиксы
This commit is contained in:
37
migrations/versions/20260330_0003_bot_admins.py
Normal file
37
migrations/versions/20260330_0003_bot_admins.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""bot admins
|
||||||
|
|
||||||
|
Revision ID: 20260330_0003
|
||||||
|
Revises: 20260327_0002
|
||||||
|
Create Date: 2026-03-30 11:30:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "20260330_0003"
|
||||||
|
down_revision: str | None = "20260327_0002"
|
||||||
|
branch_labels: Sequence[str] | None = None
|
||||||
|
depends_on: Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"bot_admins",
|
||||||
|
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||||||
|
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(op.f("ix_bot_admins_user_id"), "bot_admins", ["user_id"], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index(op.f("ix_bot_admins_user_id"), table_name="bot_admins")
|
||||||
|
op.drop_table("bot_admins")
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
from html import escape
|
from html import escape
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
from aiogram.exceptions import TelegramBadRequest
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
@@ -18,7 +21,12 @@ from glitchup_bot.bot.keyboards import (
|
|||||||
help_result_keyboard,
|
help_result_keyboard,
|
||||||
help_subscriptions_keyboard,
|
help_subscriptions_keyboard,
|
||||||
)
|
)
|
||||||
from glitchup_bot.config import settings
|
from glitchup_bot.services.admins import (
|
||||||
|
add_admin,
|
||||||
|
is_admin,
|
||||||
|
list_effective_admins,
|
||||||
|
remove_admin,
|
||||||
|
)
|
||||||
from glitchup_bot.services.digest_builder import (
|
from glitchup_bot.services.digest_builder import (
|
||||||
build_digest,
|
build_digest,
|
||||||
build_project_summary,
|
build_project_summary,
|
||||||
@@ -47,6 +55,20 @@ from glitchup_bot.services.routing import (
|
|||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
MAX_PAGE_CHARS = 3000
|
||||||
|
MAX_PAGE_LINES = 18
|
||||||
|
MAX_PAGINATION_SESSIONS = 200
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class PaginationSession:
|
||||||
|
pages: list[str]
|
||||||
|
back_callback: str
|
||||||
|
is_admin: bool
|
||||||
|
admin_mode: bool
|
||||||
|
|
||||||
|
|
||||||
|
PAGINATION_SESSIONS: OrderedDict[str, PaginationSession] = OrderedDict()
|
||||||
|
|
||||||
|
|
||||||
def _sender_id(message: Message) -> int | None:
|
def _sender_id(message: Message) -> int | None:
|
||||||
@@ -57,8 +79,72 @@ def _callback_sender_id(callback: CallbackQuery) -> int | None:
|
|||||||
return callback.from_user.id if callback.from_user else None
|
return callback.from_user.id if callback.from_user else None
|
||||||
|
|
||||||
|
|
||||||
def _is_admin_user(user_id: int | None) -> bool:
|
async def _is_admin_user(user_id: int | None) -> bool:
|
||||||
return settings.is_admin(user_id)
|
return await is_admin(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _paginate_text(text: str) -> list[str]:
|
||||||
|
if len(text) <= MAX_PAGE_CHARS and text.count("\n") + 1 <= MAX_PAGE_LINES:
|
||||||
|
return [text]
|
||||||
|
|
||||||
|
pages: list[str] = []
|
||||||
|
current_lines: list[str] = []
|
||||||
|
current_len = 0
|
||||||
|
|
||||||
|
for line in text.splitlines():
|
||||||
|
projected_len = current_len + len(line) + (1 if current_lines else 0)
|
||||||
|
if current_lines and (
|
||||||
|
projected_len > MAX_PAGE_CHARS or len(current_lines) >= MAX_PAGE_LINES
|
||||||
|
):
|
||||||
|
pages.append("\n".join(current_lines))
|
||||||
|
current_lines = [line]
|
||||||
|
current_len = len(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_lines.append(line)
|
||||||
|
current_len = projected_len
|
||||||
|
|
||||||
|
if current_lines:
|
||||||
|
pages.append("\n".join(current_lines))
|
||||||
|
|
||||||
|
return pages or [text]
|
||||||
|
|
||||||
|
|
||||||
|
def _store_pagination_session(session: PaginationSession) -> str:
|
||||||
|
token = uuid4().hex[:12]
|
||||||
|
PAGINATION_SESSIONS[token] = session
|
||||||
|
PAGINATION_SESSIONS.move_to_end(token)
|
||||||
|
|
||||||
|
while len(PAGINATION_SESSIONS) > MAX_PAGINATION_SESSIONS:
|
||||||
|
PAGINATION_SESSIONS.popitem(last=False)
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def _result_keyboard(
|
||||||
|
*,
|
||||||
|
back_callback: str,
|
||||||
|
is_admin: bool,
|
||||||
|
admin_mode: bool,
|
||||||
|
page_token: str | None = None,
|
||||||
|
page_index: int = 0,
|
||||||
|
total_pages: int = 1,
|
||||||
|
):
|
||||||
|
if admin_mode:
|
||||||
|
return admin_result_keyboard(
|
||||||
|
back_callback,
|
||||||
|
page_token=page_token,
|
||||||
|
page_index=page_index,
|
||||||
|
total_pages=total_pages,
|
||||||
|
)
|
||||||
|
|
||||||
|
return help_result_keyboard(
|
||||||
|
back_callback,
|
||||||
|
is_admin,
|
||||||
|
page_token=page_token,
|
||||||
|
page_index=page_index,
|
||||||
|
total_pages=total_pages,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _help_text(is_admin: bool) -> str:
|
def _help_text(is_admin: bool) -> str:
|
||||||
@@ -89,6 +175,9 @@ def _help_text(is_admin: bool) -> str:
|
|||||||
"<b>Для админа:</b>",
|
"<b>Для админа:</b>",
|
||||||
"• /admin — панель управления",
|
"• /admin — панель управления",
|
||||||
"• /sync — принудительная синхронизация",
|
"• /sync — принудительная синхронизация",
|
||||||
|
"• /admins — список администраторов",
|
||||||
|
"• /admin_add <user_id> или reply на сообщение",
|
||||||
|
"• /admin_del <user_id>",
|
||||||
"• /ownership — текущее распределение routing-настроек",
|
"• /ownership — текущее распределение routing-настроек",
|
||||||
"• /mute_list — список mute rules",
|
"• /mute_list — список mute rules",
|
||||||
]
|
]
|
||||||
@@ -163,6 +252,7 @@ def _admin_text() -> str:
|
|||||||
"",
|
"",
|
||||||
"• <b>Центр синхронизации</b> — запуск sync и проверка статуса",
|
"• <b>Центр синхронизации</b> — запуск sync и проверка статуса",
|
||||||
"• <b>Сводки и мониторинг</b> — основные обзорные экраны",
|
"• <b>Сводки и мониторинг</b> — основные обзорные экраны",
|
||||||
|
"• <b>Администраторы</b> — список и управление доступом",
|
||||||
"• <b>Ownership и topics</b> — текущая маршрутизация",
|
"• <b>Ownership и topics</b> — текущая маршрутизация",
|
||||||
"• <b>Mute rules</b> — правила скрытия шумных событий",
|
"• <b>Mute rules</b> — правила скрытия шумных событий",
|
||||||
]
|
]
|
||||||
@@ -198,6 +288,10 @@ def _admin_guide_text() -> str:
|
|||||||
"Через кнопки удобно смотреть состояние системы, "
|
"Через кнопки удобно смотреть состояние системы, "
|
||||||
"а точечные настройки меняются командами.",
|
"а точечные настройки меняются командами.",
|
||||||
"",
|
"",
|
||||||
|
"• <code>/admin_add 123456</code> — добавить администратора",
|
||||||
|
"• <code>/admin_add</code> ответом на сообщение — добавить автора сообщения",
|
||||||
|
"• <code>/admin_del 123456</code> — удалить runtime-администратора",
|
||||||
|
"",
|
||||||
"• <code>/owner slug backend</code> — переназначить проект в группу",
|
"• <code>/owner slug backend</code> — переназначить проект в группу",
|
||||||
"• <code>/topic backend 123</code> — сменить topic override",
|
"• <code>/topic backend 123</code> — сменить topic override",
|
||||||
"• <code>/mute_add payment.*timeout</code> — добавить mute rule",
|
"• <code>/mute_add payment.*timeout</code> — добавить mute rule",
|
||||||
@@ -218,8 +312,40 @@ def _sync_summary_text(summary) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _admins_text() -> str:
|
||||||
|
admins = await list_effective_admins()
|
||||||
|
if not admins:
|
||||||
|
return "Список администраторов пуст."
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"<b>Администраторы</b>",
|
||||||
|
"",
|
||||||
|
"Добавлять можно командой <code>/admin_add <user_id></code> "
|
||||||
|
"или ответом на сообщение пользователя.",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_target_user_id(message: Message) -> int | None:
|
||||||
|
args = message.text.split(maxsplit=1) if message.text else []
|
||||||
|
if len(args) >= 2:
|
||||||
|
raw_value = args[1].strip()
|
||||||
|
return int(raw_value) if raw_value.lstrip("-").isdigit() else None
|
||||||
|
|
||||||
|
reply = message.reply_to_message
|
||||||
|
if reply and reply.from_user:
|
||||||
|
return reply.from_user.id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _require_admin(message: Message) -> bool:
|
async def _require_admin(message: Message) -> bool:
|
||||||
if _is_admin_user(_sender_id(message)):
|
if await _is_admin_user(_sender_id(message)):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
await message.answer("Команда доступна только администраторам.")
|
await message.answer("Команда доступна только администраторам.")
|
||||||
@@ -227,7 +353,7 @@ async def _require_admin(message: Message) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def _require_admin_callback(callback: CallbackQuery) -> bool:
|
async def _require_admin_callback(callback: CallbackQuery) -> bool:
|
||||||
if _is_admin_user(_callback_sender_id(callback)):
|
if await _is_admin_user(_callback_sender_id(callback)):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
await callback.answer("Только для администраторов", show_alert=True)
|
await callback.answer("Только для администраторов", show_alert=True)
|
||||||
@@ -305,6 +431,49 @@ async def _deliver_text(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _deliver_result(
|
||||||
|
target: Message | CallbackQuery,
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
back_callback: str,
|
||||||
|
is_admin: bool,
|
||||||
|
admin_mode: bool = False,
|
||||||
|
) -> None:
|
||||||
|
pages = _paginate_text(text)
|
||||||
|
if len(pages) == 1:
|
||||||
|
await _deliver_text(
|
||||||
|
target,
|
||||||
|
pages[0],
|
||||||
|
reply_markup=_result_keyboard(
|
||||||
|
back_callback=back_callback,
|
||||||
|
is_admin=is_admin,
|
||||||
|
admin_mode=admin_mode,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
token = _store_pagination_session(
|
||||||
|
PaginationSession(
|
||||||
|
pages=pages,
|
||||||
|
back_callback=back_callback,
|
||||||
|
is_admin=is_admin,
|
||||||
|
admin_mode=admin_mode,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await _deliver_text(
|
||||||
|
target,
|
||||||
|
pages[0],
|
||||||
|
reply_markup=_result_keyboard(
|
||||||
|
back_callback=back_callback,
|
||||||
|
is_admin=is_admin,
|
||||||
|
admin_mode=admin_mode,
|
||||||
|
page_token=token,
|
||||||
|
page_index=0,
|
||||||
|
total_pages=len(pages),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_subscription_action(
|
async def _handle_subscription_action(
|
||||||
target: Message | CallbackQuery,
|
target: Message | CallbackQuery,
|
||||||
group_name: str,
|
group_name: str,
|
||||||
@@ -346,9 +515,17 @@ async def _run_summary_action(
|
|||||||
target: Message | CallbackQuery,
|
target: Message | CallbackQuery,
|
||||||
loader: Callable[[], Awaitable[str]],
|
loader: Callable[[], Awaitable[str]],
|
||||||
*,
|
*,
|
||||||
reply_markup=None,
|
back_callback: str,
|
||||||
|
is_admin: bool,
|
||||||
|
admin_mode: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
await _deliver_text(target, await loader(), reply_markup=reply_markup)
|
await _deliver_result(
|
||||||
|
target,
|
||||||
|
await loader(),
|
||||||
|
back_callback=back_callback,
|
||||||
|
is_admin=is_admin,
|
||||||
|
admin_mode=admin_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _ownership_text() -> str:
|
async def _ownership_text() -> str:
|
||||||
@@ -415,7 +592,7 @@ async def _mute_rules_text() -> str:
|
|||||||
|
|
||||||
@router.message(Command("start"))
|
@router.message(Command("start"))
|
||||||
async def cmd_start(message: Message) -> None:
|
async def cmd_start(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await message.answer(
|
||||||
_help_text(is_admin),
|
_help_text(is_admin),
|
||||||
reply_markup=help_home_keyboard(is_admin),
|
reply_markup=help_home_keyboard(is_admin),
|
||||||
@@ -425,7 +602,7 @@ async def cmd_start(message: Message) -> None:
|
|||||||
|
|
||||||
@router.message(Command("help"))
|
@router.message(Command("help"))
|
||||||
async def cmd_help(message: Message) -> None:
|
async def cmd_help(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await message.answer(
|
||||||
_help_text(is_admin),
|
_help_text(is_admin),
|
||||||
reply_markup=help_home_keyboard(is_admin),
|
reply_markup=help_home_keyboard(is_admin),
|
||||||
@@ -449,7 +626,7 @@ async def cmd_admin(message: Message) -> None:
|
|||||||
async def cb_help_actions(callback: CallbackQuery) -> None:
|
async def cb_help_actions(callback: CallbackQuery) -> None:
|
||||||
data = callback.data or ""
|
data = callback.data or ""
|
||||||
action = data.removeprefix("help:")
|
action = data.removeprefix("help:")
|
||||||
is_admin = _is_admin_user(_callback_sender_id(callback))
|
is_admin = await _is_admin_user(_callback_sender_id(callback))
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
if action == "open":
|
if action == "open":
|
||||||
@@ -491,42 +668,48 @@ async def cb_help_actions(callback: CallbackQuery) -> None:
|
|||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_digest(refresh=True),
|
lambda: build_digest(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "today":
|
if action == "today":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_today_summary(refresh=True),
|
lambda: build_today_summary(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "top":
|
if action == "top":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_top_issues(refresh=True),
|
lambda: build_top_issues(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "stale":
|
if action == "stale":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_stale_issues(refresh=True),
|
lambda: build_stale_issues(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "releases":
|
if action == "releases":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_release_summary(refresh=True),
|
lambda: build_release_summary(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:releases", is_admin),
|
back_callback="help:menu:releases",
|
||||||
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "sync_status":
|
if action == "sync_status":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
build_sync_status,
|
build_sync_status,
|
||||||
reply_markup=help_result_keyboard("help:open", is_admin),
|
back_callback="help:open",
|
||||||
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action.startswith("sub:"):
|
if action.startswith("sub:"):
|
||||||
@@ -584,96 +767,169 @@ async def cb_admin_actions(callback: CallbackQuery) -> None:
|
|||||||
reply_markup=admin_overview_keyboard(),
|
reply_markup=admin_overview_keyboard(),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
if action == "admins":
|
||||||
|
await _deliver_result(
|
||||||
|
callback,
|
||||||
|
await _admins_text(),
|
||||||
|
back_callback="admin:open",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
if action == "sync":
|
if action == "sync":
|
||||||
summary = await run_manual_sync()
|
summary = await run_manual_sync()
|
||||||
await _show_callback_screen(
|
await _deliver_result(
|
||||||
callback,
|
callback,
|
||||||
_sync_summary_text(summary),
|
_sync_summary_text(summary),
|
||||||
reply_markup=admin_result_keyboard("admin:menu:sync"),
|
back_callback="admin:menu:sync",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "sync_status":
|
if action == "sync_status":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
build_sync_status,
|
build_sync_status,
|
||||||
reply_markup=admin_result_keyboard("admin:menu:sync"),
|
back_callback="admin:menu:sync",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "ownership":
|
if action == "ownership":
|
||||||
await _show_callback_screen(
|
await _deliver_result(
|
||||||
callback,
|
callback,
|
||||||
await _ownership_text(),
|
await _ownership_text(),
|
||||||
reply_markup=admin_result_keyboard("admin:open"),
|
back_callback="admin:open",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "mute_list":
|
if action == "mute_list":
|
||||||
await _show_callback_screen(
|
await _deliver_result(
|
||||||
callback,
|
callback,
|
||||||
await _mute_rules_text(),
|
await _mute_rules_text(),
|
||||||
reply_markup=admin_result_keyboard("admin:open"),
|
back_callback="admin:open",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "releases":
|
if action == "releases":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_release_summary(refresh=True),
|
lambda: build_release_summary(refresh=True),
|
||||||
reply_markup=admin_result_keyboard("admin:menu:overview"),
|
back_callback="admin:menu:overview",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "today":
|
if action == "today":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_today_summary(refresh=True),
|
lambda: build_today_summary(refresh=True),
|
||||||
reply_markup=admin_result_keyboard("admin:menu:overview"),
|
back_callback="admin:menu:overview",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "week":
|
if action == "week":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_digest(refresh=True),
|
lambda: build_digest(refresh=True),
|
||||||
reply_markup=admin_result_keyboard("admin:menu:overview"),
|
back_callback="admin:menu:overview",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "top":
|
if action == "top":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_top_issues(refresh=True),
|
lambda: build_top_issues(refresh=True),
|
||||||
reply_markup=admin_result_keyboard("admin:menu:overview"),
|
back_callback="admin:menu:overview",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "stale":
|
if action == "stale":
|
||||||
await _run_summary_action(
|
await _run_summary_action(
|
||||||
callback,
|
callback,
|
||||||
lambda: build_stale_issues(refresh=True),
|
lambda: build_stale_issues(refresh=True),
|
||||||
reply_markup=admin_result_keyboard("admin:menu:overview"),
|
back_callback="admin:menu:overview",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if action == "guide":
|
if action == "guide":
|
||||||
await _show_callback_screen(
|
await _deliver_result(
|
||||||
callback,
|
callback,
|
||||||
_admin_guide_text(),
|
_admin_guide_text(),
|
||||||
reply_markup=admin_result_keyboard("admin:open"),
|
back_callback="admin:open",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("page:"))
|
||||||
|
async def cb_paginated_results(callback: CallbackQuery) -> None:
|
||||||
|
payload = (callback.data or "").split(":")
|
||||||
|
if len(payload) != 3:
|
||||||
|
await callback.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
_, token, raw_index = payload
|
||||||
|
session = PAGINATION_SESSIONS.get(token)
|
||||||
|
if session is None:
|
||||||
|
await callback.answer("Эта навигация устарела. Открой раздел заново.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if session.admin_mode and not await _is_admin_user(_callback_sender_id(callback)):
|
||||||
|
await callback.answer("Только для администраторов", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not raw_index.isdigit():
|
||||||
|
await callback.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
page_index = int(raw_index)
|
||||||
|
if page_index < 0 or page_index >= len(session.pages):
|
||||||
|
await callback.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.answer()
|
||||||
|
PAGINATION_SESSIONS.move_to_end(token)
|
||||||
|
await _show_callback_screen(
|
||||||
|
callback,
|
||||||
|
session.pages[page_index],
|
||||||
|
reply_markup=_result_keyboard(
|
||||||
|
back_callback=session.back_callback,
|
||||||
|
is_admin=session.is_admin,
|
||||||
|
admin_mode=session.admin_mode,
|
||||||
|
page_token=token,
|
||||||
|
page_index=page_index,
|
||||||
|
total_pages=len(session.pages),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command("week"))
|
@router.message(Command("week"))
|
||||||
async def cmd_week(message: Message) -> None:
|
async def cmd_week(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_digest(refresh=True),
|
await build_digest(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command("today"))
|
@router.message(Command("today"))
|
||||||
async def cmd_today(message: Message) -> None:
|
async def cmd_today(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_today_summary(refresh=True),
|
await build_today_summary(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -684,41 +940,45 @@ async def cmd_project(message: Message) -> None:
|
|||||||
await message.answer("Использование: /project <slug>")
|
await message.answer("Использование: /project <slug>")
|
||||||
return
|
return
|
||||||
|
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_project_summary(args[1].strip(), refresh=True),
|
await build_project_summary(args[1].strip(), refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command("top"))
|
@router.message(Command("top"))
|
||||||
async def cmd_top(message: Message) -> None:
|
async def cmd_top(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_top_issues(refresh=True),
|
await build_top_issues(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command("stale"))
|
@router.message(Command("stale"))
|
||||||
async def cmd_stale(message: Message) -> None:
|
async def cmd_stale(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_stale_issues(refresh=True),
|
await build_stale_issues(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:monitoring", is_admin),
|
back_callback="help:menu:monitoring",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command("releases"))
|
@router.message(Command("releases"))
|
||||||
async def cmd_releases(message: Message) -> None:
|
async def cmd_releases(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_release_summary(refresh=True),
|
await build_release_summary(refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:releases", is_admin),
|
back_callback="help:menu:releases",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -729,21 +989,23 @@ async def cmd_release(message: Message) -> None:
|
|||||||
await message.answer("Использование: /release <version>")
|
await message.answer("Использование: /release <version>")
|
||||||
return
|
return
|
||||||
|
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_release_detail(args[1].strip(), refresh=True),
|
await build_release_detail(args[1].strip(), refresh=True),
|
||||||
reply_markup=help_result_keyboard("help:menu:releases", is_admin),
|
back_callback="help:menu:releases",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command("sync_status"))
|
@router.message(Command("sync_status"))
|
||||||
async def cmd_sync_status(message: Message) -> None:
|
async def cmd_sync_status(message: Message) -> None:
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await build_sync_status(),
|
await build_sync_status(),
|
||||||
reply_markup=help_result_keyboard("help:open", is_admin),
|
back_callback="help:open",
|
||||||
disable_web_page_preview=True,
|
is_admin=is_admin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -753,10 +1015,78 @@ async def cmd_sync(message: Message) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
summary = await run_manual_sync()
|
summary = await run_manual_sync()
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
_sync_summary_text(summary),
|
_sync_summary_text(summary),
|
||||||
reply_markup=admin_result_keyboard("admin:menu:sync"),
|
back_callback="admin:menu:sync",
|
||||||
disable_web_page_preview=True,
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("admins"))
|
||||||
|
async def cmd_admins(message: Message) -> None:
|
||||||
|
if not await _require_admin(message):
|
||||||
|
return
|
||||||
|
|
||||||
|
await _deliver_result(
|
||||||
|
message,
|
||||||
|
await _admins_text(),
|
||||||
|
back_callback="admin:open",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("admin_add"))
|
||||||
|
async def cmd_admin_add(message: Message) -> None:
|
||||||
|
if not await _require_admin(message):
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = _extract_target_user_id(message)
|
||||||
|
if user_id is None:
|
||||||
|
await message.answer(
|
||||||
|
"Использование: /admin_add <user_id> или ответом на сообщение пользователя."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
added = await add_admin(user_id)
|
||||||
|
text = (
|
||||||
|
f"Администратор <code>{user_id}</code> добавлен."
|
||||||
|
if added
|
||||||
|
else f"Пользователь <code>{user_id}</code> уже есть среди runtime-администраторов."
|
||||||
|
)
|
||||||
|
await _deliver_result(
|
||||||
|
message,
|
||||||
|
text,
|
||||||
|
back_callback="admin:open",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("admin_del"))
|
||||||
|
async def cmd_admin_del(message: Message) -> None:
|
||||||
|
if not await _require_admin(message):
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = _extract_target_user_id(message)
|
||||||
|
if user_id is None:
|
||||||
|
await message.answer("Использование: /admin_del <user_id>")
|
||||||
|
return
|
||||||
|
|
||||||
|
removed = await remove_admin(user_id)
|
||||||
|
text = (
|
||||||
|
f"Runtime-администратор <code>{user_id}</code> удалён."
|
||||||
|
if removed
|
||||||
|
else f"Runtime-администратор <code>{user_id}</code> не найден."
|
||||||
|
)
|
||||||
|
await _deliver_result(
|
||||||
|
message,
|
||||||
|
text,
|
||||||
|
back_callback="admin:open",
|
||||||
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -767,7 +1097,7 @@ async def cmd_subscribe(message: Message) -> None:
|
|||||||
await message.answer("Использование: /subscribe <backend|frontend>")
|
await message.answer("Использование: /subscribe <backend|frontend>")
|
||||||
return
|
return
|
||||||
|
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await _handle_subscription_action(
|
await _handle_subscription_action(
|
||||||
message,
|
message,
|
||||||
args[1].strip().lower(),
|
args[1].strip().lower(),
|
||||||
@@ -784,7 +1114,7 @@ async def cmd_unsubscribe(message: Message) -> None:
|
|||||||
await message.answer("Использование: /unsubscribe <backend|frontend>")
|
await message.answer("Использование: /unsubscribe <backend|frontend>")
|
||||||
return
|
return
|
||||||
|
|
||||||
is_admin = _is_admin_user(_sender_id(message))
|
is_admin = await _is_admin_user(_sender_id(message))
|
||||||
await _handle_subscription_action(
|
await _handle_subscription_action(
|
||||||
message,
|
message,
|
||||||
args[1].strip().lower(),
|
args[1].strip().lower(),
|
||||||
@@ -799,10 +1129,12 @@ async def cmd_ownership(message: Message) -> None:
|
|||||||
if not await _require_admin(message):
|
if not await _require_admin(message):
|
||||||
return
|
return
|
||||||
|
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await _ownership_text(),
|
await _ownership_text(),
|
||||||
reply_markup=admin_result_keyboard("admin:open"),
|
back_callback="admin:open",
|
||||||
disable_web_page_preview=True,
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -897,10 +1229,12 @@ async def cmd_mute_list(message: Message) -> None:
|
|||||||
if not await _require_admin(message):
|
if not await _require_admin(message):
|
||||||
return
|
return
|
||||||
|
|
||||||
await message.answer(
|
await _deliver_result(
|
||||||
|
message,
|
||||||
await _mute_rules_text(),
|
await _mute_rules_text(),
|
||||||
reply_markup=admin_result_keyboard("admin:open"),
|
back_callback="admin:open",
|
||||||
disable_web_page_preview=True,
|
is_admin=True,
|
||||||
|
admin_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,26 @@ from aiogram.types import InlineKeyboardMarkup
|
|||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
|
||||||
|
def _add_pagination_row(
|
||||||
|
builder: InlineKeyboardBuilder,
|
||||||
|
*,
|
||||||
|
page_token: str | None,
|
||||||
|
page_index: int,
|
||||||
|
total_pages: int,
|
||||||
|
) -> None:
|
||||||
|
if not page_token or total_pages <= 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
if page_index > 0:
|
||||||
|
builder.button(text="◀", callback_data=f"page:{page_token}:{page_index - 1}")
|
||||||
|
builder.button(
|
||||||
|
text=f"{page_index + 1}/{total_pages}",
|
||||||
|
callback_data=f"page:{page_token}:{page_index}",
|
||||||
|
)
|
||||||
|
if page_index + 1 < total_pages:
|
||||||
|
builder.button(text="▶", callback_data=f"page:{page_token}:{page_index + 1}")
|
||||||
|
|
||||||
|
|
||||||
def help_home_keyboard(is_admin: bool) -> InlineKeyboardMarkup:
|
def help_home_keyboard(is_admin: bool) -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="Обзор и метрики", callback_data="help:menu:monitoring")
|
builder.button(text="Обзор и метрики", callback_data="help:menu:monitoring")
|
||||||
@@ -51,13 +71,32 @@ def help_subscriptions_keyboard(is_admin: bool) -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
def help_result_keyboard(back_callback: str, is_admin: bool) -> InlineKeyboardMarkup:
|
def help_result_keyboard(
|
||||||
|
back_callback: str,
|
||||||
|
is_admin: bool,
|
||||||
|
*,
|
||||||
|
page_token: str | None = None,
|
||||||
|
page_index: int = 0,
|
||||||
|
total_pages: int = 1,
|
||||||
|
) -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
_add_pagination_row(
|
||||||
|
builder,
|
||||||
|
page_token=page_token,
|
||||||
|
page_index=page_index,
|
||||||
|
total_pages=total_pages,
|
||||||
|
)
|
||||||
builder.button(text="Назад", callback_data=back_callback)
|
builder.button(text="Назад", callback_data=back_callback)
|
||||||
builder.button(text="Главное меню", callback_data="help:open")
|
builder.button(text="Главное меню", callback_data="help:open")
|
||||||
if is_admin:
|
if is_admin:
|
||||||
builder.button(text="Админ-панель", callback_data="admin:open")
|
builder.button(text="Админ-панель", callback_data="admin:open")
|
||||||
|
if total_pages > 1:
|
||||||
|
builder.adjust(3, 2, 1)
|
||||||
|
else:
|
||||||
builder.adjust(2, 1)
|
builder.adjust(2, 1)
|
||||||
|
else:
|
||||||
|
if total_pages > 1:
|
||||||
|
builder.adjust(3, 2)
|
||||||
else:
|
else:
|
||||||
builder.adjust(2)
|
builder.adjust(2)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
@@ -67,11 +106,12 @@ def admin_home_keyboard() -> InlineKeyboardMarkup:
|
|||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
builder.button(text="Центр синхронизации", callback_data="admin:menu:sync")
|
builder.button(text="Центр синхронизации", callback_data="admin:menu:sync")
|
||||||
builder.button(text="Сводки и мониторинг", callback_data="admin:menu:overview")
|
builder.button(text="Сводки и мониторинг", callback_data="admin:menu:overview")
|
||||||
|
builder.button(text="Администраторы", callback_data="admin:admins")
|
||||||
builder.button(text="Ownership и topics", callback_data="admin:ownership")
|
builder.button(text="Ownership и topics", callback_data="admin:ownership")
|
||||||
builder.button(text="Mute rules", callback_data="admin:mute_list")
|
builder.button(text="Mute rules", callback_data="admin:mute_list")
|
||||||
builder.button(text="Инструкция", callback_data="admin:guide")
|
builder.button(text="Инструкция", callback_data="admin:guide")
|
||||||
builder.button(text="Пользовательское меню", callback_data="help:open")
|
builder.button(text="Пользовательское меню", callback_data="help:open")
|
||||||
builder.adjust(2, 2, 1, 1)
|
builder.adjust(2, 2, 2, 1)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
@@ -97,10 +137,25 @@ def admin_overview_keyboard() -> InlineKeyboardMarkup:
|
|||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
def admin_result_keyboard(back_callback: str) -> InlineKeyboardMarkup:
|
def admin_result_keyboard(
|
||||||
|
back_callback: str,
|
||||||
|
*,
|
||||||
|
page_token: str | None = None,
|
||||||
|
page_index: int = 0,
|
||||||
|
total_pages: int = 1,
|
||||||
|
) -> InlineKeyboardMarkup:
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
_add_pagination_row(
|
||||||
|
builder,
|
||||||
|
page_token=page_token,
|
||||||
|
page_index=page_index,
|
||||||
|
total_pages=total_pages,
|
||||||
|
)
|
||||||
builder.button(text="Назад", callback_data=back_callback)
|
builder.button(text="Назад", callback_data=back_callback)
|
||||||
builder.button(text="Админ-панель", callback_data="admin:open")
|
builder.button(text="Админ-панель", callback_data="admin:open")
|
||||||
builder.button(text="Пользовательское меню", callback_data="help:open")
|
builder.button(text="Пользовательское меню", callback_data="help:open")
|
||||||
|
if total_pages > 1:
|
||||||
|
builder.adjust(3, 2, 1)
|
||||||
|
else:
|
||||||
builder.adjust(2, 1)
|
builder.adjust(2, 1)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from glitchup_bot.models.admins import BotAdmin
|
||||||
from glitchup_bot.models.base import Base
|
from glitchup_bot.models.base import Base
|
||||||
from glitchup_bot.models.issues import IssueCache
|
from glitchup_bot.models.issues import IssueCache
|
||||||
from glitchup_bot.models.mute_rules import MuteRule
|
from glitchup_bot.models.mute_rules import MuteRule
|
||||||
@@ -11,6 +12,7 @@ from glitchup_bot.models.sync import SyncState
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Base",
|
"Base",
|
||||||
|
"BotAdmin",
|
||||||
"GroupSubscriberOverride",
|
"GroupSubscriberOverride",
|
||||||
"GroupTopicOverride",
|
"GroupTopicOverride",
|
||||||
"IssueCache",
|
"IssueCache",
|
||||||
|
|||||||
14
src/glitchup_bot/models/admins.py
Normal file
14
src/glitchup_bot/models/admins.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import BigInteger, DateTime, Integer, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from glitchup_bot.models.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class BotAdmin(Base):
|
||||||
|
__tablename__ = "bot_admins"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
63
src/glitchup_bot/services/admins.py
Normal file
63
src/glitchup_bot/services/admins.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from glitchup_bot.config import settings
|
||||||
|
from glitchup_bot.models.admins import BotAdmin
|
||||||
|
from glitchup_bot.models.database import get_session_factory
|
||||||
|
|
||||||
|
|
||||||
|
async def list_runtime_admins() -> list[BotAdmin]:
|
||||||
|
async with get_session_factory()() as session:
|
||||||
|
result = await session.execute(select(BotAdmin).order_by(BotAdmin.user_id))
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def list_effective_admins() -> list[tuple[int, str]]:
|
||||||
|
sources_by_user: dict[int, set[str]] = {}
|
||||||
|
|
||||||
|
for user_id in settings.telegram_admin_ids:
|
||||||
|
sources_by_user.setdefault(user_id, set()).add("env")
|
||||||
|
|
||||||
|
for record in await list_runtime_admins():
|
||||||
|
sources_by_user.setdefault(record.user_id, set()).add("runtime")
|
||||||
|
|
||||||
|
return [
|
||||||
|
(user_id, " + ".join(sorted(sources)))
|
||||||
|
for user_id, sources in sorted(sources_by_user.items(), key=lambda item: item[0])
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def is_admin(user_id: int | None) -> bool:
|
||||||
|
if user_id is None:
|
||||||
|
return False
|
||||||
|
if user_id in settings.telegram_admin_ids:
|
||||||
|
return True
|
||||||
|
|
||||||
|
runtime_admins = await list_runtime_admins()
|
||||||
|
if any(record.user_id == user_id for record in runtime_admins):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return not settings.telegram_admin_ids and not runtime_admins
|
||||||
|
|
||||||
|
|
||||||
|
async def add_admin(user_id: int) -> bool:
|
||||||
|
async with get_session_factory()() as session:
|
||||||
|
result = await session.execute(select(BotAdmin).where(BotAdmin.user_id == user_id))
|
||||||
|
record = result.scalar_one_or_none()
|
||||||
|
if record is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
session.add(BotAdmin(user_id=user_id))
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_admin(user_id: int) -> bool:
|
||||||
|
async with get_session_factory()() as session:
|
||||||
|
result = await session.execute(select(BotAdmin).where(BotAdmin.user_id == user_id))
|
||||||
|
record = result.scalar_one_or_none()
|
||||||
|
if record is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await session.delete(record)
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
Reference in New Issue
Block a user