# SPDX-License-Identifier: MIT
from __future__ import annotations
from typing import List
import discord
from discord.ext.bridge import BridgeContext
from discord.ext.commands import Context
__all__ = (
"PaginatorButton",
"Paginator",
"PageGroup",
"PaginatorMenu",
"Page",
)
[docs]class Page:
"""Represents a page shown in the paginator.
Allows for directly referencing and modifying each page as a class instance.
Parameters
----------
content: :class:`str`
The content of the page. Corresponds to the :class:`discord.Message.content` attribute.
embeds: Optional[List[Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
The embeds of the page. Corresponds to the :class:`discord.Message.embeds` attribute.
files: Optional[List[:class:`discord.File`]]
A list of local files to be shown with the page.
custom_view: Optional[:class:`discord.ui.View`]
The custom view shown when the page is visible. Overrides the `custom_view` attribute of the main paginator.
"""
def __init__(
self,
content: str | None = None,
embeds: list[list[discord.Embed] | discord.Embed] | None = None,
custom_view: discord.ui.View | None = None,
files: list[discord.File] | None = None,
**kwargs,
):
if content is None and embeds is None:
raise discord.InvalidArgument(
"A page cannot have both content and embeds equal to None."
)
self._content = content
self._embeds = embeds or []
self._custom_view = custom_view
self._files = files or []
[docs] async def callback(self, interaction: discord.Interaction | None = None):
"""|coro|
The coroutine associated to a specific page. If `Paginator.page_action()` is used, this coroutine is called.
Parameters
----------
interaction: Optional[:class:`discord.Interaction`]
The interaction associated with the callback, if any.
"""
[docs] def update_files(self) -> list[discord.File] | None:
"""Updates :class:`discord.File` objects so that they can be sent multiple
times. This is called internally each time the page is sent.
"""
for file in self._files:
if file.fp.closed and (fn := getattr(file.fp, "name", None)):
file.fp = open(fn, "rb")
file.reset()
file.fp.close = lambda: None
return self._files
@property
def content(self) -> str | None:
"""Gets the content for the page."""
return self._content
@content.setter
def content(self, value: str | None):
"""Sets the content for the page."""
self._content = value
@property
def embeds(self) -> list[list[discord.Embed] | discord.Embed] | None:
"""Gets the embeds for the page."""
return self._embeds
@embeds.setter
def embeds(self, value: list[list[discord.Embed] | discord.Embed] | None):
"""Sets the embeds for the page."""
self._embeds = value
@property
def custom_view(self) -> discord.ui.View | None:
"""Gets the custom view assigned to the page."""
return self._custom_view
@custom_view.setter
def custom_view(self, value: discord.ui.View | None):
"""Assigns a custom view to be shown when the page is displayed."""
self._custom_view = value
@property
def files(self) -> list[discord.File] | None:
"""Gets the files associated with the page."""
return self._files
@files.setter
def files(self, value: list[discord.File] | None):
"""Sets the files associated with the page."""
self._files = value
[docs]class PageGroup:
"""Creates a group of pages which the user can switch between.
Each group of pages can have its own options, custom buttons, custom views, etc.
.. note::
If multiple :class:`PageGroup` objects have different options,
they should all be set explicitly when creating each instance.
Parameters
----------
pages: Union[List[:class:`str`], List[:class:`Page`], List[Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
The list of :class:`Page` objects, strings, embeds, or list of embeds to include in the page group.
label: :class:`str`
The label shown on the corresponding PaginatorMenu dropdown option.
Also used as the SelectOption value.
description: Optional[:class:`str`]
The description shown on the corresponding PaginatorMenu dropdown option.
emoji: Union[:class:`str`, :class:`discord.Emoji`, :class:`discord.PartialEmoji`]
The emoji shown on the corresponding PaginatorMenu dropdown option.
default: Optional[:class:`bool`]
Whether the page group should be the default page group initially shown when the paginator response is sent.
Only one ``PageGroup`` can be the default page group.
show_disabled: :class:`bool`
Whether to show disabled buttons.
show_indicator: :class:`bool`
Whether to show the page indicator when using the default buttons.
author_check: :class:`bool`
Whether only the original user of the command can change pages.
disable_on_timeout: :class:`bool`
Whether the buttons get disabled when the paginator view times out.
use_default_buttons: :class:`bool`
Whether to use the default buttons (i.e. ``first``, ``prev``, ``page_indicator``, ``next``, ``last``)
default_button_row: :class:`int`
The row where the default paginator buttons are displayed. Has no effect if custom buttons are used.
loop_pages: :class:`bool`
Whether to loop the pages when clicking prev/next while at the first/last page in the list.
custom_view: Optional[:class:`discord.ui.View`]
A custom view whose items are appended below the pagination buttons.
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the paginator before no longer accepting input.
custom_buttons: Optional[List[:class:`PaginatorButton`]]
A list of PaginatorButtons to initialize the Paginator with.
If ``use_default_buttons`` is ``True``, this parameter is ignored.
trigger_on_display: :class:`bool`
Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
Has no effect if no callback exists for a `Page`.
"""
def __init__(
self,
pages: (list[str] | list[Page] | list[list[discord.Embed] | discord.Embed]),
label: str,
description: str | None = None,
emoji: str | discord.Emoji | discord.PartialEmoji = None,
default: bool | None = None,
show_disabled: bool | None = None,
show_indicator: bool | None = None,
author_check: bool | None = None,
disable_on_timeout: bool | None = None,
use_default_buttons: bool | None = None,
default_button_row: int = 0,
loop_pages: bool | None = None,
custom_view: discord.ui.View | None = None,
timeout: float | None = None,
custom_buttons: list[PaginatorButton] | None = None,
trigger_on_display: bool | None = None,
):
self.label = label
self.description: str | None = description
self.emoji: str | discord.Emoji | discord.PartialEmoji = emoji
self.pages: (list[str] | list[list[discord.Embed] | discord.Embed]) = pages
self.default: bool | None = default
self.show_disabled = show_disabled
self.show_indicator = show_indicator
self.author_check = author_check
self.disable_on_timeout = disable_on_timeout
self.use_default_buttons = use_default_buttons
self.default_button_row = default_button_row
self.loop_pages = loop_pages
self.custom_view: discord.ui.View = custom_view
self.timeout: float = timeout
self.custom_buttons: list = custom_buttons
self.trigger_on_display = trigger_on_display
[docs]class Paginator(discord.ui.View):
"""Creates a paginator which can be sent as a message and uses buttons for navigation.
Parameters
----------
pages: Union[List[:class:`PageGroup`], List[:class:`Page`], List[:class:`str`], List[Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
The list of :class:`PageGroup` objects, :class:`Page` objects, strings, embeds, or list of embeds to paginate.
If a list of :class:`PageGroup` objects is provided and `show_menu` is ``False``,
only the first page group will be displayed.
show_disabled: :class:`bool`
Whether to show disabled buttons.
show_indicator: :class:`bool`
Whether to show the page indicator when using the default buttons.
show_menu: :class:`bool`
Whether to show a select menu that allows the user to switch between groups of pages.
menu_placeholder: :class:`str`
The placeholder text to show in the page group menu when no page group has been selected yet.
Defaults to "Select Page Group" if not provided.
author_check: :class:`bool`
Whether only the original user of the command can change pages.
disable_on_timeout: :class:`bool`
Whether the buttons get disabled when the paginator view times out.
use_default_buttons: :class:`bool`
Whether to use the default buttons (i.e. ``first``, ``prev``, ``page_indicator``, ``next``, ``last``)
default_button_row: :class:`int`
The row where the default paginator buttons are displayed. Has no effect if custom buttons are used.
loop_pages: :class:`bool`
Whether to loop the pages when clicking prev/next while at the first/last page in the list.
custom_view: Optional[:class:`discord.ui.View`]
A custom view whose items are appended below the pagination components.
If the currently displayed page has a `custom_view` assigned, it will replace these
view components when that page is displayed.
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the paginator before no longer accepting input.
custom_buttons: Optional[List[:class:`PaginatorButton`]]
A list of PaginatorButtons to initialize the Paginator with.
If ``use_default_buttons`` is ``True``, this parameter is ignored.
trigger_on_display: :class:`bool`
Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
Has no effect if no callback exists for a `Page`.
Attributes
----------
menu: Optional[List[:class:`PaginatorMenu`]]
The page group select menu associated with this paginator.
page_groups: Optional[List[:class:`PageGroup`]]
List of :class:`PageGroup` objects the user can switch between.
default_page_group: Optional[:class:`int`]
The index of the default page group shown when the paginator is initially sent.
Defined by setting ``default`` to ``True`` on a :class:`PageGroup`.
current_page: :class:`int`
A zero-indexed value showing the current page number.
page_count: :class:`int`
A zero-indexed value showing the total number of pages.
buttons: Dict[:class:`str`, Dict[:class:`str`, Union[:class:`~PaginatorButton`, :class:`bool`]]]
A dictionary containing the :class:`~PaginatorButton` objects included in this paginator.
user: Optional[Union[:class:`~discord.User`, :class:`~discord.Member`]]
The user or member that invoked the paginator.
message: Union[:class:`~discord.Message`, :class:`~discord.WebhookMessage`]
The message the paginator is attached to.
"""
def __init__(
self,
pages: (
list[PageGroup]
| list[Page]
| list[str]
| list[list[discord.Embed] | discord.Embed]
),
show_disabled: bool = True,
show_indicator=True,
show_menu=False,
menu_placeholder: str = "Select Page Group",
author_check=True,
disable_on_timeout=True,
use_default_buttons=True,
default_button_row: int = 0,
loop_pages=False,
custom_view: discord.ui.View | None = None,
timeout: float | None = 180.0,
custom_buttons: list[PaginatorButton] | None = None,
trigger_on_display: bool | None = None,
) -> None:
super().__init__(timeout=timeout)
self.timeout: float = timeout
self.pages: (
list[PageGroup]
| list[str]
| list[Page]
| list[list[discord.Embed] | discord.Embed]
) = pages
self.current_page = 0
self.menu: PaginatorMenu | None = None
self.show_menu = show_menu
self.menu_placeholder = menu_placeholder
self.page_groups: list[PageGroup] | None = None
self.default_page_group: int = 0
if all(isinstance(pg, PageGroup) for pg in pages):
self.page_groups = self.pages if show_menu else None
if sum(pg.default is True for pg in self.page_groups) > 1:
raise ValueError("Only one PageGroup can be set as the default.")
for pg in self.page_groups:
if pg.default:
self.default_page_group = self.page_groups.index(pg)
break
self.pages: list[Page] = self.get_page_group_content(
self.page_groups[self.default_page_group]
)
self.page_count = max(len(self.pages) - 1, 0)
self.buttons = {}
self.custom_buttons: list = custom_buttons
self.show_disabled = show_disabled
self.show_indicator = show_indicator
self.disable_on_timeout = disable_on_timeout
self.use_default_buttons = use_default_buttons
self.default_button_row = default_button_row
self.loop_pages = loop_pages
self.custom_view: discord.ui.View = custom_view
self.trigger_on_display = trigger_on_display
self.message: discord.Message | discord.WebhookMessage | None = None
if self.custom_buttons and not self.use_default_buttons:
for button in custom_buttons:
self.add_button(button)
elif not self.custom_buttons and self.use_default_buttons:
self.add_default_buttons()
if self.show_menu:
self.add_menu()
self.usercheck = author_check
self.user = None
[docs] async def update(
self,
pages: None
| (
list[PageGroup]
| list[Page]
| list[str]
| list[list[discord.Embed] | discord.Embed]
) = None,
show_disabled: bool | None = None,
show_indicator: bool | None = None,
show_menu: bool | None = None,
author_check: bool | None = None,
menu_placeholder: str | None = None,
disable_on_timeout: bool | None = None,
use_default_buttons: bool | None = None,
default_button_row: int | None = None,
loop_pages: bool | None = None,
custom_view: discord.ui.View | None = None,
timeout: float | None = None,
custom_buttons: list[PaginatorButton] | None = None,
trigger_on_display: bool | None = None,
interaction: discord.Interaction | None = None,
current_page: int = 0,
):
"""Updates the existing :class:`Paginator` instance with the provided options.
Parameters
----------
pages: Optional[Union[List[:class:`PageGroup`], List[:class:`Page`], List[:class:`str`], List[Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]]
The list of :class:`PageGroup` objects, :class:`Page` objects, strings,
embeds, or list of embeds to paginate.
show_disabled: :class:`bool`
Whether to show disabled buttons.
show_indicator: :class:`bool`
Whether to show the page indicator when using the default buttons.
show_menu: :class:`bool`
Whether to show a select menu that allows the user to switch between groups of pages.
author_check: :class:`bool`
Whether only the original user of the command can change pages.
menu_placeholder: :class:`str`
The placeholder text to show in the page group menu when no page group has been selected yet.
Defaults to "Select Page Group" if not provided.
disable_on_timeout: :class:`bool`
Whether the buttons get disabled when the paginator view times out.
use_default_buttons: :class:`bool`
Whether to use the default buttons (i.e. ``first``, ``prev``, ``page_indicator``, ``next``, ``last``)
default_button_row: Optional[:class:`int`]
The row where the default paginator buttons are displayed. Has no effect if custom buttons are used.
loop_pages: :class:`bool`
Whether to loop the pages when clicking prev/next while at the first/last page in the list.
custom_view: Optional[:class:`discord.ui.View`]
A custom view whose items are appended below the pagination components.
timeout: Optional[:class:`float`]
Timeout in seconds from last interaction with the paginator before no longer accepting input.
custom_buttons: Optional[List[:class:`PaginatorButton`]]
A list of PaginatorButtons to initialize the Paginator with.
If ``use_default_buttons`` is ``True``, this parameter is ignored.
trigger_on_display: :class:`bool`
Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
Has no effect if no callback exists for a `Page`.
interaction: Optional[:class:`discord.Interaction`]
The interaction to use when updating the paginator. If not provided, the paginator will be updated
by using its stored :attr:`message` attribute instead.
current_page: :class:`int`
The initial page number to display when updating the paginator.
"""
# Update pages and reset current_page to 0 (default)
self.pages: (
list[PageGroup]
| list[str]
| list[Page]
| list[list[discord.Embed] | discord.Embed]
) = (pages if pages is not None else self.pages)
self.show_menu = show_menu if show_menu is not None else self.show_menu
if pages is not None and all(isinstance(pg, PageGroup) for pg in pages):
self.page_groups = self.pages if self.show_menu else None
if sum(pg.default is True for pg in self.page_groups) > 1:
raise ValueError("Only one PageGroup can be set as the default.")
for pg in self.page_groups:
if pg.default:
self.default_page_group = self.page_groups.index(pg)
break
self.pages: list[Page] = self.get_page_group_content(
self.page_groups[self.default_page_group]
)
self.page_count = max(len(self.pages) - 1, 0)
self.current_page = current_page if current_page <= self.page_count else 0
# Apply config changes, if specified
self.show_disabled = (
show_disabled if show_disabled is not None else self.show_disabled
)
self.show_indicator = (
show_indicator if show_indicator is not None else self.show_indicator
)
self.usercheck = author_check if author_check is not None else self.usercheck
self.menu_placeholder = (
menu_placeholder if menu_placeholder is not None else self.menu_placeholder
)
self.disable_on_timeout = (
disable_on_timeout
if disable_on_timeout is not None
else self.disable_on_timeout
)
self.use_default_buttons = (
use_default_buttons
if use_default_buttons is not None
else self.use_default_buttons
)
self.default_button_row = (
default_button_row
if default_button_row is not None
else self.default_button_row
)
self.loop_pages = loop_pages if loop_pages is not None else self.loop_pages
self.custom_view: discord.ui.View = None if custom_view is None else custom_view
self.timeout: float = timeout if timeout is not None else self.timeout
self.trigger_on_display = (
trigger_on_display
if trigger_on_display is not None
else self.trigger_on_display
)
if custom_buttons and not self.use_default_buttons:
self.buttons = {}
for button in custom_buttons:
self.add_button(button)
else:
self.buttons = {}
self.add_default_buttons()
await self.goto_page(self.current_page, interaction=interaction)
[docs] async def on_timeout(self) -> None:
"""Disables all buttons when the view times out."""
if self.disable_on_timeout:
for item in self.children:
item.disabled = True
page = self.pages[self.current_page]
page = self.get_page_content(page)
files = page.update_files()
await self.message.edit(
view=self,
files=files or [],
attachments=[],
)
[docs] async def disable(
self,
include_custom: bool = False,
page: None | (str | Page | list[discord.Embed] | discord.Embed) = None,
) -> None:
"""Stops the paginator, disabling all of its components.
Parameters
----------
include_custom: :class:`bool`
Whether to disable components added via custom views.
page: Optional[Union[:class:`str`, Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
The page content to show after disabling the paginator.
"""
page = self.get_page_content(page)
for item in self.children:
if (
include_custom
or not self.custom_view
or item not in self.custom_view.children
):
item.disabled = True
if page:
await self.message.edit(
content=page.content,
embeds=page.embeds,
view=self,
)
else:
await self.message.edit(view=self)
[docs] async def cancel(
self,
include_custom: bool = False,
page: None | (str | Page | list[discord.Embed] | discord.Embed) = None,
) -> None:
"""Cancels the paginator, removing all of its components from the message.
Parameters
----------
include_custom: :class:`bool`
Whether to remove components added via custom views.
page: Optional[Union[:class:`str`, Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
The page content to show after canceling the paginator.
"""
items = self.children.copy()
page = self.get_page_content(page)
for item in items:
if (
include_custom
or not self.custom_view
or item not in self.custom_view.children
):
self.remove_item(item)
if page:
await self.message.edit(
content=page.content,
embeds=page.embeds,
view=self,
)
else:
await self.message.edit(view=self)
[docs] async def goto_page(
self, page_number: int = 0, *, interaction: discord.Interaction | None = None
) -> None:
"""Updates the paginator message to show the specified page number.
Parameters
----------
page_number: :class:`int`
The page to display.
.. note::
Page numbers are zero-indexed when referenced internally,
but appear as one-indexed when shown to the user.
interaction: Optional[:class:`discord.Interaction`]
The interaction to use when editing the message. If not provided, the message will be
edited using the paginator's stored :attr:`message` attribute instead.
Returns
-------
:class:`~discord.Message`
The message associated with the paginator.
"""
self.update_buttons()
self.current_page = page_number
if self.show_indicator:
self.buttons["page_indicator"][
"object"
].label = f"{self.current_page + 1}/{self.page_count + 1}"
page = self.pages[page_number]
page = self.get_page_content(page)
if page.custom_view:
self.update_custom_view(page.custom_view)
files = page.update_files()
if interaction:
await interaction.response.defer() # needed to force webhook message edit route for files kwarg support
await interaction.followup.edit_message(
message_id=self.message.id,
content=page.content,
embeds=page.embeds,
attachments=[],
files=files or [],
view=self,
)
else:
await self.message.edit(
content=page.content,
embeds=page.embeds,
attachments=[],
files=files or [],
view=self,
)
if self.trigger_on_display:
await self.page_action(interaction=interaction)
[docs] async def interaction_check(self, interaction: discord.Interaction) -> bool:
if self.usercheck:
return self.user == interaction.user
return True
[docs] def update_custom_view(self, custom_view: discord.ui.View):
"""Updates the custom view shown on the paginator."""
if isinstance(self.custom_view, discord.ui.View):
for item in self.custom_view.children:
self.remove_item(item)
for item in custom_view.children:
self.add_item(item)
[docs] def get_page_group_content(self, page_group: PageGroup) -> list[Page]:
"""Returns a converted list of `Page` objects for the given page group based on the content of its pages."""
return [self.get_page_content(page) for page in page_group.pages]
[docs] @staticmethod
def get_page_content(
page: Page | str | discord.Embed | list[discord.Embed],
) -> Page:
"""Converts a page into a :class:`Page` object based on its content."""
if isinstance(page, Page):
return page
elif isinstance(page, str):
return Page(content=page, embeds=[], files=[])
elif isinstance(page, discord.Embed):
return Page(content=None, embeds=[page], files=[])
elif isinstance(page, discord.File):
return Page(content=None, embeds=[], files=[page])
elif isinstance(page, List):
if all(isinstance(x, discord.Embed) for x in page):
return Page(content=None, embeds=page, files=[])
if all(isinstance(x, discord.File) for x in page):
return Page(content=None, embeds=[], files=page)
else:
raise TypeError("All list items must be embeds or files.")
else:
raise TypeError(
"Page content must be a Page object, string, an embed, a list of"
" embeds, a file, or a list of files."
)
[docs] async def page_action(self, interaction: discord.Interaction | None = None) -> None:
"""Triggers the callback associated with the current page, if any.
Parameters
----------
interaction: Optional[:class:`discord.Interaction`]
The interaction that was used to trigger the page action.
"""
if self.get_page_content(self.pages[self.current_page]).callback:
await self.get_page_content(self.pages[self.current_page]).callback(
interaction=interaction
)
[docs] async def send(
self,
ctx: Context,
target: discord.abc.Messageable | None = None,
target_message: str | None = None,
reference: None
| (discord.Message | discord.MessageReference | discord.PartialMessage) = None,
allowed_mentions: discord.AllowedMentions | None = None,
mention_author: bool | None = None,
delete_after: float | None = None,
) -> discord.Message:
"""Sends a message with the paginated items.
Parameters
----------
ctx: Union[:class:`~discord.ext.commands.Context`]
A command's invocation context.
target: Optional[:class:`~discord.abc.Messageable`]
A target where the paginated message should be sent, if different from the original :class:`Context`
target_message: Optional[:class:`str`]
An optional message shown when the paginator message is sent elsewhere.
reference: Optional[Union[:class:`discord.Message`, :class:`discord.MessageReference`, :class:`discord.PartialMessage`]]
A reference to the :class:`~discord.Message` to which you are replying with the paginator.
This can be created using :meth:`~discord.Message.to_reference` or passed directly as a
:class:`~discord.Message`. You can control whether this mentions the author of the referenced message
using the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions`` or by
setting ``mention_author``.
allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
Controls the mentions being processed in this message. If this is
passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`.
The merging behaviour only overrides attributes that have been explicitly passed
to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`.
If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions`
are used instead.
mention_author: Optional[:class:`bool`]
If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``.
delete_after: Optional[:class:`float`]
If set, deletes the paginator after the specified time.
Returns
-------
:class:`~discord.Message`
The message that was sent with the paginator.
"""
if not isinstance(ctx, Context):
raise TypeError(f"expected Context not {ctx.__class__!r}")
if target is not None and not isinstance(target, discord.abc.Messageable):
raise TypeError(f"expected abc.Messageable not {target.__class__!r}")
if reference is not None and not isinstance(
reference,
(discord.Message, discord.MessageReference, discord.PartialMessage),
):
raise TypeError(
"expected Message, MessageReference, or PartialMessage not"
f" {reference.__class__!r}"
)
if allowed_mentions is not None and not isinstance(
allowed_mentions, discord.AllowedMentions
):
raise TypeError(
f"expected AllowedMentions not {allowed_mentions.__class__!r}"
)
if mention_author is not None and not isinstance(mention_author, bool):
raise TypeError(f"expected bool not {mention_author.__class__!r}")
self.update_buttons()
page = self.pages[self.current_page]
page_content = self.get_page_content(page)
if page_content.custom_view:
self.update_custom_view(page_content.custom_view)
self.user = ctx.author
if target:
if target_message:
await ctx.send(
target_message,
reference=reference,
allowed_mentions=allowed_mentions,
mention_author=mention_author,
)
ctx = target
self.message = await ctx.send(
content=page_content.content,
embeds=page_content.embeds,
files=page_content.files,
view=self,
reference=reference,
allowed_mentions=allowed_mentions,
mention_author=mention_author,
delete_after=delete_after,
)
return self.message
[docs] async def edit(
self,
message: discord.Message,
suppress: bool | None = None,
allowed_mentions: discord.AllowedMentions | None = None,
delete_after: float | None = None,
) -> discord.Message | None:
"""Edits an existing message to replace it with the paginator contents.
.. note::
If invoked from an interaction, you will still need to respond to the interaction.
Parameters
----------
message: :class:`discord.Message`
The message to edit with the paginator.
suppress: :class:`bool`
Whether to suppress embeds for the message. This removes
all the embeds if set to ``True``. If set to ``False``
this brings the embeds back if they were suppressed.
Using this parameter requires :attr:`~.Permissions.manage_messages`.
allowed_mentions: Optional[:class:`~discord.AllowedMentions`]
Controls the mentions being processed in this message. If this is
passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`.
The merging behaviour only overrides attributes that have been explicitly passed
to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`.
If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions`
are used instead.
delete_after: Optional[:class:`float`]
If set, deletes the paginator after the specified time.
Returns
-------
Optional[:class:`discord.Message`]
The message that was edited. Returns ``None`` if the operation failed.
"""
if not isinstance(message, discord.Message):
raise TypeError(f"expected Message not {message.__class__!r}")
self.update_buttons()
page: Page | str | discord.Embed | list[discord.Embed] = self.pages[
self.current_page
]
page_content: Page = self.get_page_content(page)
if page_content.custom_view:
self.update_custom_view(page_content.custom_view)
self.user = message.author
try:
self.message = await message.edit(
content=page_content.content,
embeds=page_content.embeds,
files=page_content.files,
attachments=[],
view=self,
suppress=suppress,
allowed_mentions=allowed_mentions,
delete_after=delete_after,
)
except (discord.NotFound, discord.Forbidden):
pass
return self.message
[docs] async def respond(
self,
interaction: discord.Interaction | BridgeContext,
ephemeral: bool = False,
target: discord.abc.Messageable | None = None,
target_message: str = "Paginator sent!",
) -> discord.Message | discord.WebhookMessage:
"""Sends an interaction response or followup with the paginated items.
Parameters
----------
interaction: Union[:class:`discord.Interaction`, :class:`BridgeContext`]
The interaction or BridgeContext which invoked the paginator.
If passing a BridgeContext object, you cannot make this an ephemeral paginator.
ephemeral: :class:`bool`
Whether the paginator message and its components are ephemeral.
If ``target`` is specified, the ephemeral message content will be ``target_message`` instead.
.. warning::
If your paginator is ephemeral, it cannot have a timeout
longer than 15 minutes (and cannot be persistent).
target: Optional[:class:`~discord.abc.Messageable`]
A target where the paginated message should be sent,
if different from the original :class:`discord.Interaction`
target_message: :class:`str`
The content of the interaction response shown when the paginator message is sent elsewhere.
Returns
-------
Union[:class:`~discord.Message`, :class:`~discord.WebhookMessage`]
The :class:`~discord.Message` or :class:`~discord.WebhookMessage` that was sent with the paginator.
"""
if not isinstance(interaction, (discord.Interaction, BridgeContext)):
raise TypeError(
f"expected Interaction or BridgeContext, not {interaction.__class__!r}"
)
if target is not None and not isinstance(target, discord.abc.Messageable):
raise TypeError(f"expected abc.Messageable not {target.__class__!r}")
if ephemeral and (self.timeout >= 900 or self.timeout is None):
raise ValueError(
"paginator responses cannot be ephemeral if the paginator timeout is 15"
" minutes or greater"
)
self.update_buttons()
page: Page | str | discord.Embed | list[discord.Embed] = self.pages[
self.current_page
]
page_content: Page = self.get_page_content(page)
if page_content.custom_view:
self.update_custom_view(page_content.custom_view)
if isinstance(interaction, discord.Interaction):
self.user = interaction.user
if target:
await interaction.response.send_message(
target_message, ephemeral=ephemeral
)
msg = await target.send(
content=page_content.content,
embeds=page_content.embeds,
files=page_content.files,
view=self,
)
elif interaction.response.is_done():
msg = await interaction.followup.send(
content=page_content.content,
embeds=page_content.embeds,
files=page_content.files,
view=self,
ephemeral=ephemeral,
)
# convert from WebhookMessage to Message reference to bypass
# 15min webhook token timeout (non-ephemeral messages only)
if not ephemeral:
msg = await msg.channel.fetch_message(msg.id)
else:
msg = await interaction.response.send_message(
content=page_content.content,
embeds=page_content.embeds,
files=page_content.files,
view=self,
ephemeral=ephemeral,
)
else:
ctx = interaction
self.user = ctx.author
if target:
await ctx.respond(target_message, ephemeral=ephemeral)
msg = await ctx.send(
content=page_content.content,
embeds=page_content.embeds,
files=page_content.files,
view=self,
)
else:
msg = await ctx.respond(
content=page_content.content,
embeds=page_content.embeds,
files=page_content.files,
view=self,
)
if isinstance(msg, (discord.Message, discord.WebhookMessage)):
self.message = msg
elif isinstance(msg, discord.Interaction):
self.message = await msg.original_response()
return self.message