Source code for DiscordUtils.paginator

from __future__ import annotations

import asyncio
from typing import Any, Dict, Optional

import discord
from discord.ext import commands, menus
from discord.ext.commands import Paginator as CommandPaginator


[docs]class RoboPages(discord.ui.View): def __init__( self, source: menus.PageSource, *, ctx: commands.Context, check_embeds: bool = True, compact: bool = False, ): super().__init__() self.source: menus.PageSource = source self.check_embeds: bool = check_embeds self.ctx: commands.Context = ctx self.message: Optional[discord.Message] = None self.current_page: int = 0 self.compact: bool = compact self.input_lock = asyncio.Lock() self.clear_items() self.fill_items()
[docs] def fill_items(self) -> None: if not self.compact: self.numbered_page.row = 1 self.stop_pages.row = 1 if self.source.is_paginating(): max_pages = self.source.get_max_pages() use_last_and_first = max_pages is not None and max_pages >= 2 if use_last_and_first: self.add_item(self.go_to_first_page) # type: ignore self.add_item(self.go_to_previous_page) # type: ignore if not self.compact: self.add_item(self.go_to_current_page) # type: ignore self.add_item(self.go_to_next_page) # type: ignore if use_last_and_first: self.add_item(self.go_to_last_page) # type: ignore if not self.compact: self.add_item(self.numbered_page) # type: ignore self.add_item(self.stop_pages) # type: ignore
[docs] async def _get_kwargs_from_page(self, page: int) -> Dict[str, Any]: value = await discord.utils.maybe_coroutine(self.source.format_page, self, page) if isinstance(value, dict): return value if isinstance(value, str): return {"content": value, "embed": None} if isinstance(value, discord.Embed): return {"embed": value, "content": None} return {}
[docs] async def show_page(self, interaction: discord.Interaction, page_number: int) -> None: page = await self.source.get_page(page_number) self.current_page = page_number kwargs = await self._get_kwargs_from_page(page) self._update_labels(page_number) if kwargs: if interaction.response.is_done(): if self.message: await self.message.edit(**kwargs, view=self) else: await interaction.response.edit_message(**kwargs, view=self)
[docs] def _update_labels(self, page_number: int) -> None: self.go_to_first_page.disabled = page_number == 0 if self.compact: max_pages = self.source.get_max_pages() self.go_to_last_page.disabled = (max_pages is None or (page_number + 1) >= max_pages) self.go_to_next_page.disabled = (max_pages is not None and (page_number + 1) >= max_pages) self.go_to_previous_page.disabled = page_number == 0 return self.go_to_current_page.label = str(page_number + 1) self.go_to_previous_page.label = str(page_number) self.go_to_next_page.label = str(page_number + 2) self.go_to_next_page.disabled = False self.go_to_previous_page.disabled = False self.go_to_first_page.disabled = False max_pages = self.source.get_max_pages() if max_pages is not None: self.go_to_last_page.disabled = (page_number + 1) >= max_pages if (page_number + 1) >= max_pages: self.go_to_next_page.disabled = True self.go_to_next_page.label = "…" if page_number == 0: self.go_to_previous_page.disabled = True self.go_to_previous_page.label = "…"
[docs] async def show_checked_page(self, interaction: discord.Interaction, page_number: int) -> None: max_pages = self.source.get_max_pages() try: if max_pages is None: # If it doesn't give maximum pages, it cannot be checked await self.show_page(interaction, page_number) elif max_pages > page_number >= 0: await self.show_page(interaction, page_number) except IndexError: # An error happened that can be handled, so ignore it. pass
[docs] async def interaction_check(self,interaction: discord.Interaction) -> bool: if interaction.user and interaction.user.id in ( self.ctx.bot.owner_id, self.ctx.author.id, ): return True await interaction.response.send_message( "This pagination menu cannot be controlled by you, sorry!", ephemeral=True) return False
[docs] async def on_timeout(self) -> None: if self.message: try: await self.message.edit(view=None) except (discord.HTTPException, discord.Forbidden): pass
[docs] @staticmethod async def on_error(error: Exception, item: discord.ui.Item,interaction: discord.Interaction) -> None: if interaction.response.is_done(): await interaction.followup.send("An unknown error occurred, sorry", ephemeral=True) else: await interaction.response.send_message( "An unknown error occurred, sorry", ephemeral=True)
[docs] async def start(self) -> None: if (self.check_embeds and not self.ctx.channel.permissions_for(self.ctx.me).embed_links): await self.ctx.send( "Bot does not have embed links permission in this channel.") return await self.source._prepare_once() page = await self.source.get_page(0) kwargs = await self._get_kwargs_from_page(page) self._update_labels(0) self.message = await self.ctx.send(**kwargs, view=self)
[docs] @discord.ui.button(label="≪", style=discord.ButtonStyle.grey) async def go_to_first_page(self, button: discord.ui.Button, interaction: discord.Interaction): """Go to the first page""" await self.show_page(interaction, 0)
[docs] @discord.ui.button(label="Back", style=discord.ButtonStyle.blurple) async def go_to_previous_page(self, button: discord.ui.Button, interaction: discord.Interaction): """Go to the previous page""" await self.show_checked_page(interaction, self.current_page - 1)
[docs] @discord.ui.button(label="Current", style=discord.ButtonStyle.grey, disabled=True) async def go_to_current_page(self, button: discord.ui.Button,interaction: discord.Interaction): '''As the name suggests, goes to the current page''' pass
[docs] @discord.ui.button(label="Next", style=discord.ButtonStyle.blurple) async def go_to_next_page(self, button: discord.ui.Button, interaction: discord.Interaction): """Go to the next page""" await self.show_checked_page(interaction, self.current_page + 1)
[docs] @discord.ui.button(label="≫", style=discord.ButtonStyle.grey) async def go_to_last_page(self, button: discord.ui.Button, interaction: discord.Interaction): """Go to the last page""" # The call here is safe because it's guarded by skip_if await self.show_page(interaction, self.source.get_max_pages() - 1)
[docs] @discord.ui.button(label="Skip to page...", style=discord.ButtonStyle.grey) async def numbered_page(self, button: discord.ui.Button, interaction: discord.Interaction): """lets you type a page number to go to""" if self.input_lock.locked(): await interaction.response.send_message( "Already waiting for your response...", ephemeral=True) return if self.message is None: return async with self.input_lock: channel = self.message.channel author_id = interaction.user and interaction.user.id await interaction.response.send_message( "What page do you want to go to?", ephemeral=True) def message_check(m): return (m.author.id == author_id and channel == m.channel and m.content.isdigit()) try: msg = await self.ctx.bot.wait_for("message", check=message_check, timeout=30.0) except asyncio.TimeoutError: await interaction.followup.send("Took too long.", ephemeral=True) await asyncio.sleep(5) else: page = int(msg.content) await msg.delete() await self.show_checked_page(interaction, page - 1)
[docs] @discord.ui.button(label="Quit", style=discord.ButtonStyle.red) async def stop_pages(self, button: discord.ui.Button, interaction: discord.Interaction): """stops the pagination session.""" button.disabled = True await interaction.response.defer() await interaction.delete_original_message() self.stop()
[docs]class FieldPageSource(menus.ListPageSource): """A page source that requires (field_name, field_value) tuple items.""" def __init__(self, entries, *, per_page=12): super().__init__(entries, per_page=per_page) self.embed = discord.Embed(colour=discord.Colour.blurple())
[docs] async def format_page(self, menu, entries): self.embed.clear_fields() self.embed.description = discord.Embed.Empty for key, value in entries: self.embed.add_field(name=key, value=value, inline=False) maximum = self.get_max_pages() if maximum > 1: text = ( f"Page {menu.current_page + 1}/{maximum} ({len(self.entries)} entries)" ) self.embed.set_footer(text=text) return self.embed
[docs]class TextPageSource(menus.ListPageSource): def __init__(self, text, *, prefix="```", suffix="```", max_size=2000): pages = CommandPaginator(prefix=prefix, suffix=suffix, max_size=max_size - 200) for line in text.split("\n"): pages.add_line(line) super().__init__(entries=pages.pages, per_page=1)
[docs] async def format_page(self, menu, content): maximum = self.get_max_pages() if maximum > 1: return f"{content}\nPage {menu.current_page + 1}/{maximum}" return content
[docs] @staticmethod def is_paginating() -> bool: '''This forces the buttons to appear even in the front page''' return True
[docs]class SimplePageSource(menus.ListPageSource):
[docs] async def format_page(self, menu, entries): pages = [] for index, entry in enumerate(entries, start=menu.current_page * self.per_page): pages.append(f"{index + 1}. {entry}") maximum = self.get_max_pages() if maximum > 1: footer = ( f"Page {menu.current_page + 1}/{maximum} ({len(self.entries)} entries)" ) menu.embed.set_footer(text=footer) menu.embed.description = "\n".join(pages) return menu.embed
[docs]class SimplePages(RoboPages): """A simple pagination session reminiscent of the old Pages interface. Basically an embed with some normal formatting. """ def __init__(self, entries, *, ctx: commands.Context, per_page: int = 12): super().__init__(SimplePageSource(entries, per_page=per_page), ctx=ctx, check_embeds=True)
[docs]class EmbedPageSource(menus.ListPageSource):
[docs] @staticmethod async def format_page(menu, entries): return entries
[docs]class EmbedPaginator(RoboPages): '''A simple paginator for the embeds. In entries you provides a list of embeds ''' def __init__(self, entries, *, ctx: commands.Context): super().__init__(EmbedPageSource(entries, per_page=1), ctx=ctx) self.embed = discord.Embed(colour=discord.Colour.blurple())
[docs] @staticmethod def is_paginating() -> bool: '''The pagination is in the paginating state or not. This forces the buttons to appear even in the front page :returns: :class:`bool` ''' return True