Browse Source

Merge branch 'master' into soundboard-mention-property

pull/10081/head
owocado 1 month ago
committed by GitHub
parent
commit
21b0e5d96f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/lint.yml
  2. 5
      discord/__init__.py
  3. 10
      discord/__main__.py
  4. 7
      discord/abc.py
  5. 10
      discord/activity.py
  6. 8
      discord/app_commands/commands.py
  7. 2
      discord/app_commands/transformers.py
  8. 2
      discord/app_commands/tree.py
  9. 129
      discord/appinfo.py
  10. 8
      discord/audit_logs.py
  11. 263
      discord/client.py
  12. 6
      discord/components.py
  13. 21
      discord/embeds.py
  14. 13
      discord/enums.py
  15. 4
      discord/ext/commands/bot.py
  16. 11
      discord/ext/commands/context.py
  17. 6
      discord/ext/commands/converter.py
  18. 2
      discord/ext/commands/core.py
  19. 9
      discord/ext/commands/errors.py
  20. 2
      discord/ext/commands/flags.py
  21. 13
      discord/ext/commands/hybrid.py
  22. 89
      discord/flags.py
  23. 4
      discord/gateway.py
  24. 14
      discord/guild.py
  25. 1
      discord/http.py
  26. 214
      discord/interactions.py
  27. 2
      discord/invite.py
  28. 98
      discord/member.py
  29. 57
      discord/message.py
  30. 105
      discord/poll.py
  31. 150
      discord/presences.py
  32. 26
      discord/raw_models.py
  33. 110
      discord/role.py
  34. 68
      discord/state.py
  35. 4
      discord/subscription.py
  36. 2
      discord/threads.py
  37. 7
      discord/types/appinfo.py
  38. 4
      discord/types/embed.py
  39. 4
      discord/types/guild.py
  40. 34
      discord/types/interactions.py
  41. 1
      discord/types/message.py
  42. 1
      discord/types/subscription.py
  43. 5
      discord/ui/select.py
  44. 7
      discord/ui/view.py
  45. 16
      discord/utils.py
  46. 2
      discord/voice_state.py
  47. 45
      discord/webhook/async_.py
  48. 26
      discord/webhook/sync.py
  49. 2
      discord/widget.py
  50. 75
      docs/api.rst
  51. 5
      docs/ext/commands/api.rst
  52. 11
      docs/faq.rst
  53. 16
      docs/interactions/api.rst
  54. 154
      docs/whats_new.rst
  55. 1
      setup.py

2
.github/workflows/lint.yml

@ -38,7 +38,7 @@ jobs:
- name: Run Pyright
uses: jakebailey/pyright-action@v1
with:
version: '1.1.351'
version: '1.1.394'
warnings: false
no-comments: ${{ matrix.python-version != '3.x' }}

5
discord/__init__.py

@ -13,7 +13,7 @@ __title__ = 'discord'
__author__ = 'Rapptz'
__license__ = 'MIT'
__copyright__ = 'Copyright 2015-present Rapptz'
__version__ = '2.5.0a'
__version__ = '2.6.0a'
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
@ -72,6 +72,7 @@ from .automod import *
from .poll import *
from .soundboard import *
from .subscription import *
from .presences import *
class VersionInfo(NamedTuple):
@ -82,7 +83,7 @@ class VersionInfo(NamedTuple):
serial: int
version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel='alpha', serial=0)
version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0)
logging.getLogger(__name__).addHandler(logging.NullHandler())

10
discord/__main__.py

@ -28,7 +28,7 @@ from typing import Optional, Tuple, Dict
import argparse
import sys
from pathlib import Path
from pathlib import Path, PurePath, PureWindowsPath
import discord
import importlib.metadata
@ -225,8 +225,14 @@ def to_path(parser: argparse.ArgumentParser, name: str, *, replace_spaces: bool
)
if len(name) <= 4 and name.upper() in forbidden:
parser.error('invalid directory name given, use a different one')
path = PurePath(name)
if isinstance(path, PureWindowsPath) and path.drive:
drive, rest = path.parts[0], path.parts[1:]
transformed = tuple(map(lambda p: p.translate(_translation_table), rest))
name = drive + '\\'.join(transformed)
name = name.translate(_translation_table)
else:
name = name.translate(_translation_table)
if replace_spaces:
name = name.replace(' ', '-')
return Path(name)

7
discord/abc.py

@ -102,6 +102,9 @@ if TYPE_CHECKING:
GuildChannel as GuildChannelPayload,
OverwriteType,
)
from .types.guild import (
ChannelPositionUpdate,
)
from .types.snowflake import (
SnowflakeList,
)
@ -1232,11 +1235,11 @@ class GuildChannel:
raise ValueError('Could not resolve appropriate move position')
channels.insert(max((index + offset), 0), self)
payload = []
payload: List[ChannelPositionUpdate] = []
lock_permissions = kwargs.get('sync_permissions', False)
reason = kwargs.get('reason')
for index, channel in enumerate(channels):
d = {'id': channel.id, 'position': index}
d: ChannelPositionUpdate = {'id': channel.id, 'position': index}
if parent_id is not MISSING and channel.id == self.id:
d.update(parent_id=parent_id, lock_permissions=lock_permissions)
payload.append(d)

10
discord/activity.py

@ -273,7 +273,7 @@ class Activity(BaseActivity):
def start(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
try:
timestamp = self.timestamps['start'] / 1000
timestamp = self.timestamps['start'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
return None
else:
@ -283,7 +283,7 @@ class Activity(BaseActivity):
def end(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
try:
timestamp = self.timestamps['end'] / 1000
timestamp = self.timestamps['end'] / 1000 # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
return None
else:
@ -293,7 +293,7 @@ class Activity(BaseActivity):
def large_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable."""
try:
large_image = self.assets['large_image']
large_image = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
return None
else:
@ -303,7 +303,7 @@ class Activity(BaseActivity):
def small_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable."""
try:
small_image = self.assets['small_image']
small_image = self.assets['small_image'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
return None
else:
@ -525,7 +525,7 @@ class Streaming(BaseActivity):
"""
try:
name = self.assets['large_image']
name = self.assets['large_image'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
return None
else:

8
discord/app_commands/commands.py

@ -903,7 +903,7 @@ class Command(Generic[GroupT, P, T]):
predicates = getattr(param.autocomplete, '__discord_app_commands_checks__', [])
if predicates:
try:
passed = await async_all(f(interaction) for f in predicates)
passed = await async_all(f(interaction) for f in predicates) # type: ignore
except Exception:
passed = False
@ -1014,7 +1014,7 @@ class Command(Generic[GroupT, P, T]):
if not predicates:
return True
return await async_all(f(interaction) for f in predicates)
return await async_all(f(interaction) for f in predicates) # type: ignore
def error(self, coro: Error[GroupT]) -> Error[GroupT]:
"""A decorator that registers a coroutine as a local error handler.
@ -1308,7 +1308,7 @@ class ContextMenu:
if not predicates:
return True
return await async_all(f(interaction) for f in predicates)
return await async_all(f(interaction) for f in predicates) # type: ignore
def _has_any_error_handlers(self) -> bool:
return self.on_error is not None
@ -1842,7 +1842,7 @@ class Group:
if len(params) != 2:
raise TypeError('The error handler must have 2 parameters.')
self.on_error = coro
self.on_error = coro # type: ignore
return coro
async def interaction_check(self, interaction: Interaction, /) -> bool:

2
discord/app_commands/transformers.py

@ -235,7 +235,7 @@ class Transformer(Generic[ClientT]):
pass
def __or__(self, rhs: Any) -> Any:
return Union[self, rhs] # type: ignore
return Union[self, rhs]
@property
def type(self) -> AppCommandOptionType:

2
discord/app_commands/tree.py

@ -859,7 +859,7 @@ class CommandTree(Generic[ClientT]):
if len(params) != 2:
raise TypeError('error handler must have 2 parameters')
self.on_error = coro
self.on_error = coro # type: ignore
return coro
def command(

129
discord/appinfo.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import List, TYPE_CHECKING, Optional
from typing import List, TYPE_CHECKING, Literal, Optional
from . import utils
from .asset import Asset
@ -41,6 +41,7 @@ if TYPE_CHECKING:
PartialAppInfo as PartialAppInfoPayload,
Team as TeamPayload,
InstallParams as InstallParamsPayload,
AppIntegrationTypeConfig as AppIntegrationTypeConfigPayload,
)
from .user import User
from .state import ConnectionState
@ -49,6 +50,7 @@ __all__ = (
'AppInfo',
'PartialAppInfo',
'AppInstallParams',
'IntegrationTypeConfig',
)
@ -180,6 +182,7 @@ class AppInfo:
'redirect_uris',
'approximate_guild_count',
'approximate_user_install_count',
'_integration_types_config',
)
def __init__(self, state: ConnectionState, data: AppInfoPayload):
@ -218,6 +221,9 @@ class AppInfo:
self.redirect_uris: List[str] = data.get('redirect_uris', [])
self.approximate_guild_count: int = data.get('approximate_guild_count', 0)
self.approximate_user_install_count: Optional[int] = data.get('approximate_user_install_count')
self._integration_types_config: Dict[Literal['0', '1'], AppIntegrationTypeConfigPayload] = data.get(
'integration_types_config', {}
)
def __repr__(self) -> str:
return (
@ -260,6 +266,36 @@ class AppInfo:
"""
return ApplicationFlags._from_value(self._flags)
@property
def guild_integration_config(self) -> Optional[IntegrationTypeConfig]:
"""Optional[:class:`IntegrationTypeConfig`]: The default settings for the
application's installation context in a guild.
.. versionadded:: 2.5
"""
if not self._integration_types_config:
return None
try:
return IntegrationTypeConfig(self._integration_types_config['0'])
except KeyError:
return None
@property
def user_integration_config(self) -> Optional[IntegrationTypeConfig]:
"""Optional[:class:`IntegrationTypeConfig`]: The default settings for the
application's installation context as a user.
.. versionadded:: 2.5
"""
if not self._integration_types_config:
return None
try:
return IntegrationTypeConfig(self._integration_types_config['1'])
except KeyError:
return None
async def edit(
self,
*,
@ -274,6 +310,10 @@ class AppInfo:
cover_image: Optional[bytes] = MISSING,
interactions_endpoint_url: Optional[str] = MISSING,
tags: Optional[List[str]] = MISSING,
guild_install_scopes: Optional[List[str]] = MISSING,
guild_install_permissions: Optional[Permissions] = MISSING,
user_install_scopes: Optional[List[str]] = MISSING,
user_install_permissions: Optional[Permissions] = MISSING,
) -> AppInfo:
r"""|coro|
@ -315,6 +355,24 @@ class AppInfo:
over the gateway. Can be ``None`` to remove the URL.
tags: Optional[List[:class:`str`]]
The new list of tags describing the functionality of the application. Can be ``None`` to remove the tags.
guild_install_scopes: Optional[List[:class:`str`]]
The new list of :ddocs:`OAuth2 scopes <topics/oauth2#shared-resources-oauth2-scopes>` of
the default guild installation context. Can be ``None`` to remove the scopes.
.. versionadded: 2.5
guild_install_permissions: Optional[:class:`Permissions`]
The new permissions of the default guild installation context. Can be ``None`` to remove the permissions.
.. versionadded: 2.5
user_install_scopes: Optional[List[:class:`str`]]
The new list of :ddocs:`OAuth2 scopes <topics/oauth2#shared-resources-oauth2-scopes>` of
the default user installation context. Can be ``None`` to remove the scopes.
.. versionadded: 2.5
user_install_permissions: Optional[:class:`Permissions`]
The new permissions of the default user installation context. Can be ``None`` to remove the permissions.
.. versionadded: 2.5
reason: Optional[:class:`str`]
The reason for editing the application. Shows up on the audit log.
@ -324,7 +382,8 @@ class AppInfo:
Editing the application failed
ValueError
The image format passed in to ``icon`` or ``cover_image`` is invalid. This is also raised
when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other.
when ``install_params_scopes`` and ``install_params_permissions`` are incompatible with each other,
or when ``guild_install_scopes`` and ``guild_install_permissions`` are incompatible with each other.
Returns
-------
@ -364,7 +423,7 @@ class AppInfo:
else:
if install_params_permissions is not MISSING:
raise ValueError("install_params_scopes must be set if install_params_permissions is set")
raise ValueError('install_params_scopes must be set if install_params_permissions is set')
if flags is not MISSING:
if flags is None:
@ -389,6 +448,51 @@ class AppInfo:
if tags is not MISSING:
payload['tags'] = tags
integration_types_config: Dict[str, Any] = {}
if guild_install_scopes is not MISSING or guild_install_permissions is not MISSING:
guild_install_params: Optional[Dict[str, Any]] = {}
if guild_install_scopes in (None, MISSING):
guild_install_scopes = []
if 'bot' not in guild_install_scopes and guild_install_permissions is not MISSING:
raise ValueError("'bot' must be in guild_install_scopes if guild_install_permissions is set")
if guild_install_permissions in (None, MISSING):
guild_install_params['permissions'] = 0
else:
guild_install_params['permissions'] = guild_install_permissions.value
guild_install_params['scopes'] = guild_install_scopes
integration_types_config['0'] = {'oauth2_install_params': guild_install_params or None}
else:
if guild_install_permissions is not MISSING:
raise ValueError('guild_install_scopes must be set if guild_install_permissions is set')
if user_install_scopes is not MISSING or user_install_permissions is not MISSING:
user_install_params: Optional[Dict[str, Any]] = {}
if user_install_scopes in (None, MISSING):
user_install_scopes = []
if 'bot' not in user_install_scopes and user_install_permissions is not MISSING:
raise ValueError("'bot' must be in user_install_scopes if user_install_permissions is set")
if user_install_permissions in (None, MISSING):
user_install_params['permissions'] = 0
else:
user_install_params['permissions'] = user_install_permissions.value
user_install_params['scopes'] = user_install_scopes
integration_types_config['1'] = {'oauth2_install_params': user_install_params or None}
else:
if user_install_permissions is not MISSING:
raise ValueError('user_install_scopes must be set if user_install_permissions is set')
if integration_types_config:
payload['integration_types_config'] = integration_types_config
data = await self._state.http.edit_application_info(reason=reason, payload=payload)
return AppInfo(data=data, state=self._state)
@ -520,3 +624,22 @@ class AppInstallParams:
def __init__(self, data: InstallParamsPayload) -> None:
self.scopes: List[str] = data.get('scopes', [])
self.permissions: Permissions = Permissions(int(data['permissions']))
class IntegrationTypeConfig:
"""Represents the default settings for the application's installation context.
.. versionadded:: 2.5
Attributes
----------
oauth2_install_params: Optional[:class:`AppInstallParams`]
The install params for this installation context's default in-app authorization link.
"""
def __init__(self, data: AppIntegrationTypeConfigPayload) -> None:
self.oauth2_install_params: Optional[AppInstallParams] = None
try:
self.oauth2_install_params = AppInstallParams(data['oauth2_install_params']) # type: ignore # EAFP
except KeyError:
pass

8
discord/audit_logs.py

@ -874,7 +874,13 @@ class AuditLogEntry(Hashable):
def _convert_target_emoji(self, target_id: int) -> Union[Emoji, Object]:
return self._state.get_emoji(target_id) or Object(id=target_id, type=Emoji)
def _convert_target_message(self, target_id: int) -> Union[Member, User, Object]:
def _convert_target_message(self, target_id: Optional[int]) -> Optional[Union[Member, User, Object]]:
# The message_pin and message_unpin action types do not have a
# non-null target_id so safeguard against that
if target_id is None:
return None
return self._get_member(target_id) or Object(id=target_id, type=Member)
def _convert_target_stage_instance(self, target_id: int) -> Union[StageInstance, Object]:

263
discord/client.py

@ -237,6 +237,15 @@ class Client:
To enable these events, this must be set to ``True``. Defaults to ``False``.
.. versionadded:: 2.0
enable_raw_presences: :class:`bool`
Whether to manually enable or disable the :func:`on_raw_presence_update` event.
Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled.
By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members`
is disabled, otherwise it's set to ``False``.
.. versionadded:: 2.5
http_trace: :class:`aiohttp.TraceConfig`
The trace configuration to use for tracking HTTP requests the library does using ``aiohttp``.
This allows you to check requests the library is using. For more information, check the
@ -1204,8 +1213,8 @@ class Client:
event: Literal['raw_app_command_permissions_update'],
/,
*,
check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawAppCommandPermissionsUpdateEvent:
...
@ -1215,8 +1224,8 @@ class Client:
event: Literal['app_command_completion'],
/,
*,
check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]:
...
@ -1228,8 +1237,8 @@ class Client:
event: Literal['automod_rule_create', 'automod_rule_update', 'automod_rule_delete'],
/,
*,
check: Optional[Callable[[AutoModRule], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[AutoModRule], bool]] = ...,
timeout: Optional[float] = ...,
) -> AutoModRule:
...
@ -1239,8 +1248,8 @@ class Client:
event: Literal['automod_action'],
/,
*,
check: Optional[Callable[[AutoModAction], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[AutoModAction], bool]] = ...,
timeout: Optional[float] = ...,
) -> AutoModAction:
...
@ -1252,8 +1261,8 @@ class Client:
event: Literal['private_channel_update'],
/,
*,
check: Optional[Callable[[GroupChannel, GroupChannel], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[GroupChannel, GroupChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[GroupChannel, GroupChannel]:
...
@ -1263,8 +1272,8 @@ class Client:
event: Literal['private_channel_pins_update'],
/,
*,
check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[PrivateChannel, datetime.datetime]:
...
@ -1274,8 +1283,8 @@ class Client:
event: Literal['guild_channel_delete', 'guild_channel_create'],
/,
*,
check: Optional[Callable[[GuildChannel], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[GuildChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> GuildChannel:
...
@ -1285,8 +1294,8 @@ class Client:
event: Literal['guild_channel_update'],
/,
*,
check: Optional[Callable[[GuildChannel, GuildChannel], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[GuildChannel, GuildChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[GuildChannel, GuildChannel]:
...
@ -1302,7 +1311,7 @@ class Client:
bool,
]
],
timeout: Optional[float] = None,
timeout: Optional[float] = ...,
) -> Tuple[Union[GuildChannel, Thread], Optional[datetime.datetime]]:
...
@ -1312,8 +1321,8 @@ class Client:
event: Literal['typing'],
/,
*,
check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Messageable, Union[User, Member], datetime.datetime]:
...
@ -1323,8 +1332,8 @@ class Client:
event: Literal['raw_typing'],
/,
*,
check: Optional[Callable[[RawTypingEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawTypingEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawTypingEvent:
...
@ -1336,8 +1345,8 @@ class Client:
event: Literal['connect', 'disconnect', 'ready', 'resumed'],
/,
*,
check: Optional[Callable[[], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[], bool]] = ...,
timeout: Optional[float] = ...,
) -> None:
...
@ -1347,8 +1356,8 @@ class Client:
event: Literal['shard_connect', 'shard_disconnect', 'shard_ready', 'shard_resumed'],
/,
*,
check: Optional[Callable[[int], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[int], bool]] = ...,
timeout: Optional[float] = ...,
) -> int:
...
@ -1358,8 +1367,8 @@ class Client:
event: Literal['socket_event_type', 'socket_raw_receive'],
/,
*,
check: Optional[Callable[[str], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[str], bool]] = ...,
timeout: Optional[float] = ...,
) -> str:
...
@ -1369,8 +1378,8 @@ class Client:
event: Literal['socket_raw_send'],
/,
*,
check: Optional[Callable[[Union[str, bytes]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Union[str, bytes]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Union[str, bytes]:
...
@ -1381,8 +1390,8 @@ class Client:
event: Literal['entitlement_create', 'entitlement_update', 'entitlement_delete'],
/,
*,
check: Optional[Callable[[Entitlement], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Entitlement], bool]] = ...,
timeout: Optional[float] = ...,
) -> Entitlement:
...
@ -1399,8 +1408,8 @@ class Client:
],
/,
*,
check: Optional[Callable[[Guild], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Guild], bool]] = ...,
timeout: Optional[float] = ...,
) -> Guild:
...
@ -1410,8 +1419,8 @@ class Client:
event: Literal['guild_update'],
/,
*,
check: Optional[Callable[[Guild, Guild], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Guild, Guild], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Guild]:
...
@ -1421,8 +1430,8 @@ class Client:
event: Literal['guild_emojis_update'],
/,
*,
check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Sequence[Emoji], Sequence[Emoji]]:
...
@ -1432,8 +1441,8 @@ class Client:
event: Literal['guild_stickers_update'],
/,
*,
check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Sequence[GuildSticker], Sequence[GuildSticker]]:
...
@ -1443,8 +1452,8 @@ class Client:
event: Literal['invite_create', 'invite_delete'],
/,
*,
check: Optional[Callable[[Invite], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Invite], bool]] = ...,
timeout: Optional[float] = ...,
) -> Invite:
...
@ -1454,8 +1463,8 @@ class Client:
event: Literal['audit_log_entry_create'],
/,
*,
check: Optional[Callable[[AuditLogEntry], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[AuditLogEntry], bool]] = ...,
timeout: Optional[float] = ...,
) -> AuditLogEntry:
...
@ -1467,8 +1476,8 @@ class Client:
event: Literal['integration_create', 'integration_update'],
/,
*,
check: Optional[Callable[[Integration], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Integration], bool]] = ...,
timeout: Optional[float] = ...,
) -> Integration:
...
@ -1478,8 +1487,8 @@ class Client:
event: Literal['guild_integrations_update'],
/,
*,
check: Optional[Callable[[Guild], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Guild], bool]] = ...,
timeout: Optional[float] = ...,
) -> Guild:
...
@ -1489,8 +1498,8 @@ class Client:
event: Literal['webhooks_update'],
/,
*,
check: Optional[Callable[[GuildChannel], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[GuildChannel], bool]] = ...,
timeout: Optional[float] = ...,
) -> GuildChannel:
...
@ -1500,8 +1509,8 @@ class Client:
event: Literal['raw_integration_delete'],
/,
*,
check: Optional[Callable[[RawIntegrationDeleteEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawIntegrationDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawIntegrationDeleteEvent:
...
@ -1513,8 +1522,8 @@ class Client:
event: Literal['interaction'],
/,
*,
check: Optional[Callable[[Interaction[Self]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Interaction[Self]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Interaction[Self]:
...
@ -1526,8 +1535,8 @@ class Client:
event: Literal['member_join', 'member_remove'],
/,
*,
check: Optional[Callable[[Member], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Member], bool]] = ...,
timeout: Optional[float] = ...,
) -> Member:
...
@ -1537,8 +1546,8 @@ class Client:
event: Literal['raw_member_remove'],
/,
*,
check: Optional[Callable[[RawMemberRemoveEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawMemberRemoveEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawMemberRemoveEvent:
...
@ -1548,8 +1557,8 @@ class Client:
event: Literal['member_update', 'presence_update'],
/,
*,
check: Optional[Callable[[Member, Member], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Member, Member], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Member, Member]:
...
@ -1559,8 +1568,8 @@ class Client:
event: Literal['user_update'],
/,
*,
check: Optional[Callable[[User, User], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[User, User], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[User, User]:
...
@ -1570,8 +1579,8 @@ class Client:
event: Literal['member_ban'],
/,
*,
check: Optional[Callable[[Guild, Union[User, Member]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Guild, Union[User, Member]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, Union[User, Member]]:
...
@ -1581,8 +1590,8 @@ class Client:
event: Literal['member_unban'],
/,
*,
check: Optional[Callable[[Guild, User], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Guild, User], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Guild, User]:
...
@ -1594,8 +1603,8 @@ class Client:
event: Literal['message', 'message_delete'],
/,
*,
check: Optional[Callable[[Message], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Message], bool]] = ...,
timeout: Optional[float] = ...,
) -> Message:
...
@ -1605,8 +1614,8 @@ class Client:
event: Literal['message_edit'],
/,
*,
check: Optional[Callable[[Message, Message], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Message, Message], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Message, Message]:
...
@ -1616,8 +1625,8 @@ class Client:
event: Literal['bulk_message_delete'],
/,
*,
check: Optional[Callable[[List[Message]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[List[Message]], bool]] = ...,
timeout: Optional[float] = ...,
) -> List[Message]:
...
@ -1627,8 +1636,8 @@ class Client:
event: Literal['raw_message_edit'],
/,
*,
check: Optional[Callable[[RawMessageUpdateEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawMessageUpdateEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawMessageUpdateEvent:
...
@ -1638,8 +1647,8 @@ class Client:
event: Literal['raw_message_delete'],
/,
*,
check: Optional[Callable[[RawMessageDeleteEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawMessageDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawMessageDeleteEvent:
...
@ -1649,8 +1658,8 @@ class Client:
event: Literal['raw_bulk_message_delete'],
/,
*,
check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawBulkMessageDeleteEvent:
...
@ -1662,8 +1671,8 @@ class Client:
event: Literal['reaction_add', 'reaction_remove'],
/,
*,
check: Optional[Callable[[Reaction, Union[Member, User]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Reaction, Union[Member, User]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Reaction, Union[Member, User]]:
...
@ -1673,8 +1682,8 @@ class Client:
event: Literal['reaction_clear'],
/,
*,
check: Optional[Callable[[Message, List[Reaction]], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Message, List[Reaction]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Message, List[Reaction]]:
...
@ -1684,8 +1693,8 @@ class Client:
event: Literal['reaction_clear_emoji'],
/,
*,
check: Optional[Callable[[Reaction], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Reaction], bool]] = ...,
timeout: Optional[float] = ...,
) -> Reaction:
...
@ -1695,8 +1704,8 @@ class Client:
event: Literal['raw_reaction_add', 'raw_reaction_remove'],
/,
*,
check: Optional[Callable[[RawReactionActionEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawReactionActionEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawReactionActionEvent:
...
@ -1706,8 +1715,8 @@ class Client:
event: Literal['raw_reaction_clear'],
/,
*,
check: Optional[Callable[[RawReactionClearEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawReactionClearEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawReactionClearEvent:
...
@ -1717,8 +1726,8 @@ class Client:
event: Literal['raw_reaction_clear_emoji'],
/,
*,
check: Optional[Callable[[RawReactionClearEmojiEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawReactionClearEmojiEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawReactionClearEmojiEvent:
...
@ -1730,8 +1739,8 @@ class Client:
event: Literal['guild_role_create', 'guild_role_delete'],
/,
*,
check: Optional[Callable[[Role], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Role], bool]] = ...,
timeout: Optional[float] = ...,
) -> Role:
...
@ -1741,8 +1750,8 @@ class Client:
event: Literal['guild_role_update'],
/,
*,
check: Optional[Callable[[Role, Role], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Role, Role], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Role, Role]:
...
@ -1754,8 +1763,8 @@ class Client:
event: Literal['scheduled_event_create', 'scheduled_event_delete'],
/,
*,
check: Optional[Callable[[ScheduledEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[ScheduledEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> ScheduledEvent:
...
@ -1765,8 +1774,8 @@ class Client:
event: Literal['scheduled_event_user_add', 'scheduled_event_user_remove'],
/,
*,
check: Optional[Callable[[ScheduledEvent, User], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[ScheduledEvent, User], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[ScheduledEvent, User]:
...
@ -1778,8 +1787,8 @@ class Client:
event: Literal['stage_instance_create', 'stage_instance_delete'],
/,
*,
check: Optional[Callable[[StageInstance], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[StageInstance], bool]] = ...,
timeout: Optional[float] = ...,
) -> StageInstance:
...
@ -1789,8 +1798,8 @@ class Client:
event: Literal['stage_instance_update'],
/,
*,
check: Optional[Callable[[StageInstance, StageInstance], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[StageInstance, StageInstance], bool]] = ...,
timeout: Optional[float] = ...,
) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]:
...
@ -1801,8 +1810,8 @@ class Client:
event: Literal['subscription_create', 'subscription_update', 'subscription_delete'],
/,
*,
check: Optional[Callable[[Subscription], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Subscription], bool]] = ...,
timeout: Optional[float] = ...,
) -> Subscription:
...
@ -1813,8 +1822,8 @@ class Client:
event: Literal['thread_create', 'thread_join', 'thread_remove', 'thread_delete'],
/,
*,
check: Optional[Callable[[Thread], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Thread], bool]] = ...,
timeout: Optional[float] = ...,
) -> Thread:
...
@ -1824,8 +1833,8 @@ class Client:
event: Literal['thread_update'],
/,
*,
check: Optional[Callable[[Thread, Thread], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Thread, Thread], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Thread, Thread]:
...
@ -1835,8 +1844,8 @@ class Client:
event: Literal['raw_thread_update'],
/,
*,
check: Optional[Callable[[RawThreadUpdateEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawThreadUpdateEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawThreadUpdateEvent:
...
@ -1846,8 +1855,8 @@ class Client:
event: Literal['raw_thread_delete'],
/,
*,
check: Optional[Callable[[RawThreadDeleteEvent], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawThreadDeleteEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawThreadDeleteEvent:
...
@ -1857,8 +1866,8 @@ class Client:
event: Literal['thread_member_join', 'thread_member_remove'],
/,
*,
check: Optional[Callable[[ThreadMember], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[ThreadMember], bool]] = ...,
timeout: Optional[float] = ...,
) -> ThreadMember:
...
@ -1868,8 +1877,8 @@ class Client:
event: Literal['raw_thread_member_remove'],
/,
*,
check: Optional[Callable[[RawThreadMembersUpdate], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[RawThreadMembersUpdate], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawThreadMembersUpdate:
...
@ -1881,8 +1890,8 @@ class Client:
event: Literal['voice_state_update'],
/,
*,
check: Optional[Callable[[Member, VoiceState, VoiceState], bool]],
timeout: Optional[float] = None,
check: Optional[Callable[[Member, VoiceState, VoiceState], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Member, VoiceState, VoiceState]:
...
@ -1894,8 +1903,8 @@ class Client:
event: Literal['poll_vote_add', 'poll_vote_remove'],
/,
*,
check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = None,
timeout: Optional[float] = None,
check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Union[User, Member], PollAnswer]:
...
@ -1905,8 +1914,8 @@ class Client:
event: Literal['raw_poll_vote_add', 'raw_poll_vote_remove'],
/,
*,
check: Optional[Callable[[RawPollVoteActionEvent], bool]] = None,
timeout: Optional[float] = None,
check: Optional[Callable[[RawPollVoteActionEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> RawPollVoteActionEvent:
...
@ -1918,8 +1927,8 @@ class Client:
event: Literal["command", "command_completion"],
/,
*,
check: Optional[Callable[[Context[Any]], bool]] = None,
timeout: Optional[float] = None,
check: Optional[Callable[[Context[Any]], bool]] = ...,
timeout: Optional[float] = ...,
) -> Context[Any]:
...
@ -1929,8 +1938,8 @@ class Client:
event: Literal["command_error"],
/,
*,
check: Optional[Callable[[Context[Any], CommandError], bool]] = None,
timeout: Optional[float] = None,
check: Optional[Callable[[Context[Any], CommandError], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[Context[Any], CommandError]:
...
@ -1940,8 +1949,8 @@ class Client:
event: str,
/,
*,
check: Optional[Callable[..., bool]] = None,
timeout: Optional[float] = None,
check: Optional[Callable[..., bool]] = ...,
timeout: Optional[float] = ...,
) -> Any:
...

6
discord/components.py

@ -196,12 +196,12 @@ class Button(Component):
self.label: Optional[str] = data.get('label')
self.emoji: Optional[PartialEmoji]
try:
self.emoji = PartialEmoji.from_dict(data['emoji'])
self.emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.emoji = None
try:
self.sku_id: Optional[int] = int(data['sku_id'])
self.sku_id: Optional[int] = int(data['sku_id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.sku_id = None
@ -415,7 +415,7 @@ class SelectOption:
@classmethod
def from_dict(cls, data: SelectOptionPayload) -> SelectOption:
try:
emoji = PartialEmoji.from_dict(data['emoji'])
emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
emoji = None

21
discord/embeds.py

@ -29,6 +29,7 @@ from typing import Any, Dict, List, Mapping, Optional, Protocol, TYPE_CHECKING,
from . import utils
from .colour import Colour
from .flags import AttachmentFlags, EmbedFlags
# fmt: off
__all__ = (
@ -76,6 +77,7 @@ if TYPE_CHECKING:
proxy_url: Optional[str]
height: Optional[int]
width: Optional[int]
flags: Optional[AttachmentFlags]
class _EmbedVideoProxy(Protocol):
url: Optional[str]
@ -131,7 +133,7 @@ class Embed:
The type of embed. Usually "rich".
This can be set during initialisation.
Possible strings for embed types can be found on discord's
:ddocs:`api docs <resources/channel#embed-object-embed-types>`
:ddocs:`api docs <resources/message#embed-object-embed-types>`
description: Optional[:class:`str`]
The description of the embed.
This can be set during initialisation.
@ -146,6 +148,10 @@ class Embed:
colour: Optional[Union[:class:`Colour`, :class:`int`]]
The colour code of the embed. Aliased to ``color`` as well.
This can be set during initialisation.
flags: Optional[:class:`EmbedFlags`]
The flags of this embed.
.. versionadded:: 2.5
"""
__slots__ = (
@ -162,6 +168,7 @@ class Embed:
'_author',
'_fields',
'description',
'flags',
)
def __init__(
@ -181,6 +188,7 @@ class Embed:
self.type: EmbedType = type
self.url: Optional[str] = url
self.description: Optional[str] = description
self.flags: Optional[EmbedFlags] = None
if self.title is not None:
self.title = str(self.title)
@ -245,6 +253,11 @@ class Embed:
else:
setattr(self, '_' + attr, value)
try:
self.flags = EmbedFlags._from_value(data['flags'])
except KeyError:
pass
return self
def copy(self) -> Self:
@ -399,11 +412,15 @@ class Embed:
- ``proxy_url``
- ``width``
- ``height``
- ``flags``
If the attribute has no value then ``None`` is returned.
"""
# Lying to the type checker for better developer UX.
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
data = getattr(self, '_image', {})
if 'flags' in data:
data['flags'] = AttachmentFlags._from_value(data['flags'])
return EmbedProxy(data) # type: ignore
def set_image(self, *, url: Optional[Any]) -> Self:
"""Sets the image for the embed content.

13
discord/enums.py

@ -84,13 +84,13 @@ def _create_value_cls(name: str, comparable: bool):
# All the type ignores here are due to the type checker being unable to recognise
# Runtime type creation without exploding.
cls = namedtuple('_EnumValue_' + name, 'name value')
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>' # type: ignore
cls.__str__ = lambda self: f'{name}.{self.name}' # type: ignore
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
cls.__str__ = lambda self: f'{name}.{self.name}'
if comparable:
cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value # type: ignore
cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value # type: ignore
cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value # type: ignore
cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value # type: ignore
cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value
cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value
cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value
cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value
return cls
@ -266,6 +266,7 @@ class MessageType(Enum):
guild_incident_report_raid = 38
guild_incident_report_false_alarm = 39
purchase_notification = 44
poll_result = 46
class SpeakingState(Enum):

4
discord/ext/commands/bot.py

@ -172,7 +172,7 @@ class BotBase(GroupMixin[None]):
**options: Any,
) -> None:
super().__init__(intents=intents, **options)
self.command_prefix: PrefixType[BotT] = command_prefix
self.command_prefix: PrefixType[BotT] = command_prefix # type: ignore
self.extra_events: Dict[str, List[CoroFunc]] = {}
# Self doesn't have the ClientT bound, but since this is a mixin it technically does
self.__tree: app_commands.CommandTree[Self] = tree_cls(self) # type: ignore
@ -487,7 +487,7 @@ class BotBase(GroupMixin[None]):
if len(data) == 0:
return True
return await discord.utils.async_all(f(ctx) for f in data)
return await discord.utils.async_all(f(ctx) for f in data) # type: ignore
async def is_owner(self, user: User, /) -> bool:
"""|coro|

11
discord/ext/commands/context.py

@ -82,7 +82,7 @@ def is_cog(obj: Any) -> TypeGuard[Cog]:
return hasattr(obj, '__cog_commands__')
class DeferTyping:
class DeferTyping(Generic[BotT]):
def __init__(self, ctx: Context[BotT], *, ephemeral: bool):
self.ctx: Context[BotT] = ctx
self.ephemeral: bool = ephemeral
@ -751,7 +751,7 @@ class Context(discord.abc.Messageable, Generic[BotT]):
else:
return await self.send(content, **kwargs)
def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping]:
def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping[BotT]]:
"""Returns an asynchronous context manager that allows you to send a typing indicator to
the destination for an indefinite period of time, or 10 seconds if the context manager
is called using ``await``.
@ -1078,8 +1078,11 @@ class Context(discord.abc.Messageable, Generic[BotT]):
if self.interaction.response.is_done():
msg = await self.interaction.followup.send(**kwargs, wait=True)
else:
await self.interaction.response.send_message(**kwargs)
msg = await self.interaction.original_response()
response = await self.interaction.response.send_message(**kwargs)
if not isinstance(response.resource, discord.InteractionMessage):
msg = await self.interaction.original_response()
else:
msg = response.resource
if delete_after is not None:
await msg.delete(delay=delete_after)

6
discord/ext/commands/converter.py

@ -1125,7 +1125,7 @@ class Greedy(List[T]):
args = getattr(converter, '__args__', ())
if discord.utils.PY_310 and converter.__class__ is types.UnionType: # type: ignore
converter = Union[args] # type: ignore
converter = Union[args]
origin = getattr(converter, '__origin__', None)
@ -1138,7 +1138,7 @@ class Greedy(List[T]):
if origin is Union and type(None) in args:
raise TypeError(f'Greedy[{converter!r}] is invalid.')
return cls(converter=converter)
return cls(converter=converter) # type: ignore
@property
def constructed_converter(self) -> Any:
@ -1325,7 +1325,7 @@ async def _actual_conversion(ctx: Context[BotT], converter: Any, argument: str,
else:
return await converter().convert(ctx, argument)
elif isinstance(converter, Converter):
return await converter.convert(ctx, argument) # type: ignore
return await converter.convert(ctx, argument)
except CommandError:
raise
except Exception as exc:

2
discord/ext/commands/core.py

@ -1285,7 +1285,7 @@ class Command(_BaseCommand, Generic[CogT, P, T]):
# since we have no checks, then we just return True.
return True
return await discord.utils.async_all(predicate(ctx) for predicate in predicates)
return await discord.utils.async_all(predicate(ctx) for predicate in predicates) # type: ignore
finally:
ctx.command = original

9
discord/ext/commands/errors.py

@ -24,18 +24,19 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union, Generic
from discord.errors import ClientException, DiscordException
from discord.utils import _human_join
from ._types import BotT
if TYPE_CHECKING:
from discord.abc import GuildChannel
from discord.threads import Thread
from discord.types.snowflake import Snowflake, SnowflakeList
from discord.app_commands import AppCommandError
from ._types import BotT
from .context import Context
from .converter import Converter
from .cooldowns import BucketType, Cooldown
@ -235,7 +236,7 @@ class CheckFailure(CommandError):
pass
class CheckAnyFailure(CheckFailure):
class CheckAnyFailure(Generic[BotT], CheckFailure):
"""Exception raised when all predicates in :func:`check_any` fail.
This inherits from :exc:`CheckFailure`.
@ -1080,7 +1081,7 @@ class ExtensionNotFound(ExtensionError):
"""
def __init__(self, name: str) -> None:
msg = f'Extension {name!r} could not be loaded.'
msg = f'Extension {name!r} could not be loaded or found.'
super().__init__(msg, name=name)

2
discord/ext/commands/flags.py

@ -443,7 +443,7 @@ async def convert_flag(ctx: Context[BotT], argument: str, flag: Flag, annotation
return await convert_flag(ctx, argument, flag, annotation)
elif origin is Union and type(None) in annotation.__args__:
# typing.Optional[x]
annotation = Union[tuple(arg for arg in annotation.__args__ if arg is not type(None))] # type: ignore
annotation = Union[tuple(arg for arg in annotation.__args__ if arg is not type(None))]
return await run_converters(ctx, annotation, argument, param)
elif origin is dict:
# typing.Dict[K, V] -> typing.Tuple[K, V]

13
discord/ext/commands/hybrid.py

@ -203,9 +203,9 @@ def replace_parameter(
# Fallback to see if the behaviour needs changing
origin = getattr(converter, '__origin__', None)
args = getattr(converter, '__args__', [])
if isinstance(converter, Range):
if isinstance(converter, Range): # type: ignore # Range is not an Annotation at runtime
r = converter
param = param.replace(annotation=app_commands.Range[r.annotation, r.min, r.max])
param = param.replace(annotation=app_commands.Range[r.annotation, r.min, r.max]) # type: ignore
elif isinstance(converter, Greedy):
# Greedy is "optional" in ext.commands
# However, in here, it probably makes sense to make it required.
@ -257,7 +257,7 @@ def replace_parameter(
inner = args[0]
is_inner_transformer = is_transformer(inner)
if is_converter(inner) and not is_inner_transformer:
param = param.replace(annotation=Optional[ConverterTransformer(inner, original)]) # type: ignore
param = param.replace(annotation=Optional[ConverterTransformer(inner, original)])
else:
raise
elif origin:
@ -424,10 +424,10 @@ class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
if not ret:
return False
if self.checks and not await async_all(f(interaction) for f in self.checks):
if self.checks and not await async_all(f(interaction) for f in self.checks): # type: ignore
return False
if self.wrapped.checks and not await async_all(f(ctx) for f in self.wrapped.checks):
if self.wrapped.checks and not await async_all(f(ctx) for f in self.wrapped.checks): # type: ignore
return False
return True
@ -915,7 +915,8 @@ def hybrid_command(
def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> HybridCommand[CogT, P, T]:
if isinstance(func, Command):
raise TypeError('Callback is already a command.')
return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs)
# Pyright does not allow Command[Any] to be assigned to Command[CogT] despite it being okay here
return HybridCommand(func, name=name, with_app_command=with_app_command, **attrs) # type: ignore
return decorator

89
discord/flags.py

@ -63,6 +63,7 @@ __all__ = (
'RoleFlags',
'AppInstallationType',
'SKUFlags',
'EmbedFlags',
)
BF = TypeVar('BF', bound='BaseFlags')
@ -2173,6 +2174,30 @@ class AttachmentFlags(BaseFlags):
""":class:`bool`: Returns ``True`` if the attachment has been edited using the remix feature."""
return 1 << 2
@flag_value
def spoiler(self):
""":class:`bool`: Returns ``True`` if the attachment was marked as a spoiler.
.. versionadded:: 2.5
"""
return 1 << 3
@flag_value
def contains_explicit_media(self):
""":class:`bool`: Returns ``True`` if the attachment was flagged as sensitive content.
.. versionadded:: 2.5
"""
return 1 << 4
@flag_value
def animated(self):
""":class:`bool`: Returns ``True`` if the attachment is an animated image.
.. versionadded:: 2.5
"""
return 1 << 5
@fill_with_flags()
class RoleFlags(BaseFlags):
@ -2308,3 +2333,67 @@ class SKUFlags(BaseFlags):
def user_subscription(self):
""":class:`bool`: Returns ``True`` if the SKU is a user subscription."""
return 1 << 8
@fill_with_flags()
class EmbedFlags(BaseFlags):
r"""Wraps up the Discord Embed flags
.. versionadded:: 2.5
.. container:: operations
.. describe:: x == y
Checks if two EmbedFlags are equal.
.. describe:: x != y
Checks if two EmbedFlags are not equal.
.. describe:: x | y, x |= y
Returns an EmbedFlags instance with all enabled flags from
both x and y.
.. describe:: x ^ y, x ^= y
Returns an EmbedFlags instance with only flags enabled on
only one of x or y, not on both.
.. describe:: ~x
Returns an EmbedFlags instance with all flags inverted from x.
.. describe:: hash(x)
Returns the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
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`
The raw value. You should query flags via the properties
rather than using this raw value.
"""
@flag_value
def contains_explicit_media(self):
""":class:`bool`: Returns ``True`` if the embed was flagged as sensitive content."""
return 1 << 4
@flag_value
def content_inventory_entry(self):
""":class:`bool`: Returns ``True`` if the embed is a reply to an activity card, and is no
longer displayed.
"""
return 1 << 5

4
discord/gateway.py

@ -831,7 +831,7 @@ class DiscordVoiceWebSocket:
self._close_code: Optional[int] = None
self.secret_key: Optional[List[int]] = None
if hook:
self._hook = hook
self._hook = hook # type: ignore
async def _hook(self, *args: Any) -> None:
pass
@ -893,7 +893,7 @@ class DiscordVoiceWebSocket:
return ws
async def select_protocol(self, ip: str, port: int, mode: int) -> None:
async def select_protocol(self, ip: str, port: int, mode: str) -> None:
payload = {
'op': self.SELECT_PROTOCOL,
'd': {

14
discord/guild.py

@ -95,7 +95,7 @@ from .welcome_screen import WelcomeScreen, WelcomeChannel
from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction
from .partial_emoji import _EmojiTag, PartialEmoji
from .soundboard import SoundboardSound
from .presences import RawPresenceUpdateEvent
__all__ = (
'Guild',
@ -551,7 +551,8 @@ class Guild(Hashable):
member = self.get_member(user_id)
if member is None:
try:
member = Member(data=data['member'], state=self._state, guild=self)
member_data = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess]
member = Member(data=member_data, state=self._state, guild=self)
except KeyError:
member = None
@ -573,7 +574,7 @@ class Guild(Hashable):
def _from_data(self, guild: GuildPayload) -> None:
try:
self._member_count = guild['member_count']
self._member_count = guild['member_count'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
@ -653,10 +654,11 @@ class Guild(Hashable):
empty_tuple = ()
for presence in guild.get('presences', []):
user_id = int(presence['user']['id'])
member = self.get_member(user_id)
raw_presence = RawPresenceUpdateEvent(data=presence, state=self._state)
member = self.get_member(raw_presence.user_id)
if member is not None:
member._presence_update(presence, empty_tuple) # type: ignore
member._presence_update(raw_presence, empty_tuple) # type: ignore
if 'threads' in guild:
threads = guild['threads']

1
discord/http.py

@ -2620,6 +2620,7 @@ class HTTPClient:
'cover_image',
'interactions_endpoint_url ',
'tags',
'integration_types_config',
)
payload = {k: v for k, v in payload.items() if k in valid_keys}

214
discord/interactions.py

@ -54,6 +54,8 @@ __all__ = (
'Interaction',
'InteractionMessage',
'InteractionResponse',
'InteractionCallbackResponse',
'InteractionCallbackActivityInstance',
)
if TYPE_CHECKING:
@ -61,6 +63,8 @@ if TYPE_CHECKING:
Interaction as InteractionPayload,
InteractionData,
ApplicationCommandInteractionData,
InteractionCallback as InteractionCallbackPayload,
InteractionCallbackActivity as InteractionCallbackActivityPayload,
)
from .types.webhook import (
Webhook as WebhookPayload,
@ -90,6 +94,10 @@ if TYPE_CHECKING:
DMChannel,
GroupChannel,
]
InteractionCallbackResource = Union[
"InteractionMessage",
"InteractionCallbackActivityInstance",
]
MISSING: Any = utils.MISSING
@ -211,14 +219,15 @@ class Interaction(Generic[ClientT]):
int(k): int(v) for k, v in data.get('authorizing_integration_owners', {}).items()
}
try:
self.context = AppCommandContext._from_value([data['context']])
value = data['context'] # pyright: ignore[reportTypedDictNotRequiredAccess]
self.context = AppCommandContext._from_value([value])
except KeyError:
self.context = AppCommandContext()
self.locale: Locale = try_enum(Locale, data.get('locale', 'en-US'))
self.guild_locale: Optional[Locale]
try:
self.guild_locale = try_enum(Locale, data['guild_locale'])
self.guild_locale = try_enum(Locale, data['guild_locale']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.guild_locale = None
@ -469,6 +478,7 @@ class Interaction(Generic[ClientT]):
attachments: Sequence[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
poll: Poll = MISSING,
) -> InteractionMessage:
"""|coro|
@ -503,6 +513,14 @@ class Interaction(Generic[ClientT]):
view: Optional[:class:`~discord.ui.View`]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
poll: :class:`Poll`
The poll to create when editing the message.
.. versionadded:: 2.5
.. note::
This is only accepted when the response type is :attr:`InteractionResponseType.deferred_channel_message`.
Raises
-------
@ -532,6 +550,7 @@ class Interaction(Generic[ClientT]):
view=view,
allowed_mentions=allowed_mentions,
previous_allowed_mentions=previous_mentions,
poll=poll,
) as params:
adapter = async_context.get()
http = self._state.http
@ -624,6 +643,106 @@ class Interaction(Generic[ClientT]):
return await translator.translate(string, locale=locale, context=context)
class InteractionCallbackActivityInstance:
"""Represents an activity instance launched as an interaction response.
.. versionadded:: 2.5
Attributes
----------
id: :class:`str`
The activity instance ID.
"""
__slots__ = ('id',)
def __init__(self, data: InteractionCallbackActivityPayload) -> None:
self.id: str = data['id']
class InteractionCallbackResponse(Generic[ClientT]):
"""Represents an interaction response callback.
.. versionadded:: 2.5
Attributes
----------
id: :class:`int`
The interaction ID.
type: :class:`InteractionResponseType`
The interaction callback response type.
resource: Optional[Union[:class:`InteractionMessage`, :class:`InteractionCallbackActivityInstance`]]
The resource that the interaction response created. If a message was sent, this will be
a :class:`InteractionMessage`. If an activity was launched this will be a
:class:`InteractionCallbackActivityInstance`. In any other case, this will be ``None``.
message_id: Optional[:class:`int`]
The message ID of the resource. Only available if the resource is a :class:`InteractionMessage`.
activity_id: Optional[:class:`str`]
The activity ID of the resource. Only available if the resource is a :class:`InteractionCallbackActivityInstance`.
"""
__slots__ = (
'_state',
'_parent',
'type',
'id',
'_thinking',
'_ephemeral',
'message_id',
'activity_id',
'resource',
)
def __init__(
self,
*,
data: InteractionCallbackPayload,
parent: Interaction[ClientT],
state: ConnectionState,
type: InteractionResponseType,
) -> None:
self._state: ConnectionState = state
self._parent: Interaction[ClientT] = parent
self.type: InteractionResponseType = type
self._update(data)
def _update(self, data: InteractionCallbackPayload) -> None:
interaction = data['interaction']
self.id: int = int(interaction['id'])
self._thinking: bool = interaction.get('response_message_loading', False)
self._ephemeral: bool = interaction.get('response_message_ephemeral', False)
self.message_id: Optional[int] = utils._get_as_snowflake(interaction, 'response_message_id')
self.activity_id: Optional[str] = interaction.get('activity_instance_id')
self.resource: Optional[InteractionCallbackResource] = None
resource = data.get('resource')
if resource is not None:
self.type = try_enum(InteractionResponseType, resource['type'])
message = resource.get('message')
activity_instance = resource.get('activity_instance')
if message is not None:
self.resource = InteractionMessage(
state=_InteractionMessageState(self._parent, self._state), # pyright: ignore[reportArgumentType]
channel=self._parent.channel, # type: ignore # channel should be the correct type here
data=message,
)
elif activity_instance is not None:
self.resource = InteractionCallbackActivityInstance(activity_instance)
def is_thinking(self) -> bool:
""":class:`bool`: Whether the response was a thinking defer."""
return self._thinking
def is_ephemeral(self) -> bool:
""":class:`bool`: Whether the response was ephemeral."""
return self._ephemeral
class InteractionResponse(Generic[ClientT]):
"""Represents a Discord interaction response.
@ -653,7 +772,12 @@ class InteractionResponse(Generic[ClientT]):
""":class:`InteractionResponseType`: The type of response that was sent, ``None`` if response is not done."""
return self._response_type
async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> None:
async def defer(
self,
*,
ephemeral: bool = False,
thinking: bool = False,
) -> Optional[InteractionCallbackResponse[ClientT]]:
"""|coro|
Defers the interaction response.
@ -667,6 +791,9 @@ class InteractionResponse(Generic[ClientT]):
- :attr:`InteractionType.component`
- :attr:`InteractionType.modal_submit`
.. versionchanged:: 2.5
This now returns a :class:`InteractionCallbackResponse` instance.
Parameters
-----------
ephemeral: :class:`bool`
@ -685,6 +812,11 @@ class InteractionResponse(Generic[ClientT]):
Deferring the interaction failed.
InteractionResponded
This interaction has already been responded to before.
Returns
-------
Optional[:class:`InteractionCallbackResponse`]
The interaction callback resource, or ``None``.
"""
if self._response_type:
raise InteractionResponded(self._parent)
@ -709,7 +841,7 @@ class InteractionResponse(Generic[ClientT]):
adapter = async_context.get()
params = interaction_response_params(type=defer_type, data=data)
http = parent._state.http
await adapter.create_interaction_response(
response = await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
@ -718,6 +850,12 @@ class InteractionResponse(Generic[ClientT]):
params=params,
)
self._response_type = InteractionResponseType(defer_type)
return InteractionCallbackResponse(
data=response,
parent=self._parent,
state=self._parent._state,
type=self._response_type,
)
async def pong(self) -> None:
"""|coro|
@ -767,11 +905,14 @@ class InteractionResponse(Generic[ClientT]):
silent: bool = False,
delete_after: Optional[float] = None,
poll: Poll = MISSING,
) -> None:
) -> InteractionCallbackResponse[ClientT]:
"""|coro|
Responds to this interaction by sending a message.
.. versionchanged:: 2.5
This now returns a :class:`InteractionCallbackResponse` instance.
Parameters
-----------
content: Optional[:class:`str`]
@ -825,6 +966,11 @@ class InteractionResponse(Generic[ClientT]):
The length of ``embeds`` was invalid.
InteractionResponded
This interaction has already been responded to before.
Returns
-------
:class:`InteractionCallbackResponse`
The interaction callback data.
"""
if self._response_type:
raise InteractionResponded(self._parent)
@ -855,7 +1001,7 @@ class InteractionResponse(Generic[ClientT]):
)
http = parent._state.http
await adapter.create_interaction_response(
response = await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
@ -886,6 +1032,13 @@ class InteractionResponse(Generic[ClientT]):
asyncio.create_task(inner_call())
return InteractionCallbackResponse(
data=response,
parent=self._parent,
state=self._parent._state,
type=self._response_type,
)
async def edit_message(
self,
*,
@ -897,12 +1050,15 @@ class InteractionResponse(Generic[ClientT]):
allowed_mentions: Optional[AllowedMentions] = MISSING,
delete_after: Optional[float] = None,
suppress_embeds: bool = MISSING,
) -> None:
) -> Optional[InteractionCallbackResponse[ClientT]]:
"""|coro|
Responds to this interaction by editing the original message of
a component or modal interaction.
.. versionchanged:: 2.5
This now returns a :class:`InteractionCallbackResponse` instance.
Parameters
-----------
content: Optional[:class:`str`]
@ -948,6 +1104,11 @@ class InteractionResponse(Generic[ClientT]):
You specified both ``embed`` and ``embeds``.
InteractionResponded
This interaction has already been responded to before.
Returns
-------
Optional[:class:`InteractionCallbackResponse`]
The interaction callback data, or ``None`` if editing the message was not possible.
"""
if self._response_type:
raise InteractionResponded(self._parent)
@ -990,7 +1151,7 @@ class InteractionResponse(Generic[ClientT]):
)
http = parent._state.http
await adapter.create_interaction_response(
response = await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
@ -1015,11 +1176,21 @@ class InteractionResponse(Generic[ClientT]):
asyncio.create_task(inner_call())
async def send_modal(self, modal: Modal, /) -> None:
return InteractionCallbackResponse(
data=response,
parent=self._parent,
state=self._parent._state,
type=self._response_type,
)
async def send_modal(self, modal: Modal, /) -> InteractionCallbackResponse[ClientT]:
"""|coro|
Responds to this interaction by sending a modal.
.. versionchanged:: 2.5
This now returns a :class:`InteractionCallbackResponse` instance.
Parameters
-----------
modal: :class:`~discord.ui.Modal`
@ -1031,6 +1202,11 @@ class InteractionResponse(Generic[ClientT]):
Sending the modal failed.
InteractionResponded
This interaction has already been responded to before.
Returns
-------
:class:`InteractionCallbackResponse`
The interaction callback data.
"""
if self._response_type:
raise InteractionResponded(self._parent)
@ -1041,7 +1217,7 @@ class InteractionResponse(Generic[ClientT]):
http = parent._state.http
params = interaction_response_params(InteractionResponseType.modal.value, modal.to_dict())
await adapter.create_interaction_response(
response = await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
@ -1053,6 +1229,13 @@ class InteractionResponse(Generic[ClientT]):
self._parent._state.store_view(modal)
self._response_type = InteractionResponseType.modal
return InteractionCallbackResponse(
data=response,
parent=self._parent,
state=self._parent._state,
type=self._response_type,
)
async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None:
"""|coro|
@ -1154,6 +1337,7 @@ class InteractionMessage(Message):
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
delete_after: Optional[float] = None,
poll: Poll = MISSING,
) -> InteractionMessage:
"""|coro|
@ -1188,6 +1372,15 @@ class InteractionMessage(Message):
then it is silently ignored.
.. versionadded:: 2.2
poll: :class:`~discord.Poll`
The poll to create when editing the message.
.. versionadded:: 2.5
.. note::
This is only accepted if the interaction response's :attr:`InteractionResponse.type`
attribute is :attr:`InteractionResponseType.deferred_channel_message`.
Raises
-------
@ -1212,6 +1405,7 @@ class InteractionMessage(Message):
attachments=attachments,
view=view,
allowed_mentions=allowed_mentions,
poll=poll,
)
if delete_after is not None:
await self.delete(delay=delete_after)

2
discord/invite.py

@ -437,7 +437,7 @@ class Invite(Hashable):
def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self:
guild: Optional[Union[Guild, PartialInviteGuild]]
try:
guild_data = data['guild']
guild_data = data['guild'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
# If we're here, then this is a group DM
guild = None

98
discord/member.py

@ -36,13 +36,13 @@ from . import utils
from .asset import Asset
from .utils import MISSING
from .user import BaseUser, ClientUser, User, _UserTag
from .activity import create_activity, ActivityTypes
from .permissions import Permissions
from .enums import Status, try_enum
from .enums import Status
from .errors import ClientException
from .colour import Colour
from .object import Object
from .flags import MemberFlags
from .presences import ClientStatus
__all__ = (
'VoiceState',
@ -57,10 +57,8 @@ if TYPE_CHECKING:
from .channel import DMChannel, VoiceChannel, StageChannel
from .flags import PublicUserFlags
from .guild import Guild
from .types.activity import (
ClientStatus as ClientStatusPayload,
PartialPresenceUpdate,
)
from .activity import ActivityTypes
from .presences import RawPresenceUpdateEvent
from .types.member import (
MemberWithUser as MemberWithUserPayload,
Member as MemberPayload,
@ -168,46 +166,6 @@ class VoiceState:
return f'<{self.__class__.__name__} {inner}>'
class _ClientStatus:
__slots__ = ('_status', 'desktop', 'mobile', 'web')
def __init__(self):
self._status: str = 'offline'
self.desktop: Optional[str] = None
self.mobile: Optional[str] = None
self.web: Optional[str] = None
def __repr__(self) -> str:
attrs = [
('_status', self._status),
('desktop', self.desktop),
('mobile', self.mobile),
('web', self.web),
]
inner = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {inner}>'
def _update(self, status: str, data: ClientStatusPayload, /) -> None:
self._status = status
self.desktop = data.get('desktop')
self.mobile = data.get('mobile')
self.web = data.get('web')
@classmethod
def _copy(cls, client_status: Self, /) -> Self:
self = cls.__new__(cls) # bypass __init__
self._status = client_status._status
self.desktop = client_status.desktop
self.mobile = client_status.mobile
self.web = client_status.web
return self
def flatten_user(cls: T) -> T:
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
# ignore private/special methods
@ -306,6 +264,10 @@ class Member(discord.abc.Messageable, _UserTag):
This will be set to ``None`` or a time in the past if the user is not timed out.
.. versionadded:: 2.0
client_status: :class:`ClientStatus`
Model which holds information about the status of the member on various clients/platforms via presence updates.
.. versionadded:: 2.5
"""
__slots__ = (
@ -318,7 +280,7 @@ class Member(discord.abc.Messageable, _UserTag):
'nick',
'timed_out_until',
'_permissions',
'_client_status',
'client_status',
'_user',
'_state',
'_avatar',
@ -354,7 +316,7 @@ class Member(discord.abc.Messageable, _UserTag):
self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get('joined_at'))
self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get('premium_since'))
self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data['roles']))
self._client_status: _ClientStatus = _ClientStatus()
self.client_status: ClientStatus = ClientStatus()
self.activities: Tuple[ActivityTypes, ...] = ()
self.nick: Optional[str] = data.get('nick', None)
self.pending: bool = data.get('pending', False)
@ -364,7 +326,7 @@ class Member(discord.abc.Messageable, _UserTag):
self._flags: int = data['flags']
self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data')
try:
self._permissions = int(data['permissions'])
self._permissions = int(data['permissions']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self._permissions = None
@ -430,7 +392,7 @@ class Member(discord.abc.Messageable, _UserTag):
self._roles = utils.SnowflakeList(member._roles, is_sorted=True)
self.joined_at = member.joined_at
self.premium_since = member.premium_since
self._client_status = _ClientStatus._copy(member._client_status)
self.client_status = member.client_status
self.guild = member.guild
self.nick = member.nick
self.pending = member.pending
@ -456,12 +418,12 @@ class Member(discord.abc.Messageable, _UserTag):
# the nickname change is optional,
# if it isn't in the payload then it didn't change
try:
self.nick = data['nick']
self.nick = data['nick'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
try:
self.pending = data['pending']
self.pending = data['pending'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
@ -473,13 +435,12 @@ class Member(discord.abc.Messageable, _UserTag):
self._flags = data.get('flags', 0)
self._avatar_decoration_data = data.get('avatar_decoration_data')
def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]:
self.activities = tuple(create_activity(d, self._state) for d in data['activities'])
self._client_status._update(data['status'], data['client_status'])
def _presence_update(self, raw: RawPresenceUpdateEvent, user: UserPayload) -> Optional[Tuple[User, User]]:
self.activities = raw.activities
self.client_status = raw.client_status
if len(user) > 1:
return self._update_inner_user(user)
return None
def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
u = self._user
@ -518,7 +479,7 @@ class Member(discord.abc.Messageable, _UserTag):
@property
def status(self) -> Status:
""":class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead."""
return try_enum(Status, self._client_status._status)
return self.client_status.status
@property
def raw_status(self) -> str:
@ -526,31 +487,36 @@ class Member(discord.abc.Messageable, _UserTag):
.. versionadded:: 1.5
"""
return self._client_status._status
return self.client_status._status
@status.setter
def status(self, value: Status) -> None:
# internal use only
self._client_status._status = str(value)
self.client_status._status = str(value)
@property
def mobile_status(self) -> Status:
""":class:`Status`: The member's status on a mobile device, if applicable."""
return try_enum(Status, self._client_status.mobile or 'offline')
return self.client_status.mobile_status
@property
def desktop_status(self) -> Status:
""":class:`Status`: The member's status on the desktop client, if applicable."""
return try_enum(Status, self._client_status.desktop or 'offline')
return self.client_status.desktop_status
@property
def web_status(self) -> Status:
""":class:`Status`: The member's status on the web client, if applicable."""
return try_enum(Status, self._client_status.web or 'offline')
return self.client_status.web_status
def is_on_mobile(self) -> bool:
""":class:`bool`: A helper function that determines if a member is active on a mobile device."""
return self._client_status.mobile is not None
"""A helper function that determines if a member is active on a mobile device.
Returns
-------
:class:`bool`
"""
return self.client_status.is_on_mobile()
@property
def colour(self) -> Colour:
@ -595,7 +561,9 @@ class Member(discord.abc.Messageable, _UserTag):
role = g.get_role(role_id)
if role:
result.append(role)
result.append(g.default_role)
default_role = g.default_role
if default_role:
result.append(default_role)
result.sort()
return result

57
discord/message.py

@ -610,6 +610,11 @@ class MessageReference:
.. versionadded:: 2.5
message_id: Optional[:class:`int`]
The id of the message referenced.
This can be ``None`` when this message reference was retrieved from
a system message of one of the following types:
- :attr:`MessageType.channel_follow_add`
- :attr:`MessageType.thread_created`
channel_id: :class:`int`
The channel id of the message referenced.
guild_id: Optional[:class:`int`]
@ -773,7 +778,7 @@ class MessageInteraction(Hashable):
self.user: Union[User, Member] = MISSING
try:
payload = data['member']
payload = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.user = state.create_user(data['user'])
else:
@ -2010,9 +2015,16 @@ class Message(PartialMessage, Hashable):
The :class:`TextChannel` or :class:`Thread` that the message was sent from.
Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message.
reference: Optional[:class:`~discord.MessageReference`]
The message that this message references. This is only applicable to messages of
type :attr:`MessageType.pins_add`, crossposted messages created by a
followed channel integration, or message replies.
The message that this message references. This is only applicable to
message replies (:attr:`MessageType.reply`), crossposted messages created by
a followed channel integration, forwarded messages, and messages of type:
- :attr:`MessageType.pins_add`
- :attr:`MessageType.channel_follow_add`
- :attr:`MessageType.thread_created`
- :attr:`MessageType.thread_starter_message`
- :attr:`MessageType.poll_result`
- :attr:`MessageType.context_menu_command`
.. versionadded:: 1.5
@ -2200,7 +2212,8 @@ class Message(PartialMessage, Hashable):
self.poll: Optional[Poll] = None
try:
self.poll = Poll._from_data(data=data['poll'], message=self, state=state)
poll = data['poll'] # pyright: ignore[reportTypedDictNotRequiredAccess]
self.poll = Poll._from_data(data=poll, message=self, state=state)
except KeyError:
pass
@ -2214,7 +2227,7 @@ class Message(PartialMessage, Hashable):
if self.guild is not None:
try:
thread = data['thread']
thread = data['thread'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
else:
@ -2229,7 +2242,7 @@ class Message(PartialMessage, Hashable):
# deprecated
try:
interaction = data['interaction']
interaction = data['interaction'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
else:
@ -2237,20 +2250,20 @@ class Message(PartialMessage, Hashable):
self.interaction_metadata: Optional[MessageInteractionMetadata] = None
try:
interaction_metadata = data['interaction_metadata']
interaction_metadata = data['interaction_metadata'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
else:
self.interaction_metadata = MessageInteractionMetadata(state=state, guild=self.guild, data=interaction_metadata)
try:
ref = data['message_reference']
ref = data['message_reference'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.reference = None
else:
self.reference = ref = MessageReference.with_state(state, ref)
try:
resolved = data['referenced_message']
resolved = data['referenced_message'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
else:
@ -2268,9 +2281,16 @@ class Message(PartialMessage, Hashable):
# the channel will be the correct type here
ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore
if self.type is MessageType.poll_result:
if isinstance(self.reference.resolved, self.__class__):
self._state._update_poll_results(self, self.reference.resolved)
else:
if self.reference.message_id:
self._state._update_poll_results(self, self.reference.message_id)
self.application: Optional[MessageApplication] = None
try:
application = data['application']
application = data['application'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
else:
@ -2278,7 +2298,7 @@ class Message(PartialMessage, Hashable):
self.role_subscription: Optional[RoleSubscriptionInfo] = None
try:
role_subscription = data['role_subscription_data']
role_subscription = data['role_subscription_data'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
else:
@ -2286,7 +2306,7 @@ class Message(PartialMessage, Hashable):
self.purchase_notification: Optional[PurchaseNotification] = None
try:
purchase_notification = data['purchase_notification']
purchase_notification = data['purchase_notification'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
pass
else:
@ -2294,7 +2314,7 @@ class Message(PartialMessage, Hashable):
for handler in ('author', 'member', 'mentions', 'mention_roles', 'components', 'call'):
try:
getattr(self, f'_handle_{handler}')(data[handler])
getattr(self, f'_handle_{handler}')(data[handler]) # type: ignore
except KeyError:
continue
@ -2634,6 +2654,7 @@ class Message(PartialMessage, Hashable):
MessageType.chat_input_command,
MessageType.context_menu_command,
MessageType.thread_starter_message,
MessageType.poll_result,
)
@utils.cached_slot_property('_cs_system_content')
@ -2810,6 +2831,14 @@ class Message(PartialMessage, Hashable):
if guild_product_purchase is not None:
return f'{self.author.name} has purchased {guild_product_purchase.product_name}!'
if self.type is MessageType.poll_result:
embed = self.embeds[0] # Will always have 1 embed
poll_title = utils.get(
embed.fields,
name='poll_question_text',
)
return f'{self.author.display_name}\'s poll {poll_title.value} has closed.' # type: ignore
# Fallback for unknown message types
return ''

105
discord/poll.py

@ -29,7 +29,7 @@ from typing import Optional, List, TYPE_CHECKING, Union, AsyncIterator, Dict
import datetime
from .enums import PollLayoutType, try_enum
from .enums import PollLayoutType, try_enum, MessageType
from . import utils
from .emoji import PartialEmoji, Emoji
from .user import User
@ -125,7 +125,16 @@ class PollAnswer:
Whether the current user has voted to this answer or not.
"""
__slots__ = ('media', 'id', '_state', '_message', '_vote_count', 'self_voted', '_poll')
__slots__ = (
'media',
'id',
'_state',
'_message',
'_vote_count',
'self_voted',
'_poll',
'_victor',
)
def __init__(
self,
@ -141,6 +150,7 @@ class PollAnswer:
self._vote_count: int = 0
self.self_voted: bool = False
self._poll: Poll = poll
self._victor: bool = False
def _handle_vote_event(self, added: bool, self_voted: bool) -> None:
if added:
@ -210,6 +220,19 @@ class PollAnswer:
'poll_media': self.media.to_dict(),
}
@property
def victor(self) -> bool:
""":class:`bool`: Whether the answer is the one that had the most
votes when the poll ended.
.. versionadded:: 2.5
.. note::
If the poll has not ended, this will always return ``False``.
"""
return self._victor
async def voters(
self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None
) -> AsyncIterator[Union[User, Member]]:
@ -313,6 +336,15 @@ class Poll:
Defaults to ``False``.
layout_type: :class:`PollLayoutType`
The layout type of the poll. Defaults to :attr:`PollLayoutType.default`.
Attributes
-----------
duration: :class:`datetime.timedelta`
The duration of the poll.
multiple: :class:`bool`
Whether users are allowed to select more than one answer.
layout_type: :class:`PollLayoutType`
The layout type of the poll.
"""
__slots__ = (
@ -325,6 +357,8 @@ class Poll:
'_expiry',
'_finalized',
'_state',
'_total_votes',
'_victor_answer_id',
)
def __init__(
@ -348,6 +382,8 @@ class Poll:
self._state: Optional[ConnectionState] = None
self._finalized: bool = False
self._expiry: Optional[datetime.datetime] = None
self._total_votes: Optional[int] = None
self._victor_answer_id: Optional[int] = None
def _update(self, message: Message) -> None:
self._state = message._state
@ -360,6 +396,33 @@ class Poll:
self._expiry = message.poll.expires_at
self._finalized = message.poll._finalized
self._answers = message.poll._answers
self._update_results_from_message(message)
def _update_results_from_message(self, message: Message) -> None:
if message.type != MessageType.poll_result or not message.embeds:
return
result_embed = message.embeds[0] # Will always have 1 embed
fields: Dict[str, str] = {field.name: field.value for field in result_embed.fields} # type: ignore
total_votes = fields.get('total_votes')
if total_votes is not None:
self._total_votes = int(total_votes)
victor_answer = fields.get('victor_answer_id')
if victor_answer is None:
return # Can't do anything else without the victor answer
self._victor_answer_id = int(victor_answer)
victor_answer_votes = fields['victor_answer_votes']
answer = self._answers[self._victor_answer_id]
answer._victor = True
answer._vote_count = int(victor_answer_votes)
self._answers[answer.id] = answer # Ensure update
def _update_results(self, data: PollResultPayload) -> None:
self._finalized = data['is_finalized']
@ -432,6 +495,32 @@ class Poll:
"""List[:class:`PollAnswer`]: Returns a read-only copy of the answers."""
return list(self._answers.values())
@property
def victor_answer_id(self) -> Optional[int]:
"""Optional[:class:`int`]: The victor answer ID.
.. versionadded:: 2.5
.. note::
This will **always** be ``None`` for polls that have not yet finished.
"""
return self._victor_answer_id
@property
def victor_answer(self) -> Optional[PollAnswer]:
"""Optional[:class:`PollAnswer`]: The victor answer.
.. versionadded:: 2.5
.. note::
This will **always** be ``None`` for polls that have not yet finished.
"""
if self.victor_answer_id is None:
return None
return self.get_answer(self.victor_answer_id)
@property
def expires_at(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: A datetime object representing the poll expiry.
@ -457,12 +546,20 @@ class Poll:
@property
def message(self) -> Optional[Message]:
""":class:`Message`: The message this poll is from."""
"""Optional[:class:`Message`]: The message this poll is from."""
return self._message
@property
def total_votes(self) -> int:
""":class:`int`: Returns the sum of all the answer votes."""
""":class:`int`: Returns the sum of all the answer votes.
If the poll has not yet finished, this is an approximate vote count.
.. versionchanged:: 2.5
This now returns an exact vote count when updated from its poll results message.
"""
if self._total_votes is not None:
return self._total_votes
return sum([answer.vote_count for answer in self.answers])
def is_finalised(self) -> bool:

150
discord/presences.py

@ -0,0 +1,150 @@
"""
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 TYPE_CHECKING, Optional, Tuple
from .activity import create_activity
from .enums import Status, try_enum
from .utils import MISSING, _get_as_snowflake, _RawReprMixin
if TYPE_CHECKING:
from typing_extensions import Self
from .activity import ActivityTypes
from .guild import Guild
from .state import ConnectionState
from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate
__all__ = (
'RawPresenceUpdateEvent',
'ClientStatus',
)
class ClientStatus:
"""Represents the :ddocs:`Client Status Object <events/gateway-events#client-status-object>` from Discord,
which holds information about the status of the user on various clients/platforms, with additional helpers.
.. versionadded:: 2.5
"""
__slots__ = ('_status', 'desktop', 'mobile', 'web')
def __init__(self, *, status: str = MISSING, data: ClientStatusPayload = MISSING) -> None:
self._status: str = status or 'offline'
data = data or {}
self.desktop: Optional[str] = data.get('desktop')
self.mobile: Optional[str] = data.get('mobile')
self.web: Optional[str] = data.get('web')
def __repr__(self) -> str:
attrs = [
('_status', self._status),
('desktop', self.desktop),
('mobile', self.mobile),
('web', self.web),
]
inner = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {inner}>'
def _update(self, status: str, data: ClientStatusPayload, /) -> None:
self._status = status
self.desktop = data.get('desktop')
self.mobile = data.get('mobile')
self.web = data.get('web')
@classmethod
def _copy(cls, client_status: Self, /) -> Self:
self = cls.__new__(cls) # bypass __init__
self._status = client_status._status
self.desktop = client_status.desktop
self.mobile = client_status.mobile
self.web = client_status.web
return self
@property
def status(self) -> Status:
""":class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead."""
return try_enum(Status, self._status)
@property
def raw_status(self) -> str:
""":class:`str`: The user's overall status as a string value."""
return self._status
@property
def mobile_status(self) -> Status:
""":class:`Status`: The user's status on a mobile device, if applicable."""
return try_enum(Status, self.mobile or 'offline')
@property
def desktop_status(self) -> Status:
""":class:`Status`: The user's status on the desktop client, if applicable."""
return try_enum(Status, self.desktop or 'offline')
@property
def web_status(self) -> Status:
""":class:`Status`: The user's status on the web client, if applicable."""
return try_enum(Status, self.web or 'offline')
def is_on_mobile(self) -> bool:
""":class:`bool`: A helper function that determines if a user is active on a mobile device."""
return self.mobile is not None
class RawPresenceUpdateEvent(_RawReprMixin):
"""Represents the payload for a :func:`on_raw_presence_update` event.
.. versionadded:: 2.5
Attributes
----------
user_id: :class:`int`
The ID of the user that triggered the presence update.
guild_id: Optional[:class:`int`]
The guild ID for the users presence update. Could be ``None``.
guild: Optional[:class:`Guild`]
The guild associated with the presence update and user. Could be ``None``.
client_status: :class:`ClientStatus`
The :class:`~.ClientStatus` model which holds information about the status of the user on various clients.
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
The activities the user is currently doing. Due to a Discord API limitation, a user's Spotify activity may not appear
if they are listening to a song with a title longer than ``128`` characters. See :issue:`1738` for more information.
"""
__slots__ = ('user_id', 'guild_id', 'guild', 'client_status', 'activities')
def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None:
self.user_id: int = int(data['user']['id'])
self.client_status: ClientStatus = ClientStatus(status=data['status'], data=data['client_status'])
self.activities: Tuple[ActivityTypes, ...] = tuple(create_activity(d, state) for d in data['activities'])
self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id')
self.guild: Optional[Guild] = state._get_guild(self.guild_id)

26
discord/raw_models.py

@ -25,10 +25,10 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union
from typing import TYPE_CHECKING, Literal, Optional, Set, List, Union
from .enums import ChannelType, try_enum, ReactionType
from .utils import _get_as_snowflake
from .utils import _get_as_snowflake, _RawReprMixin
from .app_commands import AppCommandPermissions
from .colour import Colour
@ -82,14 +82,6 @@ __all__ = (
)
class _RawReprMixin:
__slots__: Tuple[str, ...] = ()
def __repr__(self) -> str:
value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__)
return f'<{self.__class__.__name__} {value}>'
class RawMessageDeleteEvent(_RawReprMixin):
"""Represents the event payload for a :func:`on_raw_message_delete` event.
@ -112,7 +104,7 @@ class RawMessageDeleteEvent(_RawReprMixin):
self.channel_id: int = int(data['channel_id'])
self.cached_message: Optional[Message] = None
try:
self.guild_id: Optional[int] = int(data['guild_id'])
self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.guild_id: Optional[int] = None
@ -140,7 +132,7 @@ class RawBulkMessageDeleteEvent(_RawReprMixin):
self.cached_messages: List[Message] = []
try:
self.guild_id: Optional[int] = int(data['guild_id'])
self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.guild_id: Optional[int] = None
@ -256,7 +248,7 @@ class RawReactionActionEvent(_RawReprMixin):
self.type: ReactionType = try_enum(ReactionType, data['type'])
try:
self.guild_id: Optional[int] = int(data['guild_id'])
self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.guild_id: Optional[int] = None
@ -289,7 +281,7 @@ class RawReactionClearEvent(_RawReprMixin):
self.channel_id: int = int(data['channel_id'])
try:
self.guild_id: Optional[int] = int(data['guild_id'])
self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.guild_id: Optional[int] = None
@ -319,7 +311,7 @@ class RawReactionClearEmojiEvent(_RawReprMixin):
self.channel_id: int = int(data['channel_id'])
try:
self.guild_id: Optional[int] = int(data['guild_id'])
self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.guild_id: Optional[int] = None
@ -346,7 +338,9 @@ class RawIntegrationDeleteEvent(_RawReprMixin):
self.guild_id: int = int(data['guild_id'])
try:
self.application_id: Optional[int] = int(data['application_id'])
self.application_id: Optional[int] = int(
data['application_id'] # pyright: ignore[reportTypedDictNotRequiredAccess]
)
except KeyError:
self.application_id: Optional[int] = None

110
discord/role.py

@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING
from typing import Any, Dict, List, Optional, Union, overload, TYPE_CHECKING
from .asset import Asset
from .permissions import Permissions
@ -286,7 +286,7 @@ class Role(Hashable):
self._flags: int = data.get('flags', 0)
try:
self.tags = RoleTags(data['tags'])
self.tags = RoleTags(data['tags']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.tags = None
@ -522,6 +522,112 @@ class Role(Hashable):
data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload)
return Role(guild=self.guild, data=data, state=self._state)
@overload
async def move(self, *, beginning: bool, offset: int = ..., reason: Optional[str] = ...):
...
@overload
async def move(self, *, end: bool, offset: int = ..., reason: Optional[str] = ...):
...
@overload
async def move(self, *, above: Role, offset: int = ..., reason: Optional[str] = ...):
...
@overload
async def move(self, *, below: Role, offset: int = ..., reason: Optional[str] = ...):
...
async def move(
self,
*,
beginning: bool = MISSING,
end: bool = MISSING,
above: Role = MISSING,
below: Role = MISSING,
offset: int = 0,
reason: Optional[str] = None,
):
"""|coro|
A rich interface to help move a role relative to other roles.
You must have :attr:`~discord.Permissions.manage_roles` to do this,
and you cannot move roles above the client's top role in the guild.
.. versionadded:: 2.5
Parameters
-----------
beginning: :class:`bool`
Whether to move this at the beginning of the role list, above the default role.
This is mutually exclusive with `end`, `above`, and `below`.
end: :class:`bool`
Whether to move this at the end of the role list.
This is mutually exclusive with `beginning`, `above`, and `below`.
above: :class:`Role`
The role that should be above our current role.
This mutually exclusive with `beginning`, `end`, and `below`.
below: :class:`Role`
The role that should be below our current role.
This mutually exclusive with `beginning`, `end`, and `above`.
offset: :class:`int`
The number of roles to offset the move by. For example,
an offset of ``2`` with ``beginning=True`` would move
it 2 above the beginning. A positive number moves it above
while a negative number moves it below. Note that this
number is relative and computed after the ``beginning``,
``end``, ``before``, and ``after`` parameters.
reason: Optional[:class:`str`]
The reason for editing this role. Shows up on the audit log.
Raises
-------
Forbidden
You cannot move the role there, or lack permissions to do so.
HTTPException
Moving the role failed.
TypeError
A bad mix of arguments were passed.
ValueError
An invalid role was passed.
Returns
--------
List[:class:`Role`]
A list of all the roles in the guild.
"""
if sum(bool(a) for a in (beginning, end, above, below)) > 1:
raise TypeError('Only one of [beginning, end, above, below] can be used.')
target = above or below
guild = self.guild
guild_roles = guild.roles
if target:
if target not in guild_roles:
raise ValueError('Target role is from a different guild')
if above == guild.default_role:
raise ValueError('Role cannot be moved below the default role')
if self == target:
raise ValueError('Target role cannot be itself')
roles = [r for r in guild_roles if r != self]
if beginning:
index = 1
elif end:
index = len(roles)
elif above in roles:
index = roles.index(above)
elif below in roles:
index = roles.index(below) + 1
else:
index = guild_roles.index(self)
roles.insert(max((index + offset), 1), self)
payload: List[RolePositionUpdate] = [{'id': role.id, 'position': idx} for idx, role in enumerate(roles)]
await self._state.http.move_role_position(guild.id, payload, reason=reason)
async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro|

68
discord/state.py

@ -62,6 +62,7 @@ from .message import Message
from .channel import *
from .channel import _channel_factory
from .raw_models import *
from .presences import RawPresenceUpdateEvent
from .member import Member
from .role import Role
from .enums import ChannelType, try_enum, Status
@ -261,6 +262,10 @@ class ConnectionState(Generic[ClientT]):
if not intents.members or cache_flags._empty:
self.store_user = self.store_user_no_intents
self.raw_presence_flag: bool = options.get('enable_raw_presences', utils.MISSING)
if self.raw_presence_flag is utils.MISSING:
self.raw_presence_flag = not intents.members and intents.presences
self.parsers: Dict[str, Callable[[Any], None]]
self.parsers = parsers = {}
for attr, func in inspect.getmembers(self):
@ -535,7 +540,7 @@ class ConnectionState(Generic[ClientT]):
) -> Tuple[Union[Channel, Thread], Optional[Guild]]:
channel_id = int(data['channel_id'])
try:
guild_id = guild_id or int(data['guild_id'])
guild_id = guild_id or int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
guild = self._get_guild(guild_id)
except KeyError:
channel = DMChannel._from_message(self, channel_id)
@ -552,6 +557,27 @@ class ConnectionState(Generic[ClientT]):
poll._handle_vote(answer_id, added, self_voted)
return poll
def _update_poll_results(self, from_: Message, to: Union[Message, int]) -> None:
if isinstance(to, Message):
cached = self._get_message(to.id)
elif isinstance(to, int):
cached = self._get_message(to)
if cached is None:
return
to = cached
else:
return
if to.poll is None:
return
to.poll._update_results_from_message(from_)
if cached is not None and cached.poll:
cached.poll._update_results_from_message(from_)
async def chunker(
self, guild_id: int, query: str = '', limit: int = 0, presences: bool = False, *, nonce: Optional[str] = None
) -> None:
@ -710,7 +736,7 @@ class ConnectionState(Generic[ClientT]):
if 'components' in data:
try:
entity_id = int(data['interaction']['id'])
entity_id = int(data['interaction']['id']) # pyright: ignore[reportTypedDictNotRequiredAccess]
except (KeyError, ValueError):
entity_id = raw.message_id
@ -806,22 +832,24 @@ class ConnectionState(Generic[ClientT]):
self.dispatch('interaction', interaction)
def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None:
guild_id = utils._get_as_snowflake(data, 'guild_id')
# guild_id won't be None here
guild = self._get_guild(guild_id)
if guild is None:
_log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id)
raw = RawPresenceUpdateEvent(data=data, state=self)
if self.raw_presence_flag:
self.dispatch('raw_presence_update', raw)
if raw.guild is None:
_log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id)
return
user = data['user']
member_id = int(user['id'])
member = guild.get_member(member_id)
member = raw.guild.get_member(raw.user_id)
if member is None:
_log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id)
_log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', raw.user_id)
return
old_member = Member._copy(member)
user_update = member._presence_update(data=data, user=user)
user_update = member._presence_update(raw=raw, user=data['user'])
if user_update:
self.dispatch('user_update', user_update[0], user_update[1])
@ -907,7 +935,7 @@ class ConnectionState(Generic[ClientT]):
def parse_channel_pins_update(self, data: gw.ChannelPinsUpdateEvent) -> None:
channel_id = int(data['channel_id'])
try:
guild = self._get_guild(int(data['guild_id']))
guild = self._get_guild(int(data['guild_id'])) # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
guild = None
channel = self._get_private_channel(channel_id)
@ -989,7 +1017,7 @@ class ConnectionState(Generic[ClientT]):
return
try:
channel_ids = {int(i) for i in data['channel_ids']}
channel_ids = {int(i) for i in data['channel_ids']} # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
# If not provided, then the entire guild is being synced
# So all previous thread data should be overwritten
@ -1409,8 +1437,10 @@ class ConnectionState(Generic[ClientT]):
user = presence['user']
member_id = user['id']
member = member_dict.get(member_id)
if member is not None:
member._presence_update(presence, user)
raw_presence = RawPresenceUpdateEvent(data=presence, state=self)
member._presence_update(raw_presence, user)
complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count')
self.process_chunk_requests(guild_id, data.get('nonce'), members, complete)
@ -1523,12 +1553,8 @@ class ConnectionState(Generic[ClientT]):
def parse_guild_scheduled_event_delete(self, data: gw.GuildScheduledEventDeleteEvent) -> None:
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
try:
scheduled_event = guild._scheduled_events.pop(int(data['id']))
except KeyError:
pass
else:
self.dispatch('scheduled_event_delete', scheduled_event)
scheduled_event = guild._scheduled_events.pop(int(data['id']), ScheduledEvent(state=self, data=data))
self.dispatch('scheduled_event_delete', scheduled_event)
else:
_log.debug('SCHEDULED_EVENT_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id'])

4
discord/subscription.py

@ -63,6 +63,8 @@ class Subscription(Hashable):
canceled_at: Optional[:class:`datetime.datetime`]
When the subscription was canceled.
This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.inactive`.
renewal_sku_ids: List[:class:`int`]
The IDs of the SKUs that the user is going to be subscribed to when renewing.
"""
__slots__ = (
@ -75,6 +77,7 @@ class Subscription(Hashable):
'current_period_end',
'status',
'canceled_at',
'renewal_sku_ids',
)
def __init__(self, *, state: ConnectionState, data: SubscriptionPayload):
@ -88,6 +91,7 @@ class Subscription(Hashable):
self.current_period_end: datetime.datetime = utils.parse_time(data['current_period_end'])
self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data['status'])
self.canceled_at: Optional[datetime.datetime] = utils.parse_time(data['canceled_at'])
self.renewal_sku_ids: List[int] = list(map(int, data['renewal_sku_ids'] or []))
def __repr__(self) -> str:
return f'<Subscription id={self.id} user_id={self.user_id} status={self.status!r}>'

2
discord/threads.py

@ -192,7 +192,7 @@ class Thread(Messageable, Hashable):
self.me: Optional[ThreadMember]
try:
member = data['member']
member = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
self.me = None
else:

7
discord/types/appinfo.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
from typing import TypedDict, List, Optional
from typing import Literal, Dict, TypedDict, List, Optional
from typing_extensions import NotRequired
from .user import User
@ -38,6 +38,10 @@ class InstallParams(TypedDict):
permissions: str
class AppIntegrationTypeConfig(TypedDict):
oauth2_install_params: NotRequired[InstallParams]
class BaseAppInfo(TypedDict):
id: Snowflake
name: str
@ -69,6 +73,7 @@ class AppInfo(BaseAppInfo):
tags: NotRequired[List[str]]
install_params: NotRequired[InstallParams]
custom_install_url: NotRequired[str]
integration_types_config: NotRequired[Dict[Literal['0', '1'], AppIntegrationTypeConfig]]
class PartialAppInfo(BaseAppInfo, total=False):

4
discord/types/embed.py

@ -50,6 +50,7 @@ class EmbedVideo(TypedDict, total=False):
proxy_url: str
height: int
width: int
flags: int
class EmbedImage(TypedDict, total=False):
@ -71,7 +72,7 @@ class EmbedAuthor(TypedDict, total=False):
proxy_icon_url: str
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link', 'poll_result']
class Embed(TypedDict, total=False):
@ -88,3 +89,4 @@ class Embed(TypedDict, total=False):
provider: EmbedProvider
author: EmbedAuthor
fields: List[EmbedField]
flags: int

4
discord/types/guild.py

@ -179,8 +179,8 @@ class GuildMFALevel(TypedDict):
class ChannelPositionUpdate(TypedDict):
id: Snowflake
position: Optional[int]
lock_permissions: Optional[bool]
parent_id: Optional[Snowflake]
lock_permissions: NotRequired[Optional[bool]]
parent_id: NotRequired[Optional[Snowflake]]
class _RolePositionRequired(TypedDict):

34
discord/types/interactions.py

@ -42,6 +42,16 @@ if TYPE_CHECKING:
InteractionType = Literal[1, 2, 3, 4, 5]
InteractionResponseType = Literal[
1,
4,
5,
6,
7,
8,
9,
10,
]
InteractionContextType = Literal[0, 1, 2]
InteractionInstallationType = Literal[0, 1]
@ -301,3 +311,27 @@ MessageInteractionMetadata = Union[
MessageComponentMessageInteractionMetadata,
ModalSubmitMessageInteractionMetadata,
]
class InteractionCallbackResponse(TypedDict):
id: Snowflake
type: InteractionType
activity_instance_id: NotRequired[str]
response_message_id: NotRequired[Snowflake]
response_message_loading: NotRequired[bool]
response_message_ephemeral: NotRequired[bool]
class InteractionCallbackActivity(TypedDict):
id: str
class InteractionCallbackResource(TypedDict):
type: InteractionResponseType
activity_instance: NotRequired[InteractionCallbackActivity]
message: NotRequired[Message]
class InteractionCallback(TypedDict):
interaction: InteractionCallbackResponse
resource: NotRequired[InteractionCallbackResource]

1
discord/types/message.py

@ -174,6 +174,7 @@ MessageType = Literal[
38,
39,
44,
46,
]

1
discord/types/subscription.py

@ -40,3 +40,4 @@ class Subscription(TypedDict):
current_period_end: str
status: SubscriptionStatus
canceled_at: Optional[str]
renewal_sku_ids: Optional[List[Snowflake]]

5
discord/ui/select.py

@ -21,6 +21,7 @@ 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 (
Any,
@ -330,7 +331,9 @@ class BaseSelect(Item[V]):
values = selected_values.get({})
payload: List[PossibleValue]
try:
resolved = Namespace._get_resolved_items(interaction, data['resolved'])
resolved = Namespace._get_resolved_items(
interaction, data['resolved'] # pyright: ignore[reportTypedDictNotRequiredAccess]
)
payload = list(resolved.values())
except KeyError:
payload = data.get("values", []) # type: ignore

7
discord/ui/view.py

@ -177,7 +177,7 @@ class View:
children = []
for func in self.__view_children_items__:
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
item.callback = _ViewCallback(func, self, item)
item.callback = _ViewCallback(func, self, item) # type: ignore
item._view = self
setattr(self, func.__name__, item)
children.append(item)
@ -214,6 +214,11 @@ class View:
# Wait N seconds to see if timeout data has been refreshed
await asyncio.sleep(self.__timeout_expiry - now)
def is_dispatchable(self) -> bool:
# this is used by webhooks to check whether a view requires a state attached
# or not, this simply is, whether a view has a component other than a url button
return any(item.is_dispatchable() for item in self.children)
def to_components(self) -> List[Dict[str, Any]]:
def key(item: Item) -> int:
return item._rendered_row or 0

16
discord/utils.py

@ -108,7 +108,7 @@ __all__ = (
)
DISCORD_EPOCH = 1420070400000
DEFAULT_FILE_SIZE_LIMIT_BYTES = 26214400
DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760
class _MissingSentinel:
@ -714,13 +714,13 @@ async def maybe_coroutine(f: MaybeAwaitableFunc[P, T], *args: P.args, **kwargs:
if _isawaitable(value):
return await value
else:
return value # type: ignore
return value
async def async_all(
gen: Iterable[Union[T, Awaitable[T]]],
*,
check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable,
check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable, # type: ignore
) -> bool:
for elem in gen:
if check(elem):
@ -1121,7 +1121,7 @@ def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]:
literal_cls = type(Literal[0])
for p in parameters:
if isinstance(p, literal_cls):
params.extend(p.__args__)
params.extend(p.__args__) # type: ignore
else:
params.append(p)
return tuple(params)
@ -1532,3 +1532,11 @@ def _format_call_duration(duration: datetime.timedelta) -> str:
formatted = f"{years} years"
return formatted
class _RawReprMixin:
__slots__: Tuple[str, ...] = ()
def __repr__(self) -> str:
value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__)
return f'<{self.__class__.__name__} {value}>'

2
discord/voice_state.py

@ -344,7 +344,7 @@ class VoiceConnectionState:
elif self.state is not ConnectionFlowState.disconnected:
# eventual consistency
if previous_token == self.token and previous_server_id == self.server_id and previous_token == self.token:
if previous_token == self.token and previous_server_id == self.server_id and previous_endpoint == self.endpoint:
return
_log.debug('Unexpected server update event, attempting to handle')

45
discord/webhook/async_.py

@ -90,6 +90,9 @@ if TYPE_CHECKING:
)
from ..types.emoji import PartialEmoji as PartialEmojiPayload
from ..types.snowflake import SnowflakeList
from ..types.interactions import (
InteractionCallback as InteractionCallbackResponsePayload,
)
BE = TypeVar('BE', bound=BaseException)
_State = Union[ConnectionState, '_WebhookState']
@ -310,8 +313,9 @@ class AsyncWebhookAdapter:
files: Optional[Sequence[File]] = None,
thread_id: Optional[int] = None,
wait: bool = False,
with_components: bool = False,
) -> Response[Optional[MessagePayload]]:
params = {'wait': int(wait)}
params = {'wait': int(wait), 'with_components': int(with_components)}
if thread_id:
params['thread_id'] = thread_id
route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token)
@ -434,13 +438,14 @@ class AsyncWebhookAdapter:
proxy: Optional[str] = None,
proxy_auth: Optional[aiohttp.BasicAuth] = None,
params: MultipartParameters,
) -> Response[None]:
) -> Response[InteractionCallbackResponsePayload]:
route = Route(
'POST',
'/interactions/{webhook_id}/{webhook_token}/callback',
webhook_id=interaction_id,
webhook_token=token,
)
request_params = {'with_response': '1'}
if params.files:
return self.request(
@ -450,9 +455,17 @@ class AsyncWebhookAdapter:
proxy_auth=proxy_auth,
files=params.files,
multipart=params.multipart,
params=request_params,
)
else:
return self.request(route, session=session, proxy=proxy, proxy_auth=proxy_auth, payload=params.payload)
return self.request(
route,
session=session,
proxy=proxy,
proxy_auth=proxy_auth,
payload=params.payload,
params=request_params,
)
def get_original_interaction_response(
self,
@ -660,6 +673,11 @@ class PartialWebhookChannel(Hashable):
def __repr__(self) -> str:
return f'<PartialWebhookChannel name={self.name!r} id={self.id}>'
@property
def mention(self) -> str:
""":class:`str`: The string that allows you to mention the channel that the webhook is following."""
return f'<#{self.id}>'
class PartialWebhookGuild(Hashable):
"""Represents a partial guild for webhooks.
@ -1710,10 +1728,9 @@ class Webhook(BaseWebhook):
.. versionadded:: 1.4
view: :class:`discord.ui.View`
The view to send with the message. You can only send a view
if this webhook is not partial and has state attached. A
webhook has state attached if the webhook is managed by the
library.
The view to send with the message. If the webhook is partial or
is not managed by the library, then you can only send URL buttons.
Otherwise, you can send views with any type of components.
.. versionadded:: 2.0
thread: :class:`~discord.abc.Snowflake`
@ -1765,7 +1782,8 @@ class Webhook(BaseWebhook):
The length of ``embeds`` was invalid, there was no token
associated with this webhook or ``ephemeral`` was passed
with the improper webhook type or there was no state
attached with this webhook when giving it a view.
attached with this webhook when giving it a view that had
components other than URL buttons.
Returns
---------
@ -1795,13 +1813,15 @@ class Webhook(BaseWebhook):
wait = True
if view is not MISSING:
if isinstance(self._state, _WebhookState):
raise ValueError('Webhook views require an associated state with the webhook')
if not hasattr(view, '__discord_ui_view__'):
raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}')
if ephemeral is True and view.timeout is None:
if isinstance(self._state, _WebhookState) and view.is_dispatchable():
raise ValueError(
'Webhook views with any component other than URL buttons require an associated state with the webhook'
)
if ephemeral is True and view.timeout is None and view.is_dispatchable():
view.timeout = 15 * 60.0
if thread_name is not MISSING and thread is not MISSING:
@ -1845,6 +1865,7 @@ class Webhook(BaseWebhook):
files=params.files,
thread_id=thread_id,
wait=wait,
with_components=view is not MISSING,
)
msg = None

26
discord/webhook/sync.py

@ -66,6 +66,7 @@ if TYPE_CHECKING:
from ..message import Attachment
from ..abc import Snowflake
from ..state import ConnectionState
from ..ui import View
from ..types.webhook import (
Webhook as WebhookPayload,
)
@ -290,8 +291,9 @@ class WebhookAdapter:
files: Optional[Sequence[File]] = None,
thread_id: Optional[int] = None,
wait: bool = False,
with_components: bool = False,
) -> MessagePayload:
params = {'wait': int(wait)}
params = {'wait': int(wait), 'with_components': int(with_components)}
if thread_id:
params['thread_id'] = thread_id
route = Route('POST', '/webhooks/{webhook_id}/{webhook_token}', webhook_id=webhook_id, webhook_token=token)
@ -919,6 +921,7 @@ class SyncWebhook(BaseWebhook):
silent: bool = False,
applied_tags: List[ForumTag] = MISSING,
poll: Poll = MISSING,
view: View = MISSING,
) -> Optional[SyncWebhookMessage]:
"""Sends a message using the webhook.
@ -991,6 +994,13 @@ class SyncWebhook(BaseWebhook):
When sending a Poll via webhook, you cannot manually end it.
.. versionadded:: 2.4
view: :class:`~discord.ui.View`
The view to send with the message. This can only have URL buttons, which donnot
require a state to be attached to it.
If you want to send a view with any component attached to it, check :meth:`Webhook.send`.
.. versionadded:: 2.5
Raises
--------
@ -1004,8 +1014,9 @@ class SyncWebhook(BaseWebhook):
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
or ``thread`` and ``thread_name``.
ValueError
The length of ``embeds`` was invalid or
there was no token associated with this webhook.
The length of ``embeds`` was invalid, there was no token
associated with this webhook or you tried to send a view
with components other than URL buttons.
Returns
---------
@ -1027,6 +1038,13 @@ class SyncWebhook(BaseWebhook):
else:
flags = MISSING
if view is not MISSING:
if not hasattr(view, '__discord_ui_view__'):
raise TypeError(f'expected view parameter to be of type View not {view.__class__.__name__}')
if view.is_dispatchable():
raise ValueError('SyncWebhook views can only contain URL buttons')
if thread_name is not MISSING and thread is not MISSING:
raise TypeError('Cannot mix thread_name and thread keyword arguments.')
@ -1050,6 +1068,7 @@ class SyncWebhook(BaseWebhook):
flags=flags,
applied_tags=applied_tag_ids,
poll=poll,
view=view,
) as params:
adapter: WebhookAdapter = _get_webhook_adapter()
thread_id: Optional[int] = None
@ -1065,6 +1084,7 @@ class SyncWebhook(BaseWebhook):
files=params.files,
thread_id=thread_id,
wait=wait,
with_components=view is not MISSING,
)
msg = None

2
discord/widget.py

@ -184,7 +184,7 @@ class WidgetMember(BaseUser):
self.suppress: Optional[bool] = data.get('suppress', False)
try:
game = data['game']
game = data['game'] # pyright: ignore[reportTypedDictNotRequiredAccess]
except KeyError:
activity = None
else:

75
docs/api.rst

@ -80,6 +80,14 @@ AppInstallParams
.. autoclass:: AppInstallParams()
:members:
IntegrationTypeConfig
~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: IntegrationTypeConfig
.. autoclass:: IntegrationTypeConfig()
:members:
Team
~~~~~
@ -916,6 +924,29 @@ Members
:param after: The updated member's updated info.
:type after: :class:`Member`
.. function:: on_raw_presence_update(payload)
Called when a :class:`Member` updates their presence.
This requires :attr:`Intents.presences` to be enabled.
Unlike :func:`on_presence_update`, when enabled, this is called regardless of the state of internal guild
and member caches, and **does not** provide a comparison between the previous and updated states of the :class:`Member`.
.. important::
By default, this event is only dispatched when :attr:`Intents.presences` is enabled **and** :attr:`Intents.members`
is disabled.
You can manually override this behaviour by setting the **enable_raw_presences** flag in the :class:`Client`,
however :attr:`Intents.presences` is always required for this event to work.
.. versionadded:: 2.5
:param payload: The raw presence update event model.
:type payload: :class:`RawPresenceUpdateEvent`
Messages
~~~~~~~~~
@ -1887,6 +1918,10 @@ of :class:`enum.Enum`.
.. versionadded:: 2.5
.. attribute:: poll_result
The system message sent when a poll has closed.
.. class:: UserFlags
Represents Discord User flags.
@ -3816,17 +3851,25 @@ of :class:`enum.Enum`.
.. versionadded:: 2.5
.. attribute:: reply
.. attribute:: default
A message reply.
A standard reference used by message replies (:attr:`MessageType.reply`),
crossposted messaged created by a followed channel integration, and messages of type:
- :attr:`MessageType.pins_add`
- :attr:`MessageType.channel_follow_add`
- :attr:`MessageType.thread_created`
- :attr:`MessageType.thread_starter_message`
- :attr:`MessageType.poll_result`
- :attr:`MessageType.context_menu_command`
.. attribute:: forward
A forwarded message.
.. attribute:: default
.. attribute:: reply
An alias for :attr:`.reply`.
An alias for :attr:`.default`.
.. _discord-api-audit-logs:
@ -5360,6 +5403,14 @@ RawPollVoteActionEvent
.. autoclass:: RawPollVoteActionEvent()
:members:
RawPresenceUpdateEvent
~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: RawPresenceUpdateEvent
.. autoclass:: RawPresenceUpdateEvent()
:members:
PartialWebhookGuild
~~~~~~~~~~~~~~~~~~~~
@ -5394,6 +5445,14 @@ MessageSnapshot
.. autoclass:: MessageSnapshot
:members:
ClientStatus
~~~~~~~~~~~~
.. attributetable:: ClientStatus
.. autoclass:: ClientStatus()
:members:
Data Classes
--------------
@ -5665,6 +5724,14 @@ SKUFlags
.. autoclass:: SKUFlags()
:members:
EmbedFlags
~~~~~~~~~~
.. attributetable:: EmbedFlags
.. autoclass:: EmbedFlags()
:members:
ForumTag
~~~~~~~~~

5
docs/ext/commands/api.rst

@ -531,6 +531,11 @@ Converters
.. autoclass:: discord.ext.commands.ScheduledEventConverter
:members:
.. attributetable:: discord.ext.commands.SoundboardSoundConverter
.. autoclass:: discord.ext.commands.SoundboardSoundConverter
:members:
.. attributetable:: discord.ext.commands.clean_content
.. autoclass:: discord.ext.commands.clean_content

11
docs/faq.rst

@ -439,7 +439,7 @@ How can I disable all items on timeout?
This requires three steps.
1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :meth:`Interaction.original_response`.
1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :attr:`InteractionCallbackResponse.resource`.
2. Inside :meth:`~ui.View.on_timeout`, loop over all items inside the view and mark them disabled.
3. Edit the message we retrieved in step 1 with the newly modified view.
@ -467,7 +467,7 @@ Putting it all together, we can do this in a text command:
# Step 1
view.message = await ctx.send('Press me!', view=view)
Application commands do not return a message when you respond with :meth:`InteractionResponse.send_message`, therefore in order to reliably do this we should retrieve the message using :meth:`Interaction.original_response`.
Application commands, when you respond with :meth:`InteractionResponse.send_message`, return an instance of :class:`InteractionCallbackResponse` which contains the message you sent. This is the message you should attach to the view.
Putting it all together, using the previous view definition:
@ -477,10 +477,13 @@ Putting it all together, using the previous view definition:
async def more_timeout_example(interaction):
"""Another example to showcase disabling buttons on timing out"""
view = MyView()
await interaction.response.send_message('Press me!', view=view)
callback = await interaction.response.send_message('Press me!', view=view)
# Step 1
view.message = await interaction.original_response()
resource = callback.resource
# making sure it's an interaction response message
if isinstance(resource, discord.InteractionMessage):
view.message = resource
Application Commands

16
docs/interactions/api.rst

@ -28,6 +28,22 @@ InteractionResponse
.. autoclass:: InteractionResponse()
:members:
InteractionCallbackResponse
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: InteractionCallbackResponse
.. autoclass:: InteractionCallbackResponse()
:members:
InteractionCallbackActivityInstance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. attributetable:: InteractionCallbackActivityInstance
.. autoclass:: InteractionCallbackActivityInstance()
:members:
InteractionMessage
~~~~~~~~~~~~~~~~~~~

154
docs/whats_new.rst

@ -11,6 +11,160 @@ Changelog
This page keeps a detailed human friendly rendering of what's new and changed
in specific versions.
.. _vp2p5p0:
v2.5.0
-------
New Features
~~~~~~~~~~~~~
- Add support for message forwarding (:issue:`9950`)
- Adds :class:`MessageReferenceType`
- Adds :class:`MessageSnapshot`
- Adds ``type`` parameter to :class:`MessageReference`, :meth:`MessageReference.from_message`, and :meth:`PartialMessage.to_reference`
- Add :meth:`PartialMessage.forward`
- Add SKU subscriptions support (:issue:`9930`)
- Adds new events :func:`on_subscription_create`, :func:`on_subscription_update`, and :func:`on_subscription_delete`
- Add :class:`SubscriptionStatus` enum
- Add :class:`Subscription` model
- Add :meth:`SKU.fetch_subscription` and :meth:`SKU.subscriptions`
- Add support for application emojis (:issue:`9891`)
- Add :meth:`Client.create_application_emoji`
- Add :meth:`Client.fetch_application_emoji`
- Add :meth:`Client.fetch_application_emojis`
- Add :meth:`Emoji.is_application_owned`
- Support for Soundboard and VC effects (:issue:`9349`)
- Add :class:`BaseSoundboardSound`, :class:`SoundboardDefaultSound`, and :class:`SoundboardSound`
- Add :class:`VoiceChannelEffect`
- Add :class:`VoiceChannelEffectAnimation`
- Add :class:`VoiceChannelEffectAnimationType`
- Add :class:`VoiceChannelSoundEffect`
- Add :meth:`VoiceChannel.send_sound`
- Add new audit log actions: :attr:`AuditLogAction.soundboard_sound_create`, :attr:`AuditLogAction.soundboard_sound_update`, and :attr:`AuditLogAction.soundboard_sound_delete`.
- Add :attr:`Intents.expressions` and make :attr:`Intents.emojis` and :attr:`Intents.emojis_and_stickers` aliases of that intent.
- Add new events: :func:`on_soundboard_sound_create`, :func:`on_soundboard_sound_update`, :func:`on_soundboard_sound_delete`, and :func:`on_voice_channel_effect`.
- Add methods and properties dealing with soundboards:
- :attr:`Client.soundboard_sounds`
- :attr:`Guild.soundboard_sounds`
- :meth:`Client.get_soundboard_sound`
- :meth:`Guild.get_soundboard_sound`
- :meth:`Client.fetch_soundboard_default_sounds`
- :meth:`Guild.fetch_soundboard_sound`
- :meth:`Guild.fetch_soundboard_sounds`
- :meth:`Guild.create_soundboard_sound`
- Add support for retrieving interaction responses when sending a response (:issue:`9957`)
- Methods from :class:`InteractionResponse` now return :class:`InteractionCallbackResponse`
- Depending on the interaction response type, :attr:`InteractionCallbackResponse.resource` will be different
- Add :attr:`PartialWebhookChannel.mention` attribute (:issue:`10101`)
- Add support for sending stateless views for :class:`SyncWebhook` or webhooks with no state (:issue:`10089`)
- Add
- Add richer :meth:`Role.move` interface (:issue:`10100`)
- Add support for :class:`EmbedFlags` via :attr:`Embed.flags` (:issue:`10085`)
- Add new flags for :class:`AttachmentFlags` (:issue:`10085`)
- Add :func:`on_raw_presence_update` event that does not depend on cache state (:issue:`10048`)
- This requires setting the ``enable_raw_presences`` keyword argument within :class:`Client`.
- Add :attr:`ForumChannel.members` property. (:issue:`10034`)
- Add ``exclude_deleted`` parameter to :meth:`Client.entitlements` (:issue:`10027`)
- Add :meth:`Client.fetch_guild_preview` (:issue:`9986`)
- Add :meth:`AutoShardedClient.fetch_session_start_limits` (:issue:`10007`)
- Add :attr:`PartialMessageable.mention` (:issue:`9988`)
- Add command target to :class:`MessageInteractionMetadata` (:issue:`10004`)
- :attr:`MessageInteractionMetadata.target_user`
- :attr:`MessageInteractionMetadata.target_message_id`
- :attr:`MessageInteractionMetadata.target_message`
- Add :attr:`Message.forward` flag (:issue:`9978`)
- Add support for purchase notification messages (:issue:`9906`)
- Add new type :attr:`MessageType.purchase_notification`
- Add new models :class:`GuildProductPurchase` and :class:`PurchaseNotification`
- Add :attr:`Message.purchase_notification`
- Add ``category`` parameter to :meth:`.abc.GuildChannel.clone` (:issue:`9941`)
- Add support for message call (:issue:`9911`)
- Add new models :class:`CallMessage`
- Add :attr:`Message.call` attribute
- Parse full message for message edit event (:issue:`10035`)
- Adds :attr:`RawMessageUpdateEvent.message` attribute
- Potentially speeds up :func:`on_message_edit` by no longer copying data
- Add support for retrieving and editing integration type configuration (:issue:`9818`)
- This adds :class:`IntegrationTypeConfig`
- Retrievable via :attr:`AppInfo.guild_integration_config` and :attr:`AppInfo.user_integration_config`.
- Editable via :meth:`AppInfo.edit`
- Allow passing ``None`` for ``scopes`` parameter in :func:`utils.oauth_url` (:issue:`10078`)
- Add support for :attr:`MessageType.poll_result` messages (:issue:`9905`)
- Add various new :class:`MessageFlags`
- Add :meth:`Member.fetch_voice` (:issue:`9908`)
- Add :attr:`Guild.dm_spam_detected_at` and :meth:`Guild.is_dm_spam_detected` (:issue:`9808`)
- Add :attr:`Guild.raid_detected_at` and :meth:`Guild.is_raid_detected` (:issue:`9808`)
- Add :meth:`Client.fetch_premium_sticker_pack` (:issue:`9909`)
- Add :attr:`AppInfo.approximate_user_install_count` (:issue:`9915`)
- Add :meth:`Guild.fetch_role` (:issue:`9921`)
- Add :attr:`Attachment.title` (:issue:`9904`)
- Add :attr:`Member.guild_banner` and :attr:`Member.display_banner`
- Re-add ``connector`` parameter that was removed during v2.0 (:issue:`9900`)
- |commands| Add :class:`~discord.ext.commands.SoundboardSoundConverter` (:issue:`9973`)
Bug Fixes
~~~~~~~~~~
- Change the default file size limit for :attr:`Guild.filesize_limit` to match new Discord limit of 10 MiB (:issue:`10084`)
- Handle improper 1000 close code closures by Discord
- This fixes an issue causing excessive IDENTIFY in large bots
- Fix potential performance regression when dealing with cookies in the library owned session (:issue:`9916`)
- Add support for AEAD XChaCha20 Poly1305 encryption mode (:issue:`9953`)
- This allows voice to continue working when the older encryption modes eventually get removed.
- Support for DAVE is still tentative.
- Fix large performance regression due to polls when creating messages
- Fix cases where :attr:`Member.roles` contains a ``None`` role (:issue:`10093`)
- Update all channel clone implementations to work as expected (:issue:`9935`)
- Fix bug in :meth:`Client.entitlements` only returning 100 entries (:issue:`10051`)
- Fix :meth:`TextChannel.clone` always sending slowmode when not applicable to news channels (:issue:`9967`)
- Fix :attr:`Message.system_content` for :attr:`MessageType.role_subscription_purchase` renewals (:issue:`9955`)
- Fix :attr:`Sticker.url` for GIF stickers (:issue:`9913`)
- Fix :attr:`User.default_avatar` for team users and webhooks (:issue:`9907`)
- Fix potential rounding error in :attr:`Poll.duration` (:issue:`9903`)
- Fix introduced potential TypeError when raising :exc:`app_commands.CommandSyncFailure`
- Fix :attr:`AuditLogEntry.target` causing errors for :attr:`AuditLogAction.message_pin` and :attr:`AuditLogAction.message_unpin` actions (:issue:`10061`).
- Fix incorrect :class:`ui.Select` maximum option check (:issue:`9878`, :issue:`9879`)
- Fix path sanitisation for absolute Windows paths when using ``__main__`` (:issue:`10096`, :issue:`10097`)
- |tasks| Fix race condition when setting timer handle when using uvloop (:issue:`10020`)
- |commands| Fix issue with category cooldowns outside of guild channels (:issue:`9959`)
- |commands| Fix :meth:`Context.defer <ext.commands.Context.defer>` unconditionally deferring
- |commands| Fix callable FlagConverter defaults on hybrid commands not being called (:issue:`10037`)
- |commands| Unwrap :class:`~discord.ext.commands.Parameter` if given as default to :func:`~ext.commands.parameter` (:issue:`9977`)
- |commands| Fix fallback behaviour not being respected when calling replace for :class:`~.ext.commands.Parameter` (:issue:`10076`, :issue:`10077`)
- |commands| Respect ``enabled`` keyword argument for hybrid app commands (:issue:`10001`)
Miscellaneous
~~~~~~~~~~~~~~
- Use a fallback package for ``audioop`` to allow the library to work in Python 3.13 or newer.
- Remove ``aiodns`` from being used on Windows (:issue:`9898`)
- Add zstd gateway compression to ``speed`` extras (:issue:`9947`)
- This can be installed using ``discord.py[speed]``
- Add proxy support fetching from the CDN (:issue:`9966`)
- Remove ``/`` from being safe from URI encoding when constructing paths internally
- Sanitize invite argument before calling the invite info endpoint
- Avoid returning in finally in specific places to prevent exception swallowing (:issue:`9981`, :issue:`9984`)
- Enforce and create random nonces when creating messages throughout the library
- Revert IPv6 block in the library (:issue:`9870`)
- Allow passing :class:`Permissions` object to :func:`app_commands.default_permissions` decorator (:issue:`9951`, :issue:`9971`)
.. _vp2p4p0:
v2.4.0

1
setup.py

@ -1,6 +1,7 @@
from setuptools import setup
import re
def derive_version() -> str:
version = ''
with open('discord/__init__.py') as f:

Loading…
Cancel
Save