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
12 changed files with 924 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

@ -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

@ -465,6 +465,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
kwargs.pop("nonce", None) kwargs.pop("nonce", None)
kwargs.pop("stickers", None) kwargs.pop("stickers", None)
kwargs.pop("reference", None) kwargs.pop("reference", None)
kwargs.pop("delete_after", None)
kwargs.pop("mention_author", None) kwargs.pop("mention_author", None)
if not ( if not (

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

@ -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

@ -337,7 +337,7 @@ class Interaction:
self._state.store_view(view, message.id) self._state.store_view(view, message.id)
return message return message
async def delete_original_message(self, delay: Optional[float] = None) -> None: async def delete_original_message(self) -> None:
"""|coro| """|coro|
Deletes the original interaction response message. Deletes the original interaction response message.
@ -345,14 +345,6 @@ class Interaction:
This is a lower level interface to :meth:`InteractionMessage.delete` in case This is a lower level interface to :meth:`InteractionMessage.delete` in case
you do not want to fetch the message and save an HTTP request. you do not want to fetch the message and save an HTTP request.
Parameters
------------
delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message.
The waiting is done in the background and deletion failures are ignored.
.. versionadded:: 2.0
Raises Raises
------- -------
HTTPException HTTPException
@ -360,8 +352,6 @@ class Interaction:
Forbidden Forbidden
Deleted a message that is not yours. Deleted a message that is not yours.
""" """
if delay is not None:
await asyncio.sleep(delay)
adapter = async_context.get() adapter = async_context.get()
await adapter.delete_original_interaction_response( await adapter.delete_original_interaction_response(
self.application_id, self.application_id,
@ -470,7 +460,6 @@ class InteractionResponse:
view: View = MISSING, view: View = MISSING,
tts: bool = False, tts: bool = False,
ephemeral: bool = False, ephemeral: bool = False,
delete_after: Optional[float] = None,
) -> None: ) -> None:
"""|coro| """|coro|
@ -494,9 +483,6 @@ class InteractionResponse:
Indicates if the message should only be visible to the user who started the interaction. Indicates if the message should only be visible to the user who started the interaction.
If a view is sent with an ephemeral message and it has no timeout set then the timeout If a view is sent with an ephemeral message and it has no timeout set then the timeout
is set to 15 minutes. is set to 15 minutes.
delete_after: Optional[:class:`float`]
The amount of seconds the bot should wait before deleting the message sent.
.. versionadded:: 2.0
Raises Raises
------- -------
@ -553,8 +539,6 @@ class InteractionResponse:
self._parent._state.store_view(view) self._parent._state.store_view(view)
self.responded_at = utils.utcnow() self.responded_at = utils.utcnow()
if delete_after is not None:
self._parent.delete_original_message(delay=delete_after)
async def edit_message( async def edit_message(
self, self,
@ -749,7 +733,7 @@ class InteractionMessage(Message):
allowed_mentions=allowed_mentions, allowed_mentions=allowed_mentions,
) )
async def delete(self, *, delay: Optional[float] = None, silent: bool = False) -> None: async def delete(self, *, delay: Optional[float] = None) -> None:
"""|coro| """|coro|
Deletes the message. Deletes the message.
@ -759,12 +743,6 @@ class InteractionMessage(Message):
delay: Optional[:class:`float`] delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message. If provided, the number of seconds to wait before deleting the message.
The waiting is done in the background and deletion failures are ignored. The waiting is done in the background and deletion failures are ignored.
silent: :class:`bool`
If silent is set to ``True``, the error will not be raised, it will be ignored.
This defaults to ``False`
.. versionadded:: 2.0
Raises Raises
------ ------
@ -779,15 +757,12 @@ class InteractionMessage(Message):
if delay is not None: if delay is not None:
async def inner_call(delay: float = delay): async def inner_call(delay: float = delay):
await asyncio.sleep(delay)
try: try:
await self._state._interaction.delete_original_message(delay=delay) await self._state._interaction.delete_original_message()
except HTTPException: except HTTPException:
pass pass
asyncio.create_task(inner_call()) asyncio.create_task(inner_call())
else: else:
try: await self._state._interaction.delete_original_message()
await self._state._interaction.delete_original_message(delay=delay)
except Exception:
if not silent:
raise

View File

@ -717,7 +717,7 @@ class WebhookMessage(Message):
allowed_mentions=allowed_mentions, allowed_mentions=allowed_mentions,
) )
async def delete(self, *, delay: Optional[float] = None, silent: bool = False) -> None: async def delete(self, *, delay: Optional[float] = None) -> None:
"""|coro| """|coro|
Deletes the message. Deletes the message.
@ -727,12 +727,6 @@ class WebhookMessage(Message):
delay: Optional[:class:`float`] delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message. If provided, the number of seconds to wait before deleting the message.
The waiting is done in the background and deletion failures are ignored. The waiting is done in the background and deletion failures are ignored.
silent: :class:`bool`
If silent is set to ``True``, the error will not be raised, it will be ignored.
This defaults to ``False`
.. versionadded:: 2.0
Raises Raises
------ ------
@ -747,18 +741,15 @@ class WebhookMessage(Message):
if delay is not None: if delay is not None:
async def inner_call(delay: float = delay): async def inner_call(delay: float = delay):
await asyncio.sleep(delay)
try: try:
await self._state._webhook.delete_message(self.id, delay) await self._state._webhook.delete_message(self.id)
except HTTPException: except HTTPException:
pass pass
asyncio.create_task(inner_call()) asyncio.create_task(inner_call())
else: else:
try: await self._state._webhook.delete_message(self.id)
await self._state._webhook.delete_message(self.id, delay)
except Exception:
if not silent:
raise
class BaseWebhook(Hashable): class BaseWebhook(Hashable):
@ -1279,7 +1270,6 @@ class Webhook(BaseWebhook):
view: View = MISSING, view: View = MISSING,
thread: Snowflake = MISSING, thread: Snowflake = MISSING,
wait: bool = False, wait: bool = False,
delete_after: Optional[float] = None,
) -> Optional[WebhookMessage]: ) -> Optional[WebhookMessage]:
"""|coro| """|coro|
@ -1345,11 +1335,6 @@ class Webhook(BaseWebhook):
The thread to send this webhook to. The thread to send this webhook to.
.. versionadded:: 2.0 .. versionadded:: 2.0
delete_after: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message.
The waiting is done in the background and deletion failures are ignored.
.. versionadded:: 2.0
Raises Raises
-------- --------
@ -1431,9 +1416,6 @@ class Webhook(BaseWebhook):
if view is not MISSING and not view.is_finished(): if view is not MISSING and not view.is_finished():
message_id = None if msg is None else msg.id message_id = None if msg is None else msg.id
self._state.store_view(view, message_id) self._state.store_view(view, message_id)
if delete_after is not None and msg is not None:
await msg.delete(delay=delete_after)
return msg return msg
@ -1588,7 +1570,7 @@ class Webhook(BaseWebhook):
self._state.store_view(view, message_id) self._state.store_view(view, message_id)
return message return message
async def delete_message(self, message_id: int, delay: Optional[float] = None, /) -> None: async def delete_message(self, message_id: int, /) -> None:
"""|coro| """|coro|
Deletes a message owned by this webhook. Deletes a message owned by this webhook.
@ -1602,12 +1584,6 @@ class Webhook(BaseWebhook):
------------ ------------
message_id: :class:`int` message_id: :class:`int`
The message ID to delete. The message ID to delete.
delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message.
The waiting is done in the background and deletion failures are ignored.
.. versionadded:: 2.0
Raises Raises
------- -------
@ -1619,9 +1595,6 @@ class Webhook(BaseWebhook):
if self.token is None: if self.token is None:
raise InvalidArgument("This webhook does not have a token associated with it") raise InvalidArgument("This webhook does not have a token associated with it")
if delay is not None:
await asyncio.sleep(delay)
adapter = async_context.get() adapter = async_context.get()
await adapter.delete_webhook_message( await adapter.delete_webhook_message(
self.id, self.id,

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