2 Commits

Author SHA1 Message Date
cddfc3272b Fix invite link 2021-09-01 20:24:29 +02:00
cdb7c50880 Fix #36 issues creation. 2021-08-31 12:27:15 +02:00
65 changed files with 308 additions and 1242 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -252,13 +252,6 @@ class Colour:
"""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:
"""A factory method that returns a :class:`Colour` with a value of ``0x992d22``."""
@ -332,14 +325,5 @@ class Colour:
"""
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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
@ -598,7 +598,6 @@ class ColourConverter(Converter[discord.Colour]):
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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -44,23 +40,7 @@ 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__)
_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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -69,10 +69,6 @@ class Object(Hashable):
Returns the object's hash.
.. describe:: int(x)
Returns the object's ID.
Attributes
-----------
id: :class:`int`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, ...] = (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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():

View File

@ -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():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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})')

View File

@ -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})')

View File

@ -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})')

View File

@ -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})')

View File

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

View File

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

View File

@ -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})')