Compare commits

..

7 Commits

Author SHA1 Message Date
Maxikinz
3201026242
Update paginator.py 2021-10-09 22:16:57 +02:00
Maxikinz
f7398c9acd
Fix paginator.py typo 2021-10-09 21:00:58 +02:00
Maxikinz
d06bfce837
Include minimal working example 2021-10-09 20:57:02 +02:00
Maxikinz
cb782ce3d8
Update paginator.py 2021-10-09 20:40:44 +02:00
Maxikinz
fae85d072f
Black paginator.py with 120 char limit per line 2021-10-09 20:37:26 +02:00
Maxikinz
c74fd90a9c
Update paginator.py 2021-10-09 20:29:00 +02:00
Maxikinz
7228ef64a5
Create a working button paginator
Simple Embed paginator that I've been using for a while. Thought it could help others too.
2021-10-09 19:03:16 +02:00
12 changed files with 240 additions and 394 deletions

View File

@ -55,7 +55,6 @@ __all__ = (
"InteractionType", "InteractionType",
"InteractionResponseType", "InteractionResponseType",
"NSFWLevel", "NSFWLevel",
"ProtocolURL",
) )
@ -530,7 +529,6 @@ class InteractionType(Enum):
ping = 1 ping = 1
application_command = 2 application_command = 2
component = 3 component = 3
application_command_autocomplete = 4
class InteractionResponseType(Enum): class InteractionResponseType(Enum):
@ -541,7 +539,6 @@ class InteractionResponseType(Enum):
deferred_channel_message = 5 # (with source) deferred_channel_message = 5 # (with source)
deferred_message_update = 6 # for components deferred_message_update = 6 # for components
message_update = 7 # for components message_update = 7 # for components
application_command_autocomplete_result = 8
class VideoQualityMode(Enum): class VideoQualityMode(Enum):
@ -593,74 +590,6 @@ class NSFWLevel(Enum, comparable=True):
age_restricted = 3 age_restricted = 3
class ProtocolURL(Enum):
# General
home = "discord://-/channels/@me/"
nitro = "discord://-/store"
apps = "discord://-/apps" # Breaks the client on windows (Shows download links for different OS)
guild_discovery = "discord://-/guild-discovery"
guild_create = "discord://-/guilds/create"
guild_invite = "discord://-/invite/{invite_code}"
# Settings
account_settings = "discord://-/settings/account"
profile_settings = "discord://-/settings/profile-customization"
privacy_settings = "discord://-/settings/privacy-and-safety"
safety_settings = "discord://-/settings/privacy-and-safety" # Alias
authorized_apps_settings = "discord://-/settings/authorized-apps"
connections_settings = "discord://-/settings/connections"
nitro_settings = "discord://-/settings/premium" # Same as store, but inside of settings
guild_premium_subscription = "discord://-/settings/premium-guild-subscription"
subscription_settings = "discord://-/settings/subscriptions"
gift_inventory_settings = "discord://-/settings/inventory"
billing_settings = "discord://-/settings/billing"
appearance_settings = "discord://-/settings/appearance"
accessibility_settings = "discord://-/settings/accessibility"
voice_video_settings = "discord://-/settings/voice"
text_images_settings = "discord://-/settings/text"
notifications_settings = "discord://-/settings/notifications"
keybinds_settings = "discord://-/settings/keybinds"
language_settings = "discord://-/settings/locale"
windows_settings = "discord://-/settings/windows" # Doesnt work if used on wrong platform
linux_settings = "discord://-/settings/linux" # Doesnt work if used on wrong platform
streamer_mode_settings = "discord://-/settings/streamer-mode"
advanced_settings = "discord://-/settings/advanced"
activity_status_settings = "discord://-/settings/activity-status"
game_overlay_settings = "discord://-/settings/overlay"
hypesquad_settings = "discord://-/settings/hypesquad-online"
changelogs = "discord://-/settings/changelogs"
# Doesn't work if you don't have it actually activated. Just blank screen.
experiments = "discord://-/settings/experiments"
developer_options = "discord://-/settings/developer-options" # Same as experiments
hotspot_options = "discord://-/settings/hotspot-options" # Same as experiments
# Users, Guilds, and DMs
user_profile = "discord://-/users/{user_id}"
dm_channel = "discord://-/channels/@me/{channel_id}"
dm_message = "discord://-/channels/@me/{channel_id}/{message_id}"
guild_channel = "discord://-/channels/{guild_id}/{channel_id}"
guild_message = "discord://-/channels/{guild_id}/{channel_id}/{message_id}"
guild_membership_screening = "discord://-/member-verification/{guild_id}"
# Library
games_library = "discord://-/library"
library_settings = "discord://-/library/settings"
def __str__(self) -> str:
return self.value
def format(self, **kwargs: Any) -> str:
return self.value.format(**kwargs)
T = TypeVar("T") T = TypeVar("T")

View File

@ -188,17 +188,16 @@ def _is_submodule(parent: str, child: str) -> bool:
def _unwrap_slash_groups( def _unwrap_slash_groups(
data: ApplicationCommandInteractionData, data: ApplicationCommandInteractionData,
) -> Tuple[str, Dict[str, ApplicationCommandInteractionDataOption]]: ) -> Tuple[str, List[ApplicationCommandInteractionDataOption]]:
command_name = data["name"] command_name = data["name"]
command_options: Any = data.get("options") or [] command_options = data.get("options") or []
while True: while any(o["type"] in {1, 2} for o in command_options): # type: ignore
try: for option in command_options: # type: ignore
option = next(o for o in command_options if o["type"] in {1, 2}) if option["type"] in {1, 2}: # type: ignore
except StopIteration: command_name += f' {option["name"]}' # type: ignore
return command_name, {o["name"]: o for o in command_options} command_options = option.get("options") or []
else:
command_name += f' {option["name"]}' return command_name, command_options
command_options = option.get("options") or []
def _quote_string_safe(string: str) -> str: def _quote_string_safe(string: str) -> str:
@ -1300,7 +1299,7 @@ class BotBase(GroupMixin):
for name, param in command.clean_params.items(): for name, param in command.clean_params.items():
if inspect.isclass(param.annotation) and issubclass(param.annotation, FlagConverter): if inspect.isclass(param.annotation) and issubclass(param.annotation, FlagConverter):
for name, flag in param.annotation.get_flags().items(): for name, flag in param.annotation.get_flags().items():
option = command_options.get(name) option = next((o for o in command_options if o["name"] == name), None)
if option is None: if option is None:
if flag.required: if flag.required:
@ -1311,7 +1310,7 @@ class BotBase(GroupMixin):
message.content += f" {prefix}{name}{delimiter}{option['value']}" # type: ignore message.content += f" {prefix}{name}{delimiter}{option['value']}" # type: ignore
continue continue
option = command_options.get(name) option = next((o for o in command_options if o["name"] == name), None)
if option is None: if option is None:
if param.default is param.empty and not command._is_typing_optional(param.annotation): if param.default is param.empty and not command._is_typing_optional(param.annotation):
raise errors.MissingRequiredArgument(param) raise errors.MissingRequiredArgument(param)

View File

@ -22,12 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
import asyncio
import inspect import inspect
import re import re
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, Generic, List, Literal, NoReturn, Optional, TYPE_CHECKING, TypeVar, Union, overload from typing import Any, Dict, Generic, List, Literal, Optional, TYPE_CHECKING, TypeVar, Union, overload
import discord.abc import discord.abc
import discord.utils import discord.utils
@ -156,7 +155,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
self.command_failed: bool = command_failed self.command_failed: bool = command_failed
self.current_parameter: Optional[inspect.Parameter] = current_parameter self.current_parameter: Optional[inspect.Parameter] = current_parameter
self._ignored_params: List[inspect.Parameter] = [] self._ignored_params: List[inspect.Parameter] = []
self._typing_task: Optional[asyncio.Task[NoReturn]] = None
self._state: ConnectionState = self.message._state self._state: ConnectionState = self.message._state
async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T: async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
@ -457,10 +455,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
In a normal context, it always returns a :class:`.Message` In a normal context, it always returns a :class:`.Message`
""" """
if self._typing_task is not None:
self._typing_task.cancel()
self._typing_task = None
if self.interaction is None or ( if self.interaction is None or (
self.interaction.response.responded_at is not None self.interaction.response.responded_at is not None
and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15) and discord.utils.utcnow() - self.interaction.response.responded_at >= timedelta(minutes=15)
@ -506,30 +500,3 @@ class Context(discord.abc.Messageable, Generic[BotT]):
self, content: Optional[str] = None, return_message: bool = True, **kwargs: Any self, content: Optional[str] = None, return_message: bool = True, **kwargs: Any
) -> Optional[Union[Message, WebhookMessage]]: ) -> Optional[Union[Message, WebhookMessage]]:
return await self.send(content, return_message=return_message, reference=self.message, **kwargs) # type: ignore return await self.send(content, return_message=return_message, reference=self.message, **kwargs) # type: ignore
async def defer(self, *, ephemeral: bool = False, trigger_typing: bool = True) -> None:
"""|coro|
Defers the Slash Command interaction if ran in a slash command **or**
Loops triggering ``Bot is typing`` in the channel if run in a message command.
Parameters
------------
trigger_typing: :class:`bool`
Indicates whether to trigger typing in a message command.
ephemeral: :class:`bool`
Indicates whether the deferred message will eventually be ephemeral in a slash command.
"""
if self.interaction is None:
if self._typing_task is None and trigger_typing:
async def typing_task():
while True:
await self.trigger_typing()
await asyncio.sleep(10)
self._typing_task = self.bot.loop.create_task(typing_task())
else:
await self.interaction.response.defer(ephemeral=ephemeral)

View File

@ -1032,8 +1032,6 @@ class Option(Generic[T, DT]): # type: ignore
The default for this option, overwrites Option during parsing. The default for this option, overwrites Option during parsing.
description: :class:`str` description: :class:`str`
The description for this option, is unpacked to :attr:`.Command.option_descriptions` The description for this option, is unpacked to :attr:`.Command.option_descriptions`
name: :class:`str`
The name of the option. This defaults to the parameter name.
""" """
description: DT description: DT
@ -1041,18 +1039,17 @@ class Option(Generic[T, DT]): # type: ignore
__slots__ = ( __slots__ = (
"default", "default",
"description", "description",
"name",
) )
def __init__( def __init__(self, default: T = inspect.Parameter.empty, *, description: DT) -> None:
self, default: T = inspect.Parameter.empty, *, description: DT, name: str = discord.utils.MISSING
) -> None:
self.description = description self.description = description
self.default = default self.default = default
self.name: str = name
Option: Any if TYPE_CHECKING:
# Terrible workaround for type checking reasons
def Option(default: T = inspect.Parameter.empty, *, description: str) -> T:
...
def _convert_to_bool(argument: str) -> bool: def _convert_to_bool(argument: str) -> bool:

View File

@ -136,14 +136,6 @@ application_option_type_lookup = {
discord.Role: 8, discord.Role: 8,
float: 10, float: 10,
} }
application_option_channel_types = {
discord.VoiceChannel: [2],
discord.TextChannel: [0, 5, 6],
discord.CategoryChannel: [4],
discord.Thread: [10, 11, 12],
discord.StageChannel: [13],
}
if TYPE_CHECKING: if TYPE_CHECKING:
P = ParamSpec("P") P = ParamSpec("P")
@ -174,12 +166,8 @@ def get_signature_parameters(
annotation = parameter.annotation annotation = parameter.annotation
if isinstance(parameter.default, Option): # type: ignore if isinstance(parameter.default, Option): # type: ignore
option = parameter.default option = parameter.default
parameter = parameter.replace(default=option.default)
if option.name is not MISSING:
name = option.name
parameter.replace(name=name)
descriptions[name] = option.description descriptions[name] = option.description
parameter = parameter.replace(default=option.default)
if annotation is parameter.empty: if annotation is parameter.empty:
params[name] = parameter params[name] = parameter
@ -1238,25 +1226,15 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
ctx.command = original ctx.command = original
def _param_to_options( def _param_to_options(
self, name: str, annotation: Any, required: bool, varadic: bool, description: Optional[str] = None self, name: str, annotation: Any, required: bool, varadic: bool
) -> List[Optional[ApplicationCommandInteractionDataOption]]: ) -> List[Optional[ApplicationCommandInteractionDataOption]]:
if description is not None:
self.option_descriptions[name] = description
description = self.option_descriptions[name]
origin = getattr(annotation, "__origin__", None) origin = getattr(annotation, "__origin__", None)
if inspect.isclass(annotation) and issubclass(annotation, FlagConverter): if inspect.isclass(annotation) and issubclass(annotation, FlagConverter):
return [ return [
param param
for name, flag in annotation.get_flags().items() for name, flag in annotation.get_flags().items()
for param in self._param_to_options( for param in self._param_to_options(
name, name, flag.annotation, required=flag.required, varadic=flag.annotation is tuple
flag.annotation,
required=flag.required,
varadic=flag.annotation is tuple,
description=flag.description if flag.description is not MISSING else None,
) )
] ]
@ -1264,16 +1242,15 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
annotation = str annotation = str
origin = None origin = None
if not required and origin is Union and annotation.__args__[-1] is type(None): if not required and origin is not None and len(annotation.__args__) == 2:
# Unpack Optional[T] (Union[T, None]) into just T # Unpack Optional[T] (Union[T, None]) into just T
annotation = annotation.__args__[0] annotation, origin = annotation.__args__[0], None
origin = getattr(annotation, "__origin__", None)
option: Dict[str, Any] = { option: Dict[str, Any] = {
"type": 3, "type": 3,
"name": name, "name": name,
"required": required, "required": required,
"description": description, "description": self.option_descriptions[name],
} }
if origin is None: if origin is None:
@ -1288,23 +1265,12 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
for python_type, discord_type in application_option_type_lookup.items(): for python_type, discord_type in application_option_type_lookup.items():
if issubclass(annotation, python_type): if issubclass(annotation, python_type):
option["type"] = discord_type option["type"] = discord_type
# Set channel types
if discord_type == 7:
option["channel_types"] = application_option_channel_types[annotation]
break break
elif origin is Union: elif origin is Union:
if annotation in {Union[discord.Member, discord.Role], Union[MemberConverter, RoleConverter]}: if annotation in {Union[discord.Member, discord.Role], Union[MemberConverter, RoleConverter]}:
option["type"] = 9 option["type"] = 9
elif all([arg in application_option_channel_types for arg in annotation.__args__]):
option["type"] = 7
option["channel_types"] = [
discord_value
for arg in annotation.__args__
for discord_value in application_option_channel_types[arg]
]
elif origin is Literal: elif origin is Literal:
literal_values = annotation.__args__ literal_values = annotation.__args__
python_type = type(literal_values[0]) python_type = type(literal_values[0])

View File

@ -81,8 +81,6 @@ class Flag:
------------ ------------
name: :class:`str` name: :class:`str`
The name of the flag. The name of the flag.
description: :class:`str`
The description of the flag.
aliases: List[:class:`str`] aliases: List[:class:`str`]
The aliases of the flag name. The aliases of the flag name.
attribute: :class:`str` attribute: :class:`str`
@ -99,7 +97,6 @@ class Flag:
""" """
name: str = MISSING name: str = MISSING
description: str = MISSING
aliases: List[str] = field(default_factory=list) aliases: List[str] = field(default_factory=list)
attribute: str = MISSING attribute: str = MISSING
annotation: Any = MISSING annotation: Any = MISSING
@ -120,7 +117,6 @@ class Flag:
def flag( def flag(
*, *,
name: str = MISSING, name: str = MISSING,
description: str = MISSING,
aliases: List[str] = MISSING, aliases: List[str] = MISSING,
default: Any = MISSING, default: Any = MISSING,
max_args: int = MISSING, max_args: int = MISSING,
@ -133,8 +129,6 @@ def flag(
------------ ------------
name: :class:`str` name: :class:`str`
The flag name. If not given, defaults to the attribute name. The flag name. If not given, defaults to the attribute name.
description: :class:`str`
Description of the flag for the slash commands options. The default value is `'no description'`.
aliases: List[:class:`str`] aliases: List[:class:`str`]
Aliases to the flag name. If not given no aliases are set. Aliases to the flag name. If not given no aliases are set.
default: Any default: Any
@ -149,9 +143,7 @@ def flag(
Whether multiple given values overrides the previous value. The default Whether multiple given values overrides the previous value. The default
value depends on the annotation given. value depends on the annotation given.
""" """
return Flag( return Flag(name=name, aliases=aliases, default=default, max_args=max_args, override=override)
name=name, description=description, aliases=aliases, default=default, max_args=max_args, override=override
)
def validate_flag_name(name: str, forbidden: Set[str]): def validate_flag_name(name: str, forbidden: Set[str]):

View File

@ -51,7 +51,6 @@ if TYPE_CHECKING:
from .types.interactions import ( from .types.interactions import (
Interaction as InteractionPayload, Interaction as InteractionPayload,
ApplicationCommandOptionChoice,
InteractionData, InteractionData,
) )
from .guild import Guild from .guild import Guild
@ -633,43 +632,6 @@ class InteractionResponse:
self.responded_at = utils.utcnow() self.responded_at = utils.utcnow()
async def autocomplete_result(self, choices: List[ApplicationCommandOptionChoice]):
"""|coro|
Responds to this autocomplete interaction with the resulting choices.
This should rarely be used.
Parameters
-----------
choices: List[Dict[:class:`str`, :class:`str`]]
The choices to be shown in the autocomplete UI of the user.
Must be a list of dictionaries with the ``name`` and ``value`` keys.
Raises
-------
HTTPException
Responding to the interaction failed.
InteractionResponded
This interaction has already been responded to before.
"""
if self.is_done():
raise InteractionResponded(self._parent)
parent = self._parent
if parent.type is not InteractionType.application_command_autocomplete:
return
adapter = async_context.get()
await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
type=InteractionResponseType.application_command_autocomplete_result.value,
data={"choices": choices},
)
self.responded_at = utils.utcnow()
class _InteractionMessageState: class _InteractionMessageState:
__slots__ = ("_parent", "_interaction") __slots__ = ("_parent", "_interaction")

View File

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union from typing import Callable, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union
import inspect import inspect
import os import os
@ -60,7 +60,7 @@ class Button(Item[V]):
The ID of the button that gets received during an interaction. The ID of the button that gets received during an interaction.
If this button is for a URL, it does not have a custom ID. If this button is for a URL, it does not have a custom ID.
url: Optional[:class:`str`] url: Optional[:class:`str`]
The URL this button sends you to. This param is automatically casted to :class:`str`. The URL this button sends you to.
disabled: :class:`bool` disabled: :class:`bool`
Whether the button is disabled or not. Whether the button is disabled or not.
label: Optional[:class:`str`] label: Optional[:class:`str`]
@ -91,7 +91,7 @@ class Button(Item[V]):
label: Optional[str] = None, label: Optional[str] = None,
disabled: bool = False, disabled: bool = False,
custom_id: Optional[str] = None, custom_id: Optional[str] = None,
url: Optional[Any] = None, url: Optional[str] = None,
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
row: Optional[int] = None, row: Optional[int] = None,
): ):
@ -117,7 +117,7 @@ class Button(Item[V]):
self._underlying = ButtonComponent._raw_construct( self._underlying = ButtonComponent._raw_construct(
type=ComponentType.button, type=ComponentType.button,
custom_id=custom_id, custom_id=custom_id,
url=str(url) if url else None, url=url,
disabled=disabled, disabled=disabled,
label=label, label=label,
style=style, style=style,

View File

@ -72,7 +72,7 @@ class Select(Item[V]):
The placeholder text that is shown if nothing is selected, if any. The placeholder text that is shown if nothing is selected, if any.
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 1 and 25.
max_values: :class:`int` max_values: :class:`int`
The maximum number of items that must be chosen for this select menu. The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25. Defaults to 1 and must be between 1 and 25.
@ -327,7 +327,7 @@ def select(
ordering. The row number must be between 0 and 4 (i.e. zero indexed). ordering. The row number must be between 0 and 4 (i.e. zero indexed).
min_values: :class:`int` min_values: :class:`int`
The minimum number of items that must be chosen for this select menu. The minimum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 0 and 25. Defaults to 1 and must be between 1 and 25.
max_values: :class:`int` max_values: :class:`int`
The maximum number of items that must be chosen for this select menu. The maximum number of items that must be chosen for this select menu.
Defaults to 1 and must be between 1 and 25. Defaults to 1 and must be between 1 and 25.

View File

@ -2656,182 +2656,6 @@ of :class:`enum.Enum`.
The guild may contain NSFW content. The guild may contain NSFW content.
.. class:: ProtocolURL
Represents the different `discord://` URLs
.. attribute:: home
The URL for the home page.
.. attribute:: nitro
The URL for the nitro page.
.. attribute:: apps
The URL for the downloads page. This breaks the client and needs it to be restarted.
.. attribute:: guild_discovery
The URL for the server discovery page.
.. attribute:: new_guild
The URL for the new server modal.
.. attribute:: guild_invite
The URL for the join server page. Needs to be formatted with `invite_code`.
.. attribute:: account_settings
The URL for the "My Account" page in settings.
.. attribute:: profile_settings
The URL for the "User Profile" page in settings.
.. attribute:: privacy_settings
The URL for the "Privacy & Safety" page in settings.
.. attribute:: safety_settings
An alias for :attr:`privacy_settings`.
.. attribute:: authorized_apps_settings
The URL for the "Authorized Apps" page in settings.
.. attribute:: connections_settings
The URL for the "Connections" page in settings.
.. attribute:: nitro_settings
The URL for the "Discord Nitro" page in settings. Same page as :attr:`nitro`, but inside of settings.
.. attribute:: guild_premium_subscription
The URL for the "Server Boost" page in settings.
.. attribute:: subscription_settings
The URL for the "Subscriptions" page in settings.
.. attribute:: gift_inventory_settings
The URL for the "Gift Inventory" page in settings.
.. attribute:: billing_settings
The URL for the "Billing" page in settings.
.. attribute:: appearance_settings
The URL for the "Appearance" page in settings.
.. attribute:: accessibility_settings
The URL for the "Accessibility" page in settings.
.. attribute:: voice_video_settings
The URL for the "Voice & Video" page in settings.
.. attribute:: test_images_settings
The URL for the "Text & Images" page in settings.
.. attribute:: notifications_settings
The URL for the "Notifications" page in settings.
.. attribute:: keybinds_settings
The URL for the "Keybinds" page in settings.
.. attribute:: language_settings
The URL for the "Language" page in settings.
.. attribute:: windows_settings
The URL for the "Windows Settings" page in settings. Leads to a blank screen if used on the wrong platform.
.. attribute:: linux_settings
The URL for the "Linux Settings" page in settings. Leads to a blank screen if used on the wrong platform.
.. attribute:: streamer_mode_settings
The URL for the "Streamer Mode" page in settings.
.. attribute:: advanced_settings
The URL for the "Advanced" page in settings.
.. attribute:: activity_status_settings
The URL for the "Activity Status" page in settings.
.. attribute:: game_overlay_settings
The URL for the "Game Overlay" page in settings.
.. attribute:: hypesquad_settings
The URL for the "Hypesquad" page in settings.
.. attribute:: changelogs
The URL for the changelogs modal.
.. attribute:: experiments
The URL for the "Experiments" page in settings. Leads to a blank screen if it isn't activated.
.. attribute:: developer_options
The URL for the "Developer Options" page in settings. Leads to a blank screen if it isn't activated.
.. attribute:: hotspot_options
The URL for the "Hotspot Options" page in settings. Leads to a blank screen if it isn't activated.
.. attribute:: user_profile
The URL for a user's profile modal. Needs to be formatted with `user_id`.
.. attribute:: dm_channel
The URL for a DM channel. Needs to be formatted with `channel_id`.
.. attribute:: dm_message
The URL for a message in a DM channel. Needs to be formatted with `channel_id` and `message_id`.
.. attribute:: guild_channel
The URL for a guild channel. Needs to be formatted with `guild_id` and `channel_id`.
.. attribute:: guild_message
The URL for a guild channel. Needs to be formatted with `guild_id`, `channel_id` and `message_id`.
.. attribute:: guild_membership_screening
The URL for a guild's membership screening page. Needs to be formatted with `guild_id`.
.. attribute:: games_library
The URL for the game library.
.. attribute:: library_settings
The URL for the library settings.
Async Iterator Async Iterator
---------------- ----------------

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

210
examples/views/paginator.py Normal file
View File

@ -0,0 +1,210 @@
import discord
from discord.ext import commands
from discord import ButtonStyle, Embed, Interaction
from discord.ui import Button, View
class Bot(commands.Bot):
def __init__(self):
super().__init__(
command_prefix=commands.when_mentioned_or("$"), intents=discord.Intents(guilds=True, messages=True)
)
async def on_ready(self):
print(f"Logged in as {self.user} (ID: {self.user.id})")
print("------")
# Define 3 View subclasses that we will switch between depending on if we are viewing the first page, a page in the middle, or the last page.
# The first page has the "left" button disabled, because there is no left page to go to
class FirstPageComps(View):
def __init__(
self,
author_id: int,
):
super().__init__()
self.value = None
self.author_id = author_id
async def interaction_check(
self,
interaction: Interaction,
):
return interaction.user.id == self.author_id
@discord.ui.button(
style=ButtonStyle.primary,
disabled=True,
label="Left",
)
async def left(
self,
button: discord.ui.Button,
interaction: Interaction,
):
self.value = "left"
self.stop()
@discord.ui.button(
style=ButtonStyle.primary,
label="Right",
)
async def right(
self,
button: discord.ui.Button,
interaction: Interaction,
):
self.value = "right"
self.stop()
# The middle pages have both left and right buttons available
class MiddlePageComps(View):
def __init__(
self,
author_id: int,
):
super().__init__()
self.value = None
self.author_id = author_id
async def interaction_check(
self,
interaction: Interaction,
):
return interaction.user.id == self.author_id
@discord.ui.button(
style=ButtonStyle.primary,
label="Left",
)
async def left(
self,
button: discord.ui.Button,
interaction: Interaction,
):
self.value = "left"
self.stop()
@discord.ui.button(
style=ButtonStyle.primary,
label="Right",
)
async def right(
self,
button: discord.ui.Button,
interaction: Interaction,
):
self.value = "right"
self.stop()
# The last page has the right button disabled, because there's no right page to go to
class LastPageComps(View):
def __init__(
self,
author_id: int,
):
super().__init__()
self.value = None
self.author_id = author_id
async def interaction_check(
self,
interaction: Interaction,
):
return interaction.user.id == self.author_id
@discord.ui.button(
style=ButtonStyle.primary,
label="Left",
)
async def left(
self,
button: discord.ui.Button,
interaction: Interaction,
):
self.value = "left"
self.stop()
@discord.ui.button(
style=ButtonStyle.primary,
label="Right",
disabled=True,
)
async def right(
self,
button: discord.ui.Button,
interaction: Interaction,
):
self.value = "right"
self.stop()
# Now we define the function that will take care of the pagination for us. It will take a list of Embeds and allow the user to cycle between them using left/right buttons
# There is also an optional title parameter in case you want to give your paginator a title, it will display in the embed title, for example if the title is Book
# then the title will look like "Book | Page 1/2". This is very optional and can be removed
async def paginate(
ctx,
pages: list,
title: str = None,
):
total_pages = len(pages)
first_page = pages[0]
last_page = pages[-1]
current_page = first_page
index = 0
embed = first_page
if title:
embed.title = f"{title} | Page {index+1}/{total_pages}"
view = FirstPageComps(ctx.author.id)
# Here we send the message with the view of the first page and the FirstPageComps buttons
msg = await ctx.send(
embed=embed,
view=view,
)
# The default timeout for Views is 3 minutes, but if a user selects to scroll between pages the timer will be reset.
# If the timer reaches 0, though, the loop will just silently stop.
while True:
wait = await view.wait()
if wait:
return
if view.value == "right":
index += 1
current_page = pages[index]
view = MiddlePageComps(ctx.author.id) if current_page != last_page else LastPageComps(ctx.author.id)
embed = current_page
if title:
embed.title = f"{title} | Page {index+1}/{total_pages}"
elif view.value == "left":
index -= 1
current_page = pages[index]
view = MiddlePageComps(ctx.author.id) if current_page != first_page else FirstPageComps(ctx.author.id)
embed = current_page
if title:
embed.title = f"{title} | Page {index+1}/{total_pages}"
await msg.edit(
embed=embed,
view=view,
)
bot = Bot()
# To test our new function, let's create a list of a couple Embeds and let our paginator deal with the sending and buttons
@bot.command()
async def sendpages(ctx):
page1 = Embed(description="This is page 1")
page2 = Embed(description="This is page 2")
page3 = Embed(description="This is page 3")
await paginate(ctx, [page1, page2, page3])
bot.run("token")