"""Main menu (reply keyboard) and in-section navigation (inline keyboard), SPEC §4.0.""" from enum import StrEnum from aiogram import F from aiogram.filters.callback_data import CallbackData from aiogram.types import ReplyKeyboardMarkup from aiogram.utils.keyboard import ReplyKeyboardBuilder from magic_filter import MagicFilter from app.texts import Texts class Section(StrEnum): COMMIT = "commit" STATUS = "status" LOGS = "logs" ALERTS = "alerts" MEMBERS = "members" class NavAction(StrEnum): BACK = "back" CANCEL = "cancel" class Nav(CallbackData, prefix="nav"): # Telegram limits callback_data to 64 bytes: only short ids here, never paths (ARCHITECTURE §2.1). action: NavAction def section_by_button(texts: Texts) -> dict[str, Section]: m = texts.menu return { m.commit: Section.COMMIT, m.status: Section.STATUS, m.logs: Section.LOGS, m.alerts: Section.ALERTS, m.members: Section.MEMBERS, } def free_input(texts: Texts) -> MagicFilter: """Input for a dialog step — anything but a main-menu button or a bot command: those must work from any step, otherwise «🩺 Статус» pressed while the bot waits for a file name becomes the file name.""" return ~F.text.in_(set(section_by_button(texts))) & ~F.text.regexp(r"^/(start|menu|help)(@\w+)?(\s|$)") def main_menu(texts: Texts, *, is_admin: bool) -> ReplyKeyboardMarkup: m = texts.menu kb = ReplyKeyboardBuilder() kb.button(text=m.commit) kb.button(text=m.status) kb.button(text=m.logs) kb.button(text=m.alerts) if is_admin: kb.button(text=m.members) kb.adjust(2, 2, 1) return kb.as_markup(resize_keyboard=True, is_persistent=True, input_field_placeholder=m.placeholder)