diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9e70f794f..79b7ac8ec 100644 --- a/.github/workflows/lint.yml +++ b/.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' }} diff --git a/discord/__init__.py b/discord/__init__.py index f850ee4ac..48fe10925 100644 --- a/discord/__init__.py +++ b/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__) @@ -83,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()) diff --git a/discord/__main__.py b/discord/__main__.py index 843274b53..f8556fcdc 100644 --- a/discord/__main__.py +++ b/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) diff --git a/discord/abc.py b/discord/abc.py index 891404b33..70531fb20 100644 --- a/discord/abc.py +++ b/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) diff --git a/discord/activity.py b/discord/activity.py index c692443f9..324bea42f 100644 --- a/discord/activity.py +++ b/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: diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index a872fb4be..d5b8d93b2 100644 --- a/discord/app_commands/commands.py +++ b/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: diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index e7b001727..c18485d8c 100644 --- a/discord/app_commands/transformers.py +++ b/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: diff --git a/discord/app_commands/tree.py b/discord/app_commands/tree.py index 90b9a21ab..3099071c0 100644 --- a/discord/app_commands/tree.py +++ b/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( diff --git a/discord/appinfo.py b/discord/appinfo.py index 932f852c2..990c7c2fe 100644 --- a/discord/appinfo.py +++ b/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 ` 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 ` 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 diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 59d563829..af67855d4 100644 --- a/discord/audit_logs.py +++ b/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]: diff --git a/discord/client.py b/discord/client.py index 83296148c..b997bd96f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -1213,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: ... @@ -1224,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]]: ... @@ -1237,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: ... @@ -1248,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: ... @@ -1261,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]: ... @@ -1272,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]: ... @@ -1283,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: ... @@ -1294,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]: ... @@ -1311,7 +1311,7 @@ class Client: bool, ] ], - timeout: Optional[float] = None, + timeout: Optional[float] = ..., ) -> Tuple[Union[GuildChannel, Thread], Optional[datetime.datetime]]: ... @@ -1321,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]: ... @@ -1332,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: ... @@ -1345,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: ... @@ -1356,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: ... @@ -1367,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: ... @@ -1378,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]: ... @@ -1390,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: ... @@ -1408,8 +1408,8 @@ class Client: ], /, *, - check: Optional[Callable[[Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Guild: ... @@ -1419,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]: ... @@ -1430,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]]: ... @@ -1441,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]]: ... @@ -1452,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: ... @@ -1463,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: ... @@ -1476,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: ... @@ -1487,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: ... @@ -1498,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: ... @@ -1509,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: ... @@ -1522,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]: ... @@ -1535,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: ... @@ -1546,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: ... @@ -1557,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]: ... @@ -1568,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]: ... @@ -1579,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]]: ... @@ -1590,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]: ... @@ -1603,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: ... @@ -1614,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]: ... @@ -1625,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]: ... @@ -1636,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: ... @@ -1647,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: ... @@ -1658,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: ... @@ -1671,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]]: ... @@ -1682,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]]: ... @@ -1693,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: ... @@ -1704,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: ... @@ -1715,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: ... @@ -1726,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: ... @@ -1739,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: ... @@ -1750,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]: ... @@ -1763,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: ... @@ -1774,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]: ... @@ -1787,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: ... @@ -1798,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]]: ... @@ -1810,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: ... @@ -1822,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: ... @@ -1833,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]: ... @@ -1844,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: ... @@ -1855,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: ... @@ -1866,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: ... @@ -1877,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: ... @@ -1890,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]: ... @@ -1903,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]: ... @@ -1914,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: ... @@ -1927,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]: ... @@ -1938,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]: ... @@ -1949,8 +1949,8 @@ class Client: event: str, /, *, - check: Optional[Callable[..., bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[..., bool]] = ..., + timeout: Optional[float] = ..., ) -> Any: ... diff --git a/discord/components.py b/discord/components.py index 141c03cc2..a0fd1148d 100644 --- a/discord/components.py +++ b/discord/components.py @@ -226,12 +226,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 @@ -451,7 +451,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 diff --git a/discord/embeds.py b/discord/embeds.py index 258ef0dfd..4be644688 100644 --- a/discord/embeds.py +++ b/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 ` + :ddocs:`api docs ` 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. diff --git a/discord/enums.py b/discord/enums.py index fc9303d19..082a1a708 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -86,13 +86,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 diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 208948335..8ce872f1a 100644 --- a/discord/ext/commands/bot.py +++ b/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| diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 5a74fa5f3..933039735 100644 --- a/discord/ext/commands/context.py +++ b/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 @@ -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) diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 744a00fd3..d316f6ccc 100644 --- a/discord/ext/commands/converter.py +++ b/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: diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 1c682a957..372fcbedf 100644 --- a/discord/ext/commands/core.py +++ b/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 diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index f81d54b4d..feb4aee27 100644 --- a/discord/ext/commands/errors.py +++ b/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`. diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 8afd29a3d..0766ecae3 100644 --- a/discord/ext/commands/flags.py +++ b/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] diff --git a/discord/ext/commands/hybrid.py b/discord/ext/commands/hybrid.py index af9e63a7b..0857003fa 100644 --- a/discord/ext/commands/hybrid.py +++ b/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 diff --git a/discord/flags.py b/discord/flags.py index 3be323983..1a9d612aa 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -63,6 +63,7 @@ __all__ = ( 'RoleFlags', 'AppInstallationType', 'SKUFlags', + 'EmbedFlags', ) BF = TypeVar('BF', bound='BaseFlags') @@ -2181,6 +2182,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): @@ -2316,3 +2341,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 diff --git a/discord/gateway.py b/discord/gateway.py index d15c617d1..44656df03 100644 --- a/discord/gateway.py +++ b/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': { diff --git a/discord/guild.py b/discord/guild.py index b7e53f0c7..20a50d4e9 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -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 diff --git a/discord/http.py b/discord/http.py index fd0acae37..6617efa27 100644 --- a/discord/http.py +++ b/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} diff --git a/discord/interactions.py b/discord/interactions.py index 49bfbfb07..b9d9a4d11 100644 --- a/discord/interactions.py +++ b/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=self._state, + 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) diff --git a/discord/invite.py b/discord/invite.py index 1d8dd1c8e..dd8cc954a 100644 --- a/discord/invite.py +++ b/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 diff --git a/discord/member.py b/discord/member.py index 2de8fbfc1..6af1571f4 100644 --- a/discord/member.py +++ b/discord/member.py @@ -326,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 @@ -418,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 @@ -561,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 diff --git a/discord/message.py b/discord/message.py index 1010e1c12..8a916083e 100644 --- a/discord/message.py +++ b/discord/message.py @@ -479,7 +479,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: @@ -1906,7 +1906,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 @@ -1920,7 +1921,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: @@ -1935,7 +1936,7 @@ class Message(PartialMessage, Hashable): # deprecated try: - interaction = data['interaction'] + interaction = data['interaction'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -1943,20 +1944,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: @@ -1983,7 +1984,7 @@ class Message(PartialMessage, Hashable): self.application: Optional[MessageApplication] = None try: - application = data['application'] + application = data['application'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -1991,7 +1992,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: @@ -1999,7 +2000,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: @@ -2007,7 +2008,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 diff --git a/discord/poll.py b/discord/poll.py index 767f8ffae..6ab680abd 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -336,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__ = ( diff --git a/discord/raw_models.py b/discord/raw_models.py index c8c8b0e38..8304559a1 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -104,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 @@ -132,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 @@ -248,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 @@ -281,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 @@ -311,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 @@ -338,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 diff --git a/discord/role.py b/discord/role.py index 8530d4a90..d7fe1e08b 100644 --- a/discord/role.py +++ b/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| diff --git a/discord/state.py b/discord/state.py index b1409f809..c4b71b368 100644 --- a/discord/state.py +++ b/discord/state.py @@ -540,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) @@ -736,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 @@ -935,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) @@ -1017,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 diff --git a/discord/threads.py b/discord/threads.py index 272886934..024b22506 100644 --- a/discord/threads.py +++ b/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: diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index 7cca955b7..9452bbbb1 100644 --- a/discord/types/appinfo.py +++ b/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): diff --git a/discord/types/embed.py b/discord/types/embed.py index 376df3a1a..f8354a3f3 100644 --- a/discord/types/embed.py +++ b/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): @@ -88,3 +89,4 @@ class Embed(TypedDict, total=False): provider: EmbedProvider author: EmbedAuthor fields: List[EmbedField] + flags: int diff --git a/discord/types/guild.py b/discord/types/guild.py index e0a1f3e54..7ac90b89e 100644 --- a/discord/types/guild.py +++ b/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): diff --git a/discord/types/interactions.py b/discord/types/interactions.py index a72a5b2ce..3f3516c3a 100644 --- a/discord/types/interactions.py +++ b/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] diff --git a/discord/ui/select.py b/discord/ui/select.py index 6738b9727..1ef085cc5 100644 --- a/discord/ui/select.py +++ b/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 diff --git a/discord/ui/view.py b/discord/ui/view.py index 2341a720f..dd44944ec 100644 --- a/discord/ui/view.py +++ b/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 diff --git a/discord/utils.py b/discord/utils.py index 1caffe7f5..bcdf922b4 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -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) diff --git a/discord/voice_state.py b/discord/voice_state.py index f10a307d6..956f639b8 100644 --- a/discord/voice_state.py +++ b/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') diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 2d9856ae3..3b62b10fa 100644 --- a/discord/webhook/async_.py +++ b/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'' + @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 diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index cf23e977b..171931b12 100644 --- a/discord/webhook/sync.py +++ b/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 diff --git a/discord/widget.py b/discord/widget.py index 822008665..cdb883fd9 100644 --- a/discord/widget.py +++ b/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: diff --git a/docs/api.rst b/docs/api.rst index 8da2ba80c..934335c5a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -80,6 +80,14 @@ AppInstallParams .. autoclass:: AppInstallParams() :members: +IntegrationTypeConfig +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: IntegrationTypeConfig + +.. autoclass:: IntegrationTypeConfig() + :members: + Team ~~~~~ @@ -5708,6 +5716,14 @@ SKUFlags .. autoclass:: SKUFlags() :members: +EmbedFlags +~~~~~~~~~~ + +.. attributetable:: EmbedFlags + +.. autoclass:: EmbedFlags() + :members: + ForumTag ~~~~~~~~~ diff --git a/docs/ext/commands/api.rst b/docs/ext/commands/api.rst index f55225614..3da5cae16 100644 --- a/docs/ext/commands/api.rst +++ b/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 diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index aeb6a25c6..feab66907 100644 --- a/docs/interactions/api.rst +++ b/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 ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/whats_new.rst b/docs/whats_new.rst index d51de610b..d3763a588 100644 --- a/docs/whats_new.rst +++ b/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 ` 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