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
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
- 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 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)
|
||||
|
||||
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:
|
||||
- name: Ask a question
|
||||
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
|
||||
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
|
||||
:target: https://discord.gg/PYAfZzpsjG
|
||||
@ -59,7 +59,7 @@ To install the development version, do the following:
|
||||
.. code:: sh
|
||||
|
||||
$ git clone https://github.com/iDevision/enhanced-discord.py
|
||||
$ cd enhanced-discord.py
|
||||
$ cd discord.py
|
||||
$ python3 -m pip install -U .[voice]
|
||||
|
||||
|
||||
|
@ -40,7 +40,6 @@ from .colour import *
|
||||
from .integrations import *
|
||||
from .invite import *
|
||||
from .template import *
|
||||
from .welcome_screen import *
|
||||
from .widget import *
|
||||
from .object import *
|
||||
from .reaction import *
|
||||
|
@ -794,13 +794,13 @@ class CustomActivity(BaseActivity):
|
||||
return hash((self.name, str(self.emoji)))
|
||||
|
||||
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)
|
||||
|
||||
if self.name:
|
||||
return f'{self.emoji} {self.name}'
|
||||
return str(self.emoji)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<CustomActivity name={self.name!r} emoji={self.emoji!r}>'
|
||||
|
||||
|
@ -313,11 +313,10 @@ class Asset(AssetMixin):
|
||||
if self._animated:
|
||||
if format not in VALID_ASSET_FORMATS:
|
||||
raise InvalidArgument(f'format must be one of {VALID_ASSET_FORMATS}')
|
||||
url = url.with_path(f'{path}.{format}')
|
||||
elif static_format is MISSING:
|
||||
else:
|
||||
if format not in 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 not in VALID_STATIC_FORMATS:
|
||||
|
@ -330,10 +330,6 @@ class AuditLogEntry(Hashable):
|
||||
|
||||
Returns the entry's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the entry's ID.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
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.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the channel's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
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."""
|
||||
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
|
||||
def threads(self) -> List[Thread]:
|
||||
"""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.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the category's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -1574,10 +1556,6 @@ class StoreChannel(discord.abc.GuildChannel, Hashable):
|
||||
|
||||
Returns the channel's name.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the channel's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -1750,10 +1728,6 @@ class DMChannel(discord.abc.Messageable, Hashable):
|
||||
|
||||
Returns a string representation of the channel
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the channel's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
recipient: Optional[:class:`User`]
|
||||
@ -1880,10 +1854,6 @@ class GroupChannel(discord.abc.Messageable, Hashable):
|
||||
|
||||
Returns a string representation of the channel
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the channel's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
recipients: List[:class:`User`]
|
||||
@ -2030,10 +2000,6 @@ class PartialMessageable(discord.abc.Messageable, Hashable):
|
||||
|
||||
Returns the partial messageable's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the messageable's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
|
@ -142,6 +142,7 @@ class Client:
|
||||
intents: :class:`Intents`
|
||||
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.
|
||||
If not given, defaults to a regularly constructed :class:`Intents` class.
|
||||
|
||||
.. versionadded:: 1.5
|
||||
member_cache_flags: :class:`MemberCacheFlags`
|
||||
@ -202,12 +203,9 @@ class Client:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
intents: Intents,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
**options: Any,
|
||||
):
|
||||
options["intents"] = intents
|
||||
|
||||
# self.ws is set in the connect method
|
||||
self.ws: DiscordWebSocket = None # type: ignore
|
||||
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)
|
||||
|
||||
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]:
|
||||
"""Returns an emoji with the given ID.
|
||||
|
||||
|
@ -251,13 +251,6 @@ class Colour:
|
||||
def red(cls: Type[CT]) -> CT:
|
||||
"""A factory method that returns a :class:`Colour` with a value of ``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
|
||||
def dark_red(cls: Type[CT]) -> CT:
|
||||
@ -331,15 +324,6 @@ class Colour:
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
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
|
||||
|
@ -72,36 +72,30 @@ if TYPE_CHECKING:
|
||||
T = TypeVar('T')
|
||||
MaybeEmpty = Union[T, _EmptyEmbed]
|
||||
|
||||
|
||||
class _EmbedFooterProxy(Protocol):
|
||||
text: MaybeEmpty[str]
|
||||
icon_url: MaybeEmpty[str]
|
||||
|
||||
|
||||
class _EmbedFieldProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
value: MaybeEmpty[str]
|
||||
inline: bool
|
||||
|
||||
|
||||
class _EmbedMediaProxy(Protocol):
|
||||
url: MaybeEmpty[str]
|
||||
proxy_url: MaybeEmpty[str]
|
||||
height: MaybeEmpty[int]
|
||||
width: MaybeEmpty[int]
|
||||
|
||||
|
||||
class _EmbedVideoProxy(Protocol):
|
||||
url: MaybeEmpty[str]
|
||||
height: MaybeEmpty[int]
|
||||
width: MaybeEmpty[int]
|
||||
|
||||
|
||||
class _EmbedProviderProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
url: MaybeEmpty[str]
|
||||
|
||||
|
||||
class _EmbedAuthorProxy(Protocol):
|
||||
name: MaybeEmpty[str]
|
||||
url: MaybeEmpty[str]
|
||||
@ -181,15 +175,15 @@ class Embed:
|
||||
Empty: Final = EmptyEmbed
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
title: MaybeEmpty[Any] = EmptyEmbed,
|
||||
type: EmbedType = 'rich',
|
||||
url: MaybeEmpty[Any] = EmptyEmbed,
|
||||
description: MaybeEmpty[Any] = EmptyEmbed,
|
||||
timestamp: datetime.datetime = None,
|
||||
self,
|
||||
*,
|
||||
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
|
||||
title: MaybeEmpty[Any] = EmptyEmbed,
|
||||
type: EmbedType = 'rich',
|
||||
url: MaybeEmpty[Any] = EmptyEmbed,
|
||||
description: MaybeEmpty[Any] = EmptyEmbed,
|
||||
timestamp: datetime.datetime = None,
|
||||
):
|
||||
|
||||
self.colour = colour if colour is not EmptyEmbed else color
|
||||
@ -372,7 +366,7 @@ class Embed:
|
||||
self._footer['icon_url'] = str(icon_url)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def remove_footer(self: E) -> E:
|
||||
"""Clears embed's footer information.
|
||||
|
||||
@ -387,7 +381,7 @@ class Embed:
|
||||
pass
|
||||
|
||||
return self
|
||||
|
||||
|
||||
@property
|
||||
def image(self) -> _EmbedMediaProxy:
|
||||
"""Returns an ``EmbedProxy`` denoting the image contents.
|
||||
@ -403,22 +397,6 @@ class Embed:
|
||||
"""
|
||||
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:
|
||||
"""Sets the image for the embed content.
|
||||
|
||||
@ -434,7 +412,15 @@ class Embed:
|
||||
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
|
||||
|
||||
@ -453,23 +439,7 @@ class Embed:
|
||||
"""
|
||||
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
|
||||
|
||||
@thumbnail.setter
|
||||
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]):
|
||||
def set_thumbnail(self: E, *, url: MaybeEmpty[Any]) -> E:
|
||||
"""Sets the thumbnail for the embed content.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
self.thumbnail = url
|
||||
if url is EmptyEmbed:
|
||||
try:
|
||||
del self._thumbnail
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self._thumbnail = {
|
||||
'url': str(url),
|
||||
}
|
||||
|
||||
return self
|
||||
|
||||
|
@ -72,10 +72,6 @@ class Emoji(_EmojiTag, AssetMixin):
|
||||
|
||||
Returns the emoji rendered for discord.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the emoji ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
@ -141,9 +137,6 @@ class Emoji(_EmojiTag, AssetMixin):
|
||||
return f'<a:{self.name}:{self.id}>'
|
||||
return f'<:{self.name}:{self.id}>'
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
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 .help import HelpCommand, DefaultHelpCommand
|
||||
from .cog import Cog
|
||||
from discord.utils import raise_expected_coro
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import importlib.machinery
|
||||
@ -121,8 +120,8 @@ class _DefaultRepr:
|
||||
_default = _DefaultRepr()
|
||||
|
||||
class BotBase(GroupMixin):
|
||||
def __init__(self, command_prefix, help_command=_default, description=None, *, intents: discord.Intents, **options):
|
||||
super().__init__(**options, intents=intents)
|
||||
def __init__(self, command_prefix, help_command=_default, description=None, **options):
|
||||
super().__init__(**options)
|
||||
self.command_prefix = command_prefix
|
||||
self.extra_events: Dict[str, List[CoroFunc]] = {}
|
||||
self.__cogs: Dict[str, Cog] = {}
|
||||
@ -345,59 +344,14 @@ class BotBase(GroupMixin):
|
||||
elif self.owner_ids:
|
||||
return user.id in self.owner_ids
|
||||
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]:
|
||||
"""|coro|
|
||||
|
||||
Returns a list of :class:`~discord.User` representing the owners of the bot.
|
||||
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]
|
||||
app = await self.application_info() # type: ignore
|
||||
if app.team:
|
||||
self.owner_ids = ids = {m.id for m in app.team.members}
|
||||
return user.id in ids
|
||||
else:
|
||||
return []
|
||||
|
||||
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
|
||||
self.owner_id = owner_id = app.owner.id
|
||||
return user.id == owner_id
|
||||
|
||||
def before_invoke(self, coro: CFT) -> CFT:
|
||||
"""A decorator that registers a coroutine as a pre-invoke hook.
|
||||
@ -425,9 +379,11 @@ class BotBase(GroupMixin):
|
||||
TypeError
|
||||
The coroutine passed is not actually a coroutine.
|
||||
"""
|
||||
return raise_expected_coro(
|
||||
coro, 'The pre-invoke hook must be a coroutine.'
|
||||
)
|
||||
if not asyncio.iscoroutinefunction(coro):
|
||||
raise TypeError('The pre-invoke hook must be a coroutine.')
|
||||
|
||||
self._before_invoke = coro
|
||||
return coro
|
||||
|
||||
def after_invoke(self, coro: CFT) -> CFT:
|
||||
r"""A decorator that registers a coroutine as a post-invoke hook.
|
||||
@ -456,10 +412,11 @@ class BotBase(GroupMixin):
|
||||
TypeError
|
||||
The coroutine passed is not actually a coroutine.
|
||||
"""
|
||||
return raise_expected_coro(
|
||||
coro, 'The post-invoke hook must be a coroutine.'
|
||||
)
|
||||
if not asyncio.iscoroutinefunction(coro):
|
||||
raise TypeError('The post-invoke hook must be a coroutine.')
|
||||
|
||||
self._after_invoke = coro
|
||||
return coro
|
||||
|
||||
# listener registration
|
||||
|
||||
@ -1118,7 +1075,7 @@ class Bot(BotBase, discord.Client):
|
||||
when passing an empty string, it should always be last as no prefix
|
||||
after it will be matched.
|
||||
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
|
||||
you require group commands to be case insensitive as well.
|
||||
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
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
@ -33,7 +32,6 @@ import discord.abc
|
||||
import discord.utils
|
||||
|
||||
from discord.message import Message
|
||||
from discord import Permissions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec
|
||||
@ -62,7 +60,10 @@ T = TypeVar('T')
|
||||
BotT = TypeVar('BotT', bound="Union[Bot, AutoShardedBot]")
|
||||
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]):
|
||||
@ -313,13 +314,6 @@ class Context(discord.abc.Messageable, Generic[BotT]):
|
||||
g = self.guild
|
||||
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:
|
||||
"""send_help(entity=<bot>)
|
||||
|
||||
|
@ -353,14 +353,14 @@ class PartialMessageConverter(Converter[discord.PartialMessage]):
|
||||
|
||||
@staticmethod
|
||||
def _resolve_channel(ctx, guild_id, channel_id) -> Optional[PartialMessageableChannel]:
|
||||
if guild_id is 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:
|
||||
return guild._resolve_channel(channel_id) # type: ignore
|
||||
if guild_id is not None:
|
||||
guild = ctx.bot.get_guild(guild_id)
|
||||
if guild is not None and channel_id is not None:
|
||||
return guild._resolve_channel(channel_id) # type: ignore
|
||||
else:
|
||||
return None
|
||||
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:
|
||||
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
|
||||
|
||||
The following formats are accepted:
|
||||
|
||||
- ``<hex>``
|
||||
|
||||
- ``0x<hex>``
|
||||
- ``#<hex>``
|
||||
- ``0x#<hex>``
|
||||
@ -669,9 +668,7 @@ class ColourConverter(Converter[discord.Colour]):
|
||||
|
||||
arg = arg.replace(' ', '_')
|
||||
method = getattr(discord.Colour, arg, None)
|
||||
if method is None:
|
||||
return self.parse_hex_number(argument)
|
||||
elif arg.startswith('from_') or not inspect.ismethod(method):
|
||||
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||
raise BadColourArgument(arg)
|
||||
return method()
|
||||
|
||||
@ -757,8 +754,8 @@ class GuildConverter(IDConverter[discord.Guild]):
|
||||
if result is None:
|
||||
result = discord.utils.get(ctx.bot.guilds, name=argument)
|
||||
|
||||
if result is None:
|
||||
raise GuildNotFound(argument)
|
||||
if result is None:
|
||||
raise GuildNotFound(argument)
|
||||
return result
|
||||
|
||||
|
||||
@ -942,7 +939,8 @@ class clean_content(Converter[str]):
|
||||
def repl(match: re.Match) -> str:
|
||||
type = match[1]
|
||||
id = int(match[2])
|
||||
return transforms[type](id)
|
||||
transformed = transforms[type](id)
|
||||
return transformed
|
||||
|
||||
result = re.sub(r'<(@[!&]?|#)([0-9]{15,20})>', repl, argument)
|
||||
if self.escape_markdown:
|
||||
|
@ -1135,10 +1135,10 @@ class GroupMixin(Generic[CogT]):
|
||||
A mapping of command name to :class:`.Command`
|
||||
objects.
|
||||
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:
|
||||
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.case_insensitive: bool = case_insensitive
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -82,7 +82,9 @@ class StringView:
|
||||
def skip_string(self, string):
|
||||
strlen = len(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
|
||||
|
||||
def read_rest(self):
|
||||
@ -93,7 +95,9 @@ class StringView:
|
||||
|
||||
def read(self, 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):
|
||||
try:
|
||||
@ -101,12 +105,9 @@ class StringView:
|
||||
except IndexError:
|
||||
result = None
|
||||
|
||||
return self._return_index(1, result)
|
||||
|
||||
def _return_index(self, arg0, arg1):
|
||||
self.previous = self.index
|
||||
self.index += arg0
|
||||
return arg1
|
||||
self.index += 1
|
||||
return result
|
||||
|
||||
def get_word(self):
|
||||
pos = 0
|
||||
|
@ -46,9 +46,7 @@ import traceback
|
||||
|
||||
from collections.abc import Sequence
|
||||
from discord.backoff import ExponentialBackoff
|
||||
from discord.utils import MISSING, raise_expected_coro
|
||||
|
||||
|
||||
from discord.utils import MISSING
|
||||
|
||||
__all__ = (
|
||||
'loop',
|
||||
@ -490,7 +488,11 @@ class Loop(Generic[LF]):
|
||||
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:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
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:
|
||||
"""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
|
||||
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:
|
||||
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))
|
||||
|
||||
return sorted(set(ret))
|
||||
ret = sorted(set(ret)) # de-dupe and sort times
|
||||
return ret
|
||||
|
||||
def change_interval(
|
||||
self,
|
||||
|
@ -480,6 +480,16 @@ class Intents(BaseFlags):
|
||||
self.value = self.DEFAULT_VALUE
|
||||
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
|
||||
def guilds(self):
|
||||
""":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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypedDict, Any, Optional, List, TypeVar, Type, Dict, Callable, Coroutine, NamedTuple, Deque
|
||||
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from collections import namedtuple, deque
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import struct
|
||||
@ -42,25 +38,9 @@ import aiohttp
|
||||
from . import utils
|
||||
from .activity import BaseActivity
|
||||
from .enums import SpeakingState
|
||||
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__)
|
||||
from .errors import ConnectionClosed, InvalidArgument
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = (
|
||||
'DiscordWebSocket',
|
||||
@ -70,49 +50,36 @@ __all__ = (
|
||||
'ReconnectWebSocket',
|
||||
)
|
||||
|
||||
|
||||
class Heartbeat(TypedDict):
|
||||
op: int
|
||||
d: int
|
||||
|
||||
|
||||
class ReconnectWebSocket(Exception):
|
||||
"""Signals to safely reconnect the websocket."""
|
||||
def __init__(self, shard_id: Optional[int], *, resume: bool = True) -> None:
|
||||
self.shard_id: Optional[int] = shard_id
|
||||
self.resume: bool = resume
|
||||
def __init__(self, shard_id, *, resume=True):
|
||||
self.shard_id = shard_id
|
||||
self.resume = resume
|
||||
self.op = 'RESUME' if resume else 'IDENTIFY'
|
||||
|
||||
|
||||
class WebSocketClosure(Exception):
|
||||
"""An exception to make up for the fact that aiohttp doesn't signal closure."""
|
||||
pass
|
||||
|
||||
|
||||
class EventListener(NamedTuple):
|
||||
predicate: Predicate
|
||||
event: str
|
||||
result: Result
|
||||
future: asyncio.Future
|
||||
|
||||
EventListener = namedtuple('EventListener', 'predicate event result future')
|
||||
|
||||
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
|
||||
self.max: int = count
|
||||
self.remaining: int = count
|
||||
self.window: float = 0.0
|
||||
self.per: float = per
|
||||
self.lock: asyncio.Lock = asyncio.Lock()
|
||||
self.shard_id: Optional[int] = None
|
||||
self.max = count
|
||||
self.remaining = count
|
||||
self.window = 0.0
|
||||
self.per = per
|
||||
self.lock = asyncio.Lock()
|
||||
self.shard_id = None
|
||||
|
||||
def is_ratelimited(self) -> bool:
|
||||
def is_ratelimited(self):
|
||||
current = time.time()
|
||||
if current > self.window + self.per:
|
||||
return False
|
||||
return self.remaining == 0
|
||||
|
||||
def get_delay(self) -> float:
|
||||
def get_delay(self):
|
||||
current = time.time()
|
||||
|
||||
if current > self.window + self.per:
|
||||
@ -130,7 +97,7 @@ class GatewayRatelimiter:
|
||||
|
||||
return 0.0
|
||||
|
||||
async def block(self) -> None:
|
||||
async def block(self):
|
||||
async with self.lock:
|
||||
delta = self.get_delay()
|
||||
if delta:
|
||||
@ -139,27 +106,27 @@ class GatewayRatelimiter:
|
||||
|
||||
|
||||
class KeepAliveHandler(threading.Thread):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
ws = kwargs.pop('ws')
|
||||
def __init__(self, *args, **kwargs):
|
||||
ws = kwargs.pop('ws', None)
|
||||
interval = kwargs.pop('interval', None)
|
||||
shard_id = kwargs.pop('shard_id', None)
|
||||
threading.Thread.__init__(self, *args, **kwargs)
|
||||
self.ws: DiscordWebSocket = ws
|
||||
self._main_thread_id: int = ws.thread_id
|
||||
self.interval: Optional[float] = interval
|
||||
self.daemon: bool = True
|
||||
self.shard_id: Optional[int] = shard_id
|
||||
self.msg: str = 'Keeping shard ID %s websocket alive with sequence %s.'
|
||||
self.block_msg: str = '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._stop_ev: threading.Event = threading.Event()
|
||||
self._last_ack: float = time.perf_counter()
|
||||
self._last_send: float = time.perf_counter()
|
||||
self._last_recv: float = time.perf_counter()
|
||||
self.latency: float = float('inf')
|
||||
self.heartbeat_timeout: float = ws._max_heartbeat_timeout
|
||||
self.ws = ws
|
||||
self._main_thread_id = ws.thread_id
|
||||
self.interval = interval
|
||||
self.daemon = True
|
||||
self.shard_id = shard_id
|
||||
self.msg = 'Keeping shard ID %s websocket alive with sequence %s.'
|
||||
self.block_msg = 'Shard ID %s heartbeat blocked for more than %s seconds.'
|
||||
self.behind_msg = 'Can\'t keep up, shard ID %s websocket is %.1fs behind.'
|
||||
self._stop_ev = threading.Event()
|
||||
self._last_ack = time.perf_counter()
|
||||
self._last_send = time.perf_counter()
|
||||
self._last_recv = time.perf_counter()
|
||||
self.latency = float('inf')
|
||||
self.heartbeat_timeout = ws._max_heartbeat_timeout
|
||||
|
||||
def run(self) -> None:
|
||||
def run(self):
|
||||
while not self._stop_ev.wait(self.interval):
|
||||
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)
|
||||
@ -201,20 +168,19 @@ class KeepAliveHandler(threading.Thread):
|
||||
else:
|
||||
self._last_send = time.perf_counter()
|
||||
|
||||
def get_payload(self) -> Heartbeat:
|
||||
def get_payload(self):
|
||||
return {
|
||||
'op': self.ws.HEARTBEAT,
|
||||
# the websocket's sequence won't be None here
|
||||
'd': self.ws.sequence # type: ignore
|
||||
'd': self.ws.sequence
|
||||
}
|
||||
|
||||
def stop(self) -> None:
|
||||
def stop(self):
|
||||
self._stop_ev.set()
|
||||
|
||||
def tick(self) -> None:
|
||||
def tick(self):
|
||||
self._last_recv = time.perf_counter()
|
||||
|
||||
def ack(self) -> None:
|
||||
def ack(self):
|
||||
ack_time = time.perf_counter()
|
||||
self._last_ack = ack_time
|
||||
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)
|
||||
|
||||
class VoiceKeepAliveHandler(KeepAliveHandler):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
def __init__(self, *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.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'
|
||||
|
||||
def get_payload(self) -> Heartbeat:
|
||||
def get_payload(self):
|
||||
return {
|
||||
'op': self.ws.HEARTBEAT,
|
||||
'd': int(time.time() * 1000)
|
||||
}
|
||||
|
||||
def ack(self) -> None:
|
||||
def ack(self):
|
||||
ack_time = time.perf_counter()
|
||||
self._last_ack = ack_time
|
||||
self._last_recv = ack_time
|
||||
self.latency = ack_time - self._last_send
|
||||
self.recent_ack_latencies.append(self.latency)
|
||||
|
||||
|
||||
class DiscordClientWebSocketResponse(aiohttp.ClientWebSocketResponse):
|
||||
async def close(self, *, code: int = 4000, message: bytes = b'') -> bool:
|
||||
return await super().close(code=code, message=message)
|
||||
|
||||
|
||||
class DiscordWebSocket:
|
||||
"""Implements a WebSocket for Discord's gateway v6.
|
||||
|
||||
@ -302,53 +266,41 @@ class DiscordWebSocket:
|
||||
HEARTBEAT_ACK = 11
|
||||
GUILD_SYNC = 12
|
||||
|
||||
def __init__(self, socket: aiohttp.ClientWebSocketResponse, *, loop: asyncio.AbstractEventLoop) -> None:
|
||||
self.socket: aiohttp.ClientWebSocketResponse = socket
|
||||
self.loop: asyncio.AbstractEventLoop = loop
|
||||
def __init__(self, socket, *, loop):
|
||||
self.socket = socket
|
||||
self.loop = loop
|
||||
|
||||
# an empty dispatcher to prevent crashes
|
||||
self._dispatch = lambda *args: None
|
||||
# generic event listeners
|
||||
self._dispatch_listeners: List[EventListener] = []
|
||||
self._dispatch_listeners = []
|
||||
# the keep alive
|
||||
self._keep_alive: Optional[KeepAliveHandler] = None
|
||||
self.thread_id: int = threading.get_ident()
|
||||
self._keep_alive = None
|
||||
self.thread_id = threading.get_ident()
|
||||
|
||||
# ws related stuff
|
||||
self.session_id: Optional[str] = None
|
||||
self.sequence: Optional[int] = None
|
||||
self.session_id = None
|
||||
self.sequence = None
|
||||
self._zlib = zlib.decompressobj()
|
||||
self._buffer: bytearray = bytearray()
|
||||
self._close_code: Optional[int] = None
|
||||
self._rate_limiter: GatewayRatelimiter = 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
|
||||
self._buffer = bytearray()
|
||||
self._close_code = None
|
||||
self._rate_limiter = GatewayRatelimiter()
|
||||
|
||||
@property
|
||||
def open(self) -> bool:
|
||||
def open(self):
|
||||
return not self.socket.closed
|
||||
|
||||
def is_ratelimited(self) -> bool:
|
||||
def is_ratelimited(self):
|
||||
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)
|
||||
|
||||
def log_receive(self, _, /) -> None:
|
||||
def log_receive(self, _, /):
|
||||
pass
|
||||
|
||||
@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`.
|
||||
|
||||
This is for internal use only.
|
||||
@ -358,9 +310,7 @@ class DiscordWebSocket:
|
||||
ws = cls(socket, loop=client.loop)
|
||||
|
||||
# dynamically add attributes needed
|
||||
|
||||
# the token won't be None here
|
||||
ws.token = client.http.token # type: ignore
|
||||
ws.token = client.http.token
|
||||
ws._connection = client._connection
|
||||
ws._discord_parsers = client._connection.parsers
|
||||
ws._dispatch = client.dispatch
|
||||
@ -392,7 +342,7 @@ class DiscordWebSocket:
|
||||
await ws.resume()
|
||||
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.
|
||||
|
||||
Parameters
|
||||
@ -417,7 +367,7 @@ class DiscordWebSocket:
|
||||
self._dispatch_listeners.append(entry)
|
||||
return future
|
||||
|
||||
async def identify(self) -> None:
|
||||
async def identify(self):
|
||||
"""Sends the IDENTIFY packet."""
|
||||
payload = {
|
||||
'op': self.IDENTIFY,
|
||||
@ -455,7 +405,7 @@ class DiscordWebSocket:
|
||||
await self.send_as_json(payload)
|
||||
_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."""
|
||||
payload = {
|
||||
'op': self.RESUME,
|
||||
@ -469,8 +419,7 @@ class DiscordWebSocket:
|
||||
await self.send_as_json(payload)
|
||||
_log.info('Shard ID %s has sent the RESUME payload.', self.shard_id)
|
||||
|
||||
|
||||
async def received_message(self, msg, /) -> None:
|
||||
async def received_message(self, msg, /):
|
||||
if type(msg) is bytes:
|
||||
self._buffer.extend(msg)
|
||||
|
||||
@ -588,16 +537,16 @@ class DiscordWebSocket:
|
||||
del self._dispatch_listeners[index]
|
||||
|
||||
@property
|
||||
def latency(self) -> float:
|
||||
def latency(self):
|
||||
""":class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds."""
|
||||
heartbeat = self._keep_alive
|
||||
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
|
||||
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.
|
||||
|
||||
Raises
|
||||
@ -635,23 +584,23 @@ class DiscordWebSocket:
|
||||
_log.info('Websocket closed with %s, cannot reconnect.', code)
|
||||
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()
|
||||
self._dispatch('socket_raw_send', 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.socket.send_str(data)
|
||||
|
||||
async def send_as_json(self, data) -> None:
|
||||
async def send_as_json(self, data):
|
||||
try:
|
||||
await self.send(utils._to_json(data))
|
||||
except RuntimeError as exc:
|
||||
if not self._can_handle_close():
|
||||
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
|
||||
try:
|
||||
await self.socket.send_str(utils._to_json(data))
|
||||
@ -659,13 +608,13 @@ class DiscordWebSocket:
|
||||
if not self._can_handle_close():
|
||||
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 not isinstance(activity, BaseActivity):
|
||||
raise InvalidArgument('activity must derive from BaseActivity.')
|
||||
activities = [activity.to_dict()]
|
||||
activity = [activity.to_dict()]
|
||||
else:
|
||||
activities = []
|
||||
activity = []
|
||||
|
||||
if status == 'idle':
|
||||
since = int(time.time() * 1000)
|
||||
@ -673,7 +622,7 @@ class DiscordWebSocket:
|
||||
payload = {
|
||||
'op': self.PRESENCE,
|
||||
'd': {
|
||||
'activities': activities,
|
||||
'activities': activity,
|
||||
'afk': False,
|
||||
'since': since,
|
||||
'status': status
|
||||
@ -684,7 +633,7 @@ class DiscordWebSocket:
|
||||
_log.debug('Sending "%s" to change status', 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 = {
|
||||
'op': self.REQUEST_MEMBERS,
|
||||
'd': {
|
||||
@ -706,7 +655,7 @@ class DiscordWebSocket:
|
||||
|
||||
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 = {
|
||||
'op': self.VOICE_STATE,
|
||||
'd': {
|
||||
@ -720,7 +669,7 @@ class DiscordWebSocket:
|
||||
_log.debug('Updating our voice state to %s.', 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:
|
||||
self._keep_alive.stop()
|
||||
self._keep_alive = None
|
||||
@ -772,31 +721,25 @@ class DiscordVoiceWebSocket:
|
||||
CLIENT_CONNECT = 12
|
||||
CLIENT_DISCONNECT = 13
|
||||
|
||||
def __init__(self, socket: aiohttp.ClientWebSocketResponse, loop: asyncio.AbstractEventLoop, *, hook: Optional[Coro] = None) -> None:
|
||||
self.ws: aiohttp.ClientWebSocketResponse = socket
|
||||
self.loop: asyncio.AbstractEventLoop = loop
|
||||
self._keep_alive: VoiceKeepAliveHandler = utils.MISSING
|
||||
self._close_code: Optional[int] = None
|
||||
self.secret_key: Optional[List[int]] = None
|
||||
self.gateway: str = utils.MISSING
|
||||
self._connection: VoiceClient = utils.MISSING
|
||||
self._max_heartbeat_timeout: float = utils.MISSING
|
||||
self.thread_id: int = utils.MISSING
|
||||
def __init__(self, socket, loop, *, hook=None):
|
||||
self.ws = socket
|
||||
self.loop = loop
|
||||
self._keep_alive = None
|
||||
self._close_code = None
|
||||
self.secret_key = None
|
||||
if hook:
|
||||
# we want to redeclare self._hook
|
||||
self._hook = hook # type: ignore
|
||||
self._hook = hook
|
||||
|
||||
async def _hook(self, *args: Any) -> Any:
|
||||
async def _hook(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
async def send_as_json(self, data) -> None:
|
||||
async def send_as_json(self, data):
|
||||
_log.debug('Sending voice websocket frame: %s.', data)
|
||||
await self.ws.send_str(utils._to_json(data))
|
||||
|
||||
send_heartbeat = send_as_json
|
||||
|
||||
async def resume(self) -> None:
|
||||
async def resume(self):
|
||||
state = self._connection
|
||||
payload = {
|
||||
'op': self.RESUME,
|
||||
@ -822,7 +765,7 @@ class DiscordVoiceWebSocket:
|
||||
await self.send_as_json(payload)
|
||||
|
||||
@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`."""
|
||||
gateway = 'wss://' + client.endpoint + '/?v=4'
|
||||
http = client._state.http
|
||||
@ -840,7 +783,7 @@ class DiscordVoiceWebSocket:
|
||||
|
||||
return ws
|
||||
|
||||
async def select_protocol(self, ip, port, mode) -> None:
|
||||
async def select_protocol(self, ip, port, mode):
|
||||
payload = {
|
||||
'op': self.SELECT_PROTOCOL,
|
||||
'd': {
|
||||
@ -855,7 +798,7 @@ class DiscordVoiceWebSocket:
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def client_connect(self) -> None:
|
||||
async def client_connect(self):
|
||||
payload = {
|
||||
'op': self.CLIENT_CONNECT,
|
||||
'd': {
|
||||
@ -865,7 +808,7 @@ class DiscordVoiceWebSocket:
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
async def speak(self, state=SpeakingState.voice) -> None:
|
||||
async def speak(self, state=SpeakingState.voice):
|
||||
payload = {
|
||||
'op': self.SPEAKING,
|
||||
'd': {
|
||||
@ -876,8 +819,7 @@ class DiscordVoiceWebSocket:
|
||||
|
||||
await self.send_as_json(payload)
|
||||
|
||||
|
||||
async def received_message(self, msg) -> None:
|
||||
async def received_message(self, msg):
|
||||
_log.debug('Voice websocket frame received: %s', msg)
|
||||
op = msg['op']
|
||||
data = msg.get('d')
|
||||
@ -898,7 +840,7 @@ class DiscordVoiceWebSocket:
|
||||
|
||||
await self._hook(self, msg)
|
||||
|
||||
async def initial_connection(self, data) -> None:
|
||||
async def initial_connection(self, data):
|
||||
state = self._connection
|
||||
state.ssrc = data['ssrc']
|
||||
state.voice_port = data['port']
|
||||
@ -929,13 +871,13 @@ class DiscordVoiceWebSocket:
|
||||
_log.info('selected the voice protocol for use (%s)', mode)
|
||||
|
||||
@property
|
||||
def latency(self) -> float:
|
||||
def latency(self):
|
||||
""":class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds."""
|
||||
heartbeat = self._keep_alive
|
||||
return float('inf') if heartbeat is None else heartbeat.latency
|
||||
|
||||
@property
|
||||
def average_latency(self) -> float:
|
||||
def average_latency(self):
|
||||
""":class:`list`: Average of last 20 HEARTBEAT latencies."""
|
||||
heartbeat = self._keep_alive
|
||||
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)
|
||||
|
||||
|
||||
async def load_secret_key(self, data) -> None:
|
||||
async def load_secret_key(self, data):
|
||||
_log.info('received secret key for voice connection')
|
||||
self.secret_key = self._connection.secret_key = data.get('secret_key')
|
||||
await self.speak()
|
||||
await self.speak(False)
|
||||
|
||||
async def poll_event(self) -> None:
|
||||
async def poll_event(self):
|
||||
# This exception is handled up the chain
|
||||
msg = await asyncio.wait_for(self.ws.receive(), timeout=30.0)
|
||||
if msg.type is aiohttp.WSMsgType.TEXT:
|
||||
@ -962,7 +903,7 @@ class DiscordVoiceWebSocket:
|
||||
_log.debug('Received %s', msg)
|
||||
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:
|
||||
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 .member import Member, VoiceState
|
||||
from .emoji import Emoji
|
||||
from .errors import InvalidData, NotFound
|
||||
from .errors import InvalidData
|
||||
from .permissions import PermissionOverwrite
|
||||
from .colour import Colour
|
||||
from .errors import InvalidArgument, ClientException
|
||||
@ -76,7 +76,6 @@ from .stage_instance import StageInstance
|
||||
from .threads import Thread, ThreadMember
|
||||
from .sticker import GuildSticker
|
||||
from .file import File
|
||||
from .welcome_screen import WelcomeScreen, WelcomeChannel
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -141,10 +140,6 @@ class Guild(Hashable):
|
||||
|
||||
Returns the guild's name.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the guild's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
name: :class:`str`
|
||||
@ -743,16 +738,12 @@ class Guild(Hashable):
|
||||
|
||||
@property
|
||||
def humans(self) -> List[Member]:
|
||||
"""List[:class:`Member`]: A list of human members that belong to this guild.
|
||||
|
||||
.. versionadded:: 2.0 """
|
||||
"""List[:class:`Member`]: A list of human members that belong to this guild."""
|
||||
return [member for member in self.members if not member.bot]
|
||||
|
||||
@property
|
||||
def bots(self) -> List[Member]:
|
||||
"""List[:class:`Member`]: A list of bots that belong to this guild.
|
||||
|
||||
.. versionadded:: 2.0 """
|
||||
"""List[:class:`Member`]: A list of bots that belong to this guild."""
|
||||
return [member for member in self.members if member.bot]
|
||||
|
||||
def get_member(self, user_id: int, /) -> Optional[Member]:
|
||||
@ -1724,8 +1715,6 @@ class Guild(Hashable):
|
||||
You do not have access to the guild.
|
||||
HTTPException
|
||||
Fetching the member failed.
|
||||
NotFound
|
||||
A member with that ID does not exist.
|
||||
|
||||
Returns
|
||||
--------
|
||||
@ -1735,34 +1724,6 @@ class Guild(Hashable):
|
||||
data = await self._state.http.get_member(self.id, member_id)
|
||||
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:
|
||||
"""|coro|
|
||||
|
||||
@ -2605,81 +2566,6 @@ class Guild(Hashable):
|
||||
|
||||
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:
|
||||
"""|coro|
|
||||
|
||||
|
@ -84,7 +84,6 @@ if TYPE_CHECKING:
|
||||
threads,
|
||||
voice,
|
||||
sticker,
|
||||
welcome_screen,
|
||||
)
|
||||
from .types.snowflake import Snowflake, SnowflakeList
|
||||
|
||||
@ -1117,20 +1116,6 @@ class HTTPClient:
|
||||
payload['icon'] = icon
|
||||
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]]:
|
||||
return self.request(Route('GET', '/guilds/{guild_id}/bans', guild_id=guild_id))
|
||||
|
||||
|
@ -230,7 +230,6 @@ class Invite(Hashable):
|
||||
|
||||
Returns the invite URL.
|
||||
|
||||
|
||||
The following table illustrates what methods will obtain the attributes:
|
||||
|
||||
+------------------------------------+------------------------------------------------------------+
|
||||
@ -434,9 +433,6 @@ class Invite(Hashable):
|
||||
def __str__(self) -> str:
|
||||
return self.url
|
||||
|
||||
def __int__(self) -> int:
|
||||
return 0 # To keep the object compatible with the hashable abc.
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
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.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the user's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
joined_at: Optional[:class:`datetime.datetime`]
|
||||
@ -304,9 +300,6 @@ class Member(discord.abc.Messageable, _UserTag):
|
||||
def __str__(self) -> str:
|
||||
return str(self._user)
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
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.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the attachment's ID.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Attachment can now be casted to :class:`str` and is hashable.
|
||||
|
||||
@ -507,14 +503,6 @@ class Message(Hashable):
|
||||
|
||||
Returns the message's hash.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the message's content.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the message's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
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}>'
|
||||
)
|
||||
|
||||
|
||||
def __str__(self) -> Optional[str]:
|
||||
return self.content
|
||||
|
||||
def _try_patch(self, data, key, transform=None) -> None:
|
||||
try:
|
||||
value = data[key]
|
||||
@ -1122,7 +1106,7 @@ class Message(Hashable):
|
||||
if self.type is MessageType.guild_invite_reminder:
|
||||
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|
|
||||
|
||||
Deletes the message.
|
||||
@ -1133,17 +1117,12 @@ class Message(Hashable):
|
||||
|
||||
.. versionchanged:: 1.1
|
||||
Added the new ``delay`` keyword-only parameter.
|
||||
.. versionchanged:: 2.0
|
||||
Added the new ``silent`` keyword-only parameter.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
delay: Optional[:class:`float`]
|
||||
If provided, the number of seconds to wait in the background
|
||||
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
|
||||
------
|
||||
@ -1165,11 +1144,7 @@ class Message(Hashable):
|
||||
|
||||
asyncio.create_task(delete(delay))
|
||||
else:
|
||||
try:
|
||||
await self._state.http.delete_message(self.channel.id, self.id)
|
||||
except Exception:
|
||||
if not silent:
|
||||
raise
|
||||
await self._state.http.delete_message(self.channel.id, self.id)
|
||||
|
||||
@overload
|
||||
async def edit(
|
||||
@ -1650,10 +1625,6 @@ class PartialMessage(Hashable):
|
||||
|
||||
Returns the partial message's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the partial message's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
channel: Union[:class:`TextChannel`, :class:`Thread`, :class:`DMChannel`]
|
||||
|
@ -43,8 +43,5 @@ class EqualityComparable:
|
||||
class Hashable(EqualityComparable):
|
||||
__slots__ = ()
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self.id >> 22
|
||||
|
@ -69,10 +69,6 @@ class Object(Hashable):
|
||||
|
||||
Returns the object's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the object's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
|
@ -299,13 +299,6 @@ class Permissions(BaseFlags):
|
||||
"""
|
||||
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
|
||||
def manage_channels(self) -> int:
|
||||
""":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
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
@ -64,7 +63,10 @@ __all__ = (
|
||||
|
||||
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:
|
||||
"""Represents an audio stream.
|
||||
@ -524,12 +526,7 @@ class FFmpegOpusAudio(FFmpegAudio):
|
||||
|
||||
@staticmethod
|
||||
def _probe_codec_native(source, executable: str = 'ffmpeg') -> Tuple[Optional[str], Optional[int]]:
|
||||
exe = (
|
||||
executable[:2] + 'probe'
|
||||
if executable in {'ffmpeg', 'avconv'}
|
||||
else executable
|
||||
)
|
||||
|
||||
exe = executable[:2] + 'probe' if executable in ('ffmpeg', 'avconv') else executable
|
||||
args = [exe, '-v', 'quiet', '-print_format', 'json', '-show_streams', '-select_streams', 'a:0', source]
|
||||
output = subprocess.check_output(args, timeout=20)
|
||||
codec = bitrate = None
|
||||
|
@ -24,7 +24,6 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Optional, Set, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -35,8 +34,7 @@ if TYPE_CHECKING:
|
||||
MessageUpdateEvent,
|
||||
ReactionClearEvent,
|
||||
ReactionClearEmojiEvent,
|
||||
IntegrationDeleteEvent,
|
||||
TypingEvent
|
||||
IntegrationDeleteEvent
|
||||
)
|
||||
from .message import Message
|
||||
from .partial_emoji import PartialEmoji
|
||||
@ -51,7 +49,6 @@ __all__ = (
|
||||
'RawReactionClearEvent',
|
||||
'RawReactionClearEmojiEvent',
|
||||
'RawIntegrationDeleteEvent',
|
||||
'RawTypingEvent'
|
||||
)
|
||||
|
||||
|
||||
@ -279,36 +276,3 @@ class RawIntegrationDeleteEvent(_RawReprMixin):
|
||||
self.application_id: Optional[int] = int(data['application_id'])
|
||||
except KeyError:
|
||||
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.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the role's ID.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the role's ID.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
id: :class:`int`
|
||||
@ -203,9 +195,6 @@ class Role(Hashable):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Role id={self.id} name={self.name!r}>'
|
||||
|
||||
|
@ -61,10 +61,6 @@ class StageInstance(Hashable):
|
||||
|
||||
Returns the stage instance's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the stage instance's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`int`
|
||||
|
@ -152,7 +152,6 @@ class ConnectionState:
|
||||
handlers: Dict[str, Callable],
|
||||
hooks: Dict[str, Callable],
|
||||
http: HTTPClient,
|
||||
intents: Intents,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
**options: Any,
|
||||
) -> None:
|
||||
@ -195,8 +194,12 @@ class ConnectionState:
|
||||
else:
|
||||
status = str(status)
|
||||
|
||||
if not isinstance(intents, Intents):
|
||||
raise TypeError(f'intents parameter must be Intent not {type(intents)!r}')
|
||||
intents = options.get('intents', None)
|
||||
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:
|
||||
_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'))
|
||||
|
||||
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)
|
||||
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:
|
||||
self.dispatch('typing', channel, user, raw.when)
|
||||
elif isinstance(channel, (Thread, TextChannel)) and guild is not None:
|
||||
# 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 isinstance(channel, DMChannel):
|
||||
return channel.recipient
|
||||
if member is None:
|
||||
member_data = data.get('member')
|
||||
if member_data:
|
||||
member = Member(data=member_data, state=self, guild=guild)
|
||||
|
||||
elif isinstance(channel, (Thread, TextChannel)) and channel.guild is not None:
|
||||
return channel.guild.get_member(user_id) # type: ignore
|
||||
elif isinstance(channel, GroupChannel):
|
||||
member = utils.find(lambda x: x.id == user_id, channel.recipients)
|
||||
|
||||
elif isinstance(channel, GroupChannel):
|
||||
return utils.find(lambda x: x.id == user_id, channel.recipients)
|
||||
|
||||
return self.get_user(user_id)
|
||||
if member is not None:
|
||||
timestamp = datetime.datetime.fromtimestamp(data.get('timestamp'), tz=datetime.timezone.utc)
|
||||
self.dispatch('typing', channel, member, timestamp)
|
||||
|
||||
def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
|
||||
if isinstance(channel, TextChannel):
|
||||
|
@ -67,14 +67,6 @@ class StickerPack(Hashable):
|
||||
|
||||
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
|
||||
|
||||
Checks if the sticker pack is equal to another sticker pack.
|
||||
|
@ -74,10 +74,6 @@ class Thread(Messageable, Hashable):
|
||||
|
||||
Returns the thread's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the thread's ID.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the thread's name.
|
||||
@ -752,10 +748,6 @@ class ThreadMember(Hashable):
|
||||
|
||||
Returns the thread member's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the thread member's ID.
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the thread member's name.
|
||||
@ -808,39 +800,3 @@ class ThreadMember(Hashable):
|
||||
def thread(self) -> Thread:
|
||||
""":class:`Thread`: The thread this member belongs to."""
|
||||
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):
|
||||
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
|
||||
def emoji(self, value: Optional[Union[str, Emoji, PartialEmoji]]): # type: ignore
|
||||
if value is None:
|
||||
self._underlying.emoji = None
|
||||
|
||||
elif isinstance(value, str):
|
||||
self._underlying.emoji = PartialEmoji.from_str(value)
|
||||
elif isinstance(value, _EmojiTag):
|
||||
self._underlying.emoji = value._to_partial()
|
||||
if value is not None:
|
||||
if isinstance(value, str):
|
||||
self._underlying.emoji = PartialEmoji.from_str(value)
|
||||
elif isinstance(value, _EmojiTag):
|
||||
self._underlying.emoji = value._to_partial()
|
||||
else:
|
||||
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__} instead')
|
||||
else:
|
||||
raise TypeError(f'expected str, Emoji, or PartialEmoji, received {value.__class__} instead')
|
||||
self._underlying.emoji = None
|
||||
|
||||
@classmethod
|
||||
def from_component(cls: Type[B], button: ButtonComponent) -> B:
|
||||
|
@ -96,9 +96,6 @@ class BaseUser(_UserTag):
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name}#{self.discriminator}'
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.id
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
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.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the user's ID.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
|
@ -499,14 +499,14 @@ else:
|
||||
|
||||
def _parse_ratelimit_header(request: Any, *, use_clock: bool = False) -> float:
|
||||
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)
|
||||
|
||||
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):
|
||||
value = f(*args, **kwargs)
|
||||
@ -659,10 +659,11 @@ def resolve_invite(invite: Union[Invite, str]) -> str:
|
||||
|
||||
if isinstance(invite, Invite):
|
||||
return invite.code
|
||||
rx = r'(?:https?\:\/\/)?discord(?:\.gg|(?:app)?\.com\/invite)\/(.+)'
|
||||
m = re.match(rx, invite)
|
||||
if m:
|
||||
return m.group(1)
|
||||
else:
|
||||
rx = r'(?:https?\:\/\/)?discord(?:\.gg|(?:app)?\.com\/invite)\/(.+)'
|
||||
m = re.match(rx, invite)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return invite
|
||||
|
||||
|
||||
@ -686,10 +687,11 @@ def resolve_template(code: Union[Template, str]) -> str:
|
||||
|
||||
if isinstance(code, Template):
|
||||
return code.code
|
||||
rx = r'(?:https?\:\/\/)?discord(?:\.new|(?:app)?\.com\/template)\/(.+)'
|
||||
m = re.match(rx, code)
|
||||
if m:
|
||||
return m.group(1)
|
||||
else:
|
||||
rx = r'(?:https?\:\/\/)?discord(?:\.new|(?:app)?\.com\/template)\/(.+)'
|
||||
m = re.match(rx, code)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return code
|
||||
|
||||
|
||||
@ -1015,9 +1017,3 @@ def format_dt(dt: datetime.datetime, /, style: Optional[TimestampStyle] = None)
|
||||
if style is None:
|
||||
return f'<t:{int(dt.timestamp())}>'
|
||||
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._lite_nonce: int = 0
|
||||
self.ws: DiscordVoiceWebSocket = MISSING
|
||||
self.ip: str = MISSING
|
||||
self.port: Tuple[Any, ...] = MISSING
|
||||
|
||||
|
||||
warn_nacl = not has_nacl
|
||||
supported_modes: Tuple[SupportedModes, ...] = (
|
||||
|
@ -886,10 +886,6 @@ class Webhook(BaseWebhook):
|
||||
|
||||
Returns the webhooks's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the webhooks's ID.
|
||||
|
||||
.. versionchanged:: 1.4
|
||||
Webhooks are now comparable and hashable.
|
||||
|
||||
|
@ -475,10 +475,6 @@ class SyncWebhook(BaseWebhook):
|
||||
|
||||
Returns the webhooks's hash.
|
||||
|
||||
.. describe:: int(x)
|
||||
|
||||
Returns the webhooks's ID.
|
||||
|
||||
.. versionchanged:: 1.4
|
||||
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.
|
||||
: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)
|
||||
|
||||
Called when a :class:`Message` is created and sent.
|
||||
@ -3792,22 +3781,6 @@ Template
|
||||
.. autoclass:: Template()
|
||||
:members:
|
||||
|
||||
WelcomeScreen
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: WelcomeScreen
|
||||
|
||||
.. autoclass:: WelcomeScreen()
|
||||
:members:
|
||||
|
||||
WelcomeChannel
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: WelcomeChannel
|
||||
|
||||
.. autoclass:: WelcomeChannel()
|
||||
:members:
|
||||
|
||||
WidgetChannel
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
@ -3873,14 +3846,6 @@ GuildSticker
|
||||
.. autoclass:: GuildSticker()
|
||||
:members:
|
||||
|
||||
RawTypingEvent
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attributetable:: RawTypingEvent
|
||||
|
||||
.. autoclass:: RawTypingEvent()
|
||||
:members:
|
||||
|
||||
RawMessageDeleteEvent
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -11,134 +11,6 @@ Changelog
|
||||
This page keeps a detailed human friendly rendering of what's new and changed
|
||||
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:
|
||||
|
||||
v1.7.3
|
||||
|
@ -26,5 +26,5 @@ class MyClient(discord.Client):
|
||||
async def before_my_task(self):
|
||||
await self.wait_until_ready() # wait until the bot logs in
|
||||
|
||||
client = MyClient(intents=discord.Intents(guilds=True))
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
@ -22,5 +22,5 @@ class MyClient(discord.Client):
|
||||
await asyncio.sleep(60) # task runs every 60 seconds
|
||||
|
||||
|
||||
client = MyClient(intents=discord.Intents(guilds=True))
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
@ -9,8 +9,10 @@ module.
|
||||
|
||||
There are a number of utility commands being showcased here.'''
|
||||
|
||||
intents = discord.Intents(guilds=True, messages=True, members=True)
|
||||
bot = commands.Bot(command_prefix='t-', description=description, intents=intents)
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
|
||||
bot = commands.Bot(command_prefix='?', description=description, intents=intents)
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
|
@ -123,11 +123,8 @@ class Music(commands.Cog):
|
||||
elif ctx.voice_client.is_playing():
|
||||
ctx.voice_client.stop()
|
||||
|
||||
bot = commands.Bot(
|
||||
command_prefix=commands.when_mentioned_or("!"),
|
||||
description='Relatively simple music bot example',
|
||||
intents=discord.Intents(guilds=True, guild_messages=True, voice_states=True)
|
||||
)
|
||||
bot = commands.Bot(command_prefix=commands.when_mentioned_or("!"),
|
||||
description='Relatively simple music bot example')
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
|
@ -5,8 +5,9 @@ import typing
|
||||
import discord
|
||||
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)
|
||||
|
||||
|
||||
|
@ -29,7 +29,7 @@ class MyBot(commands.Bot):
|
||||
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()
|
||||
async def guess(ctx, number: int):
|
||||
|
@ -17,5 +17,5 @@ class MyClient(discord.Client):
|
||||
msg = f'{message.author} has deleted the message: {message.content}'
|
||||
await message.channel.send(msg)
|
||||
|
||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
@ -16,5 +16,5 @@ class MyClient(discord.Client):
|
||||
msg = f'**{before.author}** edited their message:\n{before.content} -> {after.content}'
|
||||
await before.channel.send(msg)
|
||||
|
||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
@ -30,5 +30,5 @@ class MyClient(discord.Client):
|
||||
else:
|
||||
await message.channel.send(f'Oops. It is actually {answer}.')
|
||||
|
||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
@ -14,5 +14,8 @@ class MyClient(discord.Client):
|
||||
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')
|
||||
|
@ -78,6 +78,8 @@ class MyClient(discord.Client):
|
||||
# If we want to do something in case of errors we'd do it here.
|
||||
pass
|
||||
|
||||
intents = discord.Intents(guilds=True, members=True, guild_reactions=True)
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
|
||||
client = MyClient(intents=intents)
|
||||
client.run('token')
|
||||
|
@ -13,5 +13,5 @@ class MyClient(discord.Client):
|
||||
if message.content.startswith('!hello'):
|
||||
await message.reply('Hello!', mention_author=True)
|
||||
|
||||
client = MyClient(intents=discord.Intents(guilds=True, messages=True))
|
||||
client = MyClient()
|
||||
client.run('token')
|
||||
|
@ -3,11 +3,7 @@ import typing
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
bot = commands.Bot(
|
||||
command_prefix=commands.when_mentioned,
|
||||
description="Nothing to see here!",
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
bot = commands.Bot(command_prefix=commands.when_mentioned, description="Nothing to see here!")
|
||||
|
||||
# the `hidden` keyword argument hides it from the help command.
|
||||
@bot.group(hidden=True)
|
||||
|
@ -5,10 +5,7 @@ import discord
|
||||
|
||||
class Bot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix=commands.when_mentioned_or('$'),
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||
|
||||
async def on_ready(self):
|
||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||
|
@ -5,10 +5,7 @@ import discord
|
||||
|
||||
class CounterBot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix=commands.when_mentioned_or('$'),
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||
|
||||
async def on_ready(self):
|
||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||
|
@ -1,3 +1,5 @@
|
||||
import typing
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
@ -37,10 +39,7 @@ class DropdownView(discord.ui.View):
|
||||
|
||||
class Bot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix=commands.when_mentioned_or('$'),
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||
|
||||
async def on_ready(self):
|
||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||
|
@ -4,10 +4,7 @@ import discord
|
||||
|
||||
class EphemeralCounterBot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix=commands.when_mentioned_or('$'),
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||
|
||||
async def on_ready(self):
|
||||
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):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix=commands.when_mentioned_or('$'),
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||
|
||||
async def on_ready(self):
|
||||
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))
|
||||
|
||||
|
||||
bot.run()
|
||||
bot.run('token')
|
||||
|
@ -29,11 +29,7 @@ class PersistentView(discord.ui.View):
|
||||
|
||||
class PersistentViewBot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix=commands.when_mentioned_or('$'),
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||
self.persistent_views_added = False
|
||||
|
||||
async def on_ready(self):
|
||||
|
@ -120,10 +120,7 @@ class TicTacToe(discord.ui.View):
|
||||
|
||||
class TicTacToeBot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix=commands.when_mentioned_or('$'),
|
||||
intents=discord.Intents(guilds=True, messages=True)
|
||||
)
|
||||
super().__init__(command_prefix=commands.when_mentioned_or('$'))
|
||||
|
||||
async def on_ready(self):
|
||||
print(f'Logged in as {self.user} (ID: {self.user.id})')
|
||||
|
Reference in New Issue
Block a user