Browse Source

Merge branch 'master' into feature/guild_onboarding

pull/9260/head
Josh 2 years ago
parent
commit
b213863412
  1. 5
      .readthedocs.yml
  2. 4
      discord/__init__.py
  3. 13
      discord/abc.py
  4. 33
      discord/app_commands/commands.py
  5. 2
      discord/app_commands/transformers.py
  6. 4
      discord/audit_logs.py
  7. 10
      discord/channel.py
  8. 14
      discord/client.py
  9. 10
      discord/colour.py
  10. 4
      discord/components.py
  11. 1
      discord/enums.py
  12. 2
      discord/ext/commands/cog.py
  13. 8
      discord/ext/commands/context.py
  14. 65
      discord/ext/commands/converter.py
  15. 12
      discord/ext/commands/core.py
  16. 8
      discord/ext/commands/errors.py
  17. 5
      discord/ext/commands/help.py
  18. 27
      discord/ext/commands/parameters.py
  19. 13
      discord/ext/tasks/__init__.py
  20. 68
      discord/flags.py
  21. 168
      discord/guild.py
  22. 7
      discord/http.py
  23. 74
      discord/interactions.py
  24. 25
      discord/member.py
  25. 23
      discord/message.py
  26. 4
      discord/opus.py
  27. 2
      discord/partial_emoji.py
  28. 59
      discord/permissions.py
  29. 3
      discord/player.py
  30. 1
      discord/reaction.py
  31. 8
      discord/state.py
  32. 10
      discord/team.py
  33. 10
      discord/types/channel.py
  34. 1
      discord/types/guild.py
  35. 3
      discord/types/interactions.py
  36. 2
      discord/types/message.py
  37. 3
      discord/types/user.py
  38. 1
      discord/ui/modal.py
  39. 4
      discord/ui/select.py
  40. 3
      discord/ui/text_input.py
  41. 56
      discord/user.py
  42. 12
      discord/utils.py
  43. 17
      discord/webhook/async_.py
  44. 5
      discord/webhook/sync.py
  45. 14
      discord/widget.py
  46. 19
      docs/api.rst
  47. 2
      docs/conf.py
  48. 2
      docs/discord.rst
  49. 2
      docs/ext/commands/extensions.rst
  50. 14
      docs/migrating.rst
  51. 102
      docs/whats_new.rst
  52. 43
      tests/test_app_commands_group.py

5
.readthedocs.yml

@ -2,7 +2,9 @@ version: 2
formats: []
build:
image: latest
os: "ubuntu-22.04"
tools:
python: "3.8"
sphinx:
configuration: docs/conf.py
@ -10,7 +12,6 @@ sphinx:
builder: html
python:
version: 3.8
install:
- method: pip
path: .

4
discord/__init__.py

@ -13,7 +13,7 @@ __title__ = 'discord'
__author__ = 'Rapptz'
__license__ = 'MIT'
__copyright__ = 'Copyright 2015-present Rapptz'
__version__ = '2.3.0a'
__version__ = '2.4.0a'
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
@ -79,7 +79,7 @@ class VersionInfo(NamedTuple):
serial: int
version_info: VersionInfo = VersionInfo(major=2, minor=3, micro=0, releaselevel='alpha', serial=0)
version_info: VersionInfo = VersionInfo(major=2, minor=4, micro=0, releaselevel='alpha', serial=0)
logging.getLogger(__name__).addHandler(logging.NullHandler())

13
discord/abc.py

@ -219,7 +219,9 @@ class User(Snowflake, Protocol):
name: :class:`str`
The user's username.
discriminator: :class:`str`
The user's discriminator.
The user's discriminator. This is a legacy concept that is no longer used.
global_name: Optional[:class:`str`]
The user's global nickname.
bot: :class:`bool`
If the user is a bot account.
system: :class:`bool`
@ -228,6 +230,7 @@ class User(Snowflake, Protocol):
name: str
discriminator: str
global_name: Optional[str]
bot: bool
system: bool
@ -248,7 +251,7 @@ class User(Snowflake, Protocol):
@property
def default_avatar(self) -> Asset:
""":class:`~discord.Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
""":class:`~discord.Asset`: Returns the default avatar for a given user."""
raise NotImplementedError
@property
@ -943,8 +946,6 @@ class GuildChannel:
if len(permissions) > 0:
raise TypeError('Cannot mix overwrite and keyword arguments.')
# TODO: wait for event
if overwrite is None:
await http.delete_channel_permissions(self.id, target.id, reason=reason)
elif isinstance(overwrite, PermissionOverwrite):
@ -1722,12 +1723,12 @@ class Messageable:
async def _around_strategy(retrieve: int, around: Optional[Snowflake], limit: Optional[int]):
if not around:
return []
return [], None, 0
around_id = around.id if around else None
data = await self._state.http.logs_from(channel.id, retrieve, around=around_id)
return data, None, limit
return data, None, 0
async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]):
after_id = after.id if after else None

33
discord/app_commands/commands.py

@ -46,6 +46,7 @@ from typing import (
)
import re
from copy import copy as shallow_copy
from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
from .models import Choice
@ -707,25 +708,10 @@ class Command(Generic[GroupT, P, T]):
) -> Command:
bindings = {} if bindings is MISSING else bindings
cls = self.__class__
copy = cls.__new__(cls)
copy.name = self.name
copy._locale_name = self._locale_name
copy._guild_ids = self._guild_ids
copy.checks = self.checks
copy.description = self.description
copy._locale_description = self._locale_description
copy.default_permissions = self.default_permissions
copy.guild_only = self.guild_only
copy.nsfw = self.nsfw
copy._attr = self._attr
copy._callback = self._callback
copy.on_error = self.on_error
copy = shallow_copy(self)
copy._params = self._params.copy()
copy.module = self.module
copy.parent = parent
copy.binding = bindings.get(self.binding) if self.binding is not None else binding
copy.extras = self.extras
if copy._attr and set_on_binding:
setattr(copy.binding, copy._attr, copy)
@ -1622,22 +1608,9 @@ class Group:
) -> Group:
bindings = {} if bindings is MISSING else bindings
cls = self.__class__
copy = cls.__new__(cls)
copy.name = self.name
copy._locale_name = self._locale_name
copy._guild_ids = self._guild_ids
copy.description = self.description
copy._locale_description = self._locale_description
copy = shallow_copy(self)
copy.parent = parent
copy.module = self.module
copy.default_permissions = self.default_permissions
copy.guild_only = self.guild_only
copy.nsfw = self.nsfw
copy._attr = self._attr
copy._owner_cls = self._owner_cls
copy._children = {}
copy.extras = self.extras
bindings[self] = copy

2
discord/app_commands/transformers.py

@ -750,7 +750,7 @@ def get_supported_annotation(
try:
return (_mapping[annotation], MISSING, True)
except KeyError:
except (KeyError, TypeError):
pass
if isinstance(annotation, Transformer):

4
discord/audit_logs.py

@ -669,7 +669,9 @@ class AuditLogEntry(Hashable):
):
channel_id = utils._get_as_snowflake(extra, 'channel_id')
channel = None
if channel_id is not None:
# May be an empty string instead of None due to a Discord issue
if channel_id:
channel = self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id)
self.extra = _AuditLogProxyAutoModAction(

10
discord/channel.py

@ -1687,6 +1687,7 @@ class StageChannel(VocalGuildChannel):
*,
name: str = ...,
nsfw: bool = ...,
user_limit: int = ...,
position: int = ...,
sync_permissions: int = ...,
category: Optional[CategoryChannel] = ...,
@ -1726,6 +1727,8 @@ class StageChannel(VocalGuildChannel):
The new channel's position.
nsfw: :class:`bool`
To mark the channel as NSFW or not.
user_limit: :class:`int`
The new channel's user limit.
sync_permissions: :class:`bool`
Whether to sync permissions with the channel's new or pre-existing
category. Defaults to ``False``.
@ -2887,7 +2890,12 @@ class DMChannel(discord.abc.Messageable, discord.abc.PrivateChannel, Hashable):
def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload):
self._state: ConnectionState = state
self.recipient: Optional[User] = state.store_user(data['recipients'][0])
self.recipient: Optional[User] = None
recipients = data.get('recipients')
if recipients is not None:
self.recipient = state.store_user(recipients[0])
self.me: ClientUser = me
self.id: int = int(data['id'])

14
discord/client.py

@ -2059,13 +2059,15 @@ class Client:
limit: Optional[int] = 200,
before: Optional[SnowflakeTime] = None,
after: Optional[SnowflakeTime] = None,
with_counts: bool = True,
) -> AsyncIterator[Guild]:
"""Retrieves an :term:`asynchronous iterator` that enables receiving your guilds.
.. note::
Using this, you will only receive :attr:`.Guild.owner`, :attr:`.Guild.icon`,
:attr:`.Guild.id`, and :attr:`.Guild.name` per :class:`.Guild`.
:attr:`.Guild.id`, :attr:`.Guild.name`, :attr:`.Guild.approximate_member_count`,
and :attr:`.Guild.approximate_presence_count` per :class:`.Guild`.
.. note::
@ -2106,6 +2108,12 @@ class Client:
Retrieve guilds after this date or object.
If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
with_counts: :class:`bool`
Whether to include count information in the guilds. This fills the
:attr:`.Guild.approximate_member_count` and :attr:`.Guild.approximate_presence_count`
attributes without needing any privileged intents. Defaults to ``True``.
.. versionadded:: 2.3
Raises
------
@ -2120,7 +2128,7 @@ class Client:
async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]):
before_id = before.id if before else None
data = await self.http.get_guilds(retrieve, before=before_id)
data = await self.http.get_guilds(retrieve, before=before_id, with_counts=with_counts)
if data:
if limit is not None:
@ -2132,7 +2140,7 @@ class Client:
async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]):
after_id = after.id if after else None
data = await self.http.get_guilds(retrieve, after=after_id)
data = await self.http.get_guilds(retrieve, after=after_id, with_counts=with_counts)
if data:
if limit is not None:

10
discord/colour.py

@ -509,5 +509,15 @@ class Colour:
"""
return cls(0xEEEFF1)
@classmethod
def pink(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0xEB459F``.
.. colour:: #EB459F
.. versionadded:: 2.3
"""
return cls(0xEB459F)
Color = Colour

4
discord/components.py

@ -527,7 +527,7 @@ def _component_factory(data: ComponentPayload) -> Optional[Union[ActionRow, Acti
return ActionRow(data)
elif data['type'] == 2:
return Button(data)
elif data['type'] == 3:
return SelectMenu(data)
elif data['type'] == 4:
return TextInput(data)
elif data['type'] in (3, 5, 6, 7, 8):
return SelectMenu(data)

1
discord/enums.py

@ -296,6 +296,7 @@ class DefaultAvatar(Enum):
green = 2
orange = 3
red = 4
pink = 5
def __str__(self) -> str:
return self.name

2
discord/ext/commands/cog.py

@ -550,6 +550,8 @@ class Cog(metaclass=CogMeta):
Subclasses must replace this if they want special unloading behaviour.
Exceptions raised in this method are ignored during extension unloading.
.. versionchanged:: 2.0
This method can now be a :term:`coroutine`.

8
discord/ext/commands/context.py

@ -430,6 +430,14 @@ class Context(discord.abc.Messageable, Generic[BotT]):
return None
return self.command.cog
@property
def filesize_limit(self) -> int:
""":class:`int`: Returns the maximum number of bytes files can have when uploaded to this guild or DM channel associated with this context.
.. versionadded:: 2.3
"""
return self.guild.filesize_limit if self.guild is not None else discord.utils.DEFAULT_FILE_SIZE_LIMIT_BYTES
@discord.utils.cached_property
def guild(self) -> Optional[Guild]:
"""Optional[:class:`.Guild`]: Returns the guild associated with this context's command. None if not available."""

65
discord/ext/commands/converter.py

@ -186,9 +186,11 @@ class MemberConverter(IDConverter[discord.Member]):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name#discrim
4. Lookup by name
5. Lookup by nickname
3. Lookup by username#discriminator (deprecated).
4. Lookup by username#0 (deprecated, only gets users that migrated from their discriminator).
5. Lookup by user name.
6. Lookup by global name.
7. Lookup by guild nickname.
.. versionchanged:: 1.5
Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument`
@ -196,17 +198,29 @@ class MemberConverter(IDConverter[discord.Member]):
.. versionchanged:: 1.5.1
This converter now lazily fetches members from the gateway and HTTP APIs,
optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled.
.. deprecated:: 2.3
Looking up users by discriminator will be removed in a future version due to
the removal of discriminators in an API change.
"""
async def query_member_named(self, guild: discord.Guild, argument: str) -> Optional[discord.Member]:
cache = guild._state.member_cache_flags.joined
if len(argument) > 5 and argument[-5] == '#':
username, _, discriminator = argument.rpartition('#')
members = await guild.query_members(username, limit=100, cache=cache)
return discord.utils.get(members, name=username, discriminator=discriminator)
username, _, discriminator = argument.rpartition('#')
# If # isn't found then "discriminator" actually has the username
if not username:
discriminator, username = username, discriminator
if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()):
lookup = username
predicate = lambda m: m.name == username and m.discriminator == discriminator
else:
members = await guild.query_members(argument, limit=100, cache=cache)
return discord.utils.find(lambda m: m.name == argument or m.nick == argument, members)
lookup = argument
predicate = lambda m: m.name == argument or m.global_name == argument or m.nick == argument
members = await guild.query_members(lookup, limit=100, cache=cache)
return discord.utils.find(predicate, members)
async def query_member_by_id(self, bot: _Bot, guild: discord.Guild, user_id: int) -> Optional[discord.Member]:
ws = bot._get_websocket(shard_id=guild.shard_id)
@ -273,8 +287,10 @@ class UserConverter(IDConverter[discord.User]):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name#discrim
4. Lookup by name
3. Lookup by username#discriminator (deprecated).
4. Lookup by username#0 (deprecated, only gets users that migrated from their discriminator).
5. Lookup by user name.
6. Lookup by global name.
.. versionchanged:: 1.5
Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument`
@ -282,6 +298,10 @@ class UserConverter(IDConverter[discord.User]):
.. versionchanged:: 1.6
This converter now lazily fetches users from the HTTP APIs if an ID is passed
and it's not available in cache.
.. deprecated:: 2.3
Looking up users by discriminator will be removed in a future version due to
the removal of discriminators in an API change.
"""
async def convert(self, ctx: Context[BotT], argument: str) -> discord.User:
@ -300,25 +320,18 @@ class UserConverter(IDConverter[discord.User]):
return result # type: ignore
arg = argument
username, _, discriminator = argument.rpartition('#')
# Remove the '@' character if this is the first character from the argument
if arg[0] == '@':
# Remove first character
arg = arg[1:]
# If # isn't found then "discriminator" actually has the username
if not username:
discriminator, username = username, discriminator
# check for discriminator if it exists,
if len(arg) > 5 and arg[-5] == '#':
discrim = arg[-4:]
name = arg[:-5]
predicate = lambda u: u.name == name and u.discriminator == discrim
result = discord.utils.find(predicate, state._users.values())
if result is not None:
return result
if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()):
predicate = lambda u: u.name == username and u.discriminator == discriminator
else:
predicate = lambda u: u.name == argument or u.global_name == argument
predicate = lambda u: u.name == arg
result = discord.utils.find(predicate, state._users.values())
if result is None:
raise UserNotFound(argument)

12
discord/ext/commands/core.py

@ -151,6 +151,7 @@ def get_signature_parameters(
parameter._default = default.default
parameter._description = default._description
parameter._displayed_default = default._displayed_default
parameter._displayed_name = default._displayed_name
annotation = parameter.annotation
@ -194,8 +195,13 @@ def extract_descriptions_from_docstring(function: Callable[..., Any], params: Di
description, param_docstring = divide
for match in NUMPY_DOCSTRING_ARG_REGEX.finditer(param_docstring):
name = match.group('name')
if name not in params:
continue
is_display_name = discord.utils.get(params.values(), displayed_name=name)
if is_display_name:
name = is_display_name.name
else:
continue
param = params[name]
if param.description is None:
@ -1169,7 +1175,9 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
return ''
result = []
for name, param in params.items():
for param in params.values():
name = param.displayed_name or param.name
greedy = isinstance(param.converter, Greedy)
optional = False # postpone evaluation of if it's an optional argument

8
discord/ext/commands/errors.py

@ -182,7 +182,7 @@ class MissingRequiredArgument(UserInputError):
def __init__(self, param: Parameter) -> None:
self.param: Parameter = param
super().__init__(f'{param.name} is a required argument that is missing.')
super().__init__(f'{param.displayed_name or param.name} is a required argument that is missing.')
class MissingRequiredAttachment(UserInputError):
@ -201,7 +201,7 @@ class MissingRequiredAttachment(UserInputError):
def __init__(self, param: Parameter) -> None:
self.param: Parameter = param
super().__init__(f'{param.name} is a required argument that is missing an attachment.')
super().__init__(f'{param.displayed_name or param.name} is a required argument that is missing an attachment.')
class TooManyArguments(UserInputError):
@ -901,7 +901,7 @@ class BadUnionArgument(UserInputError):
else:
fmt = ' or '.join(to_string)
super().__init__(f'Could not convert "{param.name}" into {fmt}.')
super().__init__(f'Could not convert "{param.displayed_name or param.name}" into {fmt}.')
class BadLiteralArgument(UserInputError):
@ -938,7 +938,7 @@ class BadLiteralArgument(UserInputError):
else:
fmt = ' or '.join(to_string)
super().__init__(f'Could not convert "{param.name}" into the literal {fmt}.')
super().__init__(f'Could not convert "{param.displayed_name or param.name}" into the literal {fmt}.')
class ArgumentParsingError(UserInputError):

5
discord/ext/commands/help.py

@ -294,6 +294,9 @@ class _HelpCommandImpl(Command):
cog.walk_commands = cog.walk_commands.__wrapped__
self.cog = None
# Revert `on_error` to use the original one in case of race conditions
self.on_error = self._injected.on_help_command_error
class HelpCommand:
r"""The base implementation for help command formatting.
@ -1166,7 +1169,7 @@ class DefaultHelpCommand(HelpCommand):
get_width = discord.utils._string_width
for argument in arguments:
name = argument.name
name = argument.displayed_name or argument.name
width = max_size - (get_width(name) - len(name))
entry = f'{self.indent * " "}{name:<{width}} {argument.description or self.default_argument_description}'
# we do not want to shorten the default value, if any.

27
discord/ext/commands/parameters.py

@ -87,7 +87,7 @@ class Parameter(inspect.Parameter):
.. versionadded:: 2.0
"""
__slots__ = ('_displayed_default', '_description', '_fallback')
__slots__ = ('_displayed_default', '_description', '_fallback', '_displayed_name')
def __init__(
self,
@ -97,6 +97,7 @@ class Parameter(inspect.Parameter):
annotation: Any = empty,
description: str = empty,
displayed_default: str = empty,
displayed_name: str = empty,
) -> None:
super().__init__(name=name, kind=kind, default=default, annotation=annotation)
self._name = name
@ -106,6 +107,7 @@ class Parameter(inspect.Parameter):
self._annotation = annotation
self._displayed_default = displayed_default
self._fallback = False
self._displayed_name = displayed_name
def replace(
self,
@ -116,6 +118,7 @@ class Parameter(inspect.Parameter):
annotation: Any = MISSING,
description: str = MISSING,
displayed_default: Any = MISSING,
displayed_name: Any = MISSING,
) -> Self:
if name is MISSING:
name = self._name
@ -129,6 +132,8 @@ class Parameter(inspect.Parameter):
description = self._description
if displayed_default is MISSING:
displayed_default = self._displayed_default
if displayed_name is MISSING:
displayed_name = self._displayed_name
return self.__class__(
name=name,
@ -137,6 +142,7 @@ class Parameter(inspect.Parameter):
annotation=annotation,
description=description,
displayed_default=displayed_default,
displayed_name=displayed_name,
)
if not TYPE_CHECKING: # this is to prevent anything breaking if inspect internals change
@ -171,6 +177,14 @@ class Parameter(inspect.Parameter):
return None if self.required else str(self.default)
@property
def displayed_name(self) -> Optional[str]:
"""Optional[:class:`str`]: The name that is displayed to the user.
.. versionadded:: 2.3
"""
return self._displayed_name if self._displayed_name is not empty else None
async def get_default(self, ctx: Context[Any]) -> Any:
"""|coro|
@ -193,8 +207,9 @@ def parameter(
default: Any = empty,
description: str = empty,
displayed_default: str = empty,
displayed_name: str = empty,
) -> Any:
r"""parameter(\*, converter=..., default=..., description=..., displayed_default=...)
r"""parameter(\*, converter=..., default=..., description=..., displayed_default=..., displayed_name=...)
A way to assign custom metadata for a :class:`Command`\'s parameter.
@ -221,6 +236,10 @@ def parameter(
The description of this parameter.
displayed_default: :class:`str`
The displayed default in :attr:`Command.signature`.
displayed_name: :class:`str`
The name that is displayed to the user.
.. versionadded:: 2.3
"""
return Parameter(
name='empty',
@ -229,6 +248,7 @@ def parameter(
default=default,
description=description,
displayed_default=displayed_default,
displayed_name=displayed_name,
)
@ -240,12 +260,13 @@ class ParameterAlias(Protocol):
default: Any = empty,
description: str = empty,
displayed_default: str = empty,
displayed_name: str = empty,
) -> Any:
...
param: ParameterAlias = parameter
r"""param(\*, converter=..., default=..., description=..., displayed_default=...)
r"""param(\*, converter=..., default=..., description=..., displayed_default=..., displayed_name=...)
An alias for :func:`parameter`.

13
discord/ext/tasks/__init__.py

@ -144,6 +144,7 @@ class Loop(Generic[LF]):
time: Union[datetime.time, Sequence[datetime.time]],
count: Optional[int],
reconnect: bool,
name: Optional[str],
) -> None:
self.coro: LF = coro
self.reconnect: bool = reconnect
@ -165,6 +166,7 @@ class Loop(Generic[LF]):
self._is_being_cancelled = False
self._has_failed = False
self._stop_next_iteration = False
self._name: str = f'discord-ext-tasks: {coro.__qualname__}' if name is None else name
if self.count is not None and self.count <= 0:
raise ValueError('count must be greater than 0 or None.')
@ -282,6 +284,7 @@ class Loop(Generic[LF]):
time=self._time,
count=self.count,
reconnect=self.reconnect,
name=self._name,
)
copy._injected = obj
copy._before_loop = self._before_loop
@ -395,7 +398,7 @@ class Loop(Generic[LF]):
args = (self._injected, *args)
self._has_failed = False
self._task = asyncio.create_task(self._loop(*args, **kwargs))
self._task = asyncio.create_task(self._loop(*args, **kwargs), name=self._name)
return self._task
def stop(self) -> None:
@ -770,6 +773,7 @@ def loop(
time: Union[datetime.time, Sequence[datetime.time]] = MISSING,
count: Optional[int] = None,
reconnect: bool = True,
name: Optional[str] = None,
) -> Callable[[LF], Loop[LF]]:
"""A decorator that schedules a task in the background for you with
optional reconnect logic. The decorator returns a :class:`Loop`.
@ -802,6 +806,12 @@ def loop(
Whether to handle errors and restart the task
using an exponential back-off algorithm similar to the
one used in :meth:`discord.Client.connect`.
name: Optional[:class:`str`]
The name to assign to the internal task. By default
it is assigned a name based off of the callable name
such as ``discord-ext-tasks: function_name``.
.. versionadded:: 2.4
Raises
--------
@ -821,6 +831,7 @@ def loop(
count=count,
time=time,
reconnect=reconnect,
name=name,
)
return decorator

68
discord/flags.py

@ -239,6 +239,12 @@ class SystemChannelFlags(BaseFlags):
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
@ -361,6 +367,12 @@ class MessageFlags(BaseFlags):
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
.. versionadded:: 2.0
.. versionadded:: 1.3
Attributes
@ -451,6 +463,14 @@ class MessageFlags(BaseFlags):
"""
return 4096
@flag_value
def voice(self):
""":class:`bool`: Returns ``True`` if the message is a voice message.
.. versionadded:: 2.3
"""
return 8192
@fill_with_flags()
class PublicUserFlags(BaseFlags):
@ -501,6 +521,12 @@ class PublicUserFlags(BaseFlags):
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
.. versionadded:: 2.0
.. versionadded:: 1.4
Attributes
@ -685,6 +711,12 @@ class Intents(BaseFlags):
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
.. describe:: bool(b)
Returns whether any intent is enabled.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
@ -784,6 +816,7 @@ class Intents(BaseFlags):
- :attr:`User.name`
- :attr:`User.avatar`
- :attr:`User.discriminator`
- :attr:`User.global_name`
For more information go to the :ref:`member intent documentation <need_members_intent>`.
@ -817,7 +850,7 @@ class Intents(BaseFlags):
"""
return 1 << 2
@flag_value
@alias_flag_value
def emojis(self):
""":class:`bool`: Alias of :attr:`.emojis_and_stickers`.
@ -826,7 +859,7 @@ class Intents(BaseFlags):
"""
return 1 << 3
@alias_flag_value
@flag_value
def emojis_and_stickers(self):
""":class:`bool`: Whether guild emoji and sticker related events are enabled.
@ -1270,6 +1303,12 @@ class MemberCacheFlags(BaseFlags):
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
@ -1413,6 +1452,10 @@ class ApplicationFlags(BaseFlags):
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
.. versionadded:: 2.0
Attributes
@ -1422,6 +1465,15 @@ class ApplicationFlags(BaseFlags):
rather than using this raw value.
"""
@flag_value
def auto_mod_badge(self):
""":class:`bool`: Returns ``True`` if the application uses at least 100 automod rules across all guilds.
This shows up as a badge in the official client.
.. versionadded:: 2.3
"""
return 1 << 6
@flag_value
def gateway_presence(self):
""":class:`bool`: Returns ``True`` if the application is verified and is allowed to
@ -1539,6 +1591,10 @@ class ChannelFlags(BaseFlags):
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
.. versionadded:: 2.0
Attributes
@ -1635,6 +1691,10 @@ class AutoModPresets(ArrayFlags):
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
Attributes
-----------
value: :class:`int`
@ -1719,6 +1779,10 @@ class MemberFlags(BaseFlags):
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
Attributes
-----------

168
discord/guild.py

@ -187,8 +187,6 @@ class Guild(Hashable):
.. versionadded:: 2.0
afk_timeout: :class:`int`
The number of seconds until someone is moved to the AFK channel.
afk_channel: Optional[:class:`VoiceChannel`]
The channel that denotes the AFK channel. ``None`` if it doesn't exist.
id: :class:`int`
The guild's ID.
owner_id: :class:`int`
@ -251,13 +249,13 @@ class Guild(Hashable):
approximate_member_count: Optional[:class:`int`]
The approximate number of members in the guild. This is ``None`` unless the guild is obtained
using :meth:`Client.fetch_guild` with ``with_counts=True``.
using :meth:`Client.fetch_guild` or :meth:`Client.fetch_guilds` with ``with_counts=True``.
.. versionadded:: 2.0
approximate_presence_count: Optional[:class:`int`]
The approximate number of members currently active in the guild.
Offline members are excluded. This is ``None`` unless the guild is obtained using
:meth:`Client.fetch_guild` with ``with_counts=True``.
:meth:`Client.fetch_guild` or :meth:`Client.fetch_guilds` with ``with_counts=True``.
.. versionchanged:: 2.0
premium_progress_bar_enabled: :class:`bool`
@ -268,11 +266,14 @@ class Guild(Hashable):
Indicates if the guild has widget enabled.
.. versionadded:: 2.0
max_stage_video_users: Optional[:class:`int`]
The maximum amount of users in a stage video channel.
.. versionadded:: 2.3
"""
__slots__ = (
'afk_timeout',
'afk_channel',
'name',
'id',
'unavailable',
@ -295,6 +296,7 @@ class Guild(Hashable):
'vanity_url_code',
'widget_enabled',
'_widget_channel_id',
'_afk_channel_id',
'_members',
'_channels',
'_icon',
@ -316,12 +318,14 @@ class Guild(Hashable):
'approximate_member_count',
'approximate_presence_count',
'premium_progress_bar_enabled',
'_safety_alerts_channel_id',
'max_stage_video_users',
)
_PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = {
None: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=26214400),
0: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=26214400),
1: _GuildLimit(emoji=100, stickers=15, bitrate=128e3, filesize=26214400),
None: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=utils.DEFAULT_FILE_SIZE_LIMIT_BYTES),
0: _GuildLimit(emoji=50, stickers=5, bitrate=96e3, filesize=utils.DEFAULT_FILE_SIZE_LIMIT_BYTES),
1: _GuildLimit(emoji=100, stickers=15, bitrate=128e3, filesize=utils.DEFAULT_FILE_SIZE_LIMIT_BYTES),
2: _GuildLimit(emoji=150, stickers=30, bitrate=256e3, filesize=52428800),
3: _GuildLimit(emoji=250, stickers=60, bitrate=384e3, filesize=104857600),
}
@ -479,6 +483,7 @@ class Guild(Hashable):
self.max_presences: Optional[int] = guild.get('max_presences')
self.max_members: Optional[int] = guild.get('max_members')
self.max_video_channel_users: Optional[int] = guild.get('max_video_channel_users')
self.max_stage_video_users: Optional[int] = guild.get('max_stage_video_channel_users')
self.premium_tier: int = guild.get('premium_tier', 0)
self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0
self.vanity_url_code: Optional[str] = guild.get('vanity_url_code')
@ -489,62 +494,53 @@ class Guild(Hashable):
self._discovery_splash: Optional[str] = guild.get('discovery_splash')
self._rules_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'rules_channel_id')
self._public_updates_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'public_updates_channel_id')
self._safety_alerts_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'safety_alerts_channel_id')
self.nsfw_level: NSFWLevel = try_enum(NSFWLevel, guild.get('nsfw_level', 0))
self.mfa_level: MFALevel = try_enum(MFALevel, guild.get('mfa_level', 0))
self.approximate_presence_count: Optional[int] = guild.get('approximate_presence_count')
self.approximate_member_count: Optional[int] = guild.get('approximate_member_count')
self.premium_progress_bar_enabled: bool = guild.get('premium_progress_bar_enabled', False)
self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id')
self._sync(guild)
self._large: Optional[bool] = None if self._member_count is None else self._member_count >= 250
self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id')
self.afk_channel: Optional[VocalGuildChannel] = self.get_channel(utils._get_as_snowflake(guild, 'afk_channel_id')) # type: ignore
# TODO: refactor/remove?
def _sync(self, data: GuildPayload) -> None:
try:
self._large = data['large']
except KeyError:
pass
if 'channels' in data:
channels = data['channels']
if 'channels' in guild:
channels = guild['channels']
for c in channels:
factory, ch_type = _guild_channel_factory(c['type'])
if factory:
self._add_channel(factory(guild=self, data=c, state=self._state)) # type: ignore
for obj in data.get('voice_states', []):
for obj in guild.get('voice_states', []):
self._update_voice_state(obj, int(obj['channel_id']))
cache_joined = self._state.member_cache_flags.joined
cache_voice = self._state.member_cache_flags.voice
self_id = self._state.self_id
for mdata in data.get('members', []):
for mdata in guild.get('members', []):
member = Member(data=mdata, guild=self, state=self._state) # type: ignore # Members will have the 'user' key in this scenario
if cache_joined or member.id == self_id or (cache_voice and member.id in self._voice_states):
self._add_member(member)
empty_tuple = ()
for presence in data.get('presences', []):
for presence in guild.get('presences', []):
user_id = int(presence['user']['id'])
member = self.get_member(user_id)
if member is not None:
member._presence_update(presence, empty_tuple) # type: ignore
if 'threads' in data:
threads = data['threads']
if 'threads' in guild:
threads = guild['threads']
for thread in threads:
self._add_thread(Thread(guild=self, state=self._state, data=thread))
if 'stage_instances' in data:
for s in data['stage_instances']:
if 'stage_instances' in guild:
for s in guild['stage_instances']:
stage_instance = StageInstance(guild=self, data=s, state=self._state)
self._stage_instances[stage_instance.id] = stage_instance
if 'guild_scheduled_events' in data:
for s in data['guild_scheduled_events']:
if 'guild_scheduled_events' in guild:
for s in guild['guild_scheduled_events']:
scheduled_event = ScheduledEvent(data=s, state=self._state)
self._scheduled_events[scheduled_event.id] = scheduled_event
@ -763,6 +759,14 @@ class Guild(Hashable):
return emoji
return None
@property
def afk_channel(self) -> Optional[VocalGuildChannel]:
"""Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]]: The channel that denotes the AFK channel.
If no channel is set, then this returns ``None``.
"""
return self.get_channel(self._afk_channel_id) # type: ignore
@property
def system_channel(self) -> Optional[TextChannel]:
"""Optional[:class:`TextChannel`]: Returns the guild's channel used for system messages.
@ -802,6 +806,17 @@ class Guild(Hashable):
channel_id = self._public_updates_channel_id
return channel_id and self._channels.get(channel_id) # type: ignore
@property
def safety_alerts_channel(self) -> Optional[TextChannel]:
"""Optional[:class:`TextChannel`]: Return's the guild's channel used for safety alerts, if set.
For example, this is used for the raid protection setting. The guild must have the ``COMMUNITY`` feature.
.. versionadded:: 2.3
"""
channel_id = self._safety_alerts_channel_id
return channel_id and self._channels.get(channel_id) # type: ignore
@property
def widget_channel(self) -> Optional[Union[TextChannel, ForumChannel, VoiceChannel, StageChannel]]:
"""Optional[Union[:class:`TextChannel`, :class:`ForumChannel`, :class:`VoiceChannel`, :class:`StageChannel`]]: Returns
@ -1057,15 +1072,13 @@ class Guild(Hashable):
def get_member_named(self, name: str, /) -> Optional[Member]:
"""Returns the first member found that matches the name provided.
The name can have an optional discriminator argument, e.g. "Jake#0001"
or "Jake" will both do the lookup. However the former will give a more
precise result. Note that the discriminator must have all 4 digits
for this to work.
The name is looked up in the following order:
If a nickname is passed, then it is looked up via the nickname. Note
however, that a nickname + discriminator combo will not lookup the nickname
but rather the username + discriminator combo due to nickname + discriminator
not being unique.
- Username#Discriminator (deprecated)
- Username#0 (deprecated, only gets users that migrated from their discriminator)
- Nickname
- Global name
- Username
If no member is found, ``None`` is returned.
@ -1073,10 +1086,14 @@ class Guild(Hashable):
``name`` parameter is now positional-only.
.. deprecated:: 2.3
Looking up users via discriminator due to Discord API change.
Parameters
-----------
name: :class:`str`
The name of the member to lookup with an optional discriminator.
The name of the member to lookup.
Returns
--------
@ -1085,22 +1102,19 @@ class Guild(Hashable):
then ``None`` is returned.
"""
result = None
members = self.members
if len(name) > 5 and name[-5] == '#':
# The 5 length is checking to see if #0000 is in the string,
# as a#0000 has a length of 6, the minimum for a potential
# discriminator lookup.
potential_discriminator = name[-4:]
# do the actual lookup and return if found
# if it isn't found then we'll do a full name lookup below.
result = utils.get(members, name=name[:-5], discriminator=potential_discriminator)
if result is not None:
return result
username, _, discriminator = name.rpartition('#')
# If # isn't found then "discriminator" actually has the username
if not username:
discriminator, username = username, discriminator
if discriminator == '0' or (len(discriminator) == 4 and discriminator.isdigit()):
return utils.find(lambda m: m.name == username and m.discriminator == discriminator, members)
def pred(m: Member) -> bool:
return m.nick == name or m.name == name
return m.nick == name or m.global_name == name or m.name == name
return utils.find(pred, members)
@ -1304,7 +1318,7 @@ class Guild(Hashable):
nsfw: :class:`bool`
To mark the channel as NSFW or not.
news: :class:`bool`
Whether to create the text channel as a news channel.
Whether to create the text channel as a news channel.
.. versionadded:: 2.0
default_auto_archive_duration: :class:`int`
@ -1821,6 +1835,8 @@ class Guild(Hashable):
widget_enabled: bool = MISSING,
widget_channel: Optional[Snowflake] = MISSING,
mfa_level: MFALevel = MISSING,
raid_alerts_disabled: bool = MISSING,
safety_alerts_channel: TextChannel = MISSING,
) -> Guild:
r"""|coro|
@ -1935,6 +1951,18 @@ class Guild(Hashable):
reason: Optional[:class:`str`]
The reason for editing this guild. Shows up on the audit log.
raid_alerts_disabled: :class:`bool`
Whether the alerts for raid protection should be disabled for the guild.
.. versionadded:: 2.3
safety_alerts_channel: Optional[:class:`TextChannel`]
The new channel that is used for safety alerts. This is only available to
guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no
safety alerts channel.
.. versionadded:: 2.3
Raises
-------
Forbidden
@ -1946,9 +1974,9 @@ class Guild(Hashable):
PNG or JPG. This is also raised if you are not the owner of the
guild and request an ownership transfer.
TypeError
The type passed to the ``default_notifications``, ``verification_level``,
``explicit_content_filter``, ``system_channel_flags``, or ``mfa_level`` parameter was
of the incorrect type.
The type passed to the ``default_notifications``, ``rules_channel``, ``public_updates_channel``,
``safety_alerts_channel`` ``verification_level``, ``explicit_content_filter``,
``system_channel_flags``, or ``mfa_level`` parameter was of the incorrect type.
Returns
--------
@ -2020,14 +2048,33 @@ class Guild(Hashable):
if rules_channel is None:
fields['rules_channel_id'] = rules_channel
else:
if not isinstance(rules_channel, TextChannel):
raise TypeError(f'rules_channel must be of type TextChannel not {rules_channel.__class__.__name__}')
fields['rules_channel_id'] = rules_channel.id
if public_updates_channel is not MISSING:
if public_updates_channel is None:
fields['public_updates_channel_id'] = public_updates_channel
else:
if not isinstance(public_updates_channel, TextChannel):
raise TypeError(
f'public_updates_channel must be of type TextChannel not {public_updates_channel.__class__.__name__}'
)
fields['public_updates_channel_id'] = public_updates_channel.id
if safety_alerts_channel is not MISSING:
if safety_alerts_channel is None:
fields['safety_alerts_channel_id'] = safety_alerts_channel
else:
if not isinstance(safety_alerts_channel, TextChannel):
raise TypeError(
f'safety_alerts_channel must be of type TextChannel not {safety_alerts_channel.__class__.__name__}'
)
fields['safety_alerts_channel_id'] = safety_alerts_channel.id
if owner is not MISSING:
if self.owner_id != self._state.self_id:
raise ValueError('To transfer ownership you must be the owner of the guild.')
@ -2052,7 +2099,7 @@ class Guild(Hashable):
fields['system_channel_flags'] = system_channel_flags.value
if any(feat is not MISSING for feat in (community, discoverable, invites_disabled)):
if any(feat is not MISSING for feat in (community, discoverable, invites_disabled, raid_alerts_disabled)):
features = set(self.features)
if community is not MISSING:
@ -2078,6 +2125,12 @@ class Guild(Hashable):
else:
features.discard('INVITES_DISABLED')
if raid_alerts_disabled is not MISSING:
if raid_alerts_disabled:
features.add('RAID_ALERTS_DISABLED')
else:
features.discard('RAID_ALERTS_DISABLED')
fields['features'] = list(features)
if premium_progress_bar_enabled is not MISSING:
@ -3433,7 +3486,6 @@ class Guild(Hashable):
data = await self._state.http.create_role(self.id, reason=reason, **fields)
role = Role(guild=self, data=data, state=self._state)
# TODO: add to cache
return role
async def edit_role_positions(self, positions: Mapping[Snowflake, int], *, reason: Optional[str] = None) -> List[Role]:

7
discord/http.py

@ -48,6 +48,7 @@ from typing import (
from urllib.parse import quote as _uriquote
from collections import deque
import datetime
import socket
import aiohttp
@ -785,7 +786,8 @@ class HTTPClient:
async def static_login(self, token: str) -> user.User:
# Necessary to get aiohttp to stop complaining about session creation
if self.connector is MISSING:
self.connector = aiohttp.TCPConnector(limit=0)
# discord does not support ipv6
self.connector = aiohttp.TCPConnector(limit=0, family=socket.AF_INET)
self.__session = aiohttp.ClientSession(
connector=self.connector,
@ -1374,9 +1376,11 @@ class HTTPClient:
limit: int,
before: Optional[Snowflake] = None,
after: Optional[Snowflake] = None,
with_counts: bool = True,
) -> Response[List[guild.Guild]]:
params: Dict[str, Any] = {
'limit': limit,
'with_counts': int(with_counts),
}
if before:
@ -1427,6 +1431,7 @@ class HTTPClient:
'public_updates_channel_id',
'preferred_locale',
'premium_progress_bar_enabled',
'safety_alerts_channel_id',
)
payload = {k: v for k, v in fields.items() if k in valid_keys}

74
discord/interactions.py

@ -25,6 +25,8 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import logging
from typing import Any, Dict, Optional, Generic, TYPE_CHECKING, Sequence, Tuple, Union
import asyncio
import datetime
@ -33,7 +35,7 @@ from . import utils
from .enums import try_enum, Locale, InteractionType, InteractionResponseType
from .errors import InteractionResponded, HTTPException, ClientException, DiscordException
from .flags import MessageFlags
from .channel import PartialMessageable, ChannelType
from .channel import ChannelType
from ._types import ClientT
from .user import User
@ -44,6 +46,7 @@ from .http import handle_message_parameters
from .webhook.async_ import async_context, Webhook, interaction_response_params, interaction_message_response_params
from .app_commands.namespace import Namespace
from .app_commands.translator import locale_str, TranslationContext, TranslationContextLocation
from .channel import _threaded_channel_factory
__all__ = (
'Interaction',
@ -69,12 +72,19 @@ if TYPE_CHECKING:
from .ui.view import View
from .app_commands.models import Choice, ChoiceT
from .ui.modal import Modal
from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel
from .channel import VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, DMChannel, GroupChannel
from .threads import Thread
from .app_commands.commands import Command, ContextMenu
InteractionChannel = Union[
VoiceChannel, StageChannel, TextChannel, ForumChannel, CategoryChannel, Thread, PartialMessageable
VoiceChannel,
StageChannel,
TextChannel,
ForumChannel,
CategoryChannel,
Thread,
DMChannel,
GroupChannel,
]
MISSING: Any = utils.MISSING
@ -96,8 +106,10 @@ class Interaction(Generic[ClientT]):
The interaction type.
guild_id: Optional[:class:`int`]
The guild ID the interaction was sent from.
channel_id: Optional[:class:`int`]
The channel ID the interaction was sent from.
channel: Optional[Union[:class:`abc.GuildChannel`, :class:`abc.PrivateChannel`, :class:`Thread`]]
The channel the interaction was sent from.
Note that due to a Discord limitation, if sent from a DM channel :attr:`~DMChannel.recipient` is ``None``.
application_id: :class:`int`
The application ID that the interaction was for.
user: Union[:class:`User`, :class:`Member`]
@ -128,7 +140,6 @@ class Interaction(Generic[ClientT]):
'id',
'type',
'guild_id',
'channel_id',
'data',
'application_id',
'message',
@ -148,7 +159,7 @@ class Interaction(Generic[ClientT]):
'_original_response',
'_cs_response',
'_cs_followup',
'_cs_channel',
'channel',
'_cs_namespace',
'_cs_command',
)
@ -171,8 +182,8 @@ class Interaction(Generic[ClientT]):
self.data: Optional[InteractionData] = data.get('data')
self.token: str = data['token']
self.version: int = data['version']
self.channel_id: Optional[int] = utils._get_as_snowflake(data, 'channel_id')
self.guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id')
self.channel: Optional[InteractionChannel] = None
self.application_id: int = int(data['application_id'])
self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US'))
@ -182,6 +193,26 @@ class Interaction(Generic[ClientT]):
except KeyError:
self.guild_locale = None
guild = None
if self.guild_id:
guild = self._state._get_or_create_unavailable_guild(self.guild_id)
raw_channel = data.get('channel', {})
channel_id = utils._get_as_snowflake(raw_channel, 'id')
if channel_id is not None and guild is not None:
self.channel = guild and guild._resolve_channel(channel_id)
raw_ch_type = raw_channel.get('type')
if self.channel is None and raw_ch_type is not None:
factory, ch_type = _threaded_channel_factory(raw_ch_type) # type is never None
if factory is None:
logging.info('Unknown channel type {type} for channel ID {id}.'.format_map(raw_channel))
else:
if ch_type in (ChannelType.group, ChannelType.private):
self.channel = factory(me=self._client.user, data=raw_channel, state=self._state) # type: ignore
elif guild is not None:
self.channel = factory(guild=guild, state=self._state, data=raw_channel) # type: ignore
self.message: Optional[Message]
try:
# The channel and message payloads are mismatched yet handled properly at runtime
@ -193,9 +224,7 @@ class Interaction(Generic[ClientT]):
self._permissions: int = 0
self._app_permissions: int = int(data.get('app_permissions', 0))
if self.guild_id:
guild = self._state._get_or_create_unavailable_guild(self.guild_id)
if guild is not None:
# Upgrade Message.guild in case it's missing with partial guild data
if self.message is not None and self.message.guild is None:
self.message.guild = guild
@ -227,21 +256,10 @@ class Interaction(Generic[ClientT]):
"""Optional[:class:`Guild`]: The guild the interaction was sent from."""
return self._state and self._state._get_guild(self.guild_id)
@utils.cached_slot_property('_cs_channel')
def channel(self) -> Optional[InteractionChannel]:
"""Optional[Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]]: The channel the interaction was sent from.
Note that due to a Discord limitation, DM channels are not resolved since there is
no data to complete them. These are :class:`PartialMessageable` instead.
"""
guild = self.guild
channel = guild and guild._resolve_channel(self.channel_id)
if channel is None:
if self.channel_id is not None:
type = ChannelType.text if self.guild_id is not None else ChannelType.private
return PartialMessageable(state=self._state, guild_id=self.guild_id, id=self.channel_id, type=type)
return None
return channel
@property
def channel_id(self) -> Optional[int]:
"""Optional[:class:`int`]: The ID of the channel the interaction was sent from."""
return self.channel.id if self.channel is not None else None
@property
def permissions(self) -> Permissions:
@ -1025,8 +1043,8 @@ class _InteractionMessageState:
def _get_guild(self, guild_id):
return self._parent._get_guild(guild_id)
def store_user(self, data):
return self._parent.store_user(data)
def store_user(self, data, *, cache: bool = True):
return self._parent.store_user(data, cache=cache)
def create_user(self, data):
return self._parent.create_user(data)

25
discord/member.py

@ -274,7 +274,7 @@ class Member(discord.abc.Messageable, _UserTag):
.. describe:: str(x)
Returns the member's name with the discriminator.
Returns the member's handle (e.g. ``name`` or ``name#discriminator``).
Attributes
----------
@ -293,7 +293,7 @@ class Member(discord.abc.Messageable, _UserTag):
guild: :class:`Guild`
The guild that the member belongs to.
nick: Optional[:class:`str`]
The guild specific nickname of the user.
The guild specific nickname of the user. Takes precedence over the global name.
pending: :class:`bool`
Whether the member is pending member verification.
@ -329,6 +329,7 @@ class Member(discord.abc.Messageable, _UserTag):
name: str
id: int
discriminator: str
global_name: Optional[str]
bot: bool
system: bool
created_at: datetime.datetime
@ -368,7 +369,7 @@ class Member(discord.abc.Messageable, _UserTag):
def __repr__(self) -> str:
return (
f'<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}'
f'<Member id={self._user.id} name={self._user.name!r} global_name={self._user.global_name!r}'
f' bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>'
)
@ -463,12 +464,18 @@ class Member(discord.abc.Messageable, _UserTag):
def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
u = self._user
original = (u.name, u._avatar, u.discriminator, u._public_flags)
original = (u.name, u.discriminator, u._avatar, u.global_name, u._public_flags)
# These keys seem to always be available
modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0))
modified = (
user['username'],
user['discriminator'],
user['avatar'],
user.get('global_name'),
user.get('public_flags', 0),
)
if original != modified:
to_return = User._copy(self._user)
u.name, u._avatar, u.discriminator, u._public_flags = modified
u.name, u.discriminator, u._avatar, u.global_name, u._public_flags = modified
# Signal to dispatch on_user_update
return to_return, u
@ -581,11 +588,11 @@ class Member(discord.abc.Messageable, _UserTag):
def display_name(self) -> str:
""":class:`str`: Returns the user's display name.
For regular users this is just their username, but
if they have a guild specific nickname then that
For regular users this is just their global name or their username,
but if they have a guild specific nickname then that
is returned instead.
"""
return self.nick or self.name
return self.nick or self.global_name or self.name
@property
def display_avatar(self) -> Asset:

23
discord/message.py

@ -183,6 +183,14 @@ class Attachment(Hashable):
Whether the attachment is ephemeral.
.. versionadded:: 2.0
duration: Optional[:class:`float`]
The duration of the audio file in seconds. Returns ``None`` if it's not a voice message.
.. versionadded:: 2.3
waveform: Optional[:class:`bytes`]
The waveform (amplitudes) of the audio in bytes. Returns ``None`` if it's not a voice message.
.. versionadded:: 2.3
"""
__slots__ = (
@ -197,6 +205,8 @@ class Attachment(Hashable):
'content_type',
'description',
'ephemeral',
'duration',
'waveform',
)
def __init__(self, *, data: AttachmentPayload, state: ConnectionState):
@ -211,11 +221,19 @@ class Attachment(Hashable):
self.content_type: Optional[str] = data.get('content_type')
self.description: Optional[str] = data.get('description')
self.ephemeral: bool = data.get('ephemeral', False)
self.duration: Optional[float] = data.get('duration_secs')
waveform = data.get('waveform')
self.waveform: Optional[bytes] = utils._base64_to_bytes(waveform) if waveform is not None else None
def is_spoiler(self) -> bool:
""":class:`bool`: Whether this attachment contains a spoiler."""
return self.filename.startswith('SPOILER_')
def is_voice_message(self) -> bool:
""":class:`bool`: Whether this attachment is a voice message."""
return self.duration is not None and 'voice-message' in self.url
def __repr__(self) -> str:
return f'<Attachment id={self.id} filename={self.filename!r} url={self.url!r}>'
@ -1790,7 +1808,7 @@ class Message(PartialMessage, Hashable):
self.nonce = value
def _handle_author(self, author: UserPayload) -> None:
self.author = self._state.store_user(author)
self.author = self._state.store_user(author, cache=self.webhook_id is None)
if isinstance(self.guild, Guild):
found = self.guild.get_member(self.author.id)
if found is not None:
@ -1809,7 +1827,6 @@ class Message(PartialMessage, Hashable):
author._update_from_message(member) # type: ignore
except AttributeError:
# It's a user here
# TODO: consider adding to cache here
self.author = Member._from_message(message=self, data=member)
def _handle_mentions(self, mentions: List[UserWithMemberPayload]) -> None:
@ -2005,7 +2022,7 @@ class Message(PartialMessage, Hashable):
return f'{self.author.name} changed the channel name: **{self.content}**'
if self.type is MessageType.channel_icon_change:
return f'{self.author.name} changed the channel icon.'
return f'{self.author.name} changed the group icon.'
if self.type is MessageType.pins_add:
return f'{self.author.name} pinned a message to this channel.'

4
discord/opus.py

@ -454,7 +454,9 @@ class Decoder(_OpusStruct):
channel_count = self.CHANNELS
else:
frames = self.packet_get_nb_frames(data)
channel_count = self.packet_get_nb_channels(data)
# Discord silent frames erroneously present themselves as 1 channel instead of 2
# Therefore we need to hardcode the number instead of using packet_get_nb_channels
channel_count = self.CHANNELS
samples_per_frame = self.packet_get_samples_per_frame(data)
frame_size = frames * samples_per_frame

2
discord/partial_emoji.py

@ -94,7 +94,7 @@ class PartialEmoji(_EmojiTag, AssetMixin):
__slots__ = ('animated', 'name', 'id', '_state')
_CUSTOM_EMOJI_RE = re.compile(r'<?(?P<animated>a)?:?(?P<name>[A-Za-z0-9\_]+):(?P<id>[0-9]{13,20})>?')
_CUSTOM_EMOJI_RE = re.compile(r'<?(?:(?P<animated>a)?:)?(?P<name>[A-Za-z0-9\_]+):(?P<id>[0-9]{13,20})>?')
if TYPE_CHECKING:
id: Optional[int]

59
discord/permissions.py

@ -119,6 +119,12 @@ class Permissions(BaseFlags):
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether the permissions object has any permissions set to ``True``.
.. versionadded:: 2.0
Attributes
-----------
value: :class:`int`
@ -177,7 +183,7 @@ class Permissions(BaseFlags):
"""A factory method that creates a :class:`Permissions` with all
permissions set to ``True``.
"""
return cls(0b1111111111111111111111111111111111111111111111)
return cls(0b11111111111111111111111111111111111111111111111)
@classmethod
def _timeout_mask(cls) -> int:
@ -204,7 +210,7 @@ class Permissions(BaseFlags):
``True`` and the guild-specific ones set to ``False``. The guild-specific
permissions are currently:
- :attr:`manage_guild_expressions`
- :attr:`manage_expressions`
- :attr:`view_audit_log`
- :attr:`view_guild_insights`
- :attr:`manage_guild`
@ -213,6 +219,7 @@ class Permissions(BaseFlags):
- :attr:`kick_members`
- :attr:`ban_members`
- :attr:`administrator`
- :attr:`create_expressions`
.. versionchanged:: 1.7
Added :attr:`stream`, :attr:`priority_speaker` and :attr:`use_application_commands` permissions.
@ -223,9 +230,9 @@ class Permissions(BaseFlags):
:attr:`request_to_speak` permissions.
.. versionchanged:: 2.3
Added :attr:`use_soundboard`
Added :attr:`use_soundboard`, :attr:`create_expressions` permissions.
"""
return cls(0b1000111110110110011111101111111111101010001)
return cls(0b01000111110110110011111101111111111101010001)
@classmethod
def general(cls) -> Self:
@ -237,8 +244,11 @@ class Permissions(BaseFlags):
permissions :attr:`administrator`, :attr:`create_instant_invite`, :attr:`kick_members`,
:attr:`ban_members`, :attr:`change_nickname` and :attr:`manage_nicknames` are
no longer part of the general permissions.
.. versionchanged:: 2.3
Added :attr:`create_expressions` permission.
"""
return cls(0b01110000000010000000010010110000)
return cls(0b10000000000001110000000010000000010010110000)
@classmethod
def membership(cls) -> Self:
@ -261,8 +271,11 @@ class Permissions(BaseFlags):
.. versionchanged:: 2.0
Added :attr:`create_public_threads`, :attr:`create_private_threads`, :attr:`manage_threads`,
:attr:`send_messages_in_threads` and :attr:`use_external_stickers` permissions.
.. versionchanged:: 2.3
Added :attr:`send_voice_messages` permission.
"""
return cls(0b111110010000000000001111111100001000000)
return cls(0b10000000111110010000000000001111111100001000000)
@classmethod
def voice(cls) -> Self:
@ -308,7 +321,7 @@ class Permissions(BaseFlags):
- :attr:`manage_messages`
- :attr:`manage_roles`
- :attr:`manage_webhooks`
- :attr:`manage_guild_expressions`
- :attr:`manage_expressions`
- :attr:`manage_threads`
- :attr:`moderate_members`
@ -547,21 +560,21 @@ class Permissions(BaseFlags):
return 1 << 29
@flag_value
def manage_guild_expressions(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create, edit, or delete emojis, stickers, and soundboard sounds.
def manage_expressions(self) -> int:
""":class:`bool`: Returns ``True`` if a user can edit or delete emojis, stickers, and soundboard sounds.
.. versionadded:: 2.3
"""
return 1 << 30
@make_permission_alias('manage_guild_expressions')
@make_permission_alias('manage_expressions')
def manage_emojis(self) -> int:
""":class:`bool`: An alias for :attr:`manage_guild_expressions`."""
""":class:`bool`: An alias for :attr:`manage_expressions`."""
return 1 << 30
@make_permission_alias('manage_guild_expressions')
@make_permission_alias('manage_expressions')
def manage_emojis_and_stickers(self) -> int:
""":class:`bool`: An alias for :attr:`manage_guild_expressions`.
""":class:`bool`: An alias for :attr:`manage_expressions`.
.. versionadded:: 2.0
"""
@ -663,6 +676,14 @@ class Permissions(BaseFlags):
"""
return 1 << 42
@flag_value
def create_expressions(self) -> int:
""":class:`bool`: Returns ``True`` if a user can create emojis, stickers, and soundboard sounds.
.. versionadded:: 2.3
"""
return 1 << 43
@flag_value
def use_external_sounds(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use sounds from other guilds.
@ -671,6 +692,14 @@ class Permissions(BaseFlags):
"""
return 1 << 45
@flag_value
def send_voice_messages(self) -> int:
""":class:`bool`: Returns ``True`` if a user can send voice messages.
.. versionadded:: 2.3
"""
return 1 << 46
def _augment_from_permissions(cls):
cls.VALID_NAMES = set(Permissions.VALID_FLAGS)
@ -772,7 +801,7 @@ class PermissionOverwrite:
manage_roles: Optional[bool]
manage_permissions: Optional[bool]
manage_webhooks: Optional[bool]
manage_guild_expressions: Optional[bool]
manage_expressions: Optional[bool]
manage_emojis: Optional[bool]
manage_emojis_and_stickers: Optional[bool]
use_application_commands: Optional[bool]
@ -788,6 +817,8 @@ class PermissionOverwrite:
moderate_members: Optional[bool]
use_soundboard: Optional[bool]
use_external_sounds: Optional[bool]
send_voice_messages: Optional[bool]
create_expressions: Optional[bool]
def __init__(self, **kwargs: Optional[bool]):
self._values: Dict[str, Optional[bool]] = {}

3
discord/player.py

@ -210,7 +210,8 @@ class FFmpegAudio(AudioSource):
# arbitrarily large read size
data = source.read(8192)
if not data:
self._process.terminate()
if self._stdin is not None:
self._stdin.close()
return
try:
if self._stdin is not None:

1
discord/reaction.py

@ -89,7 +89,6 @@ class Reaction:
self.count: int = data.get('count', 1)
self.me: bool = data['me']
# TODO: typeguard
def is_custom_emoji(self) -> bool:
""":class:`bool`: If this is a custom emoji."""
return not isinstance(self.emoji, str)

8
discord/state.py

@ -349,18 +349,18 @@ class ConnectionState(Generic[ClientT]):
for vc in self.voice_clients:
vc.main_ws = ws # type: ignore # Silencing the unknown attribute (ok at runtime).
def store_user(self, data: Union[UserPayload, PartialUserPayload]) -> User:
def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> User:
# this way is 300% faster than `dict.setdefault`.
user_id = int(data['id'])
try:
return self._users[user_id]
except KeyError:
user = User(state=self, data=data)
if user.discriminator != '0000':
if cache:
self._users[user_id] = user
return user
def store_user_no_intents(self, data: Union[UserPayload, PartialUserPayload]) -> User:
def store_user_no_intents(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> User:
return User(state=self, data=data)
def create_user(self, data: Union[UserPayload, PartialUserPayload]) -> User:
@ -614,7 +614,7 @@ class ConnectionState(Generic[ClientT]):
if self._messages is not None:
self._messages.append(message)
# we ensure that the channel is either a TextChannel, VoiceChannel, or Thread
if channel and channel.__class__ in (TextChannel, VoiceChannel, Thread):
if channel and channel.__class__ in (TextChannel, VoiceChannel, Thread, StageChannel):
channel.last_message_id = message.id # type: ignore
def parse_message_delete(self, data: gw.MessageDeleteEvent) -> None:

10
discord/team.py

@ -108,7 +108,7 @@ class TeamMember(BaseUser):
.. describe:: str(x)
Returns the team member's name with discriminator.
Returns the team member's handle (e.g. ``name`` or ``name#discriminator``).
.. versionadded:: 1.3
@ -119,7 +119,11 @@ class TeamMember(BaseUser):
id: :class:`int`
The team member's unique ID.
discriminator: :class:`str`
The team member's discriminator. This is given when the username has conflicts.
The team member's discriminator. This is a legacy concept that is no longer used.
global_name: Optional[:class:`str`]
The team member's global nickname, taking precedence over the username in display.
.. versionadded:: 2.3
bot: :class:`bool`
Specifies if the user is a bot account.
team: :class:`Team`
@ -139,5 +143,5 @@ class TeamMember(BaseUser):
def __repr__(self) -> str:
return (
f'<{self.__class__.__name__} id={self.id} name={self.name!r} '
f'discriminator={self.discriminator!r} membership_state={self.membership_state!r}>'
f'global_name={self.global_name!r} membership_state={self.membership_state!r}>'
)

10
discord/types/channel.py

@ -150,16 +150,24 @@ class ForumChannel(_BaseTextChannel):
GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel, ForumChannel]
class DMChannel(_BaseChannel):
class _BaseDMChannel(_BaseChannel):
type: Literal[1]
last_message_id: Optional[Snowflake]
class DMChannel(_BaseDMChannel):
recipients: List[PartialUser]
class InteractionDMChannel(_BaseDMChannel):
recipients: NotRequired[List[PartialUser]]
class GroupDMChannel(_BaseChannel):
type: Literal[3]
icon: Optional[str]
owner_id: Snowflake
recipients: List[PartialUser]
Channel = Union[GuildChannel, DMChannel, GroupDMChannel]

1
discord/types/guild.py

@ -84,6 +84,7 @@ GuildFeature = Literal[
'VERIFIED',
'VIP_REGIONS',
'WELCOME_SCREEN_ENABLED',
'RAID_ALERTS_DISABLED',
]

3
discord/types/interactions.py

@ -27,7 +27,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Dict, List, Literal, TypedDict, Union
from typing_extensions import NotRequired
from .channel import ChannelTypeWithoutThread, ThreadMetadata
from .channel import ChannelTypeWithoutThread, ThreadMetadata, GuildChannel, InteractionDMChannel, GroupDMChannel
from .threads import ThreadType
from .member import Member
from .message import Attachment
@ -204,6 +204,7 @@ class _BaseInteraction(TypedDict):
version: Literal[1]
guild_id: NotRequired[Snowflake]
channel_id: NotRequired[Snowflake]
channel: Union[GuildChannel, InteractionDMChannel, GroupDMChannel]
app_permissions: NotRequired[str]
locale: NotRequired[str]
guild_locale: NotRequired[str]

2
discord/types/message.py

@ -68,6 +68,8 @@ class Attachment(TypedDict):
content_type: NotRequired[str]
spoiler: NotRequired[bool]
ephemeral: NotRequired[bool]
duration_secs: NotRequired[float]
waveform: NotRequired[str]
MessageActivityType = Literal[1, 2, 3, 5]

3
discord/types/user.py

@ -31,6 +31,7 @@ class PartialUser(TypedDict):
username: str
discriminator: str
avatar: Optional[str]
global_name: Optional[str]
PremiumType = Literal[0, 1, 2]
@ -40,7 +41,7 @@ class User(PartialUser, total=False):
bot: bool
system: bool
mfa_enabled: bool
local: str
locale: str
verified: bool
email: Optional[str]
flags: int

1
discord/ui/modal.py

@ -64,6 +64,7 @@ class Modal(View):
.. code-block:: python3
import discord
from discord import ui
class Questionnaire(ui.Modal, title='Questionnaire Response'):

4
discord/ui/select.py

@ -162,10 +162,10 @@ class BaseSelect(Item[V]):
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be None or str')
raise TypeError('custom_id must be a str')
self._underlying.custom_id = value
self._provided_custom_id = value is not None
self._provided_custom_id = True
@property
def placeholder(self) -> Optional[str]:

3
discord/ui/text_input.py

@ -137,9 +137,10 @@ class TextInput(Item[V]):
@custom_id.setter
def custom_id(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError('custom_id must be None or str')
raise TypeError('custom_id must be a str')
self._underlying.custom_id = value
self._provided_custom_id = True
@property
def width(self) -> int:

56
discord/user.py

@ -65,6 +65,7 @@ class BaseUser(_UserTag):
'name',
'id',
'discriminator',
'global_name',
'_avatar',
'_banner',
'_accent_colour',
@ -78,6 +79,7 @@ class BaseUser(_UserTag):
name: str
id: int
discriminator: str
global_name: Optional[str]
bot: bool
system: bool
_state: ConnectionState
@ -92,11 +94,13 @@ class BaseUser(_UserTag):
def __repr__(self) -> str:
return (
f"<BaseUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}"
f"<BaseUser id={self.id} name={self.name!r} global_name={self.global_name!r}"
f" bot={self.bot} system={self.system}>"
)
def __str__(self) -> str:
if self.discriminator == '0':
return self.name
return f'{self.name}#{self.discriminator}'
def __eq__(self, other: object) -> bool:
@ -112,6 +116,7 @@ class BaseUser(_UserTag):
self.name = data['username']
self.id = int(data['id'])
self.discriminator = data['discriminator']
self.global_name = data.get('global_name')
self._avatar = data['avatar']
self._banner = data.get('banner', None)
self._accent_colour = data.get('accent_color', None)
@ -126,6 +131,7 @@ class BaseUser(_UserTag):
self.name = user.name
self.id = user.id
self.discriminator = user.discriminator
self.global_name = user.global_name
self._avatar = user._avatar
self._banner = user._banner
self._accent_colour = user._accent_colour
@ -141,6 +147,7 @@ class BaseUser(_UserTag):
'id': self.id,
'avatar': self._avatar,
'discriminator': self.discriminator,
'global_name': self.global_name,
'bot': self.bot,
}
@ -162,8 +169,13 @@ class BaseUser(_UserTag):
@property
def default_avatar(self) -> Asset:
""":class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator."""
return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
""":class:`Asset`: Returns the default avatar for a given user."""
if self.discriminator == '0':
avatar_id = (self.id >> 22) % len(DefaultAvatar)
else:
avatar_id = int(self.discriminator) % 5
return Asset._from_default_avatar(self._state, avatar_id)
@property
def display_avatar(self) -> Asset:
@ -260,10 +272,12 @@ class BaseUser(_UserTag):
def display_name(self) -> str:
""":class:`str`: Returns the user's display name.
For regular users this is just their username, but
if they have a guild specific nickname then that
For regular users this is just their global name or their username,
but if they have a guild specific nickname then that
is returned instead.
"""
if self.global_name:
return self.global_name
return self.name
def mentioned_in(self, message: Message) -> bool:
@ -305,7 +319,7 @@ class ClientUser(BaseUser):
.. describe:: str(x)
Returns the user's name with discriminator.
Returns the user's handle (e.g. ``name`` or ``name#discriminator``).
Attributes
-----------
@ -314,7 +328,11 @@ class ClientUser(BaseUser):
id: :class:`int`
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
The user's discriminator. This is a legacy concept that is no longer used.
global_name: Optional[:class:`str`]
The user's global nickname, taking precedence over the username in display.
.. versionadded:: 2.3
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
@ -343,7 +361,7 @@ class ClientUser(BaseUser):
def __repr__(self) -> str:
return (
f'<ClientUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}'
f'<ClientUser id={self.id} name={self.name!r} global_name={self.global_name!r}'
f' bot={self.bot} verified={self.verified} mfa_enabled={self.mfa_enabled}>'
)
@ -409,6 +427,18 @@ class ClientUser(BaseUser):
data: UserPayload = await self._state.http.edit_profile(payload)
return ClientUser(state=self._state, data=data)
@property
def mutual_guilds(self) -> List[Guild]:
"""List[:class:`Guild`]: The guilds that the user shares with the client.
.. note::
This will only return mutual guilds within the client's internal cache.
.. versionadded:: 1.7
"""
return list(self._state.guilds)
class User(BaseUser, discord.abc.Messageable):
"""Represents a Discord user.
@ -429,7 +459,7 @@ class User(BaseUser, discord.abc.Messageable):
.. describe:: str(x)
Returns the user's name with discriminator.
Returns the user's handle (e.g. ``name`` or ``name#discriminator``).
Attributes
-----------
@ -438,7 +468,11 @@ class User(BaseUser, discord.abc.Messageable):
id: :class:`int`
The user's unique ID.
discriminator: :class:`str`
The user's discriminator. This is given when the username has conflicts.
The user's discriminator. This is a legacy concept that is no longer used.
global_name: Optional[:class:`str`]
The user's global nickname, taking precedence over the username in display.
.. versionadded:: 2.3
bot: :class:`bool`
Specifies if the user is a bot account.
system: :class:`bool`
@ -448,7 +482,7 @@ class User(BaseUser, discord.abc.Messageable):
__slots__ = ('__weakref__',)
def __repr__(self) -> str:
return f'<User id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot}>'
return f'<User id={self.id} name={self.name!r} global_name={self.global_name!r} bot={self.bot}>'
async def _get_channel(self) -> DMChannel:
ch = await self.create_dm()

12
discord/utils.py

@ -56,7 +56,7 @@ from typing import (
TYPE_CHECKING,
)
import unicodedata
from base64 import b64encode
from base64 import b64encode, b64decode
from bisect import bisect_left
import datetime
import functools
@ -100,6 +100,7 @@ __all__ = (
)
DISCORD_EPOCH = 1420070400000
DEFAULT_FILE_SIZE_LIMIT_BYTES = 26214400
class _MissingSentinel:
@ -628,6 +629,10 @@ def _bytes_to_base64_data(data: bytes) -> str:
return fmt.format(mime=mime, data=b64)
def _base64_to_bytes(data: str) -> bytes:
return b64decode(data.encode('ascii'))
def _is_submodule(parent: str, child: str) -> bool:
return parent == child or child.startswith(parent + '.')
@ -1239,11 +1244,12 @@ def is_docker() -> bool:
def stream_supports_colour(stream: Any) -> bool:
is_a_tty = hasattr(stream, 'isatty') and stream.isatty()
# Pycharm and Vscode support colour in their inbuilt editors
if 'PYCHARM_HOSTED' in os.environ or os.environ.get('TERM_PROGRAM') == 'vscode':
return True
return is_a_tty
is_a_tty = hasattr(stream, 'isatty') and stream.isatty()
if sys.platform != 'win32':
# Docker does not consistently have a tty attached to it
return is_a_tty or is_docker()

17
discord/webhook/async_.py

@ -715,9 +715,9 @@ class _WebhookState:
return self._parent._get_guild(guild_id)
return None
def store_user(self, data: Union[UserPayload, PartialUserPayload]) -> BaseUser:
def store_user(self, data: Union[UserPayload, PartialUserPayload], *, cache: bool = True) -> BaseUser:
if self._parent is not None:
return self._parent.store_user(data)
return self._parent.store_user(data, cache=cache)
# state parameter is artificial
return BaseUser(state=self, data=data) # type: ignore
@ -1275,7 +1275,7 @@ class Webhook(BaseWebhook):
A partial :class:`Webhook`.
A partial webhook is just a webhook object with an ID and a token.
"""
m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url)
m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,})', url)
if m is None:
raise ValueError('Invalid webhook URL given.')
@ -1301,7 +1301,13 @@ class Webhook(BaseWebhook):
'name': name,
'channel_id': channel.id,
'guild_id': channel.guild.id,
'user': {'username': user.name, 'discriminator': user.discriminator, 'id': user.id, 'avatar': user._avatar},
'user': {
'username': user.name,
'discriminator': user.discriminator,
'global_name': user.global_name,
'id': user.id,
'avatar': user._avatar,
},
}
state = channel._state
@ -1511,8 +1517,7 @@ class Webhook(BaseWebhook):
proxy_auth=self.proxy_auth,
reason=reason,
)
if prefer_auth and self.auth_token:
elif prefer_auth and self.auth_token:
data = await adapter.edit_webhook(
self.id,
self.auth_token,

5
discord/webhook/sync.py

@ -682,7 +682,7 @@ class SyncWebhook(BaseWebhook):
A partial :class:`SyncWebhook`.
A partial :class:`SyncWebhook` is just a :class:`SyncWebhook` object with an ID and a token.
"""
m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,68})', url)
m = re.search(r'discord(?:app)?\.com/api/webhooks/(?P<id>[0-9]{17,20})/(?P<token>[A-Za-z0-9\.\-\_]{60,})', url)
if m is None:
raise ValueError('Invalid webhook URL given.')
@ -835,8 +835,7 @@ class SyncWebhook(BaseWebhook):
payload['channel_id'] = channel.id
data = adapter.edit_webhook(self.id, self.auth_token, payload=payload, session=self.session, reason=reason)
if prefer_auth and self.auth_token:
elif prefer_auth and self.auth_token:
data = adapter.edit_webhook(self.id, self.auth_token, payload=payload, session=self.session, reason=reason)
elif self.token:
data = adapter.edit_webhook_with_token(self.id, self.token, payload=payload, session=self.session, reason=reason)

14
discord/widget.py

@ -121,7 +121,7 @@ class WidgetMember(BaseUser):
.. describe:: str(x)
Returns the widget member's ``name#discriminator``.
Returns the widget member's handle (e.g. ``name`` or ``name#discriminator``).
Attributes
-----------
@ -130,13 +130,17 @@ class WidgetMember(BaseUser):
name: :class:`str`
The member's username.
discriminator: :class:`str`
The member's discriminator.
The member's discriminator. This is a legacy concept that is no longer used.
global_name: Optional[:class:`str`]
The member's global nickname, taking precedence over the username in display.
.. versionadded:: 2.3
bot: :class:`bool`
Whether the member is a bot.
status: :class:`Status`
The member's status.
nick: Optional[:class:`str`]
The member's nickname.
The member's guild-specific nickname. Takes precedence over the global name.
avatar: Optional[:class:`str`]
The member's avatar hash.
activity: Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]
@ -191,9 +195,7 @@ class WidgetMember(BaseUser):
self.connected_channel: Optional[WidgetChannel] = connected_channel
def __repr__(self) -> str:
return (
f"<WidgetMember name={self.name!r} discriminator={self.discriminator!r}" f" bot={self.bot} nick={self.nick!r}>"
)
return f"<WidgetMember name={self.name!r} global_name={self.global_name!r}" f" bot={self.bot} nick={self.nick!r}>"
@property
def display_name(self) -> str:

19
docs/api.rst

@ -493,6 +493,7 @@ Debug
:param payload: The message that is about to be passed on to the
WebSocket library. It can be :class:`bytes` to denote a binary
message or :class:`str` to denote a regular text message.
:type payload: Union[:class:`bytes`, :class:`str`]
Gateway
@ -1363,7 +1364,7 @@ Threads
.. versionadded:: 2.0
:param payload: The raw event payload data.
:type member: :class:`RawThreadMembersUpdate`
:type payload: :class:`RawThreadMembersUpdate`
Voice
~~~~~~
@ -2914,27 +2915,33 @@ of :class:`enum.Enum`.
.. attribute:: blurple
Represents the default avatar with the color blurple.
Represents the default avatar with the colour blurple.
See also :attr:`Colour.blurple`
.. attribute:: grey
Represents the default avatar with the color grey.
Represents the default avatar with the colour grey.
See also :attr:`Colour.greyple`
.. attribute:: gray
An alias for :attr:`grey`.
.. attribute:: green
Represents the default avatar with the color green.
Represents the default avatar with the colour green.
See also :attr:`Colour.green`
.. attribute:: orange
Represents the default avatar with the color orange.
Represents the default avatar with the colour orange.
See also :attr:`Colour.orange`
.. attribute:: red
Represents the default avatar with the color red.
Represents the default avatar with the colour red.
See also :attr:`Colour.red`
.. attribute:: pink
Represents the default avatar with the colour pink.
See also :attr:`Colour.pink`
.. versionadded:: 2.3
.. class:: StickerType

2
docs/conf.py

@ -51,7 +51,7 @@ autodoc_typehints = 'none'
# napoleon_attr_annotations = False
extlinks = {
'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'GH-'),
'issue': ('https://github.com/Rapptz/discord.py/issues/%s', 'GH-%s'),
'ddocs': ('https://discord.com/developers/docs/%s', None),
}

2
docs/discord.rst

@ -61,7 +61,7 @@ If you want to invite your bot you must create an invite URL for it.
1. Make sure you're logged on to the `Discord website <https://discord.com>`_.
2. Navigate to the `application page <https://discord.com/developers/applications>`_
3. Click on your bot's page.
4. Go to the "OAuth2" tab.
4. Go to the "OAuth2 > URL Generator" tab.
.. image:: /images/discord_oauth2.png
:alt: How the OAuth2 page should look like.

2
docs/ext/commands/extensions.rst

@ -54,6 +54,8 @@ Cleaning Up
Although rare, sometimes an extension needs to clean-up or know when it's being unloaded. For cases like these, there is another entry point named ``teardown`` which is similar to ``setup`` except called when the extension is unloaded.
Exceptions raised in the ``teardown`` function are ignored, and the extension is still unloaded.
.. code-block:: python3
:caption: basic_ext.py

14
docs/migrating.rst

@ -1006,6 +1006,20 @@ Due to a breaking API change by Discord, :meth:`Guild.bans` no longer returns a
async for ban in guild.bans(limit=1000):
...
Flag classes now have a custom ``bool()`` implementation
--------------------------------------------------------
To allow library users to easily check whether an instance of a flag class has any flags enabled,
using `bool` on them will now only return ``True`` if at least one flag is enabled.
This means that evaluating instances of the following classes in a bool context (such as ``if obj:``) may no longer return ``True``:
- :class:`Intents`
- :class:`MemberCacheFlags`
- :class:`MessageFlags`
- :class:`Permissions`
- :class:`PublicUserFlags`
- :class:`SystemChannelFlags`
Function Signature Changes
----------------------------

102
docs/whats_new.rst

@ -11,6 +11,108 @@ Changelog
This page keeps a detailed human friendly rendering of what's new and changed
in specific versions.
.. _vp2p3p1:
v2.3.1
-------
Bug Fixes
~~~~~~~~~~
- Fix username lookup in :meth:`Guild.get_member_named` (:issue:`9451`).
- Use cache data first for :attr:`Interaction.channel` instead of API data.
- This bug usually manifested in incomplete channel objects (e.g. no ``overwrites``) because Discord does not provide this data.
- Fix false positives in :meth:`PartialEmoji.from_str` inappropriately setting ``animated`` to ``True`` (:issue:`9456`, :issue:`9457`).
- Fix certain select types not appearing in :attr:`Message.components` (:issue:`9462`).
- |commands| Change lookup order for :class:`~ext.commands.MemberConverter` and :class:`~ext.commands.UserConverter` to prioritise usernames instead of nicknames.
.. _vp2p3p0:
v2.3.0
--------
New Features
~~~~~~~~~~~~~
- Add support for the new username system (also known as "pomelo").
- Add :attr:`User.global_name` to get their global nickname or "display name".
- Update :attr:`User.display_name` and :attr:`Member.display_name` to understand global nicknames.
- Update ``__str__`` for :class:`User` to drop discriminators if the user has been migrated.
- Update :meth:`Guild.get_member_named` to work with migrated users.
- Update :attr:`User.default_avatar` to work with migrated users.
- |commands| Update user and member converters to understand migrated users.
- Add :attr:`DefaultAvatar.pink` for new pink default avatars.
- Add :meth:`Colour.pink` to get the pink default avatar colour.
- Add support for voice messages (:issue:`9358`)
- Add :attr:`MessageFlags.voice`
- Add :attr:`Attachment.duration` and :attr:`Attachment.waveform`
- Add :meth:`Attachment.is_voice_message`
- This does not support *sending* voice messages because this is currently unsupported by the API.
- Add support for new :attr:`Interaction.channel` attribute from the API update (:issue:`9339`).
- Add support for :attr:`TextChannel.default_thread_slowmode_delay` (:issue:`9291`).
- Add support for :attr:`ForumChannel.default_sort_order` (:issue:`9290`).
- Add support for ``default_reaction_emoji`` and ``default_forum_layout`` in :meth:`Guild.create_forum` (:issue:`9300`).
- Add support for ``widget_channel``, ``widget_enabled``, and ``mfa_level`` in :meth:`Guild.edit` (:issue:`9302`, :issue:`9303`).
- Add various new :class:`Permissions` and changes (:issue:`9312`, :issue:`9325`, :issue:`9358`, :issue:`9378`)
- Add new :attr:`~Permissions.manage_expressions`, :attr:`~Permissions.use_external_sounds`, :attr:`~Permissions.use_soundboard`, :attr:`~Permissions.send_voice_messages`, :attr:`~Permissions.create_expressions` permissions.
- Change :attr:`Permissions.manage_emojis` to be an alias of :attr:`~Permissions.manage_expressions`.
- Add various new properties to :class:`PartialAppInfo` and :class:`AppInfo` (:issue:`9298`).
- Add support for ``with_counts`` parameter to :meth:`Client.fetch_guilds` (:issue:`9369`).
- Add new :meth:`Guild.get_emoji` helper (:issue:`9296`).
- Add :attr:`ApplicationFlags.auto_mod_badge` (:issue:`9313`).
- Add :attr:`Guild.max_stage_video_users` and :attr:`Guild.safety_alerts_channel` (:issue:`9318`).
- Add support for ``raid_alerts_disabled`` and ``safety_alerts_channel`` in :meth:`Guild.edit` (:issue:`9318`).
- |commands| Add :attr:`BadLiteralArgument.argument <ext.commands.BadLiteralArgument.argument>` to get the failed argument's value (:issue:`9283`).
- |commands| Add :attr:`Context.filesize_limit <ext.commands.Context.filesize_limit>` property (:issue:`9416`).
- |commands| Add support for :attr:`Parameter.displayed_name <ext.commands.Parameter.displayed_name>` (:issue:`9427`).
Bug Fixes
~~~~~~~~~~
- Fix ``FileHandler`` handlers being written ANSI characters when the bot is executed inside PyCharm.
- This has the side effect of removing coloured logs from the PyCharm terminal due an upstream bug involving TTY detection. This issue is tracked under `PY-43798 <https://youtrack.jetbrains.com/issue/PY-43798>`_.
- Fix channel edits with :meth:`Webhook.edit` sending two requests instead of one.
- Fix :attr:`StageChannel.last_message_id` always being ``None`` (:issue:`9422`).
- Fix piped audio input ending prematurely (:issue:`9001`, :issue:`9380`).
- Fix persistent detection for :class:`ui.TextInput` being incorrect if the ``custom_id`` is set later (:issue:`9438`).
- Fix custom attributes not being copied over when inheriting from :class:`app_commands.Group` (:issue:`9383`).
- Fix AutoMod audit log entry error due to empty channel_id (:issue:`9384`).
- Fix handling of ``around`` parameter in :meth:`abc.Messageable.history` (:issue:`9388`).
- Fix occasional :exc:`AttributeError` when accessing the :attr:`ClientUser.mutual_guilds` property (:issue:`9387`).
- Fix :func:`utils.escape_markdown` not escaping the new markdown (:issue:`9361`).
- Fix webhook targets not being converted in audit logs (:issue:`9332`).
- Fix error when not passing ``enabled`` in :meth:`Guild.create_automod_rule` (:issue:`9292`).
- Fix how various parameters are handled in :meth:`Guild.create_scheduled_event` (:issue:`9275`).
- Fix not sending the ``ssrc`` parameter when sending the SPEAKING payload (:issue:`9301`).
- Fix :attr:`Message.guild` being ``None`` sometimes when received via an interaction.
- Fix :attr:`Message.system_content` for :attr:`MessageType.channel_icon_change` (:issue:`9410`).
Miscellaneous
~~~~~~~~~~~~~~
- Update the base :attr:`Guild.filesize_limit` to 25MiB (:issue:`9353`).
- Allow Interaction webhook URLs to be used in :meth:`Webhook.from_url`.
- Set the socket family of internal connector to ``AF_INET`` to prevent IPv6 connections (:issue:`9442`, :issue:`9443`).
.. _vp2p2p3:
v2.2.3
-------
Bug Fixes
~~~~~~~~~~
- Fix crash from Discord sending null ``channel_id`` for automod audit logs.
- Fix ``channel`` edits when using :meth:`Webhook.edit` sending two requests.
- Fix :attr:`AuditLogEntry.target` being ``None`` for invites (:issue:`9336`).
- Fix :exc:`KeyError` when accessing data for :class:`GuildSticker` (:issue:`9324`).
.. _vp2p2p2:
v2.2.2

43
tests/test_app_commands_group.py

@ -354,3 +354,46 @@ def test_cog_group_with_subclassed_subclass_group():
assert cog.sub_group.my_command.parent is cog.sub_group
assert cog.my_cog_command.parent is cog.sub_group
assert cog.my_cog_command.binding is cog
def test_cog_group_with_custom_state_issue9383():
class InnerGroup(app_commands.Group):
def __init__(self):
super().__init__()
self.state: int = 20
@app_commands.command()
async def my_command(self, interaction: discord.Interaction) -> None:
...
class MyCog(commands.GroupCog):
inner = InnerGroup()
@app_commands.command()
async def my_regular_command(self, interaction: discord.Interaction) -> None:
...
@inner.command()
async def my_inner_command(self, interaction: discord.Interaction) -> None:
...
cog = MyCog()
assert cog.inner.state == 20
assert cog.my_regular_command is not MyCog.my_regular_command
# Basically the same tests as above... (superset?)
assert MyCog.__cog_app_commands__[0].parent is not cog
assert MyCog.__cog_app_commands__[0].parent is not cog.__cog_app_commands_group__
assert InnerGroup.__discord_app_commands_group_children__[0].parent is not cog.inner
assert InnerGroup.__discord_app_commands_group_children__[0].parent is not cog.inner
assert cog.inner is not MyCog.inner
assert cog.inner.my_command is not InnerGroup.my_command
assert cog.inner.my_command is not InnerGroup.my_command
assert cog.my_inner_command is not MyCog.my_inner_command
assert not hasattr(cog.inner, 'my_inner_command')
assert cog.__cog_app_commands_group__ is not None
assert cog.__cog_app_commands_group__.parent is None
assert cog.inner.parent is cog.__cog_app_commands_group__
assert cog.inner.my_command.parent is cog.inner
assert cog.my_inner_command.parent is cog.inner
assert cog.my_inner_command.binding is cog

Loading…
Cancel
Save