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