From 4a2f8630b1e934a7e77111f5017734c298485f6c Mon Sep 17 00:00:00 2001 From: Amrou Bellalouna Date: Mon, 11 Mar 2024 15:17:54 +0100 Subject: Use paginator from botcore (#1444) --- bot/exts/core/help.py | 17 ++- bot/utils/pagination.py | 313 ++++++++---------------------------------------- 2 files changed, 59 insertions(+), 271 deletions(-) diff --git a/bot/exts/core/help.py b/bot/exts/core/help.py index 7721d200..2960d722 100644 --- a/bot/exts/core/help.py +++ b/bot/exts/core/help.py @@ -11,19 +11,16 @@ from pydis_core.utils.logging import get_logger from bot import constants from bot.bot import Bot -from bot.constants import Emojis from bot.utils.commands import get_command_suggestions from bot.utils.decorators import whitelist_override -from bot.utils.pagination import FIRST_EMOJI, LAST_EMOJI, LEFT_EMOJI, LinePaginator, RIGHT_EMOJI - -DELETE_EMOJI = Emojis.trashcan +from bot.utils.pagination import LinePaginator, PAGINATION_EMOJI REACTIONS = { - FIRST_EMOJI: "first", - LEFT_EMOJI: "back", - RIGHT_EMOJI: "next", - LAST_EMOJI: "end", - DELETE_EMOJI: "stop", + PAGINATION_EMOJI.first: "first", + PAGINATION_EMOJI.left: "back", + PAGINATION_EMOJI.right: "next", + PAGINATION_EMOJI.last: "end", + PAGINATION_EMOJI.delete: "stop", } @@ -236,7 +233,7 @@ class HelpSession: # if single-page else: - self._bot.loop.create_task(self.message.add_reaction(DELETE_EMOJI)) + self._bot.loop.create_task(self.message.add_reaction(PAGINATION_EMOJI.delete)) def _category_key(self, cmd: Command) -> str: """ diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py index b2da37ed..dca08568 100644 --- a/bot/utils/pagination.py +++ b/bot/utils/pagination.py @@ -1,276 +1,66 @@ -from collections.abc import Iterable +from collections.abc import Sequence -from discord import Embed, Member, Reaction +from discord import Embed, Interaction, Member, Message, Reaction from discord.abc import User from discord.ext.commands import Context, Paginator from pydis_core.utils.logging import get_logger +from pydis_core.utils.pagination import EmptyPaginatorEmbedError, LinePaginator as _LinePaginator, PaginationEmojis from bot.constants import Emojis -FIRST_EMOJI = "\u23EE" # [:track_previous:] -LEFT_EMOJI = "\u2B05" # [:arrow_left:] -RIGHT_EMOJI = "\u27A1" # [:arrow_right:] -LAST_EMOJI = "\u23ED" # [:track_next:] -DELETE_EMOJI = Emojis.trashcan # [:trashcan:] - -PAGINATION_EMOJI = (FIRST_EMOJI, LEFT_EMOJI, RIGHT_EMOJI, LAST_EMOJI, DELETE_EMOJI) +PAGINATION_EMOJI = PaginationEmojis(delete=Emojis.trashcan) log = get_logger(__name__) -class EmptyPaginatorEmbedError(Exception): - """Base Exception class for an empty paginator embed.""" - - -class LinePaginator(Paginator): +class LinePaginator(_LinePaginator): """A class that aids in paginating code blocks for Discord messages.""" - def __init__( - self, - prefix: str = "```", - suffix: str = "```", - max_size: int = 2000, - max_lines: int | None = None, - linesep: str = "\n" - ): - """ - Overrides the Paginator.__init__ from inside discord.ext.commands. - - `prefix` and `suffix` will be prepended and appended respectively to every page. - - `max_size` and `max_lines` denote the maximum amount of codepoints and lines - allowed per page. - """ - super().__init__( - prefix, - suffix, - max_size - len(suffix), - linesep - ) - - self.max_lines = max_lines - self._current_page = [prefix] - self._linecount = 0 - self._count = len(prefix) + 1 # prefix + newline - self._pages = [] - - def add_line(self, line: str = "", *, empty: bool = False) -> None: - """ - Adds a line to the current page. - - If the line exceeds the `max_size` then a RuntimeError is raised. - - Overrides the Paginator.add_line from inside discord.ext.commands in order to allow - configuration of the maximum number of lines per page. - - If `empty` is True, an empty line will be placed after the a given `line`. - """ - if len(line) > self.max_size - len(self.prefix) - 2: - raise RuntimeError("Line exceeds maximum page size %s" % (self.max_size - len(self.prefix) - 2)) - - if self.max_lines is not None: - if self._linecount >= self.max_lines: - self._linecount = 0 - self.close_page() - - self._linecount += 1 - if self._count + len(line) + 1 > self.max_size: - self.close_page() - - self._count += len(line) + 1 - self._current_page.append(line) - - if empty: - self._current_page.append("") - self._count += 1 - @classmethod async def paginate( - cls, lines: Iterable[str], ctx: Context, - embed: Embed, prefix: str = "", suffix: str = "", - max_lines: int | None = None, max_size: int = 500, empty: bool = True, - restrict_to_user: User = None, timeout: float = 300, footer_text: str | None = None, - url: str | None = None, exception_on_empty_embed: bool = False - ) -> None: + cls, + lines: list[str], + ctx: Context | Interaction, + embed: Embed, + prefix: str = "", + suffix: str = "", + max_lines: int | None = None, + max_size: int = 500, + scale_to_size: int = 4000, + empty: bool = True, + restrict_to_user: User | None = None, + timeout: int = 300, + footer_text: str | None = None, + url: str | None = None, + exception_on_empty_embed: bool = False, + reply: bool = False, + allowed_roles: Sequence[int] | None = None, + ) -> Message | None: """ Use a paginator and set of reactions to provide pagination over a set of lines. - The reactions are used to switch page, or to finish with pagination. - - When used, this will send a message using `ctx.send()` and apply a set of reactions to it. - These reactions may be used to change page, or to remove pagination from the message. - - Pagination will also be removed automatically if no reaction is added for `timeout` seconds, - defaulting to five minutes (300 seconds). - - If `empty` is True, an empty line will be placed between each given line. - - >>> embed = Embed() - >>> embed.set_author(name="Some Operation", url=url, icon_url=icon) - >>> await LinePaginator.paginate( - ... (line for line in lines), - ... ctx, embed - ... ) + Acts as a wrapper for the super class' `paginate` method to provide the pagination emojis by default. + Consult the super class's `paginate` method for detailed information. """ - def event_check(reaction_: Reaction, user_: Member) -> bool: - """Make sure that this reaction is what we want to operate on.""" - no_restrictions = ( - # Pagination is not restricted - not restrict_to_user - # The reaction was by a whitelisted user - or user_.id == restrict_to_user.id - ) - - return ( - # Conditions for a successful pagination: - all(( - # Reaction is on this message - reaction_.message.id == message.id, - # Reaction is one of the pagination emotes - str(reaction_.emoji) in PAGINATION_EMOJI, # Note: DELETE_EMOJI is a string and not unicode - # Reaction was not made by the Bot - user_.id != ctx.bot.user.id, - # There were no restrictions - no_restrictions - )) - ) - - paginator = cls(prefix=prefix, suffix=suffix, max_size=max_size, max_lines=max_lines) - current_page = 0 - - if not lines: - if exception_on_empty_embed: - log.exception("Pagination asked for empty lines iterable") - raise EmptyPaginatorEmbedError("No lines to paginate") - - log.debug("No lines to add to paginator, adding '(nothing to display)' message") - lines.append("(nothing to display)") - - for line in lines: - try: - paginator.add_line(line, empty=empty) - except Exception: - log.exception(f"Failed to add line to paginator: '{line}'") - raise # Should propagate - else: - log.trace(f"Added line to paginator: '{line}'") - - log.debug(f"Paginator created with {len(paginator.pages)} pages") - - embed.description = paginator.pages[current_page] - - if len(paginator.pages) <= 1: - if footer_text: - embed.set_footer(text=footer_text) - log.trace(f"Setting embed footer to '{footer_text}'") - - if url: - embed.url = url - log.trace(f"Setting embed url to '{url}'") - - log.debug("There's less than two pages, so we won't paginate - sending single page on its own") - await ctx.send(embed=embed) - return None - - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - log.trace(f"Setting embed footer to '{embed.footer.text}'") - - if url: - embed.url = url - log.trace(f"Setting embed url to '{url}'") - - log.debug("Sending first page to channel...") - message = await ctx.send(embed=embed) - - log.debug("Adding emoji reactions to message...") - - for emoji in PAGINATION_EMOJI: - # Add all the applicable emoji to the message - log.trace(f"Adding reaction: {emoji!r}") - await message.add_reaction(emoji) - - while True: - try: - reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=event_check) - log.trace(f"Got reaction: {reaction}") - except TimeoutError: - log.debug("Timed out waiting for a reaction") - break # We're done, no reactions for the last 5 minutes - - if str(reaction.emoji) == DELETE_EMOJI: # Note: DELETE_EMOJI is a string and not unicode - log.debug("Got delete reaction") - return await message.delete() - - if reaction.emoji == FIRST_EMOJI: - await message.remove_reaction(reaction.emoji, user) - current_page = 0 - - log.debug(f"Got first page reaction - changing to page 1/{len(paginator.pages)}") - - embed.description = paginator.pages[current_page] - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - await message.edit(embed=embed) - - if reaction.emoji == LAST_EMOJI: - await message.remove_reaction(reaction.emoji, user) - current_page = len(paginator.pages) - 1 - - log.debug(f"Got last page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - - embed.description = paginator.pages[current_page] - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - await message.edit(embed=embed) - - if reaction.emoji == LEFT_EMOJI: - await message.remove_reaction(reaction.emoji, user) - - if current_page <= 0: - log.debug("Got previous page reaction, but we're on the first page - ignoring") - continue - - current_page -= 1 - log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - - embed.description = paginator.pages[current_page] - - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - - await message.edit(embed=embed) - - if reaction.emoji == RIGHT_EMOJI: - await message.remove_reaction(reaction.emoji, user) - - if current_page >= len(paginator.pages) - 1: - log.debug("Got next page reaction, but we're on the last page - ignoring") - continue - - current_page += 1 - log.debug(f"Got next page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") - - embed.description = paginator.pages[current_page] - - if footer_text: - embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") - else: - embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") - - await message.edit(embed=embed) - - log.debug("Ending pagination and clearing reactions...") - await message.clear_reactions() - return None + return await super().paginate( + pagination_emojis=PAGINATION_EMOJI, + lines=lines, + ctx=ctx, + embed=embed, + prefix=prefix, + suffix=suffix, + max_lines=max_lines, + max_size=max_size, + scale_to_size=scale_to_size, + empty=empty, + restrict_to_user=restrict_to_user, + timeout=timeout, + footer_text=footer_text, + url=url, + exception_on_empty_embed=exception_on_empty_embed, + reply=reply, + allowed_roles=allowed_roles, + ) class ImagePaginator(Paginator): @@ -331,7 +121,8 @@ class ImagePaginator(Paginator): # Reaction is on the same message sent reaction_.message.id == message.id, # The reaction is part of the navigation menu - str(reaction_.emoji) in PAGINATION_EMOJI, # Note: DELETE_EMOJI is a string and not unicode + # Note: DELETE_EMOJI is a string and not unicode + str(reaction_.emoji) in PAGINATION_EMOJI.model_dump().values(), # The reactor is not a bot not member.bot )) @@ -364,7 +155,7 @@ class ImagePaginator(Paginator): embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}") message = await ctx.send(embed=embed) - for emoji in PAGINATION_EMOJI: + for emoji in PAGINATION_EMOJI.model_dump().values(): await message.add_reaction(emoji) while True: @@ -379,12 +170,12 @@ class ImagePaginator(Paginator): await message.remove_reaction(reaction.emoji, user) # Delete reaction press - [:trashcan:] - if str(reaction.emoji) == DELETE_EMOJI: # Note: DELETE_EMOJI is a string and not unicode + if str(reaction.emoji) == PAGINATION_EMOJI.delete: # Note: DELETE_EMOJI is a string and not unicode log.debug("Got delete reaction") return await message.delete() # First reaction press - [:track_previous:] - if reaction.emoji == FIRST_EMOJI: + if reaction.emoji == PAGINATION_EMOJI.first: if current_page == 0: log.debug("Got first page reaction, but we're on the first page - ignoring") continue @@ -393,7 +184,7 @@ class ImagePaginator(Paginator): reaction_type = "first" # Last reaction press - [:track_next:] - if reaction.emoji == LAST_EMOJI: + if reaction.emoji == PAGINATION_EMOJI.last: if current_page >= len(paginator.pages) - 1: log.debug("Got last page reaction, but we're on the last page - ignoring") continue @@ -402,7 +193,7 @@ class ImagePaginator(Paginator): reaction_type = "last" # Previous reaction press - [:arrow_left: ] - if reaction.emoji == LEFT_EMOJI: + if reaction.emoji == PAGINATION_EMOJI.left: if current_page <= 0: log.debug("Got previous page reaction, but we're on the first page - ignoring") continue @@ -411,7 +202,7 @@ class ImagePaginator(Paginator): reaction_type = "previous" # Next reaction press - [:arrow_right:] - if reaction.emoji == RIGHT_EMOJI: + if reaction.emoji == PAGINATION_EMOJI.right: if current_page >= len(paginator.pages) - 1: log.debug("Got next page reaction, but we're on the last page - ignoring") continue -- cgit v1.2.3