14 Commits

Author SHA1 Message Date
3201026242 Update paginator.py 2021-10-09 22:16:57 +02:00
f7398c9acd Fix paginator.py typo 2021-10-09 21:00:58 +02:00
d06bfce837 Include minimal working example 2021-10-09 20:57:02 +02:00
cb782ce3d8 Update paginator.py 2021-10-09 20:40:44 +02:00
fae85d072f Black paginator.py with 120 char limit per line 2021-10-09 20:37:26 +02:00
c74fd90a9c Update paginator.py 2021-10-09 20:29:00 +02:00
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
0abac8698d Fix slash command flag parsing
Also removes the extra space at the end of fake message content
2021-10-08 20:06:05 +01:00
d781af8be5 Remove maintainer list from README.rst
This list became outdated straight away, and is a bad idea in general.
2021-10-08 18:24:22 +01:00
9e31aad96d Fix code style issues with Black 2021-10-07 17:34:29 +01:00
eca1d9a470 Sort events by categories (#88) 2021-10-07 16:48:38 +01:00
0bbcfd7f33 Update resource links (#65)
* Updated links

* Remove github discussions from getting help
2021-10-06 20:32:48 +01:00
ec1e2add21 Update user-agent (#92) 2021-10-04 21:11:10 +01:00
4277f65051 Implement _FakeSlashMessage.clean_content
Closes #83
2021-10-03 21:05:00 +01:00
13 changed files with 916 additions and 701 deletions

View File

@ -17,18 +17,6 @@ The Future of enhanced-discord.py
-------------------------- --------------------------
Enhanced discord.py is a fork of Rapptz's discord.py, that went unmaintained (`gist <https://gist.github.com/Rapptz/4a2f62751b9600a31a0d3c78100287f1>`_) Enhanced discord.py is a fork of Rapptz's discord.py, that went unmaintained (`gist <https://gist.github.com/Rapptz/4a2f62751b9600a31a0d3c78100287f1>`_)
It is currently maintained by (in alphabetical order)
- Chillymosh#8175
- Daggy#9889
- dank Had0cK#6081
- Dutchy#6127
- Eyesofcreeper#0001
- Gnome!#6669
- IAmTomahawkx#1000
- Jadon#2494
An overview of added features is available on the `custom features page <https://enhanced-dpy.readthedocs.io/en/latest/index.html#custom-features>`_. An overview of added features is available on the `custom features page <https://enhanced-dpy.readthedocs.io/en/latest/index.html#custom-features>`_.
Key Features Key Features

View File

@ -245,15 +245,6 @@ class Asset(AssetMixin):
animated=animated, animated=animated,
) )
@classmethod
def _from_role_icon(cls, state, role_id: int, role_hash: str) -> Asset:
return cls(
state,
url=f"{cls.BASE}/role-icons/{role_id}/{role_hash}.png",
key=role_hash,
animated=False,
)
def __str__(self) -> str: def __str__(self) -> str:
return self._url return self._url

View File

@ -28,6 +28,7 @@ from __future__ import annotations
import asyncio import asyncio
import collections import collections
import collections.abc import collections.abc
from functools import cached_property
import inspect import inspect
import importlib.util import importlib.util
@ -72,7 +73,9 @@ from .cog import Cog
if TYPE_CHECKING: if TYPE_CHECKING:
import importlib.machinery import importlib.machinery
from discord.role import Role
from discord.message import Message from discord.message import Message
from discord.abc import PartialMessageableChannel
from ._types import ( from ._types import (
Check, Check,
CoroFunc, CoroFunc,
@ -94,10 +97,17 @@ CXT = TypeVar("CXT", bound="Context")
class _FakeSlashMessage(discord.PartialMessage): class _FakeSlashMessage(discord.PartialMessage):
activity = application = edited_at = reference = webhook_id = None activity = application = edited_at = reference = webhook_id = None
attachments = components = reactions = stickers = mentions = [] attachments = components = reactions = stickers = []
author: Union[discord.User, discord.Member]
tts = False tts = False
raw_mentions = discord.Message.raw_mentions
clean_content = discord.Message.clean_content
channel_mentions = discord.Message.channel_mentions
raw_role_mentions = discord.Message.raw_role_mentions
raw_channel_mentions = discord.Message.raw_channel_mentions
author: Union[discord.User, discord.Member]
@classmethod @classmethod
def from_interaction( def from_interaction(
cls, interaction: discord.Interaction, channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread] cls, interaction: discord.Interaction, channel: Union[discord.TextChannel, discord.DMChannel, discord.Thread]
@ -108,6 +118,22 @@ class _FakeSlashMessage(discord.PartialMessage):
return self return self
@cached_property
def mentions(self) -> List[Union[discord.Member, discord.User]]:
client = self._state._get_client()
if self.guild:
ensure_user = lambda id: self.guild.get_member(id) or client.get_user(id) # type: ignore
else:
ensure_user = client.get_user
return discord.utils._unique(filter(None, map(ensure_user, self.raw_mentions)))
@cached_property
def role_mentions(self) -> List[Role]:
if self.guild is None:
return []
return discord.utils._unique(filter(None, map(self.guild.get_role, self.raw_role_mentions)))
def when_mentioned(bot: Union[Bot, AutoShardedBot], msg: Message) -> List[str]: def when_mentioned(bot: Union[Bot, AutoShardedBot], msg: Message) -> List[str]:
"""A callable that implements a command prefix equivalent to being mentioned. """A callable that implements a command prefix equivalent to being mentioned.
@ -1266,7 +1292,7 @@ class BotBase(GroupMixin):
# Make our fake message so we can pass it to ext.commands # Make our fake message so we can pass it to ext.commands
message: discord.Message = _FakeSlashMessage.from_interaction(interaction, channel) # type: ignore message: discord.Message = _FakeSlashMessage.from_interaction(interaction, channel) # type: ignore
message.content = f"/{command_name} " message.content = f"/{command_name}"
# Add arguments to fake message content, in the right order # Add arguments to fake message content, in the right order
ignore_params: List[inspect.Parameter] = [] ignore_params: List[inspect.Parameter] = []
@ -1281,7 +1307,7 @@ class BotBase(GroupMixin):
else: else:
prefix = param.annotation.__commands_flag_prefix__ prefix = param.annotation.__commands_flag_prefix__
delimiter = param.annotation.__commands_flag_delimiter__ delimiter = param.annotation.__commands_flag_delimiter__
message.content += f"{prefix}{name} {option['value']}{delimiter}" # type: ignore message.content += f" {prefix}{name}{delimiter}{option['value']}" # type: ignore
continue continue
option = next((o for o in command_options if o["name"] == name), None) option = next((o for o in command_options if o["name"] == name), None)
@ -1297,9 +1323,9 @@ class BotBase(GroupMixin):
): ):
# String with space in without "consume rest" # String with space in without "consume rest"
option = cast(_ApplicationCommandInteractionDataOptionString, option) option = cast(_ApplicationCommandInteractionDataOptionString, option)
message.content += f"{_quote_string_safe(option['value'])} " message.content += f" {_quote_string_safe(option['value'])}"
else: else:
message.content += f'{option.get("value", "")} ' message.content += f' {option.get("value", "")}'
ctx = await self.get_context(message) ctx = await self.get_context(message)
ctx._ignored_params = ignore_params ctx._ignored_params = ignore_params

View File

@ -1694,7 +1694,9 @@ class Group(GroupMixin[CogT], Command[CogT, P, T]):
"name": self.name, "name": self.name,
"type": int(not (nested - 1)) + 1, "type": int(not (nested - 1)) + 1,
"description": self.short_doc or "no description", "description": self.short_doc or "no description",
"options": [cmd.to_application_command(nested=nested + 1) for cmd in sorted(self.commands, key=lambda x: x.name)], "options": [
cmd.to_application_command(nested=nested + 1) for cmd in sorted(self.commands, key=lambda x: x.name)
],
} }

View File

@ -455,7 +455,7 @@ class BadInviteArgument(BadArgument):
This inherits from :exc:`BadArgument` This inherits from :exc:`BadArgument`
.. versionadded:: 1.5 .. versionadded:: 1.5
Attributes Attributes
----------- -----------
argument: :class:`str` argument: :class:`str`

View File

@ -2448,8 +2448,6 @@ class Guild(Hashable):
colour: Union[Colour, int] = ..., colour: Union[Colour, int] = ...,
hoist: bool = ..., hoist: bool = ...,
mentionable: bool = ..., mentionable: bool = ...,
icon: bytes = ...,
emoji: str = ...,
) -> Role: ) -> Role:
... ...
@ -2463,8 +2461,6 @@ class Guild(Hashable):
color: Union[Colour, int] = ..., color: Union[Colour, int] = ...,
hoist: bool = ..., hoist: bool = ...,
mentionable: bool = ..., mentionable: bool = ...,
icon: bytes = ...,
emoji: str = ...,
) -> Role: ) -> Role:
... ...
@ -2477,8 +2473,6 @@ class Guild(Hashable):
colour: Union[Colour, int] = MISSING, colour: Union[Colour, int] = MISSING,
hoist: bool = MISSING, hoist: bool = MISSING,
mentionable: bool = MISSING, mentionable: bool = MISSING,
icon: bytes = MISSING,
emoji: str = MISSING,
reason: Optional[str] = None, reason: Optional[str] = None,
) -> Role: ) -> Role:
"""|coro| """|coro|
@ -2508,10 +2502,6 @@ class Guild(Hashable):
mentionable: :class:`bool` mentionable: :class:`bool`
Indicates if the role should be mentionable by others. Indicates if the role should be mentionable by others.
Defaults to ``False``. Defaults to ``False``.
emoji: :class:`str`
The unicode emoji that is shown next to users with the role.
icon: :class:`bytes`
A custom image that is shown next to users with the role.
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for creating this role. Shows up on the audit log. The reason for creating this role. Shows up on the audit log.
@ -2550,12 +2540,6 @@ class Guild(Hashable):
if name is not MISSING: if name is not MISSING:
fields["name"] = name fields["name"] = name
if emoji is not MISSING:
fields["unicode_emoji"] = emoji
if icon is not MISSING:
fields["icon"] = utils._bytes_to_base64_data(icon)
data = await self._state.http.create_role(self.id, reason=reason, **fields) data = await self._state.http.create_role(self.id, reason=reason, **fields)
role = Role(guild=self, data=data, state=self._state) role = Role(guild=self, data=data, state=self._state)

View File

@ -188,8 +188,8 @@ class HTTPClient:
self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth self.proxy_auth: Optional[aiohttp.BasicAuth] = proxy_auth
self.use_clock: bool = not unsync_clock self.use_clock: bool = not unsync_clock
user_agent = "DiscordBot (https://github.com/Rapptz/discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}" u_agent = "DiscordBot (https://github.com/iDevision/enhanced-discord.py {0}) Python/{1[0]}.{1[1]} aiohttp/{2}"
self.user_agent: str = user_agent.format(__version__, sys.version_info, aiohttp.__version__) self.user_agent: str = u_agent.format(__version__, sys.version_info, aiohttp.__version__)
def recreate(self) -> None: def recreate(self) -> None:
if self.__session.closed: if self.__session.closed:

View File

@ -23,14 +23,13 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List, Optional, TypeVar, Union, TYPE_CHECKING from typing import Any, Dict, List, Optional, TypeVar, Union, overload, TYPE_CHECKING
from .asset import Asset
from .permissions import Permissions from .permissions import Permissions
from .errors import InvalidArgument from .errors import InvalidArgument
from .colour import Colour from .colour import Colour
from .mixins import Hashable from .mixins import Hashable
from .utils import snowflake_time, _get_as_snowflake, MISSING, _bytes_to_base64_data from .utils import snowflake_time, _get_as_snowflake, MISSING
__all__ = ( __all__ = (
"RoleTags", "RoleTags",
@ -159,15 +158,7 @@ class Role(Hashable):
guild: :class:`Guild` guild: :class:`Guild`
The guild the role belongs to. The guild the role belongs to.
hoist: :class:`bool` hoist: :class:`bool`
Indicates if the role will be displayed separately from other members. Indicates if the role will be displayed separately from other members.
icon: Optional[:class:`Asset`]
A custom image that is shown next to users with the role.
.. versionadded:: 2.0
emoji: Optional[:class:`str`]
The unicode emoji that is shown next to users with the role.
.. versionadded:: 2.0
position: :class:`int` position: :class:`int`
The position of the role. This number is usually positive. The bottom The position of the role. This number is usually positive. The bottom
role has a position of 0. role has a position of 0.
@ -200,8 +191,6 @@ class Role(Hashable):
"hoist", "hoist",
"guild", "guild",
"tags", "tags",
"_icon",
"emoji",
"_state", "_state",
) )
@ -262,8 +251,6 @@ class Role(Hashable):
self.position: int = data.get("position", 0) self.position: int = data.get("position", 0)
self._colour: int = data.get("color", 0) self._colour: int = data.get("color", 0)
self.hoist: bool = data.get("hoist", False) self.hoist: bool = data.get("hoist", False)
self.emoji: Optional[str] = data.get("unicode_emoji")
self._icon: Optional[str] = data.get("icon")
self.managed: bool = data.get("managed", False) self.managed: bool = data.get("managed", False)
self.mentionable: bool = data.get("mentionable", False) self.mentionable: bool = data.get("mentionable", False)
self.tags: Optional[RoleTags] self.tags: Optional[RoleTags]
@ -331,13 +318,6 @@ class Role(Hashable):
""":class:`str`: Returns a string that allows you to mention a role.""" """:class:`str`: Returns a string that allows you to mention a role."""
return f"<@&{self.id}>" return f"<@&{self.id}>"
@property
def icon(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the custom icon shown next to users with the role, if it exists."""
if self._icon is None:
return
return Asset._from_role_icon(self._state, self.id, self._icon)
@property @property
def members(self) -> List[Member]: def members(self) -> List[Member]:
"""List[:class:`Member`]: Returns all the members with this role.""" """List[:class:`Member`]: Returns all the members with this role."""
@ -381,8 +361,6 @@ class Role(Hashable):
hoist: bool = MISSING, hoist: bool = MISSING,
mentionable: bool = MISSING, mentionable: bool = MISSING,
position: int = MISSING, position: int = MISSING,
icon: bytes = MISSING,
emoji: str = MISSING,
reason: Optional[str] = MISSING, reason: Optional[str] = MISSING,
) -> Optional[Role]: ) -> Optional[Role]:
"""|coro| """|coro|
@ -415,10 +393,6 @@ class Role(Hashable):
position: :class:`int` position: :class:`int`
The new role's position. This must be below your top role's The new role's position. This must be below your top role's
position or it will fail. position or it will fail.
emoji: :class:`str`
The unicode emoji that is shown next to users with the role.
icon: :class:`bytes`
A custom image that is shown next to users with the role.
reason: Optional[:class:`str`] reason: Optional[:class:`str`]
The reason for editing this role. Shows up on the audit log. The reason for editing this role. Shows up on the audit log.
@ -462,12 +436,6 @@ class Role(Hashable):
if mentionable is not MISSING: if mentionable is not MISSING:
payload["mentionable"] = mentionable payload["mentionable"] = mentionable
if emoji is not MISSING:
payload["unicode_emoji"] = emoji
if icon is not MISSING:
payload["icon"] = _bytes_to_base64_data(icon)
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
return Role(guild=self.guild, data=data, state=self._state) return Role(guild=self.guild, data=data, state=self._state)

View File

@ -29,9 +29,7 @@ from .snowflake import Snowflake
class _RoleOptional(TypedDict, total=False): class _RoleOptional(TypedDict, total=False):
icon: str
tags: RoleTags tags: RoleTags
unicode_emoji: str
class Role(_RoleOptional): class Role(_RoleOptional):

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@ autodoc_typehints = "none"
# napoleon_attr_annotations = False # napoleon_attr_annotations = False
extlinks = { extlinks = {
"issue": ("https://github.com/Rapptz/discord.py/issues/%s", "GH-"), "issue": ("https://github.com/iDevision/enhanced-discord.py/issues/%s", "GH-"),
} }
# Links used for cross-referencing stuff in other documentation # Links used for cross-referencing stuff in other documentation
@ -168,9 +168,8 @@ html_context = {
resource_links = { resource_links = {
"discord": "https://discord.gg/TvqYBrGXEm", "discord": "https://discord.gg/TvqYBrGXEm",
"issues": "https://github.com/Rapptz/discord.py/issues", "issues": "https://github.com/iDevision/enhanced-discord.py/issues",
"discussions": "https://github.com/Rapptz/discord.py/discussions", "examples": f"https://github.com/iDevision/enhanced-discord.py/tree/{branch}/examples",
"examples": f"https://github.com/Rapptz/discord.py/tree/{branch}/examples",
} }
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme

View File

@ -38,7 +38,6 @@ If you're having trouble with something, these resources might help.
- Ask us and hang out with us in our :resource:`Discord <discord>` server. - Ask us and hang out with us in our :resource:`Discord <discord>` server.
- If you're looking for something specific, try the :ref:`index <genindex>` or :ref:`searching <search>`. - If you're looking for something specific, try the :ref:`index <genindex>` or :ref:`searching <search>`.
- Report bugs in the :resource:`issue tracker <issues>`. - Report bugs in the :resource:`issue tracker <issues>`.
- Ask in our :resource:`GitHub discussions page <discussions>`.
Extensions Extensions
------------ ------------

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")