Compare commits
2 Commits
Astrea49/c
...
paris-ci/i
Author | SHA1 | Date | |
---|---|---|---|
cddfc3272b | |||
cdb7c50880 |
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@ -1,7 +1,5 @@
|
|||||||
## Contributing to discord.py
|
## Contributing to discord.py
|
||||||
|
|
||||||
Credits to the `original lib` by Rapptz <https://github.com/Rapptz/discord.py>
|
|
||||||
|
|
||||||
First off, thanks for taking the time to contribute. It makes the library substantially better. :+1:
|
First off, thanks for taking the time to contribute. It makes the library substantially better. :+1:
|
||||||
|
|
||||||
The following is a set of guidelines for contributing to the repository. These are guidelines, not hard rules.
|
The following is a set of guidelines for contributing to the repository. These are guidelines, not hard rules.
|
||||||
@ -10,9 +8,9 @@ The following is a set of guidelines for contributing to the repository. These a
|
|||||||
|
|
||||||
Generally speaking questions are better suited in our resources below.
|
Generally speaking questions are better suited in our resources below.
|
||||||
|
|
||||||
- The official support server: https://discord.gg/TvqYBrGXEm
|
- The official support server: https://discord.gg/r3sSKJJ
|
||||||
- The Discord API server under #python_discord-py: https://discord.gg/discord-api
|
- The Discord API server under #python_discord-py: https://discord.gg/discord-api
|
||||||
- [The FAQ in the documentation](https://enhanced-dpy.readthedocs.io/en/latest/faq.html)
|
- [The FAQ in the documentation](https://discordpy.readthedocs.io/en/latest/faq.html)
|
||||||
- [StackOverflow's `discord.py` tag](https://stackoverflow.com/questions/tagged/discord.py)
|
- [StackOverflow's `discord.py` tag](https://stackoverflow.com/questions/tagged/discord.py)
|
||||||
|
|
||||||
Please try your best not to ask questions in our issue tracker. Most of them don't belong there unless they provide value to a larger audience.
|
Please try your best not to ask questions in our issue tracker. Most of them don't belong there unless they provide value to a larger audience.
|
||||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Ask a question
|
- name: Ask a question
|
||||||
about: Ask questions and discuss with other users of the library.
|
about: Ask questions and discuss with other users of the library.
|
||||||
url: https://github.com/Rapptz/discord.py/discussions
|
url: https://github.com/iDevision/enhanced-discord.py/issues/new
|
||||||
- name: Discord Server
|
- name: Discord Server
|
||||||
about: Use our official Discord server to ask for help and questions as well.
|
about: Use our official Discord server to ask for help and questions as well.
|
||||||
url: https://discord.gg/r3sSKJJ
|
url: https://discord.gg/TvqYBrGXEm
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
enhanced-discord.py
|
discord.py
|
||||||
===================
|
==========
|
||||||
|
|
||||||
.. image:: https://discord.com/api/guilds/514232441498763279/embed.png
|
.. image:: https://discord.com/api/guilds/514232441498763279/embed.png
|
||||||
:target: https://discord.gg/PYAfZzpsjG
|
:target: https://discord.gg/PYAfZzpsjG
|
||||||
@ -59,7 +59,7 @@ To install the development version, do the following:
|
|||||||
.. code:: sh
|
.. code:: sh
|
||||||
|
|
||||||
$ git clone https://github.com/iDevision/enhanced-discord.py
|
$ git clone https://github.com/iDevision/enhanced-discord.py
|
||||||
$ cd enhanced-discord.py
|
$ cd discord.py
|
||||||
$ python3 -m pip install -U .[voice]
|
$ python3 -m pip install -U .[voice]
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ from .colour import *
|
|||||||
from .integrations import *
|
from .integrations import *
|
||||||
from .invite import *
|
from .invite import *
|
||||||
from .template import *
|
from .template import *
|
||||||
from .welcome_screen import *
|
|
||||||
from .widget import *
|
from .widget import *
|
||||||
from .object import *
|
from .object import *
|
||||||
from .reaction import *
|
from .reaction import *
|
||||||
|
@ -794,13 +794,13 @@ class CustomActivity(BaseActivity):
|
|||||||
return hash((self.name, str(self.emoji)))
|
return hash((self.name, str(self.emoji)))
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if not self.emoji:
|
if self.emoji:
|
||||||
|
if self.name:
|
||||||
|
return f'{self.emoji} {self.name}'
|
||||||
|
return str(self.emoji)
|
||||||
|
else:
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
if self.name:
|
|
||||||
return f'{self.emoji} {self.name}'
|
|
||||||
return str(self.emoji)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<CustomActivity name={self.name!r} emoji={self.emoji!r}>'
|
return f'<CustomActivity name={self.name!r} emoji={self.emoji!r}>'
|
||||||
|
|
||||||
|
@ -313,11 +313,10 @@ class Asset(AssetMixin):
|
|||||||
if self._animated:
|
if self._animated:
|
||||||
if format not in VALID_ASSET_FORMATS:
|
if format not in VALID_ASSET_FORMATS:
|
||||||
raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}')
|
raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}')
|
||||||
url = url.with_path(f'{path}.{format}')
|
else:
|
||||||
elif static_format is MISSING:
|
|
||||||
if format not in VALID_STATIC_FORMATS:
|
if format not in VALID_STATIC_FORMATS:
|
||||||
raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}')
|
raise InvalidArgument(f'format must be one of {VALID_STATIC_FORMATS}')
|
||||||
url = url.with_path(f'{path}.{format}')
|
url = url.with_path(f'{path}.{format}')
|
||||||
|
|
||||||
if static_format is not MISSING and not self._animated:
|
if static_format is not MISSING and not self._animated:
|
||||||
if static_format not in VALID_STATIC_FORMATS:
|
if static_format not in VALID_STATIC_FORMATS:
|
||||||
|
@ -330,10 +330,6 @@ class AuditLogEntry(Hashable):
|
|||||||
|
|
||||||
Returns the entry's hash.
|
Returns the entry's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the entry's ID.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
Audit log entries are now comparable and hashable.
|
Audit log entries are now comparable and hashable.
|
||||||
|
|
||||||
|
@ -115,10 +115,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
|
|
||||||
Returns the channel's name.
|
Returns the channel's name.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the channel's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@ -228,16 +224,6 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
|
|||||||
"""List[:class:`Member`]: Returns all members that can see this channel."""
|
"""List[:class:`Member`]: Returns all members that can see this channel."""
|
||||||
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
|
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
|
||||||
|
|
||||||
@property
|
|
||||||
def bots(self) -> List[Member]:
|
|
||||||
"""List[:class:`Member`]: Returns all bots that can see this channel."""
|
|
||||||
return [m for m in self.guild.members if m.bot and self.permissions_for(m).read_messages]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def humans(self) -> List[Member]:
|
|
||||||
"""List[:class:`Member`]: Returns all human members that can see this channel."""
|
|
||||||
return [m for m in self.guild.members if not m.bot and self.permissions_for(m).read_messages]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def threads(self) -> List[Thread]:
|
def threads(self) -> List[Thread]:
|
||||||
"""List[:class:`Thread`]: Returns all the threads that you can see.
|
"""List[:class:`Thread`]: Returns all the threads that you can see.
|
||||||
@ -1348,10 +1334,6 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
|
|
||||||
Returns the category's name.
|
Returns the category's name.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the category's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@ -1574,10 +1556,6 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
|||||||
|
|
||||||
Returns the channel's name.
|
Returns the channel's name.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the channel's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@ -1750,10 +1728,6 @@ class DMChannel(discord.abc.Messageable, Hashable):
|
|||||||
|
|
||||||
Returns a string representation of the channel
|
Returns a string representation of the channel
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the channel's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
recipient: Optional[:class:`User`]
|
recipient: Optional[:class:`User`]
|
||||||
@ -1880,10 +1854,6 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
|||||||
|
|
||||||
Returns a string representation of the channel
|
Returns a string representation of the channel
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the channel's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
recipients: List[:class:`User`]
|
recipients: List[:class:`User`]
|
||||||
@ -2030,10 +2000,6 @@ class PartialMessageable(discord.abc.Messageable, Hashable):
|
|||||||
|
|
||||||
Returns the partial messageable's hash.
|
Returns the partial messageable's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the messageable's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
|
@ -142,6 +142,7 @@ class Client:
|
|||||||
intents: :class:`Intents`
|
intents: :class:`Intents`
|
||||||
The intents that you want to enable for the session. This is a way of
|
The intents that you want to enable for the session. This is a way of
|
||||||
disabling and enabling certain gateway events from triggering and being sent.
|
disabling and enabling certain gateway events from triggering and being sent.
|
||||||
|
If not given, defaults to a regularly constructed :class:`Intents` class.
|
||||||
|
|
||||||
.. versionadded:: 1.5
|
.. versionadded:: 1.5
|
||||||
member_cache_flags: :class:`MemberCacheFlags`
|
member_cache_flags: :class:`MemberCacheFlags`
|
||||||
@ -202,12 +203,9 @@ class Client:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
intents: Intents,
|
|
||||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||||
**options: Any,
|
**options: Any,
|
||||||
):
|
):
|
||||||
options["intents"] = intents
|
|
||||||
|
|
||||||
# self.ws is set in the connect method
|
# self.ws is set in the connect method
|
||||||
self.ws: DiscordWebSocket = None # type: ignore
|
self.ws: DiscordWebSocket = None # type: ignore
|
||||||
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() if loop is None else loop
|
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() if loop is None else loop
|
||||||
@ -829,38 +827,6 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
return self._connection.get_user(id)
|
return self._connection.get_user(id)
|
||||||
|
|
||||||
async def try_user(self, id: int, /) -> Optional[User]:
|
|
||||||
"""|coro|
|
|
||||||
Returns a user with the given ID. If not from cache, the user will be requested from the API.
|
|
||||||
|
|
||||||
You do not have to share any guilds with the user to get this information from the API,
|
|
||||||
however many operations do require that you do.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This method is an API call. If you have :attr:`discord.Intents.members` and member cache enabled, consider :meth:`get_user` instead.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
id: :class:`int`
|
|
||||||
The ID to search for.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
Optional[:class:`~discord.User`]
|
|
||||||
The user or ``None`` if not found.
|
|
||||||
"""
|
|
||||||
maybe_user = self.get_user(id)
|
|
||||||
|
|
||||||
if maybe_user is not None:
|
|
||||||
return maybe_user
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await self.fetch_user(id)
|
|
||||||
except NotFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_emoji(self, id: int, /) -> Optional[Emoji]:
|
def get_emoji(self, id: int, /) -> Optional[Emoji]:
|
||||||
"""Returns an emoji with the given ID.
|
"""Returns an emoji with the given ID.
|
||||||
|
|
||||||
|
@ -251,13 +251,6 @@ class Colour:
|
|||||||
def red(cls: Type[CT]) -> CT:
|
def red(cls: Type[CT]) -> CT:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``."""
|
||||||
return cls(0xe74c3c)
|
return cls(0xe74c3c)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def nitro_booster(cls):
|
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0xf47fff``.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0"""
|
|
||||||
return cls(0xf47fff)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dark_red(cls: Type[CT]) -> CT:
|
def dark_red(cls: Type[CT]) -> CT:
|
||||||
@ -331,15 +324,6 @@ class Colour:
|
|||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
return cls(0xFEE75C)
|
return cls(0xFEE75C)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def dark_blurple(cls: Type[CT]) -> CT:
|
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0x4E5D94``.
|
|
||||||
This is the original Dark Blurple branding.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
"""
|
|
||||||
return cls(0x4E5D94)
|
|
||||||
|
|
||||||
|
|
||||||
Color = Colour
|
Color = Colour
|
||||||
|
@ -72,36 +72,30 @@ if TYPE_CHECKING:
|
|||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
MaybeEmpty = Union[T, _EmptyEmbed]
|
MaybeEmpty = Union[T, _EmptyEmbed]
|
||||||
|
|
||||||
|
|
||||||
class _EmbedFooterProxy(Protocol):
|
class _EmbedFooterProxy(Protocol):
|
||||||
text: MaybeEmpty[str]
|
text: MaybeEmpty[str]
|
||||||
icon_url: MaybeEmpty[str]
|
icon_url: MaybeEmpty[str]
|
||||||
|
|
||||||
|
|
||||||
class _EmbedFieldProxy(Protocol):
|
class _EmbedFieldProxy(Protocol):
|
||||||
name: MaybeEmpty[str]
|
name: MaybeEmpty[str]
|
||||||
value: MaybeEmpty[str]
|
value: MaybeEmpty[str]
|
||||||
inline: bool
|
inline: bool
|
||||||
|
|
||||||
|
|
||||||
class _EmbedMediaProxy(Protocol):
|
class _EmbedMediaProxy(Protocol):
|
||||||
url: MaybeEmpty[str]
|
url: MaybeEmpty[str]
|
||||||
proxy_url: MaybeEmpty[str]
|
proxy_url: MaybeEmpty[str]
|
||||||
height: MaybeEmpty[int]
|
height: MaybeEmpty[int]
|
||||||
width: MaybeEmpty[int]
|
width: MaybeEmpty[int]
|
||||||
|
|
||||||
|
|
||||||
class _EmbedVideoProxy(Protocol):
|
class _EmbedVideoProxy(Protocol):
|
||||||
url: MaybeEmpty[str]
|
url: MaybeEmpty[str]
|
||||||
height: MaybeEmpty[int]
|
height: MaybeEmpty[int]
|
||||||
width: MaybeEmpty[int]
|
width: MaybeEmpty[int]
|
||||||
|
|
||||||
|
|
||||||
class _EmbedProviderProxy(Protocol):
|
class _EmbedProviderProxy(Protocol):
|
||||||
name: MaybeEmpty[str]
|
name: MaybeEmpty[str]
|
||||||
url: MaybeEmpty[str]
|
url: MaybeEmpty[str]
|
||||||
|
|
||||||
|
|
||||||
class _EmbedAuthorProxy(Protocol):
|
class _EmbedAuthorProxy(Protocol):
|
||||||
name: MaybeEmpty[str]
|
name: MaybeEmpty[str]
|
||||||
url: MaybeEmpty[str]
|
url: MaybeEmpty[str]
|
||||||
@ -181,15 +175,15 @@ class Embed:
|
|||||||
Empty: Final = EmptyEmbed
|
Empty: Final = EmptyEmbed
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||||
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||||
title: MaybeEmpty[Any] = EmptyEmbed,
|
title: MaybeEmpty[Any] = EmptyEmbed,
|
||||||
type: EmbedType = 'rich',
|
type: EmbedType = 'rich',
|
||||||
url: MaybeEmpty[Any] = EmptyEmbed,
|
url: MaybeEmpty[Any] = EmptyEmbed,
|
||||||
description: MaybeEmpty[Any] = EmptyEmbed,
|
description: MaybeEmpty[Any] = EmptyEmbed,
|
||||||
timestamp: datetime.datetime = None,
|
timestamp: datetime.datetime = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
self.colour = colour if colour is not EmptyEmbed else color
|
self.colour = colour if colour is not EmptyEmbed else color
|
||||||
@ -372,7 +366,7 @@ class Embed:
|
|||||||
self._footer['icon_url'] = str(icon_url)
|
self._footer['icon_url'] = str(icon_url)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def remove_footer(self: E) -> E:
|
def remove_footer(self: E) -> E:
|
||||||
"""Clears embed's footer information.
|
"""Clears embed's footer information.
|
||||||
|
|
||||||
@ -387,7 +381,7 @@ class Embed:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> _EmbedMediaProxy:
|
def image(self) -> _EmbedMediaProxy:
|
||||||
"""Returns an ``EmbedProxy`` denoting the image contents.
|
"""Returns an ``EmbedProxy`` denoting the image contents.
|
||||||
@ -403,22 +397,6 @@ class Embed:
|
|||||||
"""
|
"""
|
||||||
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
|
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
|
||||||
|
|
||||||
@image.setter
|
|
||||||
def image(self: E, url: Any):
|
|
||||||
if url is EmptyEmbed:
|
|
||||||
del self._image
|
|
||||||
else:
|
|
||||||
self._image = {
|
|
||||||
'url': str(url),
|
|
||||||
}
|
|
||||||
|
|
||||||
@image.deleter
|
|
||||||
def image(self: E):
|
|
||||||
try:
|
|
||||||
del self._image
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_image(self: E, *, url: MaybeEmpty[Any]) -> E:
|
def set_image(self: E, *, url: MaybeEmpty[Any]) -> E:
|
||||||
"""Sets the image for the embed content.
|
"""Sets the image for the embed content.
|
||||||
|
|
||||||
@ -434,7 +412,15 @@ class Embed:
|
|||||||
The source URL for the image. Only HTTP(S) is supported.
|
The source URL for the image. Only HTTP(S) is supported.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.image = url
|
if url is EmptyEmbed:
|
||||||
|
try:
|
||||||
|
del self._image
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._image = {
|
||||||
|
'url': str(url),
|
||||||
|
}
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -453,23 +439,7 @@ class Embed:
|
|||||||
"""
|
"""
|
||||||
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
||||||
|
|
||||||
@thumbnail.setter
|
def set_thumbnail(self: E, *, url: MaybeEmpty[Any]) -> E:
|
||||||
def thumbnail(self: E, url: Any):
|
|
||||||
if url is EmptyEmbed:
|
|
||||||
del self._thumbnail
|
|
||||||
else:
|
|
||||||
self._thumbnail = {
|
|
||||||
'url': str(url),
|
|
||||||
}
|
|
||||||
|
|
||||||
@thumbnail.deleter
|
|
||||||
def thumbnail(self):
|
|
||||||
try:
|
|
||||||
del self._thumbnail
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_thumbnail(self: E, *, url: MaybeEmpty[Any]):
|
|
||||||
"""Sets the thumbnail for the embed content.
|
"""Sets the thumbnail for the embed content.
|
||||||
|
|
||||||
This function returns the class instance to allow for fluent-style
|
This function returns the class instance to allow for fluent-style
|
||||||
@ -484,7 +454,15 @@ class Embed:
|
|||||||
The source URL for the thumbnail. Only HTTP(S) is supported.
|
The source URL for the thumbnail. Only HTTP(S) is supported.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.thumbnail = url
|
if url is EmptyEmbed:
|
||||||
|
try:
|
||||||
|
del self._thumbnail
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self._thumbnail = {
|
||||||
|
'url': str(url),
|
||||||
|
}
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -72,10 +72,6 @@ class Emoji(_EmojiTag, AssetMixin):
|
|||||||
|
|
||||||
Returns the emoji rendered for discord.
|
Returns the emoji rendered for discord.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the emoji ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@ -141,9 +137,6 @@ class Emoji(_EmojiTag, AssetMixin):
|
|||||||
return f'<a:{self.name}:{self.id}>'
|
return f'<a:{self.name}:{self.id}>'
|
||||||
return f'<:{self.name}:{self.id}>'
|
return f'<:{self.name}:{self.id}>'
|
||||||
|
|
||||||
def __int__(self) -> int:
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>'
|
return f'<Emoji id={self.id} name={self.name!r} animated={self.animated} managed={self.managed}>'
|
||||||
|
|
||||||
|
@ -43,7 +43,6 @@ from .context import Context
|
|||||||
from . import errors
|
from . import errors
|
||||||
from .help import HelpCommand, DefaultHelpCommand
|
from .help import HelpCommand, DefaultHelpCommand
|
||||||
from .cog import Cog
|
from .cog import Cog
|
||||||
from discord.utils import raise_expected_coro
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import importlib.machinery
|
import importlib.machinery
|
||||||
@ -121,8 +120,8 @@ class _DefaultRepr:
|
|||||||
_default = _DefaultRepr()
|
_default = _DefaultRepr()
|
||||||
|
|
||||||
class BotBase(GroupMixin):
|
class BotBase(GroupMixin):
|
||||||
def __init__(self, command_prefix, help_command=_default, description=None, *, intents: discord.Intents, **options):
|
def __init__(self, command_prefix, help_command=_default, description=None, **options):
|
||||||
super().__init__(**options, intents=intents)
|
super().__init__(**options)
|
||||||
self.command_prefix = command_prefix
|
self.command_prefix = command_prefix
|
||||||
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
||||||
self.__cogs: Dict[str, Cog] = {}
|
self.__cogs: Dict[str, Cog] = {}
|
||||||
@ -345,59 +344,14 @@ class BotBase(GroupMixin):
|
|||||||
elif self.owner_ids:
|
elif self.owner_ids:
|
||||||
return user.id in self.owner_ids
|
return user.id in self.owner_ids
|
||||||
else:
|
else:
|
||||||
# Populate the used fields, then retry the check. This is only done at-most once in the bot lifetime.
|
|
||||||
await self.populate_owners()
|
|
||||||
return await self.is_owner(user)
|
|
||||||
|
|
||||||
async def try_owners(self) -> List[discord.User]:
|
app = await self.application_info() # type: ignore
|
||||||
"""|coro|
|
if app.team:
|
||||||
|
self.owner_ids = ids = {m.id for m in app.team.members}
|
||||||
Returns a list of :class:`~discord.User` representing the owners of the bot.
|
return user.id in ids
|
||||||
It uses the :attr:`owner_id` and :attr:`owner_ids`, if set.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
The function also checks if the application is team-owned if
|
|
||||||
:attr:`owner_ids` is not set.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
List[:class:`~discord.User`]
|
|
||||||
List of owners of the bot.
|
|
||||||
"""
|
|
||||||
if self.owner_id:
|
|
||||||
owner = await self.try_user(self.owner_id)
|
|
||||||
|
|
||||||
if owner:
|
|
||||||
return [owner]
|
|
||||||
else:
|
else:
|
||||||
return []
|
self.owner_id = owner_id = app.owner.id
|
||||||
|
return user.id == owner_id
|
||||||
elif self.owner_ids:
|
|
||||||
owners = []
|
|
||||||
|
|
||||||
for owner_id in self.owner_ids:
|
|
||||||
owner = await self.try_user(owner_id)
|
|
||||||
if owner:
|
|
||||||
owners.append(owner)
|
|
||||||
|
|
||||||
return owners
|
|
||||||
else:
|
|
||||||
# We didn't have owners cached yet, cache them and retry.
|
|
||||||
await self.populate_owners()
|
|
||||||
return await self.try_owners()
|
|
||||||
|
|
||||||
async def populate_owners(self):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Populate the :attr:`owner_id` and :attr:`owner_ids` through the use of :meth:`~.Bot.application_info`.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
"""
|
|
||||||
app = await self.application_info() # type: ignore
|
|
||||||
if app.team:
|
|
||||||
self.owner_ids = {m.id for m in app.team.members}
|
|
||||||
else:
|
|
||||||
self.owner_id = app.owner.id
|
|
||||||
|
|
||||||
def before_invoke(self, coro: CFT) -> CFT:
|
def before_invoke(self, coro: CFT) -> CFT:
|
||||||
"""A decorator that registers a coroutine as a pre-invoke hook.
|
"""A decorator that registers a coroutine as a pre-invoke hook.
|
||||||
@ -425,9 +379,11 @@ class BotBase(GroupMixin):
|
|||||||
TypeError
|
TypeError
|
||||||
The coroutine passed is not actually a coroutine.
|
The coroutine passed is not actually a coroutine.
|
||||||
"""
|
"""
|
||||||
return raise_expected_coro(
|
if not asyncio.iscoroutinefunction(coro):
|
||||||
coro, 'The pre-invoke hook must be a coroutine.'
|
raise TypeError('The pre-invoke hook must be a coroutine.')
|
||||||
)
|
|
||||||
|
self._before_invoke = coro
|
||||||
|
return coro
|
||||||
|
|
||||||
def after_invoke(self, coro: CFT) -> CFT:
|
def after_invoke(self, coro: CFT) -> CFT:
|
||||||
r"""A decorator that registers a coroutine as a post-invoke hook.
|
r"""A decorator that registers a coroutine as a post-invoke hook.
|
||||||
@ -456,10 +412,11 @@ class BotBase(GroupMixin):
|
|||||||
TypeError
|
TypeError
|
||||||
The coroutine passed is not actually a coroutine.
|
The coroutine passed is not actually a coroutine.
|
||||||
"""
|
"""
|
||||||
return raise_expected_coro(
|
if not asyncio.iscoroutinefunction(coro):
|
||||||
coro, 'The post-invoke hook must be a coroutine.'
|
raise TypeError('The post-invoke hook must be a coroutine.')
|
||||||
)
|
|
||||||
|
|
||||||
|
self._after_invoke = coro
|
||||||
|
return coro
|
||||||
|
|
||||||
# listener registration
|
# listener registration
|
||||||
|
|
||||||
@ -1118,7 +1075,7 @@ class Bot(BotBase, discord.Client):
|
|||||||
when passing an empty string, it should always be last as no prefix
|
when passing an empty string, it should always be last as no prefix
|
||||||
after it will be matched.
|
after it will be matched.
|
||||||
case_insensitive: :class:`bool`
|
case_insensitive: :class:`bool`
|
||||||
Whether the commands should be case insensitive. Defaults to ``True``. This
|
Whether the commands should be case insensitive. Defaults to ``False``. This
|
||||||
attribute does not carry over to groups. You must set it to every group if
|
attribute does not carry over to groups. You must set it to every group if
|
||||||
you require group commands to be case insensitive as well.
|
you require group commands to be case insensitive as well.
|
||||||
description: :class:`str`
|
description: :class:`str`
|
||||||
|
@ -21,7 +21,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
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 inspect
|
import inspect
|
||||||
@ -33,7 +32,6 @@ import discord.abc
|
|||||||
import discord.utils
|
import discord.utils
|
||||||
|
|
||||||
from discord.message import Message
|
from discord.message import Message
|
||||||
from discord import Permissions
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
@ -62,7 +60,10 @@ T = TypeVar('T')
|
|||||||
BotT = TypeVar('BotT', bound="Union[Bot, AutoShardedBot]")
|
BotT = TypeVar('BotT', bound="Union[Bot, AutoShardedBot]")
|
||||||
CogT = TypeVar('CogT', bound="Cog")
|
CogT = TypeVar('CogT', bound="Cog")
|
||||||
|
|
||||||
P = ParamSpec('P') if TYPE_CHECKING else TypeVar('P')
|
if TYPE_CHECKING:
|
||||||
|
P = ParamSpec('P')
|
||||||
|
else:
|
||||||
|
P = TypeVar('P')
|
||||||
|
|
||||||
|
|
||||||
class Context(discord.abc.Messageable, Generic[BotT]):
|
class Context(discord.abc.Messageable, Generic[BotT]):
|
||||||
@ -313,13 +314,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
|||||||
g = self.guild
|
g = self.guild
|
||||||
return g.voice_client if g else None
|
return g.voice_client if g else None
|
||||||
|
|
||||||
def author_permissions(self) -> Permissions:
|
|
||||||
"""Returns the author permissions in the given channel.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
"""
|
|
||||||
return self.channel.permissions_for(self.author)
|
|
||||||
|
|
||||||
async def send_help(self, *args: Any) -> Any:
|
async def send_help(self, *args: Any) -> Any:
|
||||||
"""send_help(entity=<bot>)
|
"""send_help(entity=<bot>)
|
||||||
|
|
||||||
|
@ -353,14 +353,14 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_channel(ctx, guild_id, channel_id) -> Optional[PartialMessageableChannel]:
|
def _resolve_channel(ctx, guild_id, channel_id) -> Optional[PartialMessageableChannel]:
|
||||||
if guild_id is None:
|
if guild_id is not None:
|
||||||
return ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
guild = ctx.bot.get_guild(guild_id)
|
||||||
|
if guild is not None and channel_id is not None:
|
||||||
guild = ctx.bot.get_guild(guild_id)
|
return guild._resolve_channel(channel_id) # type: ignore
|
||||||
if guild is not None and channel_id is not None:
|
else:
|
||||||
return guild._resolve_channel(channel_id) # type: ignore
|
return None
|
||||||
else:
|
else:
|
||||||
return None
|
return ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||||
|
|
||||||
async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
|
async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
|
||||||
guild_id, message_id, channel_id = self._get_id_matches(ctx, argument)
|
guild_id, message_id, channel_id = self._get_id_matches(ctx, argument)
|
||||||
@ -597,8 +597,7 @@ class ColourConverter(Converter[discord.Colour]):
|
|||||||
Add an alias named ColorConverter
|
Add an alias named ColorConverter
|
||||||
|
|
||||||
The following formats are accepted:
|
The following formats are accepted:
|
||||||
|
|
||||||
- ``<hex>``
|
|
||||||
- ``0x<hex>``
|
- ``0x<hex>``
|
||||||
- ``#<hex>``
|
- ``#<hex>``
|
||||||
- ``0x#<hex>``
|
- ``0x#<hex>``
|
||||||
@ -669,9 +668,7 @@ class ColourConverter(Converter[discord.Colour]):
|
|||||||
|
|
||||||
arg = arg.replace(' ', '_')
|
arg = arg.replace(' ', '_')
|
||||||
method = getattr(discord.Colour, arg, None)
|
method = getattr(discord.Colour, arg, None)
|
||||||
if method is None:
|
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||||
return self.parse_hex_number(argument)
|
|
||||||
elif arg.startswith('from_') or not inspect.ismethod(method):
|
|
||||||
raise BadColourArgument(arg)
|
raise BadColourArgument(arg)
|
||||||
return method()
|
return method()
|
||||||
|
|
||||||
@ -757,8 +754,8 @@ class GuildConverter(IDConverter[discord.Guild]):
|
|||||||
if result is None:
|
if result is None:
|
||||||
result = discord.utils.get(ctx.bot.guilds, name=argument)
|
result = discord.utils.get(ctx.bot.guilds, name=argument)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
raise GuildNotFound(argument)
|
raise GuildNotFound(argument)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@ -942,7 +939,8 @@ class clean_content(Converter[str]):
|
|||||||
def repl(match: re.Match) -> str:
|
def repl(match: re.Match) -> str:
|
||||||
type = match[1]
|
type = match[1]
|
||||||
id = int(match[2])
|
id = int(match[2])
|
||||||
return transforms[type](id)
|
transformed = transforms[type](id)
|
||||||
|
return transformed
|
||||||
|
|
||||||
result = re.sub(r'<(@[!&]?|#)([0-9]{15,20})>', repl, argument)
|
result = re.sub(r'<(@[!&]?|#)([0-9]{15,20})>', repl, argument)
|
||||||
if self.escape_markdown:
|
if self.escape_markdown:
|
||||||
|
@ -1135,10 +1135,10 @@ class GroupMixin(Generic[CogT]):
|
|||||||
A mapping of command name to :class:`.Command`
|
A mapping of command name to :class:`.Command`
|
||||||
objects.
|
objects.
|
||||||
case_insensitive: :class:`bool`
|
case_insensitive: :class:`bool`
|
||||||
Whether the commands should be case insensitive. Defaults to ``True``.
|
Whether the commands should be case insensitive. Defaults to ``False``.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
case_insensitive = kwargs.get('case_insensitive', True)
|
case_insensitive = kwargs.get('case_insensitive', False)
|
||||||
self.all_commands: Dict[str, Command[CogT, Any, Any]] = _CaseInsensitiveDict() if case_insensitive else {}
|
self.all_commands: Dict[str, Command[CogT, Any, Any]] = _CaseInsensitiveDict() if case_insensitive else {}
|
||||||
self.case_insensitive: bool = case_insensitive
|
self.case_insensitive: bool = case_insensitive
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -82,7 +82,9 @@ class StringView:
|
|||||||
def skip_string(self, string):
|
def skip_string(self, string):
|
||||||
strlen = len(string)
|
strlen = len(string)
|
||||||
if self.buffer[self.index:self.index + strlen] == string:
|
if self.buffer[self.index:self.index + strlen] == string:
|
||||||
return self._return_index(strlen, True)
|
self.previous = self.index
|
||||||
|
self.index += strlen
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def read_rest(self):
|
def read_rest(self):
|
||||||
@ -93,7 +95,9 @@ class StringView:
|
|||||||
|
|
||||||
def read(self, n):
|
def read(self, n):
|
||||||
result = self.buffer[self.index:self.index + n]
|
result = self.buffer[self.index:self.index + n]
|
||||||
return self._return_index(n, result)
|
self.previous = self.index
|
||||||
|
self.index += n
|
||||||
|
return result
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
try:
|
try:
|
||||||
@ -101,12 +105,9 @@ class StringView:
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
return self._return_index(1, result)
|
|
||||||
|
|
||||||
def _return_index(self, arg0, arg1):
|
|
||||||
self.previous = self.index
|
self.previous = self.index
|
||||||
self.index += arg0
|
self.index += 1
|
||||||
return arg1
|
return result
|
||||||
|
|
||||||
def get_word(self):
|
def get_word(self):
|
||||||
pos = 0
|
pos = 0
|
||||||
|
@ -46,9 +46,7 @@ import traceback
|
|||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from discord.backoff import ExponentialBackoff
|
from discord.backoff import ExponentialBackoff
|
||||||
from discord.utils import MISSING, raise_expected_coro
|
from discord.utils import MISSING
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'loop',
|
'loop',
|
||||||
@ -490,7 +488,11 @@ class Loop(Generic[LF]):
|
|||||||
The function was not a coroutine.
|
The function was not a coroutine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return raise_expected_coro(coro, f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
if not inspect.iscoroutinefunction(coro):
|
||||||
|
raise TypeError(f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
||||||
|
|
||||||
|
self._before_loop = coro
|
||||||
|
return coro
|
||||||
|
|
||||||
def after_loop(self, coro: FT) -> FT:
|
def after_loop(self, coro: FT) -> FT:
|
||||||
"""A decorator that register a coroutine to be called after the loop finished running.
|
"""A decorator that register a coroutine to be called after the loop finished running.
|
||||||
@ -514,7 +516,11 @@ class Loop(Generic[LF]):
|
|||||||
The function was not a coroutine.
|
The function was not a coroutine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return raise_expected_coro(coro, f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
if not inspect.iscoroutinefunction(coro):
|
||||||
|
raise TypeError(f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
||||||
|
|
||||||
|
self._after_loop = coro
|
||||||
|
return coro
|
||||||
|
|
||||||
def error(self, coro: ET) -> ET:
|
def error(self, coro: ET) -> ET:
|
||||||
"""A decorator that registers a coroutine to be called if the task encounters an unhandled exception.
|
"""A decorator that registers a coroutine to be called if the task encounters an unhandled exception.
|
||||||
@ -536,7 +542,11 @@ class Loop(Generic[LF]):
|
|||||||
TypeError
|
TypeError
|
||||||
The function was not a coroutine.
|
The function was not a coroutine.
|
||||||
"""
|
"""
|
||||||
return raise_expected_coro(coro, f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
if not inspect.iscoroutinefunction(coro):
|
||||||
|
raise TypeError(f'Expected coroutine function, received {coro.__class__.__name__!r}.')
|
||||||
|
|
||||||
|
self._error = coro # type: ignore
|
||||||
|
return coro
|
||||||
|
|
||||||
def _get_next_sleep_time(self) -> datetime.datetime:
|
def _get_next_sleep_time(self) -> datetime.datetime:
|
||||||
if self._sleep is not MISSING:
|
if self._sleep is not MISSING:
|
||||||
@ -604,7 +614,8 @@ class Loop(Generic[LF]):
|
|||||||
)
|
)
|
||||||
ret.append(t if t.tzinfo is not None else t.replace(tzinfo=utc))
|
ret.append(t if t.tzinfo is not None else t.replace(tzinfo=utc))
|
||||||
|
|
||||||
return sorted(set(ret))
|
ret = sorted(set(ret)) # de-dupe and sort times
|
||||||
|
return ret
|
||||||
|
|
||||||
def change_interval(
|
def change_interval(
|
||||||
self,
|
self,
|
||||||
|
@ -480,6 +480,16 @@ class Intents(BaseFlags):
|
|||||||
self.value = self.DEFAULT_VALUE
|
self.value = self.DEFAULT_VALUE
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default(cls: Type[Intents]) -> Intents:
|
||||||
|
"""A factory method that creates a :class:`Intents` with everything enabled
|
||||||
|
except :attr:`presences` and :attr:`members`.
|
||||||
|
"""
|
||||||
|
self = cls.all()
|
||||||
|
self.presences = False
|
||||||
|
self.members = False
|
||||||
|
return self
|
||||||
|
|
||||||
@flag_value
|
@flag_value
|
||||||
def guilds(self):
|
def guilds(self):
|
||||||
""":class:`bool`: Whether guild related events are enabled.
|
""":class:`bool`: Whether guild related events are enabled.
|
||||||
|
@ -22,12 +22,8 @@ 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 typing import TYPE_CHECKING, TypedDict, Any, Optional, List, TypeVar, Type, Dict, Callable, Coroutine, NamedTuple, Deque
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import deque
|
from collections import namedtuple, deque
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
@ -42,25 +38,9 @@ import aiohttp
|
|||||||
from . import utils
|
from . import utils
|
||||||
from .activity import BaseActivity
|
from .activity import BaseActivity
|
||||||
from .enums import SpeakingState
|
from .enums import SpeakingState
|
||||||
from .errors import ConnectionClosed, InvalidArgument
|
from .errors import ConnectionClosed, InvalidArgument
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .client import Client
|
|
||||||
from .state import ConnectionState
|
|
||||||
from .voice_client import VoiceClient
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
DWS = TypeVar('DWS', bound='DiscordWebSocket')
|
|
||||||
DVWS = TypeVar('DVWS', bound='DiscordVoiceWebSocket')
|
|
||||||
|
|
||||||
Coro = Callable[..., Coroutine[Any, Any, Any]]
|
|
||||||
Predicate = Callable[[Dict[str, Any]], bool]
|
|
||||||
DataCallable = Callable[[Dict[str, Any]], T]
|
|
||||||
Result = Optional[DataCallable[Any]]
|
|
||||||
|
|
||||||
|
|
||||||
_log: logging.Logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'DiscordWebSocket',
|
'DiscordWebSocket',
|
||||||
@ -70,49 +50,36 @@ __all__ = (
|
|||||||
'ReconnectWebSocket',
|
'ReconnectWebSocket',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Heartbeat(TypedDict):
|
|
||||||
op: int
|
|
||||||
d: int
|
|
||||||
|
|
||||||
|
|
||||||
class ReconnectWebSocket(Exception):
|
class ReconnectWebSocket(Exception):
|
||||||
"""Signals to safely reconnect the websocket."""
|
"""Signals to safely reconnect the websocket."""
|
||||||
def __init__(self, shard_id: Optional[int], *, resume: bool = True) -> None:
|
def __init__(self, shard_id, *, resume=True):
|
||||||
self.shard_id: Optional[int] = shard_id
|
self.shard_id = shard_id
|
||||||
self.resume: bool = resume
|
self.resume = resume
|
||||||
self.op = 'RESUME' if resume else 'IDENTIFY'
|
self.op = 'RESUME' if resume else 'IDENTIFY'
|
||||||
|
|
||||||
|
|
||||||
class WebSocketClosure(Exception):
|
class WebSocketClosure(Exception):
|
||||||
"""An exception to make up for the fact that aiohttp doesn't signal closure."""
|
"""An exception to make up for the fact that aiohttp doesn't signal closure."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
EventListener = namedtuple('EventListener', 'predicate event result future')
|
||||||
class EventListener(NamedTuple):
|
|
||||||
predicate: Predicate
|
|
||||||
event: str
|
|
||||||
result: Result
|
|
||||||
future: asyncio.Future
|
|
||||||
|
|
||||||
|
|
||||||
class GatewayRatelimiter:
|
class GatewayRatelimiter:
|
||||||
def __init__(self, count: int = 110, per: float = 60.0) -> None:
|
def __init__(self, count=110, per=60.0):
|
||||||
# The default is 110 to give room for at least 10 heartbeats per minute
|
# The default is 110 to give room for at least 10 heartbeats per minute
|
||||||
self.max: int = count
|
self.max = count
|
||||||
self.remaining: int = count
|
self.remaining = count
|
||||||
self.window: float = 0.0
|
self.window = 0.0
|
||||||
self.per: float = per
|
self.per = per
|
||||||
self.lock: asyncio.Lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
self.shard_id: Optional[int] = None
|
self.shard_id = None
|
||||||
|
|
||||||
def is_ratelimited(self) -> bool:
|
def is_ratelimited(self):
|
||||||
current = time.time()
|
current = time.time()
|
||||||
if current > self.window + self.per:
|
if current > self.window + self.per:
|
||||||
return False
|
return False
|
||||||
return self.remaining == 0
|
return self.remaining == 0
|
||||||
|
|
||||||
def get_delay(self) -> float:
|
def get_delay(self):
|
||||||
current = time.time()
|
current = time.time()
|
||||||
|
|
||||||
if current > self.window + self.per:
|
if current > self.window + self.per:
|
||||||
@ -130,7 +97,7 @@ class GatewayRatelimiter:
|
|||||||
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
async def block(self) -> None:
|
async def block(self):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
delta = self.get_delay()
|
delta = self.get_delay()
|
||||||
if delta:
|
if delta:
|
||||||
@ -139,27 +106,27 @@ class GatewayRatelimiter:
|
|||||||
|
|
||||||
|
|
||||||
class KeepAliveHandler(threading.Thread):
|
class KeepAliveHandler(threading.Thread):
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args, **kwargs):
|
||||||
ws = kwargs.pop('ws')
|
ws = kwargs.pop('ws', None)
|
||||||
interval = kwargs.pop('interval', None)
|
interval = kwargs.pop('interval', None)
|
||||||
shard_id = kwargs.pop('shard_id', None)
|
shard_id = kwargs.pop('shard_id', None)
|
||||||
threading.Thread.__init__(self, *args, **kwargs)
|
threading.Thread.__init__(self, *args, **kwargs)
|
||||||
self.ws: DiscordWebSocket = ws
|
self.ws = ws
|
||||||
self._main_thread_id: int = ws.thread_id
|
self._main_thread_id = ws.thread_id
|
||||||
self.interval: Optional[float] = interval
|
self.interval = interval
|
||||||
self.daemon: bool = True
|
self.daemon = True
|
||||||
self.shard_id: Optional[int] = shard_id
|
self.shard_id = shard_id
|
||||||
self.msg: str = 'Keeping shard ID %s websocket alive with sequence %s.'
|
self.msg = 'Keeping shard ID %s websocket alive with sequence %s.'
|
||||||
self.block_msg: str = 'Shard ID %s heartbeat blocked for more than %s seconds.'
|
self.block_msg = 'Shard ID %s heartbeat blocked for more than %s seconds.'
|
||||||
self.behind_msg: str = 'Can\'t keep up, shard ID %s websocket is %.1fs behind.'
|
self.behind_msg = 'Can\'t keep up, shard ID %s websocket is %.1fs behind.'
|
||||||
self._stop_ev: threading.Event = threading.Event()
|
self._stop_ev = threading.Event()
|
||||||
self._last_ack: float = time.perf_counter()
|
self._last_ack = time.perf_counter()
|
||||||
self._last_send: float = time.perf_counter()
|
self._last_send = time.perf_counter()
|
||||||
self._last_recv: float = time.perf_counter()
|
self._last_recv = time.perf_counter()
|
||||||
self.latency: float = float('inf')
|
self.latency = float('inf')
|
||||||
self.heartbeat_timeout: float = ws._max_heartbeat_timeout
|
self.heartbeat_timeout = ws._max_heartbeat_timeout
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self):
|
||||||
while not self._stop_ev.wait(self.interval):
|
while not self._stop_ev.wait(self.interval):
|
||||||
if self._last_recv + self.heartbeat_timeout < time.perf_counter():
|
if self._last_recv + self.heartbeat_timeout < time.perf_counter():
|
||||||
_log.warning("Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id)
|
_log.warning("Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id)
|
||||||
@ -201,20 +168,19 @@ class KeepAliveHandler(threading.Thread):
|
|||||||
else:
|
else:
|
||||||
self._last_send = time.perf_counter()
|
self._last_send = time.perf_counter()
|
||||||
|
|
||||||
def get_payload(self) -> Heartbeat:
|
def get_payload(self):
|
||||||
return {
|
return {
|
||||||
'op': self.ws.HEARTBEAT,
|
'op': self.ws.HEARTBEAT,
|
||||||
# the websocket's sequence won't be None here
|
'd': self.ws.sequence
|
||||||
'd': self.ws.sequence # type: ignore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self):
|
||||||
self._stop_ev.set()
|
self._stop_ev.set()
|
||||||
|
|
||||||
def tick(self) -> None:
|
def tick(self):
|
||||||
self._last_recv = time.perf_counter()
|
self._last_recv = time.perf_counter()
|
||||||
|
|
||||||
def ack(self) -> None:
|
def ack(self):
|
||||||
ack_time = time.perf_counter()
|
ack_time = time.perf_counter()
|
||||||
self._last_ack = ack_time
|
self._last_ack = ack_time
|
||||||
self.latency = ack_time - self._last_send
|
self.latency = ack_time - self._last_send
|
||||||
@ -222,32 +188,30 @@ class KeepAliveHandler(threading.Thread):
|
|||||||
_log.warning(self.behind_msg, self.shard_id, self.latency)
|
_log.warning(self.behind_msg, self.shard_id, self.latency)
|
||||||
|
|
||||||
class VoiceKeepAliveHandler(KeepAliveHandler):
|
class VoiceKeepAliveHandler(KeepAliveHandler):
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.recent_ack_latencies: Deque[float] = deque(maxlen=20)
|
self.recent_ack_latencies = deque(maxlen=20)
|
||||||
self.msg = 'Keeping shard ID %s voice websocket alive with timestamp %s.'
|
self.msg = 'Keeping shard ID %s voice websocket alive with timestamp %s.'
|
||||||
self.block_msg = 'Shard ID %s voice heartbeat blocked for more than %s seconds'
|
self.block_msg = 'Shard ID %s voice heartbeat blocked for more than %s seconds'
|
||||||
self.behind_msg = 'High socket latency, shard ID %s heartbeat is %.1fs behind'
|
self.behind_msg = 'High socket latency, shard ID %s heartbeat is %.1fs behind'
|
||||||
|
|
||||||
def get_payload(self) -> Heartbeat:
|
def get_payload(self):
|
||||||
return {
|
return {
|
||||||
'op': self.ws.HEARTBEAT,
|
'op': self.ws.HEARTBEAT,
|
||||||
'd': int(time.time() * 1000)
|
'd': int(time.time() * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
def ack(self) -> None:
|
def ack(self):
|
||||||
ack_time = time.perf_counter()
|
ack_time = time.perf_counter()
|
||||||
self._last_ack = ack_time
|
self._last_ack = ack_time
|
||||||
self._last_recv = ack_time
|
self._last_recv = ack_time
|
||||||
self.latency = ack_time - self._last_send
|
self.latency = ack_time - self._last_send
|
||||||
self.recent_ack_latencies.append(self.latency)
|
self.recent_ack_latencies.append(self.latency)
|
||||||
|
|
||||||
|
|
||||||
class DiscordClientWebSocketResponse(aiohttp.ClientWebSocketResponse):
|
class DiscordClientWebSocketResponse(aiohttp.ClientWebSocketResponse):
|
||||||
async def close(self, *, code: int = 4000, message: bytes = b'') -> bool:
|
async def close(self, *, code: int = 4000, message: bytes = b'') -> bool:
|
||||||
return await super().close(code=code, message=message)
|
return await super().close(code=code, message=message)
|
||||||
|
|
||||||
|
|
||||||
class DiscordWebSocket:
|
class DiscordWebSocket:
|
||||||
"""Implements a WebSocket for Discord's gateway v6.
|
"""Implements a WebSocket for Discord's gateway v6.
|
||||||
|
|
||||||
@ -302,53 +266,41 @@ class DiscordWebSocket:
|
|||||||
HEARTBEAT_ACK = 11
|
HEARTBEAT_ACK = 11
|
||||||
GUILD_SYNC = 12
|
GUILD_SYNC = 12
|
||||||
|
|
||||||
def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop) -> None:
|
def __init__(self, socket, *, loop):
|
||||||
self.socket: aiohttp.ClientWebSocketResponse = socket
|
self.socket = socket
|
||||||
self.loop: asyncio.AbstractEventLoop = loop
|
self.loop = loop
|
||||||
|
|
||||||
# an empty dispatcher to prevent crashes
|
# an empty dispatcher to prevent crashes
|
||||||
self._dispatch = lambda *args: None
|
self._dispatch = lambda *args: None
|
||||||
# generic event listeners
|
# generic event listeners
|
||||||
self._dispatch_listeners: List[EventListener] = []
|
self._dispatch_listeners = []
|
||||||
# the keep alive
|
# the keep alive
|
||||||
self._keep_alive: Optional[KeepAliveHandler] = None
|
self._keep_alive = None
|
||||||
self.thread_id: int = threading.get_ident()
|
self.thread_id = threading.get_ident()
|
||||||
|
|
||||||
# ws related stuff
|
# ws related stuff
|
||||||
self.session_id: Optional[str] = None
|
self.session_id = None
|
||||||
self.sequence: Optional[int] = None
|
self.sequence = None
|
||||||
self._zlib = zlib.decompressobj()
|
self._zlib = zlib.decompressobj()
|
||||||
self._buffer: bytearray = bytearray()
|
self._buffer = bytearray()
|
||||||
self._close_code: Optional[int] = None
|
self._close_code = None
|
||||||
self._rate_limiter: GatewayRatelimiter = GatewayRatelimiter()
|
self._rate_limiter = GatewayRatelimiter()
|
||||||
|
|
||||||
# attributes that get set in from_client
|
|
||||||
self.token: str = utils.MISSING
|
|
||||||
self._connection: ConnectionState = utils.MISSING
|
|
||||||
self._discord_parsers: Dict[str, DataCallable[None]] = utils.MISSING
|
|
||||||
self.gateway: str = utils.MISSING
|
|
||||||
self.call_hooks: Coro = utils.MISSING
|
|
||||||
self._initial_identify: bool = utils.MISSING
|
|
||||||
self.shard_id: Optional[int] = utils.MISSING
|
|
||||||
self.shard_count: Optional[int] = utils.MISSING
|
|
||||||
self.session_id: Optional[str] = utils.MISSING
|
|
||||||
self._max_heartbeat_timeout: float = utils.MISSING
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open(self) -> bool:
|
def open(self):
|
||||||
return not self.socket.closed
|
return not self.socket.closed
|
||||||
|
|
||||||
def is_ratelimited(self) -> bool:
|
def is_ratelimited(self):
|
||||||
return self._rate_limiter.is_ratelimited()
|
return self._rate_limiter.is_ratelimited()
|
||||||
|
|
||||||
def debug_log_receive(self, data, /) -> None:
|
def debug_log_receive(self, data, /):
|
||||||
self._dispatch('socket_raw_receive', data)
|
self._dispatch('socket_raw_receive', data)
|
||||||
|
|
||||||
def log_receive(self, _, /) -> None:
|
def log_receive(self, _, /):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_client(cls: Type[DWS], client: Client, *, initial: bool = False, gateway: Optional[str] = None, shard_id: Optional[int] = None, session: Optional[str] = None, sequence: Optional[int] = None, resume: bool = False) -> DWS:
|
async def from_client(cls, client, *, initial=False, gateway=None, shard_id=None, session=None, sequence=None, resume=False):
|
||||||
"""Creates a main websocket for Discord from a :class:`Client`.
|
"""Creates a main websocket for Discord from a :class:`Client`.
|
||||||
|
|
||||||
This is for internal use only.
|
This is for internal use only.
|
||||||
@ -358,9 +310,7 @@ class DiscordWebSocket:
|
|||||||
ws = cls(socket, loop=client.loop)
|
ws = cls(socket, loop=client.loop)
|
||||||
|
|
||||||
# dynamically add attributes needed
|
# dynamically add attributes needed
|
||||||
|
ws.token = client.http.token
|
||||||
# the token won't be None here
|
|
||||||
ws.token = client.http.token # type: ignore
|
|
||||||
ws._connection = client._connection
|
ws._connection = client._connection
|
||||||
ws._discord_parsers = client._connection.parsers
|
ws._discord_parsers = client._connection.parsers
|
||||||
ws._dispatch = client.dispatch
|
ws._dispatch = client.dispatch
|
||||||
@ -392,7 +342,7 @@ class DiscordWebSocket:
|
|||||||
await ws.resume()
|
await ws.resume()
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
def wait_for(self, event: str, predicate: Predicate, result: Result = None) -> asyncio.Future:
|
def wait_for(self, event, predicate, result=None):
|
||||||
"""Waits for a DISPATCH'd event that meets the predicate.
|
"""Waits for a DISPATCH'd event that meets the predicate.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@ -417,7 +367,7 @@ class DiscordWebSocket:
|
|||||||
self._dispatch_listeners.append(entry)
|
self._dispatch_listeners.append(entry)
|
||||||
return future
|
return future
|
||||||
|
|
||||||
async def identify(self) -> None:
|
async def identify(self):
|
||||||
"""Sends the IDENTIFY packet."""
|
"""Sends the IDENTIFY packet."""
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.IDENTIFY,
|
'op': self.IDENTIFY,
|
||||||
@ -455,7 +405,7 @@ class DiscordWebSocket:
|
|||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
_log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id)
|
_log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id)
|
||||||
|
|
||||||
async def resume(self) -> None:
|
async def resume(self):
|
||||||
"""Sends the RESUME packet."""
|
"""Sends the RESUME packet."""
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.RESUME,
|
'op': self.RESUME,
|
||||||
@ -469,8 +419,7 @@ class DiscordWebSocket:
|
|||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
_log.info('Shard ID %s has sent the RESUME payload.', self.shard_id)
|
_log.info('Shard ID %s has sent the RESUME payload.', self.shard_id)
|
||||||
|
|
||||||
|
async def received_message(self, msg, /):
|
||||||
async def received_message(self, msg, /) -> None:
|
|
||||||
if type(msg) is bytes:
|
if type(msg) is bytes:
|
||||||
self._buffer.extend(msg)
|
self._buffer.extend(msg)
|
||||||
|
|
||||||
@ -588,16 +537,16 @@ class DiscordWebSocket:
|
|||||||
del self._dispatch_listeners[index]
|
del self._dispatch_listeners[index]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latency(self) -> float:
|
def latency(self):
|
||||||
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds."""
|
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds."""
|
||||||
heartbeat = self._keep_alive
|
heartbeat = self._keep_alive
|
||||||
return float('inf') if heartbeat is None else heartbeat.latency
|
return float('inf') if heartbeat is None else heartbeat.latency
|
||||||
|
|
||||||
def _can_handle_close(self) -> bool:
|
def _can_handle_close(self):
|
||||||
code = self._close_code or self.socket.close_code
|
code = self._close_code or self.socket.close_code
|
||||||
return code not in (1000, 4004, 4010, 4011, 4012, 4013, 4014)
|
return code not in (1000, 4004, 4010, 4011, 4012, 4013, 4014)
|
||||||
|
|
||||||
async def poll_event(self) -> None:
|
async def poll_event(self):
|
||||||
"""Polls for a DISPATCH event and handles the general gateway loop.
|
"""Polls for a DISPATCH event and handles the general gateway loop.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
@ -635,23 +584,23 @@ class DiscordWebSocket:
|
|||||||
_log.info('Websocket closed with %s, cannot reconnect.', code)
|
_log.info('Websocket closed with %s, cannot reconnect.', code)
|
||||||
raise ConnectionClosed(self.socket, shard_id=self.shard_id, code=code) from None
|
raise ConnectionClosed(self.socket, shard_id=self.shard_id, code=code) from None
|
||||||
|
|
||||||
async def debug_send(self, data, /) -> None:
|
async def debug_send(self, data, /):
|
||||||
await self._rate_limiter.block()
|
await self._rate_limiter.block()
|
||||||
self._dispatch('socket_raw_send', data)
|
self._dispatch('socket_raw_send', data)
|
||||||
await self.socket.send_str(data)
|
await self.socket.send_str(data)
|
||||||
|
|
||||||
async def send(self, data, /) -> None:
|
async def send(self, data, /):
|
||||||
await self._rate_limiter.block()
|
await self._rate_limiter.block()
|
||||||
await self.socket.send_str(data)
|
await self.socket.send_str(data)
|
||||||
|
|
||||||
async def send_as_json(self, data) -> None:
|
async def send_as_json(self, data):
|
||||||
try:
|
try:
|
||||||
await self.send(utils._to_json(data))
|
await self.send(utils._to_json(data))
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
if not self._can_handle_close():
|
if not self._can_handle_close():
|
||||||
raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc
|
raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc
|
||||||
|
|
||||||
async def send_heartbeat(self, data: Heartbeat) -> None:
|
async def send_heartbeat(self, data):
|
||||||
# This bypasses the rate limit handling code since it has a higher priority
|
# This bypasses the rate limit handling code since it has a higher priority
|
||||||
try:
|
try:
|
||||||
await self.socket.send_str(utils._to_json(data))
|
await self.socket.send_str(utils._to_json(data))
|
||||||
@ -659,13 +608,13 @@ class DiscordWebSocket:
|
|||||||
if not self._can_handle_close():
|
if not self._can_handle_close():
|
||||||
raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc
|
raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc
|
||||||
|
|
||||||
async def change_presence(self, *, activity: Optional[BaseActivity] = None, status: Optional[str] = None, since: float = 0.0) -> None:
|
async def change_presence(self, *, activity=None, status=None, since=0.0):
|
||||||
if activity is not None:
|
if activity is not None:
|
||||||
if not isinstance(activity, BaseActivity):
|
if not isinstance(activity, BaseActivity):
|
||||||
raise InvalidArgument('activity must derive from BaseActivity.')
|
raise InvalidArgument('activity must derive from BaseActivity.')
|
||||||
activities = [activity.to_dict()]
|
activity = [activity.to_dict()]
|
||||||
else:
|
else:
|
||||||
activities = []
|
activity = []
|
||||||
|
|
||||||
if status == 'idle':
|
if status == 'idle':
|
||||||
since = int(time.time() * 1000)
|
since = int(time.time() * 1000)
|
||||||
@ -673,7 +622,7 @@ class DiscordWebSocket:
|
|||||||
payload = {
|
payload = {
|
||||||
'op': self.PRESENCE,
|
'op': self.PRESENCE,
|
||||||
'd': {
|
'd': {
|
||||||
'activities': activities,
|
'activities': activity,
|
||||||
'afk': False,
|
'afk': False,
|
||||||
'since': since,
|
'since': since,
|
||||||
'status': status
|
'status': status
|
||||||
@ -684,7 +633,7 @@ class DiscordWebSocket:
|
|||||||
_log.debug('Sending "%s" to change status', sent)
|
_log.debug('Sending "%s" to change status', sent)
|
||||||
await self.send(sent)
|
await self.send(sent)
|
||||||
|
|
||||||
async def request_chunks(self, guild_id: int, query: Optional[str] = None, *, limit: int, user_ids: Optional[List[int]] = None, presences: bool = False, nonce: Optional[int] = None) -> None:
|
async def request_chunks(self, guild_id, query=None, *, limit, user_ids=None, presences=False, nonce=None):
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.REQUEST_MEMBERS,
|
'op': self.REQUEST_MEMBERS,
|
||||||
'd': {
|
'd': {
|
||||||
@ -706,7 +655,7 @@ class DiscordWebSocket:
|
|||||||
|
|
||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
|
|
||||||
async def voice_state(self, guild_id: int, channel_id: int, self_mute: bool = False, self_deaf: bool = False) -> None:
|
async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=False):
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.VOICE_STATE,
|
'op': self.VOICE_STATE,
|
||||||
'd': {
|
'd': {
|
||||||
@ -720,7 +669,7 @@ class DiscordWebSocket:
|
|||||||
_log.debug('Updating our voice state to %s.', payload)
|
_log.debug('Updating our voice state to %s.', payload)
|
||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
|
|
||||||
async def close(self, code: int = 4000) -> None:
|
async def close(self, code=4000):
|
||||||
if self._keep_alive:
|
if self._keep_alive:
|
||||||
self._keep_alive.stop()
|
self._keep_alive.stop()
|
||||||
self._keep_alive = None
|
self._keep_alive = None
|
||||||
@ -772,31 +721,25 @@ class DiscordVoiceWebSocket:
|
|||||||
CLIENT_CONNECT = 12
|
CLIENT_CONNECT = 12
|
||||||
CLIENT_DISCONNECT = 13
|
CLIENT_DISCONNECT = 13
|
||||||
|
|
||||||
def __init__(self, socket: aiohttp.ClientWebSocketResponse, loop: asyncio.AbstractEventLoop, *, hook: Optional[Coro] = None) -> None:
|
def __init__(self, socket, loop, *, hook=None):
|
||||||
self.ws: aiohttp.ClientWebSocketResponse = socket
|
self.ws = socket
|
||||||
self.loop: asyncio.AbstractEventLoop = loop
|
self.loop = loop
|
||||||
self._keep_alive: VoiceKeepAliveHandler = utils.MISSING
|
self._keep_alive = None
|
||||||
self._close_code: Optional[int] = None
|
self._close_code = None
|
||||||
self.secret_key: Optional[List[int]] = None
|
self.secret_key = None
|
||||||
self.gateway: str = utils.MISSING
|
|
||||||
self._connection: VoiceClient = utils.MISSING
|
|
||||||
self._max_heartbeat_timeout: float = utils.MISSING
|
|
||||||
self.thread_id: int = utils.MISSING
|
|
||||||
if hook:
|
if hook:
|
||||||
# we want to redeclare self._hook
|
self._hook = hook
|
||||||
self._hook = hook # type: ignore
|
|
||||||
|
|
||||||
async def _hook(self, *args: Any) -> Any:
|
async def _hook(self, *args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def send_as_json(self, data):
|
||||||
async def send_as_json(self, data) -> None:
|
|
||||||
_log.debug('Sending voice websocket frame: %s.', data)
|
_log.debug('Sending voice websocket frame: %s.', data)
|
||||||
await self.ws.send_str(utils._to_json(data))
|
await self.ws.send_str(utils._to_json(data))
|
||||||
|
|
||||||
send_heartbeat = send_as_json
|
send_heartbeat = send_as_json
|
||||||
|
|
||||||
async def resume(self) -> None:
|
async def resume(self):
|
||||||
state = self._connection
|
state = self._connection
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.RESUME,
|
'op': self.RESUME,
|
||||||
@ -822,7 +765,7 @@ class DiscordVoiceWebSocket:
|
|||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_client(cls: Type[DVWS], client: VoiceClient, *, resume: bool = False, hook: Optional[Coro] = None) -> DVWS:
|
async def from_client(cls, client, *, resume=False, hook=None):
|
||||||
"""Creates a voice websocket for the :class:`VoiceClient`."""
|
"""Creates a voice websocket for the :class:`VoiceClient`."""
|
||||||
gateway = 'wss://' + client.endpoint + '/?v=4'
|
gateway = 'wss://' + client.endpoint + '/?v=4'
|
||||||
http = client._state.http
|
http = client._state.http
|
||||||
@ -840,7 +783,7 @@ class DiscordVoiceWebSocket:
|
|||||||
|
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
async def select_protocol(self, ip, port, mode) -> None:
|
async def select_protocol(self, ip, port, mode):
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.SELECT_PROTOCOL,
|
'op': self.SELECT_PROTOCOL,
|
||||||
'd': {
|
'd': {
|
||||||
@ -855,7 +798,7 @@ class DiscordVoiceWebSocket:
|
|||||||
|
|
||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
|
|
||||||
async def client_connect(self) -> None:
|
async def client_connect(self):
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.CLIENT_CONNECT,
|
'op': self.CLIENT_CONNECT,
|
||||||
'd': {
|
'd': {
|
||||||
@ -865,7 +808,7 @@ class DiscordVoiceWebSocket:
|
|||||||
|
|
||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
|
|
||||||
async def speak(self, state=SpeakingState.voice) -> None:
|
async def speak(self, state=SpeakingState.voice):
|
||||||
payload = {
|
payload = {
|
||||||
'op': self.SPEAKING,
|
'op': self.SPEAKING,
|
||||||
'd': {
|
'd': {
|
||||||
@ -876,8 +819,7 @@ class DiscordVoiceWebSocket:
|
|||||||
|
|
||||||
await self.send_as_json(payload)
|
await self.send_as_json(payload)
|
||||||
|
|
||||||
|
async def received_message(self, msg):
|
||||||
async def received_message(self, msg) -> None:
|
|
||||||
_log.debug('Voice websocket frame received: %s', msg)
|
_log.debug('Voice websocket frame received: %s', msg)
|
||||||
op = msg['op']
|
op = msg['op']
|
||||||
data = msg.get('d')
|
data = msg.get('d')
|
||||||
@ -898,7 +840,7 @@ class DiscordVoiceWebSocket:
|
|||||||
|
|
||||||
await self._hook(self, msg)
|
await self._hook(self, msg)
|
||||||
|
|
||||||
async def initial_connection(self, data) -> None:
|
async def initial_connection(self, data):
|
||||||
state = self._connection
|
state = self._connection
|
||||||
state.ssrc = data['ssrc']
|
state.ssrc = data['ssrc']
|
||||||
state.voice_port = data['port']
|
state.voice_port = data['port']
|
||||||
@ -929,13 +871,13 @@ class DiscordVoiceWebSocket:
|
|||||||
_log.info('selected the voice protocol for use (%s)', mode)
|
_log.info('selected the voice protocol for use (%s)', mode)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latency(self) -> float:
|
def latency(self):
|
||||||
""":class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds."""
|
""":class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds."""
|
||||||
heartbeat = self._keep_alive
|
heartbeat = self._keep_alive
|
||||||
return float('inf') if heartbeat is None else heartbeat.latency
|
return float('inf') if heartbeat is None else heartbeat.latency
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def average_latency(self) -> float:
|
def average_latency(self):
|
||||||
""":class:`list`: Average of last 20 HEARTBEAT latencies."""
|
""":class:`list`: Average of last 20 HEARTBEAT latencies."""
|
||||||
heartbeat = self._keep_alive
|
heartbeat = self._keep_alive
|
||||||
if heartbeat is None or not heartbeat.recent_ack_latencies:
|
if heartbeat is None or not heartbeat.recent_ack_latencies:
|
||||||
@ -943,14 +885,13 @@ class DiscordVoiceWebSocket:
|
|||||||
|
|
||||||
return sum(heartbeat.recent_ack_latencies) / len(heartbeat.recent_ack_latencies)
|
return sum(heartbeat.recent_ack_latencies) / len(heartbeat.recent_ack_latencies)
|
||||||
|
|
||||||
|
async def load_secret_key(self, data):
|
||||||
async def load_secret_key(self, data) -> None:
|
|
||||||
_log.info('received secret key for voice connection')
|
_log.info('received secret key for voice connection')
|
||||||
self.secret_key = self._connection.secret_key = data.get('secret_key')
|
self.secret_key = self._connection.secret_key = data.get('secret_key')
|
||||||
await self.speak()
|
await self.speak()
|
||||||
await self.speak(False)
|
await self.speak(False)
|
||||||
|
|
||||||
async def poll_event(self) -> None:
|
async def poll_event(self):
|
||||||
# This exception is handled up the chain
|
# This exception is handled up the chain
|
||||||
msg = await asyncio.wait_for(self.ws.receive(), timeout=30.0)
|
msg = await asyncio.wait_for(self.ws.receive(), timeout=30.0)
|
||||||
if msg.type is aiohttp.WSMsgType.TEXT:
|
if msg.type is aiohttp.WSMsgType.TEXT:
|
||||||
@ -962,7 +903,7 @@ class DiscordVoiceWebSocket:
|
|||||||
_log.debug('Received %s', msg)
|
_log.debug('Received %s', msg)
|
||||||
raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code)
|
raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code)
|
||||||
|
|
||||||
async def close(self, code: int = 1000) -> None:
|
async def close(self, code=1000):
|
||||||
if self._keep_alive is not None:
|
if self._keep_alive is not None:
|
||||||
self._keep_alive.stop()
|
self._keep_alive.stop()
|
||||||
|
|
||||||
|
120
discord/guild.py
120
discord/guild.py
@ -46,7 +46,7 @@ from . import utils, abc
|
|||||||
from .role import Role
|
from .role import Role
|
||||||
from .member import Member, VoiceState
|
from .member import Member, VoiceState
|
||||||
from .emoji import Emoji
|
from .emoji import Emoji
|
||||||
from .errors import InvalidData, NotFound
|
from .errors import InvalidData
|
||||||
from .permissions import PermissionOverwrite
|
from .permissions import PermissionOverwrite
|
||||||
from .colour import Colour
|
from .colour import Colour
|
||||||
from .errors import InvalidArgument, ClientException
|
from .errors import InvalidArgument, ClientException
|
||||||
@ -76,7 +76,6 @@ from .stage_instance import StageInstance
|
|||||||
from .threads import Thread, ThreadMember
|
from .threads import Thread, ThreadMember
|
||||||
from .sticker import GuildSticker
|
from .sticker import GuildSticker
|
||||||
from .file import File
|
from .file import File
|
||||||
from .welcome_screen import WelcomeScreen, WelcomeChannel
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -141,10 +140,6 @@ class Guild(Hashable):
|
|||||||
|
|
||||||
Returns the guild's name.
|
Returns the guild's name.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the guild's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
@ -743,16 +738,12 @@ class Guild(Hashable):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def humans(self) -> List[Member]:
|
def humans(self) -> List[Member]:
|
||||||
"""List[:class:`Member`]: A list of human members that belong to this guild.
|
"""List[:class:`Member`]: A list of human members that belong to this guild."""
|
||||||
|
|
||||||
.. versionadded:: 2.0 """
|
|
||||||
return [member for member in self.members if not member.bot]
|
return [member for member in self.members if not member.bot]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bots(self) -> List[Member]:
|
def bots(self) -> List[Member]:
|
||||||
"""List[:class:`Member`]: A list of bots that belong to this guild.
|
"""List[:class:`Member`]: A list of bots that belong to this guild."""
|
||||||
|
|
||||||
.. versionadded:: 2.0 """
|
|
||||||
return [member for member in self.members if member.bot]
|
return [member for member in self.members if member.bot]
|
||||||
|
|
||||||
def get_member(self, user_id: int, /) -> Optional[Member]:
|
def get_member(self, user_id: int, /) -> Optional[Member]:
|
||||||
@ -1724,8 +1715,6 @@ class Guild(Hashable):
|
|||||||
You do not have access to the guild.
|
You do not have access to the guild.
|
||||||
HTTPException
|
HTTPException
|
||||||
Fetching the member failed.
|
Fetching the member failed.
|
||||||
NotFound
|
|
||||||
A member with that ID does not exist.
|
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
--------
|
--------
|
||||||
@ -1735,34 +1724,6 @@ class Guild(Hashable):
|
|||||||
data = await self._state.http.get_member(self.id, member_id)
|
data = await self._state.http.get_member(self.id, member_id)
|
||||||
return Member(data=data, state=self._state, guild=self)
|
return Member(data=data, state=self._state, guild=self)
|
||||||
|
|
||||||
async def try_member(self, member_id: int, /) -> Optional[Member]:
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Returns a member with the given ID. This uses the cache first, and if not found, it'll request using :meth:`fetch_member`.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This method might result in an API call.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
-----------
|
|
||||||
member_id: :class:`int`
|
|
||||||
The ID to search for.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
Optional[:class:`Member`]
|
|
||||||
The member or ``None`` if not found.
|
|
||||||
"""
|
|
||||||
member = self.get_member(member_id)
|
|
||||||
|
|
||||||
if member:
|
|
||||||
return member
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return await self.fetch_member(member_id)
|
|
||||||
except NotFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def fetch_ban(self, user: Snowflake) -> BanEntry:
|
async def fetch_ban(self, user: Snowflake) -> BanEntry:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
@ -2605,81 +2566,6 @@ class Guild(Hashable):
|
|||||||
|
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
async def welcome_screen(self) -> WelcomeScreen:
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Returns the guild's welcome screen.
|
|
||||||
|
|
||||||
The guild must have ``COMMUNITY`` in :attr:`~Guild.features`.
|
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_guild` permission to use
|
|
||||||
this as well.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
Forbidden
|
|
||||||
You do not have the proper permissions to get this.
|
|
||||||
HTTPException
|
|
||||||
Retrieving the welcome screen failed.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`WelcomeScreen`
|
|
||||||
The welcome screen.
|
|
||||||
"""
|
|
||||||
data = await self._state.http.get_welcome_screen(self.id)
|
|
||||||
return WelcomeScreen(data=data, guild=self)
|
|
||||||
|
|
||||||
@overload
|
|
||||||
async def edit_welcome_screen(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
description: Optional[str] = ...,
|
|
||||||
welcome_channels: Optional[List[WelcomeChannel]] = ...,
|
|
||||||
enabled: Optional[bool] = ...,
|
|
||||||
) -> WelcomeScreen:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
async def edit_welcome_screen(self) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
async def edit_welcome_screen(self, **kwargs):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
A shorthand method of :attr:`WelcomeScreen.edit` without needing
|
|
||||||
to fetch the welcome screen beforehand.
|
|
||||||
|
|
||||||
The guild must have ``COMMUNITY`` in :attr:`~Guild.features`.
|
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_guild` permission to use
|
|
||||||
this as well.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`WelcomeScreen`
|
|
||||||
The edited welcome screen.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
welcome_channels = kwargs['welcome_channels']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
welcome_channels_serialised = []
|
|
||||||
for wc in welcome_channels:
|
|
||||||
if not isinstance(wc, WelcomeChannel):
|
|
||||||
raise InvalidArgument('welcome_channels parameter must be a list of WelcomeChannel')
|
|
||||||
welcome_channels_serialised.append(wc.to_dict())
|
|
||||||
kwargs['welcome_channels'] = welcome_channels_serialised
|
|
||||||
|
|
||||||
if kwargs:
|
|
||||||
data = await self._state.http.edit_welcome_screen(self.id, kwargs)
|
|
||||||
return WelcomeScreen(data=data, guild=self)
|
|
||||||
|
|
||||||
async def kick(self, user: Snowflake, *, reason: Optional[str] = None) -> None:
|
async def kick(self, user: Snowflake, *, reason: Optional[str] = None) -> None:
|
||||||
"""|coro|
|
"""|coro|
|
||||||
|
|
||||||
|
@ -84,7 +84,6 @@ if TYPE_CHECKING:
|
|||||||
threads,
|
threads,
|
||||||
voice,
|
voice,
|
||||||
sticker,
|
sticker,
|
||||||
welcome_screen,
|
|
||||||
)
|
)
|
||||||
from .types.snowflake import Snowflake, SnowflakeList
|
from .types.snowflake import Snowflake, SnowflakeList
|
||||||
|
|
||||||
@ -1117,20 +1116,6 @@ class HTTPClient:
|
|||||||
payload['icon'] = icon
|
payload['icon'] = icon
|
||||||
return self.request(Route('POST', '/guilds/templates/{code}', code=code), json=payload)
|
return self.request(Route('POST', '/guilds/templates/{code}', code=code), json=payload)
|
||||||
|
|
||||||
def get_welcome_screen(self, guild_id: Snowflake) -> Response[welcome_screen.WelcomeScreen]:
|
|
||||||
return self.request(Route('GET', '/guilds/{guild_id}/welcome-screen', guild_id=guild_id))
|
|
||||||
|
|
||||||
def edit_welcome_screen(self, guild_id: Snowflake, payload: Any) -> Response[welcome_screen.WelcomeScreen]:
|
|
||||||
valid_keys = (
|
|
||||||
'description',
|
|
||||||
'welcome_channels',
|
|
||||||
'enabled',
|
|
||||||
)
|
|
||||||
payload = {
|
|
||||||
k: v for k, v in payload.items() if k in valid_keys
|
|
||||||
}
|
|
||||||
return self.request(Route('PATCH', '/guilds/{guild_id}/welcome-screen', guild_id=guild_id), json=payload)
|
|
||||||
|
|
||||||
def get_bans(self, guild_id: Snowflake) -> Response[List[guild.Ban]]:
|
def get_bans(self, guild_id: Snowflake) -> Response[List[guild.Ban]]:
|
||||||
return self.request(Route('GET', '/guilds/{guild_id}/bans', guild_id=guild_id))
|
return self.request(Route('GET', '/guilds/{guild_id}/bans', guild_id=guild_id))
|
||||||
|
|
||||||
|
@ -230,7 +230,6 @@ class Invite(Hashable):
|
|||||||
|
|
||||||
Returns the invite URL.
|
Returns the invite URL.
|
||||||
|
|
||||||
|
|
||||||
The following table illustrates what methods will obtain the attributes:
|
The following table illustrates what methods will obtain the attributes:
|
||||||
|
|
||||||
+------------------------------------+------------------------------------------------------------+
|
+------------------------------------+------------------------------------------------------------+
|
||||||
@ -434,9 +433,6 @@ class Invite(Hashable):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
def __int__(self) -> int:
|
|
||||||
return 0 # To keep the object compatible with the hashable abc.
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
f'<Invite code={self.code!r} guild={self.guild!r} '
|
f'<Invite code={self.code!r} guild={self.guild!r} '
|
||||||
|
@ -226,10 +226,6 @@ class Member(discord.abc.Messageable, _UserTag):
|
|||||||
|
|
||||||
Returns the member's name with the discriminator.
|
Returns the member's name with the discriminator.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the user's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
joined_at: Optional[:class:`datetime.datetime`]
|
joined_at: Optional[:class:`datetime.datetime`]
|
||||||
@ -304,9 +300,6 @@ class Member(discord.abc.Messageable, _UserTag):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self._user)
|
return str(self._user)
|
||||||
|
|
||||||
def __int__(self) -> int:
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}'
|
f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}'
|
||||||
|
@ -125,10 +125,6 @@ class Attachment(Hashable):
|
|||||||
|
|
||||||
Returns the hash of the attachment.
|
Returns the hash of the attachment.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the attachment's ID.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
Attachment can now be casted to :class:`str` and is hashable.
|
Attachment can now be casted to :class:`str` and is hashable.
|
||||||
|
|
||||||
@ -507,14 +503,6 @@ class Message(Hashable):
|
|||||||
|
|
||||||
Returns the message's hash.
|
Returns the message's hash.
|
||||||
|
|
||||||
.. describe:: str(x)
|
|
||||||
|
|
||||||
Returns the message's content.
|
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the message's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
tts: :class:`bool`
|
tts: :class:`bool`
|
||||||
@ -724,10 +712,6 @@ class Message(Hashable):
|
|||||||
f'<{name} id={self.id} channel={self.channel!r} type={self.type!r} author={self.author!r} flags={self.flags!r}>'
|
f'<{name} id={self.id} channel={self.channel!r} type={self.type!r} author={self.author!r} flags={self.flags!r}>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def __str__(self) -> Optional[str]:
|
|
||||||
return self.content
|
|
||||||
|
|
||||||
def _try_patch(self, data, key, transform=None) -> None:
|
def _try_patch(self, data, key, transform=None) -> None:
|
||||||
try:
|
try:
|
||||||
value = data[key]
|
value = data[key]
|
||||||
@ -1122,7 +1106,7 @@ class Message(Hashable):
|
|||||||
if self.type is MessageType.guild_invite_reminder:
|
if self.type is MessageType.guild_invite_reminder:
|
||||||
return 'Wondering who to invite?\nStart by inviting anyone who can help you build the server!'
|
return 'Wondering who to invite?\nStart by inviting anyone who can help you build the server!'
|
||||||
|
|
||||||
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.
|
||||||
@ -1133,17 +1117,12 @@ class Message(Hashable):
|
|||||||
|
|
||||||
.. versionchanged:: 1.1
|
.. versionchanged:: 1.1
|
||||||
Added the new ``delay`` keyword-only parameter.
|
Added the new ``delay`` keyword-only parameter.
|
||||||
.. versionchanged:: 2.0
|
|
||||||
Added the new ``silent`` keyword-only parameter.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
-----------
|
-----------
|
||||||
delay: Optional[:class:`float`]
|
delay: Optional[:class:`float`]
|
||||||
If provided, the number of seconds to wait in the background
|
If provided, the number of seconds to wait in the background
|
||||||
before deleting the message. If the deletion fails then it is silently ignored.
|
before deleting the message. If the deletion fails then it is silently ignored.
|
||||||
silent: :class:`bool`
|
|
||||||
If silent is set to ``True``, the error will not be raised, it will be ignored.
|
|
||||||
This defaults to ``False``
|
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@ -1165,11 +1144,7 @@ class Message(Hashable):
|
|||||||
|
|
||||||
asyncio.create_task(delete(delay))
|
asyncio.create_task(delete(delay))
|
||||||
else:
|
else:
|
||||||
try:
|
await self._state.http.delete_message(self.channel.id, self.id)
|
||||||
await self._state.http.delete_message(self.channel.id, self.id)
|
|
||||||
except Exception:
|
|
||||||
if not silent:
|
|
||||||
raise
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
async def edit(
|
async def edit(
|
||||||
@ -1650,10 +1625,6 @@ class PartialMessage(Hashable):
|
|||||||
|
|
||||||
Returns the partial message's hash.
|
Returns the partial message's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the partial message's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`]
|
channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`]
|
||||||
|
@ -43,8 +43,5 @@ class EqualityComparable:
|
|||||||
class Hashable(EqualityComparable):
|
class Hashable(EqualityComparable):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __int__(self) -> int:
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return self.id >> 22
|
return self.id >> 22
|
||||||
|
@ -69,10 +69,6 @@ class Object(Hashable):
|
|||||||
|
|
||||||
Returns the object's hash.
|
Returns the object's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the object's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
|
@ -299,13 +299,6 @@ class Permissions(BaseFlags):
|
|||||||
"""
|
"""
|
||||||
return 1 << 3
|
return 1 << 3
|
||||||
|
|
||||||
@make_permission_alias('administrator')
|
|
||||||
def admin(self) -> int:
|
|
||||||
""":class:`bool`: An alias for :attr:`administrator`.
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
"""
|
|
||||||
return 1 << 3
|
|
||||||
|
|
||||||
@flag_value
|
@flag_value
|
||||||
def manage_channels(self) -> int:
|
def manage_channels(self) -> int:
|
||||||
""":class:`bool`: Returns ``True`` if a user can edit, delete, or create channels in the guild.
|
""":class:`bool`: Returns ``True`` if a user can edit, delete, or create channels in the guild.
|
||||||
|
@ -21,7 +21,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
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 threading
|
import threading
|
||||||
@ -64,7 +63,10 @@ __all__ = (
|
|||||||
|
|
||||||
CREATE_NO_WINDOW: int
|
CREATE_NO_WINDOW: int
|
||||||
|
|
||||||
CREATE_NO_WINDOW = 0 if sys.platform != 'win32' else 0x08000000
|
if sys.platform != 'win32':
|
||||||
|
CREATE_NO_WINDOW = 0
|
||||||
|
else:
|
||||||
|
CREATE_NO_WINDOW = 0x08000000
|
||||||
|
|
||||||
class AudioSource:
|
class AudioSource:
|
||||||
"""Represents an audio stream.
|
"""Represents an audio stream.
|
||||||
@ -524,12 +526,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _probe_codec_native(source, executable: str = 'ffmpeg') -> Tuple[Optional[str], Optional[int]]:
|
def _probe_codec_native(source, executable: str = 'ffmpeg') -> Tuple[Optional[str], Optional[int]]:
|
||||||
exe = (
|
exe = executable[:2] + 'probe' if executable in ('ffmpeg', 'avconv') else executable
|
||||||
executable[:2] + 'probe'
|
|
||||||
if executable in {'ffmpeg', 'avconv'}
|
|
||||||
else executable
|
|
||||||
)
|
|
||||||
|
|
||||||
args = [exe, '-v', 'quiet', '-print_format', 'json', '-show_streams', '-select_streams', 'a:0', source]
|
args = [exe, '-v', 'quiet', '-print_format', 'json', '-show_streams', '-select_streams', 'a:0', source]
|
||||||
output = subprocess.check_output(args, timeout=20)
|
output = subprocess.check_output(args, timeout=20)
|
||||||
codec = bitrate = None
|
codec = bitrate = None
|
||||||
|
@ -24,7 +24,6 @@ DEALINGS IN THE SOFTWARE.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
|
||||||
from typing import TYPE_CHECKING, Optional, Set, List
|
from typing import TYPE_CHECKING, Optional, Set, List
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -35,8 +34,7 @@ if TYPE_CHECKING:
|
|||||||
MessageUpdateEvent,
|
MessageUpdateEvent,
|
||||||
ReactionClearEvent,
|
ReactionClearEvent,
|
||||||
ReactionClearEmojiEvent,
|
ReactionClearEmojiEvent,
|
||||||
IntegrationDeleteEvent,
|
IntegrationDeleteEvent
|
||||||
TypingEvent
|
|
||||||
)
|
)
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .partial_emoji import PartialEmoji
|
from .partial_emoji import PartialEmoji
|
||||||
@ -51,7 +49,6 @@ __all__ = (
|
|||||||
'RawReactionClearEvent',
|
'RawReactionClearEvent',
|
||||||
'RawReactionClearEmojiEvent',
|
'RawReactionClearEmojiEvent',
|
||||||
'RawIntegrationDeleteEvent',
|
'RawIntegrationDeleteEvent',
|
||||||
'RawTypingEvent'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -279,36 +276,3 @@ class RawIntegrationDeleteEvent(_RawReprMixin):
|
|||||||
self.application_id: Optional[int] = int(data['application_id'])
|
self.application_id: Optional[int] = int(data['application_id'])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.application_id: Optional[int] = None
|
self.application_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class RawTypingEvent(_RawReprMixin):
|
|
||||||
"""Represents the payload for a :func:`on_raw_typing` event.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
channel_id: :class:`int`
|
|
||||||
The channel ID where the typing originated from.
|
|
||||||
user_id: :class:`int`
|
|
||||||
The ID of the user that started typing.
|
|
||||||
when: :class:`datetime.datetime`
|
|
||||||
When the typing started as an aware datetime in UTC.
|
|
||||||
guild_id: Optional[:class:`int`]
|
|
||||||
The guild ID where the typing originated from, if applicable.
|
|
||||||
member: Optional[:class:`Member`]
|
|
||||||
The member who started typing. Only available if the member started typing in a guild.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("channel_id", "user_id", "when", "guild_id", "member")
|
|
||||||
|
|
||||||
def __init__(self, data: TypingEvent) -> None:
|
|
||||||
self.channel_id: int = int(data['channel_id'])
|
|
||||||
self.user_id: int = int(data['user_id'])
|
|
||||||
self.when: datetime.datetime = datetime.datetime.fromtimestamp(data.get('timestamp'), tz=datetime.timezone.utc)
|
|
||||||
self.member: Optional[Member] = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.guild_id: Optional[int] = int(data['guild_id'])
|
|
||||||
except KeyError:
|
|
||||||
self.guild_id: Optional[int] = None
|
|
@ -141,14 +141,6 @@ class Role(Hashable):
|
|||||||
|
|
||||||
Returns the role's name.
|
Returns the role's name.
|
||||||
|
|
||||||
.. describe:: str(x)
|
|
||||||
|
|
||||||
Returns the role's ID.
|
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the role's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
@ -203,9 +195,6 @@ class Role(Hashable):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __int__(self) -> int:
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<Role id={self.id} name={self.name!r}>'
|
return f'<Role id={self.id} name={self.name!r}>'
|
||||||
|
|
||||||
|
@ -61,10 +61,6 @@ class StageInstance(Hashable):
|
|||||||
|
|
||||||
Returns the stage instance's hash.
|
Returns the stage instance's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the stage instance's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
id: :class:`int`
|
id: :class:`int`
|
||||||
|
@ -152,7 +152,6 @@ class ConnectionState:
|
|||||||
handlers: Dict[str, Callable],
|
handlers: Dict[str, Callable],
|
||||||
hooks: Dict[str, Callable],
|
hooks: Dict[str, Callable],
|
||||||
http: HTTPClient,
|
http: HTTPClient,
|
||||||
intents: Intents,
|
|
||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
**options: Any,
|
**options: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -195,8 +194,12 @@ class ConnectionState:
|
|||||||
else:
|
else:
|
||||||
status = str(status)
|
status = str(status)
|
||||||
|
|
||||||
if not isinstance(intents, Intents):
|
intents = options.get('intents', None)
|
||||||
raise TypeError(f'intents parameter must be Intent not {type(intents)!r}')
|
if intents is not None:
|
||||||
|
if not isinstance(intents, Intents):
|
||||||
|
raise TypeError(f'intents parameter must be Intent not {type(intents)!r}')
|
||||||
|
else:
|
||||||
|
intents = Intents.default()
|
||||||
|
|
||||||
if not intents.guilds:
|
if not intents.guilds:
|
||||||
_log.warning('Guilds intent seems to be disabled. This may cause state related issues.')
|
_log.warning('Guilds intent seems to be disabled. This may cause state related issues.')
|
||||||
@ -1327,37 +1330,28 @@ class ConnectionState:
|
|||||||
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
||||||
|
|
||||||
def parse_typing_start(self, data) -> None:
|
def parse_typing_start(self, data) -> None:
|
||||||
raw = RawTypingEvent(data)
|
|
||||||
|
|
||||||
member_data = data.get('member')
|
|
||||||
if member_data:
|
|
||||||
guild = self._get_guild(raw.guild_id)
|
|
||||||
if guild is not None:
|
|
||||||
raw.member = Member(data=member_data, guild=guild, state=self)
|
|
||||||
else:
|
|
||||||
raw.member = None
|
|
||||||
else:
|
|
||||||
raw.member = None
|
|
||||||
self.dispatch('raw_typing', raw)
|
|
||||||
|
|
||||||
channel, guild = self._get_guild_channel(data)
|
channel, guild = self._get_guild_channel(data)
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
user = raw.member or self._get_typing_user(channel, raw.user_id)
|
member = None
|
||||||
|
user_id = utils._get_as_snowflake(data, 'user_id')
|
||||||
|
if isinstance(channel, DMChannel):
|
||||||
|
member = channel.recipient
|
||||||
|
|
||||||
if user is not None:
|
elif isinstance(channel, (Thread, TextChannel)) and guild is not None:
|
||||||
self.dispatch('typing', channel, user, raw.when)
|
# user_id won't be None
|
||||||
|
member = guild.get_member(user_id) # type: ignore
|
||||||
|
|
||||||
def _get_typing_user(self, channel: Optional[MessageableChannel], user_id: int) -> Optional[Union[User, Member]]:
|
if member is None:
|
||||||
if isinstance(channel, DMChannel):
|
member_data = data.get('member')
|
||||||
return channel.recipient
|
if member_data:
|
||||||
|
member = Member(data=member_data, state=self, guild=guild)
|
||||||
|
|
||||||
elif isinstance(channel, (Thread, TextChannel)) and channel.guild is not None:
|
elif isinstance(channel, GroupChannel):
|
||||||
return channel.guild.get_member(user_id) # type: ignore
|
member = utils.find(lambda x: x.id == user_id, channel.recipients)
|
||||||
|
|
||||||
elif isinstance(channel, GroupChannel):
|
if member is not None:
|
||||||
return utils.find(lambda x: x.id == user_id, channel.recipients)
|
timestamp = datetime.datetime.fromtimestamp(data.get('timestamp'), tz=datetime.timezone.utc)
|
||||||
|
self.dispatch('typing', channel, member, timestamp)
|
||||||
return self.get_user(user_id)
|
|
||||||
|
|
||||||
def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
|
def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
|
||||||
if isinstance(channel, TextChannel):
|
if isinstance(channel, TextChannel):
|
||||||
|
@ -67,14 +67,6 @@ class StickerPack(Hashable):
|
|||||||
|
|
||||||
Returns the name of the sticker pack.
|
Returns the name of the sticker pack.
|
||||||
|
|
||||||
.. describe:: hash(x)
|
|
||||||
|
|
||||||
Returns the hash of the sticker pack.
|
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the ID of the sticker pack.
|
|
||||||
|
|
||||||
.. describe:: x == y
|
.. describe:: x == y
|
||||||
|
|
||||||
Checks if the sticker pack is equal to another sticker pack.
|
Checks if the sticker pack is equal to another sticker pack.
|
||||||
|
@ -74,10 +74,6 @@ class Thread(Messageable, Hashable):
|
|||||||
|
|
||||||
Returns the thread's hash.
|
Returns the thread's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the thread's ID.
|
|
||||||
|
|
||||||
.. describe:: str(x)
|
.. describe:: str(x)
|
||||||
|
|
||||||
Returns the thread's name.
|
Returns the thread's name.
|
||||||
@ -752,10 +748,6 @@ class ThreadMember(Hashable):
|
|||||||
|
|
||||||
Returns the thread member's hash.
|
Returns the thread member's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the thread member's ID.
|
|
||||||
|
|
||||||
.. describe:: str(x)
|
.. describe:: str(x)
|
||||||
|
|
||||||
Returns the thread member's name.
|
Returns the thread member's name.
|
||||||
@ -808,39 +800,3 @@ class ThreadMember(Hashable):
|
|||||||
def thread(self) -> Thread:
|
def thread(self) -> Thread:
|
||||||
""":class:`Thread`: The thread this member belongs to."""
|
""":class:`Thread`: The thread this member belongs to."""
|
||||||
return self.parent
|
return self.parent
|
||||||
|
|
||||||
async def fetch_member(self) -> Member:
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Retrieves a :class:`Member` from the ThreadMember object.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This method is an API call. If you have :attr:`Intents.members` and member cache enabled, consider :meth:`get_member` instead.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
Forbidden
|
|
||||||
You do not have access to the guild.
|
|
||||||
HTTPException
|
|
||||||
Fetching the member failed.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
:class:`Member`
|
|
||||||
The member.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return await self.thread.guild.fetch_member(self.id)
|
|
||||||
|
|
||||||
def get_member(self) -> Optional[Member]:
|
|
||||||
"""
|
|
||||||
Get the :class:`Member` from cache for the ThreadMember object.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
--------
|
|
||||||
Optional[:class:`Member`]
|
|
||||||
The member or ``None`` if not found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return await self.thread.guild.get_member(self.id)
|
|
||||||
|
@ -85,13 +85,3 @@ class _IntegrationDeleteEventOptional(TypedDict, total=False):
|
|||||||
class IntegrationDeleteEvent(_IntegrationDeleteEventOptional):
|
class IntegrationDeleteEvent(_IntegrationDeleteEventOptional):
|
||||||
id: Snowflake
|
id: Snowflake
|
||||||
guild_id: Snowflake
|
guild_id: Snowflake
|
||||||
|
|
||||||
|
|
||||||
class _TypingEventOptional(TypedDict, total=False):
|
|
||||||
guild_id: Snowflake
|
|
||||||
member: Member
|
|
||||||
|
|
||||||
class TypingEvent(_TypingEventOptional):
|
|
||||||
channel_id: Snowflake
|
|
||||||
user_id: Snowflake
|
|
||||||
timestamp: int
|
|
@ -185,15 +185,15 @@ class Button(Item[V]):
|
|||||||
|
|
||||||
@emoji.setter
|
@emoji.setter
|
||||||
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]): # type: ignore
|
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]): # type: ignore
|
||||||
if value is None:
|
if value is not None:
|
||||||
self._underlying.emoji = None
|
if isinstance(value, str):
|
||||||
|
self._underlying.emoji = PartialEmoji.from_str(value)
|
||||||
elif isinstance(value, str):
|
elif isinstance(value, _EmojiTag):
|
||||||
self._underlying.emoji = PartialEmoji.from_str(value)
|
self._underlying.emoji = value._to_partial()
|
||||||
elif isinstance(value, _EmojiTag):
|
else:
|
||||||
self._underlying.emoji = value._to_partial()
|
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__} instead')
|
||||||
else:
|
else:
|
||||||
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__} instead')
|
self._underlying.emoji = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_component(cls: Type[B], button: ButtonComponent) -> B:
|
def from_component(cls: Type[B], button: ButtonComponent) -> B:
|
||||||
|
@ -96,9 +96,6 @@ class BaseUser(_UserTag):
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f'{self.name}#{self.discriminator}'
|
return f'{self.name}#{self.discriminator}'
|
||||||
|
|
||||||
def __int__(self) -> int:
|
|
||||||
return self.id
|
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
return isinstance(other, _UserTag) and other.id == self.id
|
return isinstance(other, _UserTag) and other.id == self.id
|
||||||
|
|
||||||
@ -418,10 +415,6 @@ class User(BaseUser, discord.abc.Messageable):
|
|||||||
|
|
||||||
Returns the user's name with discriminator.
|
Returns the user's name with discriminator.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the user's ID.
|
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
-----------
|
-----------
|
||||||
name: :class:`str`
|
name: :class:`str`
|
||||||
|
@ -499,14 +499,14 @@ else:
|
|||||||
|
|
||||||
def _parse_ratelimit_header(request: Any, *, use_clock: bool = False) -> float:
|
def _parse_ratelimit_header(request: Any, *, use_clock: bool = False) -> float:
|
||||||
reset_after: Optional[str] = request.headers.get('X-Ratelimit-Reset-After')
|
reset_after: Optional[str] = request.headers.get('X-Ratelimit-Reset-After')
|
||||||
if not use_clock and reset_after:
|
if use_clock or not reset_after:
|
||||||
|
utc = datetime.timezone.utc
|
||||||
|
now = datetime.datetime.now(utc)
|
||||||
|
reset = datetime.datetime.fromtimestamp(float(request.headers['X-Ratelimit-Reset']), utc)
|
||||||
|
return (reset - now).total_seconds()
|
||||||
|
else:
|
||||||
return float(reset_after)
|
return float(reset_after)
|
||||||
|
|
||||||
utc = datetime.timezone.utc
|
|
||||||
now = datetime.datetime.now(utc)
|
|
||||||
reset = datetime.datetime.fromtimestamp(float(request.headers['X-Ratelimit-Reset']), utc)
|
|
||||||
return (reset - now).total_seconds()
|
|
||||||
|
|
||||||
|
|
||||||
async def maybe_coroutine(f, *args, **kwargs):
|
async def maybe_coroutine(f, *args, **kwargs):
|
||||||
value = f(*args, **kwargs)
|
value = f(*args, **kwargs)
|
||||||
@ -659,10 +659,11 @@ def resolve_invite(invite: Union[Invite, str]) -> str:
|
|||||||
|
|
||||||
if isinstance(invite, Invite):
|
if isinstance(invite, Invite):
|
||||||
return invite.code
|
return invite.code
|
||||||
rx = r'(?:https?\:\/\/)?discord(?:\.gg|(?:app)?\.com\/invite)\/(.+)'
|
else:
|
||||||
m = re.match(rx, invite)
|
rx = r'(?:https?\:\/\/)?discord(?:\.gg|(?:app)?\.com\/invite)\/(.+)'
|
||||||
if m:
|
m = re.match(rx, invite)
|
||||||
return m.group(1)
|
if m:
|
||||||
|
return m.group(1)
|
||||||
return invite
|
return invite
|
||||||
|
|
||||||
|
|
||||||
@ -686,10 +687,11 @@ def resolve_template(code: Union[Template, str]) -> str:
|
|||||||
|
|
||||||
if isinstance(code, Template):
|
if isinstance(code, Template):
|
||||||
return code.code
|
return code.code
|
||||||
rx = r'(?:https?\:\/\/)?discord(?:\.new|(?:app)?\.com\/template)\/(.+)'
|
else:
|
||||||
m = re.match(rx, code)
|
rx = r'(?:https?\:\/\/)?discord(?:\.new|(?:app)?\.com\/template)\/(.+)'
|
||||||
if m:
|
m = re.match(rx, code)
|
||||||
return m.group(1)
|
if m:
|
||||||
|
return m.group(1)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
@ -1015,9 +1017,3 @@ def format_dt(dt: datetime.datetime, /, style: Optional[TimestampStyle] = None)
|
|||||||
if style is None:
|
if style is None:
|
||||||
return f'<t:{int(dt.timestamp())}>'
|
return f'<t:{int(dt.timestamp())}>'
|
||||||
return f'<t:{int(dt.timestamp())}:{style}>'
|
return f'<t:{int(dt.timestamp())}:{style}>'
|
||||||
|
|
||||||
|
|
||||||
def raise_expected_coro(coro, error: str)-> TypeError:
|
|
||||||
if not asyncio.iscoroutinefunction(coro):
|
|
||||||
raise TypeError(error)
|
|
||||||
return coro
|
|
||||||
|
@ -255,9 +255,6 @@ class VoiceClient(VoiceProtocol):
|
|||||||
self.encoder: Encoder = MISSING
|
self.encoder: Encoder = MISSING
|
||||||
self._lite_nonce: int = 0
|
self._lite_nonce: int = 0
|
||||||
self.ws: DiscordVoiceWebSocket = MISSING
|
self.ws: DiscordVoiceWebSocket = MISSING
|
||||||
self.ip: str = MISSING
|
|
||||||
self.port: Tuple[Any, ...] = MISSING
|
|
||||||
|
|
||||||
|
|
||||||
warn_nacl = not has_nacl
|
warn_nacl = not has_nacl
|
||||||
supported_modes: Tuple[SupportedModes, ...] = (
|
supported_modes: Tuple[SupportedModes, ...] = (
|
||||||
|
@ -886,10 +886,6 @@ class Webhook(BaseWebhook):
|
|||||||
|
|
||||||
Returns the webhooks's hash.
|
Returns the webhooks's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the webhooks's ID.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.4
|
.. versionchanged:: 1.4
|
||||||
Webhooks are now comparable and hashable.
|
Webhooks are now comparable and hashable.
|
||||||
|
|
||||||
|
@ -475,10 +475,6 @@ class SyncWebhook(BaseWebhook):
|
|||||||
|
|
||||||
Returns the webhooks's hash.
|
Returns the webhooks's hash.
|
||||||
|
|
||||||
.. describe:: int(x)
|
|
||||||
|
|
||||||
Returns the webhooks's ID.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.4
|
.. versionchanged:: 1.4
|
||||||
Webhooks are now comparable and hashable.
|
Webhooks are now comparable and hashable.
|
||||||
|
|
||||||
|
@ -1,216 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2015-present Rapptz
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a
|
|
||||||
copy of this software and associated documentation files (the "Software"),
|
|
||||||
to deal in the Software without restriction, including without limitation
|
|
||||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
||||||
and/or sell copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
DEALINGS IN THE SOFTWARE.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Dict, List, Optional, TYPE_CHECKING, Union, overload
|
|
||||||
from .utils import _get_as_snowflake, get
|
|
||||||
from .errors import InvalidArgument
|
|
||||||
from .partial_emoji import _EmojiTag
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'WelcomeChannel',
|
|
||||||
'WelcomeScreen',
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .types.welcome_screen import (
|
|
||||||
WelcomeScreen as WelcomeScreenPayload,
|
|
||||||
WelcomeScreenChannel as WelcomeScreenChannelPayload,
|
|
||||||
)
|
|
||||||
from .abc import Snowflake
|
|
||||||
from .guild import Guild
|
|
||||||
from .partial_emoji import PartialEmoji
|
|
||||||
from .emoji import Emoji
|
|
||||||
|
|
||||||
|
|
||||||
class WelcomeChannel:
|
|
||||||
"""Represents a :class:`WelcomeScreen` welcome channel.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
channel: :class:`abc.Snowflake`
|
|
||||||
The guild channel that is being referenced.
|
|
||||||
description: :class:`str`
|
|
||||||
The description shown of the channel.
|
|
||||||
emoji: Optional[:class:`PartialEmoji`, :class:`Emoji`, :class:`str`]
|
|
||||||
The emoji used beside the channel description.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *, channel: Snowflake, description: str, emoji: Union[PartialEmoji, Emoji, str] = None):
|
|
||||||
self.channel = channel
|
|
||||||
self.description = description
|
|
||||||
self.emoji = emoji
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f'<WelcomeChannel channel={self.channel!r} description={self.description!r} emoji={self.emoji!r}>'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _from_dict(cls, *, data: WelcomeScreenChannelPayload, guild: Guild) -> WelcomeChannel:
|
|
||||||
channel_id = _get_as_snowflake(data, 'channel_id')
|
|
||||||
channel = guild.get_channel(channel_id)
|
|
||||||
description = data['description']
|
|
||||||
_emoji_id = _get_as_snowflake(data, 'emoji_id')
|
|
||||||
_emoji_name = data['emoji_name']
|
|
||||||
|
|
||||||
if _emoji_id:
|
|
||||||
# custom
|
|
||||||
emoji = get(guild.emojis, id=_emoji_id)
|
|
||||||
else:
|
|
||||||
# unicode or None
|
|
||||||
emoji = _emoji_name
|
|
||||||
|
|
||||||
return cls(channel=channel, description=description, emoji=emoji) # type: ignore
|
|
||||||
|
|
||||||
def to_dict(self) -> WelcomeScreenChannelPayload:
|
|
||||||
ret: WelcomeScreenChannelPayload = {
|
|
||||||
'channel_id': self.channel.id,
|
|
||||||
'description': self.description,
|
|
||||||
'emoji_id': None,
|
|
||||||
'emoji_name': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
if isinstance(self.emoji, _EmojiTag):
|
|
||||||
ret['emoji_id'] = self.emoji.id # type: ignore
|
|
||||||
ret['emoji_name'] = self.emoji.name # type: ignore
|
|
||||||
else:
|
|
||||||
# unicode or None
|
|
||||||
ret['emoji_name'] = self.emoji
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class WelcomeScreen:
|
|
||||||
"""Represents a :class:`Guild` welcome screen.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
description: :class:`str`
|
|
||||||
The description shown on the welcome screen.
|
|
||||||
welcome_channels: List[:class:`WelcomeChannel`]
|
|
||||||
The channels shown on the welcome screen.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *, data: WelcomeScreenPayload, guild: Guild):
|
|
||||||
self._state = guild._state
|
|
||||||
self._guild = guild
|
|
||||||
self._store(data)
|
|
||||||
|
|
||||||
def _store(self, data: WelcomeScreenPayload) -> None:
|
|
||||||
self.description = data['description']
|
|
||||||
welcome_channels = data.get('welcome_channels', [])
|
|
||||||
self.welcome_channels = [WelcomeChannel._from_dict(data=wc, guild=self._guild) for wc in welcome_channels]
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f'<WelcomeScreen description={self.description!r} welcome_channels={self.welcome_channels!r} enabled={self.enabled}>'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def enabled(self) -> bool:
|
|
||||||
""":class:`bool`: Whether the welcome screen is displayed.
|
|
||||||
|
|
||||||
This is equivalent to checking if ``WELCOME_SCREEN_ENABLED``
|
|
||||||
is present in :attr:`Guild.features`.
|
|
||||||
"""
|
|
||||||
return 'WELCOME_SCREEN_ENABLED' in self._guild.features
|
|
||||||
|
|
||||||
@overload
|
|
||||||
async def edit(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
description: Optional[str] = ...,
|
|
||||||
welcome_channels: Optional[List[WelcomeChannel]] = ...,
|
|
||||||
enabled: Optional[bool] = ...,
|
|
||||||
) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
async def edit(self) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
async def edit(self, **kwargs):
|
|
||||||
"""|coro|
|
|
||||||
|
|
||||||
Edit the welcome screen.
|
|
||||||
|
|
||||||
You must have the :attr:`~Permissions.manage_guild` permission in the
|
|
||||||
guild to do this.
|
|
||||||
|
|
||||||
Usage: ::
|
|
||||||
|
|
||||||
rules_channel = guild.get_channel(12345678)
|
|
||||||
announcements_channel = guild.get_channel(87654321)
|
|
||||||
|
|
||||||
custom_emoji = utils.get(guild.emojis, name='loudspeaker')
|
|
||||||
|
|
||||||
await welcome_screen.edit(
|
|
||||||
description='This is a very cool community server!',
|
|
||||||
welcome_channels=[
|
|
||||||
WelcomeChannel(channel=rules_channel, description='Read the rules!', emoji='👨🏫'),
|
|
||||||
WelcomeChannel(channel=announcements_channel, description='Watch out for announcements!', emoji=custom_emoji),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Welcome channels can only accept custom emojis if :attr:`~Guild.premium_tier` is level 2 or above.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
------------
|
|
||||||
description: Optional[:class:`str`]
|
|
||||||
The template's description.
|
|
||||||
welcome_channels: Optional[List[:class:`WelcomeChannel`]]
|
|
||||||
The welcome channels, in their respective order.
|
|
||||||
enabled: Optional[:class:`bool`]
|
|
||||||
Whether the welcome screen should be displayed.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
-------
|
|
||||||
HTTPException
|
|
||||||
Editing the welcome screen failed failed.
|
|
||||||
Forbidden
|
|
||||||
You don't have permissions to edit the welcome screen.
|
|
||||||
NotFound
|
|
||||||
This welcome screen does not exist.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
welcome_channels = kwargs['welcome_channels']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
welcome_channels_serialised = []
|
|
||||||
for wc in welcome_channels:
|
|
||||||
if not isinstance(wc, WelcomeChannel):
|
|
||||||
raise InvalidArgument('welcome_channels parameter must be a list of WelcomeChannel')
|
|
||||||
welcome_channels_serialised.append(wc.to_dict())
|
|
||||||
kwargs['welcome_channels'] = welcome_channels_serialised
|
|
||||||
|
|
||||||
if kwargs:
|
|
||||||
data = await self._state.http.edit_welcome_screen(self._guild.id, kwargs)
|
|
||||||
self._store(data)
|
|
35
docs/api.rst
35
docs/api.rst
@ -369,17 +369,6 @@ to handle it, which defaults to print a traceback and ignoring the exception.
|
|||||||
:param when: When the typing started as an aware datetime in UTC.
|
:param when: When the typing started as an aware datetime in UTC.
|
||||||
:type when: :class:`datetime.datetime`
|
:type when: :class:`datetime.datetime`
|
||||||
|
|
||||||
.. function:: on_raw_typing(payload)
|
|
||||||
|
|
||||||
Called when someone begins typing a message. Unlike :func:`on_typing`, this is
|
|
||||||
called regardless if the user can be found or not. This most often happens
|
|
||||||
when a user types in DMs.
|
|
||||||
|
|
||||||
This requires :attr:`Intents.typing` to be enabled.
|
|
||||||
|
|
||||||
:param payload: The raw typing payload.
|
|
||||||
:type payload: :class:`RawTypingEvent`
|
|
||||||
|
|
||||||
.. function:: on_message(message)
|
.. function:: on_message(message)
|
||||||
|
|
||||||
Called when a :class:`Message` is created and sent.
|
Called when a :class:`Message` is created and sent.
|
||||||
@ -3792,22 +3781,6 @@ Template
|
|||||||
.. autoclass:: Template()
|
.. autoclass:: Template()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
WelcomeScreen
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. attributetable:: WelcomeScreen
|
|
||||||
|
|
||||||
.. autoclass:: WelcomeScreen()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
WelcomeChannel
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. attributetable:: WelcomeChannel
|
|
||||||
|
|
||||||
.. autoclass:: WelcomeChannel()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
WidgetChannel
|
WidgetChannel
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -3873,14 +3846,6 @@ GuildSticker
|
|||||||
.. autoclass:: GuildSticker()
|
.. autoclass:: GuildSticker()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
RawTypingEvent
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. attributetable:: RawTypingEvent
|
|
||||||
|
|
||||||
.. autoclass:: RawTypingEvent()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
RawMessageDeleteEvent
|
RawMessageDeleteEvent
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -11,134 +11,6 @@ Changelog
|
|||||||
This page keeps a detailed human friendly rendering of what's new and changed
|
This page keeps a detailed human friendly rendering of what's new and changed
|
||||||
in specific versions.
|
in specific versions.
|
||||||
|
|
||||||
.. _vp2p0p0:
|
|
||||||
|
|
||||||
v2.0.0
|
|
||||||
--------
|
|
||||||
|
|
||||||
This version was partly developed by Danny, and partly by the enhanced-discord.py contributors.
|
|
||||||
The library has been updated with breaking changes, and as such the major version was changed.
|
|
||||||
|
|
||||||
|
|
||||||
- Performance of the library has improved significantly (all times with 1 process and 1 AutoShardedBot):
|
|
||||||
- 735 guilds boot up time (with chunking): 57s/1.7 GiB RAM -> 42s/1.4 GiB RAM
|
|
||||||
- 27k guilds boot up time (with chunking): 477s/8 GiB RAM -> 303s/7.2 GiB RAM
|
|
||||||
- 48k guilds boot up time (without chunking): 109s -> 67s
|
|
||||||
- 106k guilds boot up time (without chunking): 3300s -> 3090s
|
|
||||||
- The entire public API of the library is now completely type hinted.
|
|
||||||
- There may still be bugs however.
|
|
||||||
- For best type hinting experience consider using Pyright.
|
|
||||||
- Almost all edit methods now return their updated counterpart rather than doing an in-place edit.
|
|
||||||
- Japanese docs were removed, as we are no longer able to keep them in sync.
|
|
||||||
|
|
||||||
|
|
||||||
Breaking Changes
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
- :meth:`Asset.replace` now only accepts keyword arguments
|
|
||||||
- ``Asset.with_`` functions now only accept positional only arguments
|
|
||||||
- :meth:`TextChannel.get_partial_message` is now pos-only
|
|
||||||
- :meth:`TextChannel.get_thread` is now pos-only
|
|
||||||
- ``permissions_for`` is now pos-only
|
|
||||||
- :attr:`GroupChannel.owner` is now Optional
|
|
||||||
- ``edit`` methods now only accept None if it actually means something (e.g. clearing it)
|
|
||||||
- ``timeout`` parameter for ``ui.View.__init__`` is now keyword only
|
|
||||||
- When an interaction has already been responded and another one is sent, :exc:`InteractionResponded`is now raised.
|
|
||||||
- Discord's API only allows a single :attr:`interaction.response`.
|
|
||||||
- Separate :func:`on_member_update` and :func:`on_presence_update`
|
|
||||||
- The new event :func:`on_presence_update` is now called when status/activity is changed.
|
|
||||||
- :func:`on_member_update` will now no longer have status/activity changes.
|
|
||||||
- afk parameter in :meth:`Client.change_presence` is removed
|
|
||||||
- The undocumented private :func:`on_socket_response` event got removed.
|
|
||||||
- Consider using the newer documented :func:`on_socket_event_type` event instead.
|
|
||||||
- Using :func:`on_socket_raw_receive` and :func:`on_socket_raw_send` are now opt-in via :attr:`enable_debug_events` toggle.
|
|
||||||
- :func:`on_socket_raw_receive` is now only dispatched after decompressing the payload.
|
|
||||||
- Persistent View dispatch mechanism now uses the ``message_id`` key if provided.
|
|
||||||
- :meth:`Message.start_thread` was renamed to :meth:`Message.create_thread`
|
|
||||||
- :meth:`TextChannel.start_thread` was renamed to :meth:`TextChannel.create_thread`
|
|
||||||
- All ``get_`` lookup functions now use positional-only parameters for the id parameter.
|
|
||||||
- Remove :meth:`TextChannel.active_threads` due to the endpoint being deprecated and slated for removal.
|
|
||||||
- Use :meth:`Guild.active_threads` instead.
|
|
||||||
- :attr:`User.avatar` now returns None if the user did not upload an avatar.
|
|
||||||
- Use :attr:`User.display_avatar` to get the avatar and fallback to the default avatar to go back to the old behaviour.
|
|
||||||
|
|
||||||
New Features
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
- Channel types are now typed
|
|
||||||
- Member is now typed
|
|
||||||
- Client is now typed
|
|
||||||
- Permissions are now typed
|
|
||||||
- Core library errors are now typed
|
|
||||||
- Add various examples showing how to use views. There are more to come.
|
|
||||||
- :attr:`GroupChannel.owner_id` now gets the owner ID
|
|
||||||
- ``edit`` methods now don't rely on previous state
|
|
||||||
- :meth:`View.from_message` converts a Message.components to a View
|
|
||||||
- :attr:`Thread.type` to get the thread channel type
|
|
||||||
- :attr:`ButtonStyle.url` alias for :attr:`ButtonStyle.link`
|
|
||||||
- Add default style for :class:`ui.Button` constructor
|
|
||||||
- This makes it so creating a URL button is as simple as ``ui.Button(url='...', label='...')``
|
|
||||||
- :attr:`Thread.mention` to get the mention string for a thread
|
|
||||||
- :meth:`Thread.is_nsfw` to check whether the parent channel of the thread is NSFW
|
|
||||||
- Add support for fetching the original interaction response message.
|
|
||||||
- :meth:`Interaction.original_message` will retrieve it and returns an InteractionMessage
|
|
||||||
- :meth:`InteractionMessage.edit` or :meth:`Interaction.edit_original_message` will edit it
|
|
||||||
- :meth:`InteractionMessage.delete` or :meth:`Interaction.delete_original_message` will delete it
|
|
||||||
- :attr:`MessageFlags.ephemeral` to get whether a message is ephemeral
|
|
||||||
- :meth:`Client.fetch_channel` now fetches threads as well
|
|
||||||
- :class:`SelectOption` now has a __str__ that matches the client representation.
|
|
||||||
- This might change in the future to remove the description from it.
|
|
||||||
- Add a converter for :class:`discord.Thread`
|
|
||||||
- Allow ``clean_content`` converter to work stand-alone
|
|
||||||
- Add :meth:`User.banner` to get a user's banner and :meth:`User.accent_colour` to get the set banner colour.
|
|
||||||
- Due to an API limitation this requires using :meth:`Client.fetch_user`.
|
|
||||||
- Add ``reason`` keyword argument to more methods
|
|
||||||
- Add audit log events for threads
|
|
||||||
- Allow public threads to be created without a starter message
|
|
||||||
- Add :meth:`Guild.get_channel_or_thread` helper method
|
|
||||||
- Add full support for the new sticker API
|
|
||||||
- Add :func:`on_socket_event_type` event to get the event type of an event
|
|
||||||
- Add :attr:`TextChannel.default_auto_archive_duration`
|
|
||||||
- Add :class:`PartialMessageable` type to allow for sending messages to a channel using only its ``channel_id``.
|
|
||||||
- This is constructed with :meth:`Client.get_partial_messageable`.
|
|
||||||
- Add :meth:`Guild.active_threads` to get all of a guild's active threads.
|
|
||||||
- Add :attr:`Thread.members` to get all cached :class:`ThreadMember` instances of a thread.
|
|
||||||
- Add :meth:`Thread.fetch_members` to fetch all :class:`ThreadMember` instances of a thread.
|
|
||||||
- These two require :attr:`Intents.members` to be useful.
|
|
||||||
- Add support for guild avatars for members under :attr:`Member.guild_avatar`
|
|
||||||
- Add :attr:`User.display_avatar` and :attr:`Member.display_avatar` to get the user's displayed avatar.
|
|
||||||
- Add :attr:`Colour.brand_green` and :attr:`Colour.brand_red`
|
|
||||||
- |commands| :attr:`CommandOnCooldown.type` to get back the type of the cooldown since it was removed from :class:`Cooldown`
|
|
||||||
- Add :attr:`Guild.bots` and :attr:`Guild.humans`
|
|
||||||
|
|
||||||
|
|
||||||
Bug Fixes
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
- :class:`Channel` converters now work in DMs again
|
|
||||||
- Fix :attr:`Interaction.channel` being None in threads
|
|
||||||
- Change timeouts in :class:`ui.View` to work as documented
|
|
||||||
- :attr:`Message.__repr__` now shows the proper type, e.g. :class:`WebhookMessage` and :class:`InteractionMessage`
|
|
||||||
- Change :class:`Cooldown` handling to not reset token window when the number of tokens reaches 0
|
|
||||||
- Fix audit log permission construction breaking due to unexpected type errors.
|
|
||||||
- Fix :func:`on_thread_join` not dispatching when a thread is unarchived
|
|
||||||
- Fix :attr:`Message.guild` being None when a thread is unarchived due to a new message
|
|
||||||
- :class:`MessageConverter` now works with threads
|
|
||||||
- Retry requests when a 504 is hit
|
|
||||||
- Fix :attr:`Thread.slowmode_delay` not updating on edit
|
|
||||||
- Fix ``permissions_for`` for roles
|
|
||||||
- Update :attr:`Message.system_content` for newer message types
|
|
||||||
- Fix :class:`PartialMessage` not working with threads
|
|
||||||
- Fix crash with stage instances not having the documented ``discoverable_enabled`` key
|
|
||||||
- Fix some built-in checks not working with threads
|
|
||||||
- Fix :class:`SyncWebhook` not working in a multi-threaded context
|
|
||||||
- Fix :func:`on_thread_member_remove` not dispatching properly
|
|
||||||
- Fix :func:`on_typing` not dispatching for threads
|
|
||||||
- Update :attr:`Message.is_system` to work with newer message types
|
|
||||||
- Fix some enums like :class:`VerificationLevel` not being comparable.
|
|
||||||
- Fix ``io.BytesIO`` sources not working with ffmpeg players
|
|
||||||
- Fix :meth:`Client.fetch_channel` and :meth:`Guild.fetch_channel` not returning threads
|
|
||||||
|
|
||||||
.. _vp1p7p3:
|
.. _vp1p7p3:
|
||||||
|
|
||||||
v1.7.3
|
v1.7.3
|
||||||
|
@ -26,5 +26,5 @@ class MyClient(discord.Client):
|
|||||||
async def before_my_task(self):
|
async def before_my_task(self):
|
||||||
await self.wait_until_ready() # wait until the bot logs in
|
await self.wait_until_ready() # wait until the bot logs in
|
||||||
|
|
||||||
client = MyClient(intents=discord.Intents(guilds=True))
|
client = MyClient()
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -22,5 +22,5 @@ class MyClient(discord.Client):
|
|||||||
await asyncio.sleep(60) # task runs every 60 seconds
|
await asyncio.sleep(60) # task runs every 60 seconds
|
||||||
|
|
||||||
|
|
||||||
client = MyClient(intents=discord.Intents(guilds=True))
|
client = MyClient()
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -9,8 +9,10 @@ module.
|
|||||||
|
|
||||||
There are a number of utility commands being showcased here.'''
|
There are a number of utility commands being showcased here.'''
|
||||||
|
|
||||||
intents = discord.Intents(guilds=True, messages=True, members=True)
|
intents = discord.Intents.default()
|
||||||
bot = commands.Bot(command_prefix='t-', description=description, intents=intents)
|
intents.members = True
|
||||||
|
|
||||||
|
bot = commands.Bot(command_prefix='?', description=description, intents=intents)
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
|
@ -123,11 +123,8 @@ class Music(commands.Cog):
|
|||||||
elif ctx.voice_client.is_playing():
|
elif ctx.voice_client.is_playing():
|
||||||
ctx.voice_client.stop()
|
ctx.voice_client.stop()
|
||||||
|
|
||||||
bot = commands.Bot(
|
bot = commands.Bot(command_prefix=commands.when_mentioned_or("!"),
|
||||||
command_prefix=commands.when_mentioned_or("!"),
|
description='Relatively simple music bot example')
|
||||||
description='Relatively simple music bot example',
|
|
||||||
intents=discord.Intents(guilds=True, guild_messages=True, voice_states=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
|
@ -5,8 +5,9 @@ import typing
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
intents = discord.Intents(guilds=True, messages=True, members=True)
|
|
||||||
bot = commands.Bot('!', intents=intents)
|
bot = commands.Bot('!', intents=intents)
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class MyBot(commands.Bot):
|
|||||||
return await super().get_context(message, cls=cls)
|
return await super().get_context(message, cls=cls)
|
||||||
|
|
||||||
|
|
||||||
bot = MyBot(command_prefix='!', intents=discord.Intents(guilds=True, messages=True))
|
bot = MyBot(command_prefix='!')
|
||||||
|
|
||||||
@bot.command()
|
@bot.command()
|
||||||
async def guess(ctx, number: int):
|
async def guess(ctx, number: int):
|
||||||
|
@ -17,5 +17,5 @@ class MyClient(discord.Client):
|
|||||||
msg = f'{message.author} has deleted the message: {message.content}'
|
msg = f'{message.author} has deleted the message: {message.content}'
|
||||||
await message.channel.send(msg)
|
await message.channel.send(msg)
|
||||||
|
|
||||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
client = MyClient()
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -16,5 +16,5 @@ class MyClient(discord.Client):
|
|||||||
msg = f'**{before.author}** edited their message:\n{before.content} -> {after.content}'
|
msg = f'**{before.author}** edited their message:\n{before.content} -> {after.content}'
|
||||||
await before.channel.send(msg)
|
await before.channel.send(msg)
|
||||||
|
|
||||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
client = MyClient()
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -30,5 +30,5 @@ class MyClient(discord.Client):
|
|||||||
else:
|
else:
|
||||||
await message.channel.send(f'Oops. It is actually {answer}.')
|
await message.channel.send(f'Oops. It is actually {answer}.')
|
||||||
|
|
||||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
client = MyClient()
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -14,5 +14,8 @@ class MyClient(discord.Client):
|
|||||||
await guild.system_channel.send(to_send)
|
await guild.system_channel.send(to_send)
|
||||||
|
|
||||||
|
|
||||||
client = MyClient(intents=discord.Intents(guilds=True, members=True))
|
intents = discord.Intents.default()
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
|
client = MyClient(intents=intents)
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -78,6 +78,8 @@ class MyClient(discord.Client):
|
|||||||
# If we want to do something in case of errors we'd do it here.
|
# If we want to do something in case of errors we'd do it here.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
intents = discord.Intents(guilds=True, members=True, guild_reactions=True)
|
intents = discord.Intents.default()
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
client = MyClient(intents=intents)
|
client = MyClient(intents=intents)
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -13,5 +13,5 @@ class MyClient(discord.Client):
|
|||||||
if message.content.startswith('!hello'):
|
if message.content.startswith('!hello'):
|
||||||
await message.reply('Hello!', mention_author=True)
|
await message.reply('Hello!', mention_author=True)
|
||||||
|
|
||||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
client = MyClient()
|
||||||
client.run('token')
|
client.run('token')
|
||||||
|
@ -3,11 +3,7 @@ import typing
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
bot = commands.Bot(
|
bot = commands.Bot(command_prefix=commands.when_mentioned, description="Nothing to see here!")
|
||||||
command_prefix=commands.when_mentioned,
|
|
||||||
description="Nothing to see here!",
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
# the `hidden` keyword argument hides it from the help command.
|
# the `hidden` keyword argument hides it from the help command.
|
||||||
@bot.group(hidden=True)
|
@bot.group(hidden=True)
|
||||||
|
@ -5,10 +5,7 @@ import discord
|
|||||||
|
|
||||||
class Bot(commands.Bot):
|
class Bot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||||
command_prefix=commands.when_mentioned_or('$'),
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||||
|
@ -5,10 +5,7 @@ import discord
|
|||||||
|
|
||||||
class CounterBot(commands.Bot):
|
class CounterBot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||||
command_prefix=commands.when_mentioned_or('$'),
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
@ -37,10 +39,7 @@ class DropdownView(discord.ui.View):
|
|||||||
|
|
||||||
class Bot(commands.Bot):
|
class Bot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||||
command_prefix=commands.when_mentioned_or('$'),
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||||
|
@ -4,10 +4,7 @@ import discord
|
|||||||
|
|
||||||
class EphemeralCounterBot(commands.Bot):
|
class EphemeralCounterBot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||||
command_prefix=commands.when_mentioned_or('$'),
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||||
|
@ -5,10 +5,7 @@ from urllib.parse import quote_plus
|
|||||||
|
|
||||||
class GoogleBot(commands.Bot):
|
class GoogleBot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||||
command_prefix=commands.when_mentioned_or('$'),
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||||
@ -39,4 +36,4 @@ async def google(ctx: commands.Context, *, query: str):
|
|||||||
await ctx.send(f'Google Result for: `{query}`', view=Google(query))
|
await ctx.send(f'Google Result for: `{query}`', view=Google(query))
|
||||||
|
|
||||||
|
|
||||||
bot.run()
|
bot.run('token')
|
||||||
|
@ -29,11 +29,7 @@ class PersistentView(discord.ui.View):
|
|||||||
|
|
||||||
class PersistentViewBot(commands.Bot):
|
class PersistentViewBot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||||
command_prefix=commands.when_mentioned_or('$'),
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.persistent_views_added = False
|
self.persistent_views_added = False
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
|
@ -120,10 +120,7 @@ class TicTacToe(discord.ui.View):
|
|||||||
|
|
||||||
class TicTacToeBot(commands.Bot):
|
class TicTacToeBot(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||||
command_prefix=commands.when_mentioned_or('$'),
|
|
||||||
intents=discord.Intents(guilds=True, messages=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||||
|
Reference in New Issue
Block a user