from __future__ import annotations import logging import re from pathlib import Path import discord from discord import app_commands import strings as S _PATCHNOTES_PATH = Path(__file__).resolve().parent.parent / "docs" / "PATCHNOTES.md" _VERSION_RE = re.compile(r"^##\s+(.+?)\s*$") _EMBED_DESC_MAX = 4096 _SELECT_OPTIONS_MAX = 25 def _load_versions() -> list[tuple[str, str]]: try: text = _PATCHNOTES_PATH.read_text(encoding="utf-8") except FileNotFoundError: return [] versions: list[tuple[str, str]] = [] cur_header: str | None = None cur_body: list[str] = [] for line in text.splitlines(): m = _VERSION_RE.match(line) if m: if cur_header is not None: versions.append((cur_header, "\n".join(cur_body).strip())) cur_header = m.group(1).strip() cur_body = [] elif cur_header is not None: cur_body.append(line) if cur_header is not None: versions.append((cur_header, "\n".join(cur_body).strip())) return versions def _build_embed(versions: list[tuple[str, str]], idx: int) -> discord.Embed: header, body = versions[idx] if len(body) > _EMBED_DESC_MAX: body = body[: _EMBED_DESC_MAX - 1] + "…" embed = discord.Embed( title=S.PATCHNOTES_UI["title"].format(version=header), description=body or S.PATCHNOTES_UI["empty_version"], color=0x5865F2, ) embed.set_footer( text=S.PATCHNOTES_UI["footer"].format(idx=idx + 1, total=len(versions)) ) return embed def register_info_commands( tree: app_commands.CommandTree, bot: discord.Client, log: logging.Logger, ) -> None: @tree.command(name="patchnotes", description=S.CMD["patchnotes"]) async def cmd_patchnotes(interaction: discord.Interaction): versions = _load_versions() if not versions: await interaction.response.send_message( S.PATCHNOTES_UI["empty_file"], ephemeral=True ) return invoker_id = interaction.user.id class PatchNotesView(discord.ui.View): def __init__(self, idx: int = 0): super().__init__(timeout=180) self.idx = idx self._rebuild() def _rebuild(self): self.clear_items() newer_btn = discord.ui.Button( label=S.PATCHNOTES_UI["btn_newer"], style=discord.ButtonStyle.secondary, disabled=self.idx <= 0, ) older_btn = discord.ui.Button( label=S.PATCHNOTES_UI["btn_older"], style=discord.ButtonStyle.secondary, disabled=self.idx >= len(versions) - 1, ) newer_btn.callback = self._make_step_cb(-1) older_btn.callback = self._make_step_cb(+1) self.add_item(newer_btn) self.add_item(older_btn) opts: list[discord.SelectOption] = [] for i, (hdr, _) in enumerate(versions[:_SELECT_OPTIONS_MAX]): opts.append( discord.SelectOption( label=hdr[:100], value=str(i), default=(i == self.idx), ) ) if len(opts) > 1: select = discord.ui.Select( placeholder=S.PATCHNOTES_UI["select_placeholder"], options=opts, min_values=1, max_values=1, ) select.callback = self._make_select_cb(select) self.add_item(select) def _make_step_cb(self, delta: int): async def _cb(interaction: discord.Interaction): if interaction.user.id != invoker_id: await interaction.response.send_message( S.ERR["not_your_menu"], ephemeral=True ) return self.idx = max(0, min(len(versions) - 1, self.idx + delta)) self._rebuild() await interaction.response.edit_message( embed=_build_embed(versions, self.idx), view=self ) return _cb def _make_select_cb(self, select: discord.ui.Select): async def _cb(interaction: discord.Interaction): if interaction.user.id != invoker_id: await interaction.response.send_message( S.ERR["not_your_menu"], ephemeral=True ) return self.idx = int(select.values[0]) self._rebuild() await interaction.response.edit_message( embed=_build_embed(versions, self.idx), view=self ) return _cb view = PatchNotesView(0) await interaction.response.send_message( embed=_build_embed(versions, 0), view=view, ephemeral=True ) log.info("/patchnotes by %s (%d versions)", interaction.user, len(versions))