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 c206f650f..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__) @@ -72,6 +72,7 @@ from .automod import * from .poll import * from .soundboard import * from .subscription import * +from .presences import * class VersionInfo(NamedTuple): @@ -82,7 +83,7 @@ class VersionInfo(NamedTuple): serial: int -version_info: VersionInfo = VersionInfo(major=2, minor=5, micro=0, releaselevel='alpha', serial=0) +version_info: VersionInfo = VersionInfo(major=2, minor=6, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) 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 ef7980ec4..b997bd96f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -237,6 +237,15 @@ class Client: To enable these events, this must be set to ``True``. Defaults to ``False``. .. versionadded:: 2.0 + enable_raw_presences: :class:`bool` + Whether to manually enable or disable the :func:`on_raw_presence_update` event. + + Setting this flag to ``True`` requires :attr:`Intents.presences` to be enabled. + + By default, this flag is set to ``True`` only when :attr:`Intents.presences` is enabled and :attr:`Intents.members` + is disabled, otherwise it's set to ``False``. + + .. versionadded:: 2.5 http_trace: :class:`aiohttp.TraceConfig` The trace configuration to use for tracking HTTP requests the library does using ``aiohttp``. This allows you to check requests the library is using. For more information, check the @@ -1204,8 +1213,8 @@ class Client: event: Literal['raw_app_command_permissions_update'], /, *, - check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawAppCommandPermissionsUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawAppCommandPermissionsUpdateEvent: ... @@ -1215,8 +1224,8 @@ class Client: event: Literal['app_command_completion'], /, *, - check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Interaction[Self], Union[Command[Any, ..., Any], ContextMenu]]: ... @@ -1228,8 +1237,8 @@ class Client: event: Literal['automod_rule_create', 'automod_rule_update', 'automod_rule_delete'], /, *, - check: Optional[Callable[[AutoModRule], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AutoModRule], bool]] = ..., + timeout: Optional[float] = ..., ) -> AutoModRule: ... @@ -1239,8 +1248,8 @@ class Client: event: Literal['automod_action'], /, *, - check: Optional[Callable[[AutoModAction], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AutoModAction], bool]] = ..., + timeout: Optional[float] = ..., ) -> AutoModAction: ... @@ -1252,8 +1261,8 @@ class Client: event: Literal['private_channel_update'], /, *, - check: Optional[Callable[[GroupChannel, GroupChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GroupChannel, GroupChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[GroupChannel, GroupChannel]: ... @@ -1263,8 +1272,8 @@ class Client: event: Literal['private_channel_pins_update'], /, *, - check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[PrivateChannel, datetime.datetime], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[PrivateChannel, datetime.datetime]: ... @@ -1274,8 +1283,8 @@ class Client: event: Literal['guild_channel_delete', 'guild_channel_create'], /, *, - check: Optional[Callable[[GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> GuildChannel: ... @@ -1285,8 +1294,8 @@ class Client: event: Literal['guild_channel_update'], /, *, - check: Optional[Callable[[GuildChannel, GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel, GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[GuildChannel, GuildChannel]: ... @@ -1302,7 +1311,7 @@ class Client: bool, ] ], - timeout: Optional[float] = None, + timeout: Optional[float] = ..., ) -> Tuple[Union[GuildChannel, Thread], Optional[datetime.datetime]]: ... @@ -1312,8 +1321,8 @@ class Client: event: Literal['typing'], /, *, - check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Messageable, Union[User, Member], datetime.datetime], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Messageable, Union[User, Member], datetime.datetime]: ... @@ -1323,8 +1332,8 @@ class Client: event: Literal['raw_typing'], /, *, - check: Optional[Callable[[RawTypingEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawTypingEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawTypingEvent: ... @@ -1336,8 +1345,8 @@ class Client: event: Literal['connect', 'disconnect', 'ready', 'resumed'], /, *, - check: Optional[Callable[[], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[], bool]] = ..., + timeout: Optional[float] = ..., ) -> None: ... @@ -1347,8 +1356,8 @@ class Client: event: Literal['shard_connect', 'shard_disconnect', 'shard_ready', 'shard_resumed'], /, *, - check: Optional[Callable[[int], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[int], bool]] = ..., + timeout: Optional[float] = ..., ) -> int: ... @@ -1358,8 +1367,8 @@ class Client: event: Literal['socket_event_type', 'socket_raw_receive'], /, *, - check: Optional[Callable[[str], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[str], bool]] = ..., + timeout: Optional[float] = ..., ) -> str: ... @@ -1369,8 +1378,8 @@ class Client: event: Literal['socket_raw_send'], /, *, - check: Optional[Callable[[Union[str, bytes]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Union[str, bytes]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Union[str, bytes]: ... @@ -1381,8 +1390,8 @@ class Client: event: Literal['entitlement_create', 'entitlement_update', 'entitlement_delete'], /, *, - check: Optional[Callable[[Entitlement], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Entitlement], bool]] = ..., + timeout: Optional[float] = ..., ) -> Entitlement: ... @@ -1399,8 +1408,8 @@ class Client: ], /, *, - check: Optional[Callable[[Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Guild: ... @@ -1410,8 +1419,8 @@ class Client: event: Literal['guild_update'], /, *, - check: Optional[Callable[[Guild, Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Guild]: ... @@ -1421,8 +1430,8 @@ class Client: event: Literal['guild_emojis_update'], /, *, - check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Sequence[Emoji], Sequence[Emoji]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Sequence[Emoji], Sequence[Emoji]]: ... @@ -1432,8 +1441,8 @@ class Client: event: Literal['guild_stickers_update'], /, *, - check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Sequence[GuildSticker], Sequence[GuildSticker]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Sequence[GuildSticker], Sequence[GuildSticker]]: ... @@ -1443,8 +1452,8 @@ class Client: event: Literal['invite_create', 'invite_delete'], /, *, - check: Optional[Callable[[Invite], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Invite], bool]] = ..., + timeout: Optional[float] = ..., ) -> Invite: ... @@ -1454,8 +1463,8 @@ class Client: event: Literal['audit_log_entry_create'], /, *, - check: Optional[Callable[[AuditLogEntry], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[AuditLogEntry], bool]] = ..., + timeout: Optional[float] = ..., ) -> AuditLogEntry: ... @@ -1467,8 +1476,8 @@ class Client: event: Literal['integration_create', 'integration_update'], /, *, - check: Optional[Callable[[Integration], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Integration], bool]] = ..., + timeout: Optional[float] = ..., ) -> Integration: ... @@ -1478,8 +1487,8 @@ class Client: event: Literal['guild_integrations_update'], /, *, - check: Optional[Callable[[Guild], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild], bool]] = ..., + timeout: Optional[float] = ..., ) -> Guild: ... @@ -1489,8 +1498,8 @@ class Client: event: Literal['webhooks_update'], /, *, - check: Optional[Callable[[GuildChannel], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[GuildChannel], bool]] = ..., + timeout: Optional[float] = ..., ) -> GuildChannel: ... @@ -1500,8 +1509,8 @@ class Client: event: Literal['raw_integration_delete'], /, *, - check: Optional[Callable[[RawIntegrationDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawIntegrationDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawIntegrationDeleteEvent: ... @@ -1513,8 +1522,8 @@ class Client: event: Literal['interaction'], /, *, - check: Optional[Callable[[Interaction[Self]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Interaction[Self]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Interaction[Self]: ... @@ -1526,8 +1535,8 @@ class Client: event: Literal['member_join', 'member_remove'], /, *, - check: Optional[Callable[[Member], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member], bool]] = ..., + timeout: Optional[float] = ..., ) -> Member: ... @@ -1537,8 +1546,8 @@ class Client: event: Literal['raw_member_remove'], /, *, - check: Optional[Callable[[RawMemberRemoveEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMemberRemoveEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMemberRemoveEvent: ... @@ -1548,8 +1557,8 @@ class Client: event: Literal['member_update', 'presence_update'], /, *, - check: Optional[Callable[[Member, Member], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member, Member], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Member, Member]: ... @@ -1559,8 +1568,8 @@ class Client: event: Literal['user_update'], /, *, - check: Optional[Callable[[User, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[User, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[User, User]: ... @@ -1570,8 +1579,8 @@ class Client: event: Literal['member_ban'], /, *, - check: Optional[Callable[[Guild, Union[User, Member]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, Union[User, Member]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, Union[User, Member]]: ... @@ -1581,8 +1590,8 @@ class Client: event: Literal['member_unban'], /, *, - check: Optional[Callable[[Guild, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Guild, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Guild, User]: ... @@ -1594,8 +1603,8 @@ class Client: event: Literal['message', 'message_delete'], /, *, - check: Optional[Callable[[Message], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message], bool]] = ..., + timeout: Optional[float] = ..., ) -> Message: ... @@ -1605,8 +1614,8 @@ class Client: event: Literal['message_edit'], /, *, - check: Optional[Callable[[Message, Message], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message, Message], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Message, Message]: ... @@ -1616,8 +1625,8 @@ class Client: event: Literal['bulk_message_delete'], /, *, - check: Optional[Callable[[List[Message]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[List[Message]], bool]] = ..., + timeout: Optional[float] = ..., ) -> List[Message]: ... @@ -1627,8 +1636,8 @@ class Client: event: Literal['raw_message_edit'], /, *, - check: Optional[Callable[[RawMessageUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMessageUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMessageUpdateEvent: ... @@ -1638,8 +1647,8 @@ class Client: event: Literal['raw_message_delete'], /, *, - check: Optional[Callable[[RawMessageDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawMessageDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawMessageDeleteEvent: ... @@ -1649,8 +1658,8 @@ class Client: event: Literal['raw_bulk_message_delete'], /, *, - check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawBulkMessageDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawBulkMessageDeleteEvent: ... @@ -1662,8 +1671,8 @@ class Client: event: Literal['reaction_add', 'reaction_remove'], /, *, - check: Optional[Callable[[Reaction, Union[Member, User]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Reaction, Union[Member, User]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Reaction, Union[Member, User]]: ... @@ -1673,8 +1682,8 @@ class Client: event: Literal['reaction_clear'], /, *, - check: Optional[Callable[[Message, List[Reaction]], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Message, List[Reaction]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Message, List[Reaction]]: ... @@ -1684,8 +1693,8 @@ class Client: event: Literal['reaction_clear_emoji'], /, *, - check: Optional[Callable[[Reaction], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Reaction], bool]] = ..., + timeout: Optional[float] = ..., ) -> Reaction: ... @@ -1695,8 +1704,8 @@ class Client: event: Literal['raw_reaction_add', 'raw_reaction_remove'], /, *, - check: Optional[Callable[[RawReactionActionEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionActionEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionActionEvent: ... @@ -1706,8 +1715,8 @@ class Client: event: Literal['raw_reaction_clear'], /, *, - check: Optional[Callable[[RawReactionClearEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionClearEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionClearEvent: ... @@ -1717,8 +1726,8 @@ class Client: event: Literal['raw_reaction_clear_emoji'], /, *, - check: Optional[Callable[[RawReactionClearEmojiEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawReactionClearEmojiEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawReactionClearEmojiEvent: ... @@ -1730,8 +1739,8 @@ class Client: event: Literal['guild_role_create', 'guild_role_delete'], /, *, - check: Optional[Callable[[Role], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Role], bool]] = ..., + timeout: Optional[float] = ..., ) -> Role: ... @@ -1741,8 +1750,8 @@ class Client: event: Literal['guild_role_update'], /, *, - check: Optional[Callable[[Role, Role], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Role, Role], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Role, Role]: ... @@ -1754,8 +1763,8 @@ class Client: event: Literal['scheduled_event_create', 'scheduled_event_delete'], /, *, - check: Optional[Callable[[ScheduledEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ScheduledEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> ScheduledEvent: ... @@ -1765,8 +1774,8 @@ class Client: event: Literal['scheduled_event_user_add', 'scheduled_event_user_remove'], /, *, - check: Optional[Callable[[ScheduledEvent, User], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ScheduledEvent, User], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[ScheduledEvent, User]: ... @@ -1778,8 +1787,8 @@ class Client: event: Literal['stage_instance_create', 'stage_instance_delete'], /, *, - check: Optional[Callable[[StageInstance], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[StageInstance], bool]] = ..., + timeout: Optional[float] = ..., ) -> StageInstance: ... @@ -1789,8 +1798,8 @@ class Client: event: Literal['stage_instance_update'], /, *, - check: Optional[Callable[[StageInstance, StageInstance], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[StageInstance, StageInstance], bool]] = ..., + timeout: Optional[float] = ..., ) -> Coroutine[Any, Any, Tuple[StageInstance, StageInstance]]: ... @@ -1801,8 +1810,8 @@ class Client: event: Literal['subscription_create', 'subscription_update', 'subscription_delete'], /, *, - check: Optional[Callable[[Subscription], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Subscription], bool]] = ..., + timeout: Optional[float] = ..., ) -> Subscription: ... @@ -1813,8 +1822,8 @@ class Client: event: Literal['thread_create', 'thread_join', 'thread_remove', 'thread_delete'], /, *, - check: Optional[Callable[[Thread], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Thread], bool]] = ..., + timeout: Optional[float] = ..., ) -> Thread: ... @@ -1824,8 +1833,8 @@ class Client: event: Literal['thread_update'], /, *, - check: Optional[Callable[[Thread, Thread], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Thread, Thread], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Thread, Thread]: ... @@ -1835,8 +1844,8 @@ class Client: event: Literal['raw_thread_update'], /, *, - check: Optional[Callable[[RawThreadUpdateEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadUpdateEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadUpdateEvent: ... @@ -1846,8 +1855,8 @@ class Client: event: Literal['raw_thread_delete'], /, *, - check: Optional[Callable[[RawThreadDeleteEvent], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadDeleteEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadDeleteEvent: ... @@ -1857,8 +1866,8 @@ class Client: event: Literal['thread_member_join', 'thread_member_remove'], /, *, - check: Optional[Callable[[ThreadMember], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[ThreadMember], bool]] = ..., + timeout: Optional[float] = ..., ) -> ThreadMember: ... @@ -1868,8 +1877,8 @@ class Client: event: Literal['raw_thread_member_remove'], /, *, - check: Optional[Callable[[RawThreadMembersUpdate], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[RawThreadMembersUpdate], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawThreadMembersUpdate: ... @@ -1881,8 +1890,8 @@ class Client: event: Literal['voice_state_update'], /, *, - check: Optional[Callable[[Member, VoiceState, VoiceState], bool]], - timeout: Optional[float] = None, + check: Optional[Callable[[Member, VoiceState, VoiceState], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Member, VoiceState, VoiceState]: ... @@ -1894,8 +1903,8 @@ class Client: event: Literal['poll_vote_add', 'poll_vote_remove'], /, *, - check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Union[User, Member], PollAnswer], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Union[User, Member], PollAnswer]: ... @@ -1905,8 +1914,8 @@ class Client: event: Literal['raw_poll_vote_add', 'raw_poll_vote_remove'], /, *, - check: Optional[Callable[[RawPollVoteActionEvent], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[RawPollVoteActionEvent], bool]] = ..., + timeout: Optional[float] = ..., ) -> RawPollVoteActionEvent: ... @@ -1918,8 +1927,8 @@ class Client: event: Literal["command", "command_completion"], /, *, - check: Optional[Callable[[Context[Any]], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Context[Any]], bool]] = ..., + timeout: Optional[float] = ..., ) -> Context[Any]: ... @@ -1929,8 +1938,8 @@ class Client: event: Literal["command_error"], /, *, - check: Optional[Callable[[Context[Any], CommandError], bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[[Context[Any], CommandError], bool]] = ..., + timeout: Optional[float] = ..., ) -> Tuple[Context[Any], CommandError]: ... @@ -1940,8 +1949,8 @@ class Client: event: str, /, *, - check: Optional[Callable[..., bool]] = None, - timeout: Optional[float] = None, + check: Optional[Callable[..., bool]] = ..., + timeout: Optional[float] = ..., ) -> Any: ... diff --git a/discord/components.py b/discord/components.py index 2af2d6d20..b3f978eb1 100644 --- a/discord/components.py +++ b/discord/components.py @@ -196,12 +196,12 @@ class Button(Component): self.label: Optional[str] = data.get('label') self.emoji: Optional[PartialEmoji] try: - self.emoji = PartialEmoji.from_dict(data['emoji']) + self.emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.emoji = None try: - self.sku_id: Optional[int] = int(data['sku_id']) + self.sku_id: Optional[int] = int(data['sku_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.sku_id = None @@ -415,7 +415,7 @@ class SelectOption: @classmethod def from_dict(cls, data: SelectOptionPayload) -> SelectOption: try: - emoji = PartialEmoji.from_dict(data['emoji']) + emoji = PartialEmoji.from_dict(data['emoji']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: emoji = None 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 4fe5f3ffa..7915bcb4b 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -84,13 +84,13 @@ def _create_value_cls(name: str, comparable: bool): # All the type ignores here are due to the type checker being unable to recognise # Runtime type creation without exploding. cls = namedtuple('_EnumValue_' + name, 'name value') - cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>' # type: ignore - cls.__str__ = lambda self: f'{name}.{self.name}' # type: ignore + cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>' + cls.__str__ = lambda self: f'{name}.{self.name}' if comparable: - cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value # type: ignore - cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value # type: ignore - cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value # type: ignore - cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value # type: ignore + cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value + cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value + cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value + cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value return cls @@ -266,6 +266,7 @@ class MessageType(Enum): guild_incident_report_raid = 38 guild_incident_report_false_alarm = 39 purchase_notification = 44 + poll_result = 46 class SpeakingState(Enum): 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..7198c1206 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 @@ -751,7 +751,7 @@ class Context(discord.abc.Messageable, Generic[BotT]): else: return await self.send(content, **kwargs) - def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping]: + def typing(self, *, ephemeral: bool = False) -> Union[Typing, DeferTyping[BotT]]: """Returns an asynchronous context manager that allows you to send a typing indicator to the destination for an indefinite period of time, or 10 seconds if the context manager is called using ``await``. @@ -1078,8 +1078,11 @@ class Context(discord.abc.Messageable, Generic[BotT]): if self.interaction.response.is_done(): msg = await self.interaction.followup.send(**kwargs, wait=True) else: - await self.interaction.response.send_message(**kwargs) - msg = await self.interaction.original_response() + response = await self.interaction.response.send_message(**kwargs) + if not isinstance(response.resource, discord.InteractionMessage): + msg = await self.interaction.original_response() + else: + msg = response.resource if delete_after is not None: await msg.delete(delay=delete_after) 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 0c3cfa0c4..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`. @@ -1080,7 +1081,7 @@ class ExtensionNotFound(ExtensionError): """ def __init__(self, name: str) -> None: - msg = f'Extension {name!r} could not be loaded.' + msg = f'Extension {name!r} could not be loaded or found.' super().__init__(msg, name=name) 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 de806ba9c..20f8c5470 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -63,6 +63,7 @@ __all__ = ( 'RoleFlags', 'AppInstallationType', 'SKUFlags', + 'EmbedFlags', ) BF = TypeVar('BF', bound='BaseFlags') @@ -2173,6 +2174,30 @@ class AttachmentFlags(BaseFlags): """:class:`bool`: Returns ``True`` if the attachment has been edited using the remix feature.""" return 1 << 2 + @flag_value + def spoiler(self): + """:class:`bool`: Returns ``True`` if the attachment was marked as a spoiler. + + .. versionadded:: 2.5 + """ + return 1 << 3 + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the attachment was flagged as sensitive content. + + .. versionadded:: 2.5 + """ + return 1 << 4 + + @flag_value + def animated(self): + """:class:`bool`: Returns ``True`` if the attachment is an animated image. + + .. versionadded:: 2.5 + """ + return 1 << 5 + @fill_with_flags() class RoleFlags(BaseFlags): @@ -2308,3 +2333,67 @@ class SKUFlags(BaseFlags): def user_subscription(self): """:class:`bool`: Returns ``True`` if the SKU is a user subscription.""" return 1 << 8 + + +@fill_with_flags() +class EmbedFlags(BaseFlags): + r"""Wraps up the Discord Embed flags + + .. versionadded:: 2.5 + + .. container:: operations + + .. describe:: x == y + + Checks if two EmbedFlags are equal. + + .. describe:: x != y + + Checks if two EmbedFlags are not equal. + + .. describe:: x | y, x |= y + + Returns an EmbedFlags instance with all enabled flags from + both x and y. + + .. describe:: x ^ y, x ^= y + + Returns an EmbedFlags instance with only flags enabled on + only one of x or y, not on both. + + .. describe:: ~x + + Returns an EmbedFlags instance with all flags inverted from x. + + .. describe:: hash(x) + + Returns the flag's hash. + + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + Note that aliases are not shown. + + .. describe:: bool(b) + + Returns whether any flag is set to ``True``. + + Attributes + ---------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + @flag_value + def contains_explicit_media(self): + """:class:`bool`: Returns ``True`` if the embed was flagged as sensitive content.""" + return 1 << 4 + + @flag_value + def content_inventory_entry(self): + """:class:`bool`: Returns ``True`` if the embed is a reply to an activity card, and is no + longer displayed. + """ + return 1 << 5 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 faf64e279..20a50d4e9 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -95,7 +95,7 @@ from .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji from .soundboard import SoundboardSound - +from .presences import RawPresenceUpdateEvent __all__ = ( 'Guild', @@ -551,7 +551,8 @@ class Guild(Hashable): member = self.get_member(user_id) if member is None: try: - member = Member(data=data['member'], state=self._state, guild=self) + member_data = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] + member = Member(data=member_data, state=self._state, guild=self) except KeyError: member = None @@ -573,7 +574,7 @@ class Guild(Hashable): def _from_data(self, guild: GuildPayload) -> None: try: - self._member_count = guild['member_count'] + self._member_count = guild['member_count'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass @@ -653,10 +654,11 @@ class Guild(Hashable): empty_tuple = () for presence in guild.get('presences', []): - user_id = int(presence['user']['id']) - member = self.get_member(user_id) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self._state) + member = self.get_member(raw_presence.user_id) + if member is not None: - member._presence_update(presence, empty_tuple) # type: ignore + member._presence_update(raw_presence, empty_tuple) # type: ignore if 'threads' in guild: threads = guild['threads'] 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..a983d8ab0 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=_InteractionMessageState(self._parent, self._state), # pyright: ignore[reportArgumentType] + channel=self._parent.channel, # type: ignore # channel should be the correct type here + data=message, + ) + elif activity_instance is not None: + self.resource = InteractionCallbackActivityInstance(activity_instance) + + def is_thinking(self) -> bool: + """:class:`bool`: Whether the response was a thinking defer.""" + return self._thinking + + def is_ephemeral(self) -> bool: + """:class:`bool`: Whether the response was ephemeral.""" + return self._ephemeral + + class InteractionResponse(Generic[ClientT]): """Represents a Discord interaction response. @@ -653,7 +772,12 @@ class InteractionResponse(Generic[ClientT]): """:class:`InteractionResponseType`: The type of response that was sent, ``None`` if response is not done.""" return self._response_type - async def defer(self, *, ephemeral: bool = False, thinking: bool = False) -> None: + async def defer( + self, + *, + ephemeral: bool = False, + thinking: bool = False, + ) -> Optional[InteractionCallbackResponse[ClientT]]: """|coro| Defers the interaction response. @@ -667,6 +791,9 @@ class InteractionResponse(Generic[ClientT]): - :attr:`InteractionType.component` - :attr:`InteractionType.modal_submit` + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- ephemeral: :class:`bool` @@ -685,6 +812,11 @@ class InteractionResponse(Generic[ClientT]): Deferring the interaction failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallbackResponse`] + The interaction callback resource, or ``None``. """ if self._response_type: raise InteractionResponded(self._parent) @@ -709,7 +841,7 @@ class InteractionResponse(Generic[ClientT]): adapter = async_context.get() params = interaction_response_params(type=defer_type, data=data) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -718,6 +850,12 @@ class InteractionResponse(Generic[ClientT]): params=params, ) self._response_type = InteractionResponseType(defer_type) + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) async def pong(self) -> None: """|coro| @@ -767,11 +905,14 @@ class InteractionResponse(Generic[ClientT]): silent: bool = False, delete_after: Optional[float] = None, poll: Poll = MISSING, - ) -> None: + ) -> InteractionCallbackResponse[ClientT]: """|coro| Responds to this interaction by sending a message. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -825,6 +966,11 @@ class InteractionResponse(Generic[ClientT]): The length of ``embeds`` was invalid. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallbackResponse` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -855,7 +1001,7 @@ class InteractionResponse(Generic[ClientT]): ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -886,6 +1032,13 @@ class InteractionResponse(Generic[ClientT]): asyncio.create_task(inner_call()) + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + async def edit_message( self, *, @@ -897,12 +1050,15 @@ class InteractionResponse(Generic[ClientT]): allowed_mentions: Optional[AllowedMentions] = MISSING, delete_after: Optional[float] = None, suppress_embeds: bool = MISSING, - ) -> None: + ) -> Optional[InteractionCallbackResponse[ClientT]]: """|coro| Responds to this interaction by editing the original message of a component or modal interaction. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- content: Optional[:class:`str`] @@ -948,6 +1104,11 @@ class InteractionResponse(Generic[ClientT]): You specified both ``embed`` and ``embeds``. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + Optional[:class:`InteractionCallbackResponse`] + The interaction callback data, or ``None`` if editing the message was not possible. """ if self._response_type: raise InteractionResponded(self._parent) @@ -990,7 +1151,7 @@ class InteractionResponse(Generic[ClientT]): ) http = parent._state.http - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -1015,11 +1176,21 @@ class InteractionResponse(Generic[ClientT]): asyncio.create_task(inner_call()) - async def send_modal(self, modal: Modal, /) -> None: + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + + async def send_modal(self, modal: Modal, /) -> InteractionCallbackResponse[ClientT]: """|coro| Responds to this interaction by sending a modal. + .. versionchanged:: 2.5 + This now returns a :class:`InteractionCallbackResponse` instance. + Parameters ----------- modal: :class:`~discord.ui.Modal` @@ -1031,6 +1202,11 @@ class InteractionResponse(Generic[ClientT]): Sending the modal failed. InteractionResponded This interaction has already been responded to before. + + Returns + ------- + :class:`InteractionCallbackResponse` + The interaction callback data. """ if self._response_type: raise InteractionResponded(self._parent) @@ -1041,7 +1217,7 @@ class InteractionResponse(Generic[ClientT]): http = parent._state.http params = interaction_response_params(InteractionResponseType.modal.value, modal.to_dict()) - await adapter.create_interaction_response( + response = await adapter.create_interaction_response( parent.id, parent.token, session=parent._session, @@ -1053,6 +1229,13 @@ class InteractionResponse(Generic[ClientT]): self._parent._state.store_view(modal) self._response_type = InteractionResponseType.modal + return InteractionCallbackResponse( + data=response, + parent=self._parent, + state=self._parent._state, + type=self._response_type, + ) + async def autocomplete(self, choices: Sequence[Choice[ChoiceT]]) -> None: """|coro| @@ -1154,6 +1337,7 @@ class InteractionMessage(Message): view: Optional[View] = MISSING, allowed_mentions: Optional[AllowedMentions] = None, delete_after: Optional[float] = None, + poll: Poll = MISSING, ) -> InteractionMessage: """|coro| @@ -1188,6 +1372,15 @@ class InteractionMessage(Message): then it is silently ignored. .. versionadded:: 2.2 + poll: :class:`~discord.Poll` + The poll to create when editing the message. + + .. versionadded:: 2.5 + + .. note:: + + This is only accepted if the interaction response's :attr:`InteractionResponse.type` + attribute is :attr:`InteractionResponseType.deferred_channel_message`. Raises ------- @@ -1212,6 +1405,7 @@ class InteractionMessage(Message): attachments=attachments, view=view, allowed_mentions=allowed_mentions, + poll=poll, ) if delete_after is not None: await self.delete(delay=delete_after) 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 6118e3267..6af1571f4 100644 --- a/discord/member.py +++ b/discord/member.py @@ -36,13 +36,13 @@ from . import utils from .asset import Asset from .utils import MISSING from .user import BaseUser, ClientUser, User, _UserTag -from .activity import create_activity, ActivityTypes from .permissions import Permissions -from .enums import Status, try_enum +from .enums import Status from .errors import ClientException from .colour import Colour from .object import Object from .flags import MemberFlags +from .presences import ClientStatus __all__ = ( 'VoiceState', @@ -57,10 +57,8 @@ if TYPE_CHECKING: from .channel import DMChannel, VoiceChannel, StageChannel from .flags import PublicUserFlags from .guild import Guild - from .types.activity import ( - ClientStatus as ClientStatusPayload, - PartialPresenceUpdate, - ) + from .activity import ActivityTypes + from .presences import RawPresenceUpdateEvent from .types.member import ( MemberWithUser as MemberWithUserPayload, Member as MemberPayload, @@ -168,46 +166,6 @@ class VoiceState: return f'<{self.__class__.__name__} {inner}>' -class _ClientStatus: - __slots__ = ('_status', 'desktop', 'mobile', 'web') - - def __init__(self): - self._status: str = 'offline' - - self.desktop: Optional[str] = None - self.mobile: Optional[str] = None - self.web: Optional[str] = None - - def __repr__(self) -> str: - attrs = [ - ('_status', self._status), - ('desktop', self.desktop), - ('mobile', self.mobile), - ('web', self.web), - ] - inner = ' '.join('%s=%r' % t for t in attrs) - return f'<{self.__class__.__name__} {inner}>' - - def _update(self, status: str, data: ClientStatusPayload, /) -> None: - self._status = status - - self.desktop = data.get('desktop') - self.mobile = data.get('mobile') - self.web = data.get('web') - - @classmethod - def _copy(cls, client_status: Self, /) -> Self: - self = cls.__new__(cls) # bypass __init__ - - self._status = client_status._status - - self.desktop = client_status.desktop - self.mobile = client_status.mobile - self.web = client_status.web - - return self - - def flatten_user(cls: T) -> T: for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()): # ignore private/special methods @@ -306,6 +264,10 @@ class Member(discord.abc.Messageable, _UserTag): This will be set to ``None`` or a time in the past if the user is not timed out. .. versionadded:: 2.0 + client_status: :class:`ClientStatus` + Model which holds information about the status of the member on various clients/platforms via presence updates. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -318,7 +280,7 @@ class Member(discord.abc.Messageable, _UserTag): 'nick', 'timed_out_until', '_permissions', - '_client_status', + 'client_status', '_user', '_state', '_avatar', @@ -354,7 +316,7 @@ class Member(discord.abc.Messageable, _UserTag): self.joined_at: Optional[datetime.datetime] = utils.parse_time(data.get('joined_at')) self.premium_since: Optional[datetime.datetime] = utils.parse_time(data.get('premium_since')) self._roles: utils.SnowflakeList = utils.SnowflakeList(map(int, data['roles'])) - self._client_status: _ClientStatus = _ClientStatus() + self.client_status: ClientStatus = ClientStatus() self.activities: Tuple[ActivityTypes, ...] = () self.nick: Optional[str] = data.get('nick', None) self.pending: bool = data.get('pending', False) @@ -364,7 +326,7 @@ class Member(discord.abc.Messageable, _UserTag): self._flags: int = data['flags'] self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data') try: - self._permissions = int(data['permissions']) + self._permissions = int(data['permissions']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self._permissions = None @@ -430,7 +392,7 @@ class Member(discord.abc.Messageable, _UserTag): self._roles = utils.SnowflakeList(member._roles, is_sorted=True) self.joined_at = member.joined_at self.premium_since = member.premium_since - self._client_status = _ClientStatus._copy(member._client_status) + self.client_status = member.client_status self.guild = member.guild self.nick = member.nick self.pending = member.pending @@ -456,12 +418,12 @@ class Member(discord.abc.Messageable, _UserTag): # the nickname change is optional, # if it isn't in the payload then it didn't change try: - self.nick = data['nick'] + self.nick = data['nick'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass try: - self.pending = data['pending'] + self.pending = data['pending'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass @@ -473,13 +435,12 @@ class Member(discord.abc.Messageable, _UserTag): self._flags = data.get('flags', 0) self._avatar_decoration_data = data.get('avatar_decoration_data') - def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]: - self.activities = tuple(create_activity(d, self._state) for d in data['activities']) - self._client_status._update(data['status'], data['client_status']) + def _presence_update(self, raw: RawPresenceUpdateEvent, user: UserPayload) -> Optional[Tuple[User, User]]: + self.activities = raw.activities + self.client_status = raw.client_status if len(user) > 1: return self._update_inner_user(user) - return None def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: u = self._user @@ -518,7 +479,7 @@ class Member(discord.abc.Messageable, _UserTag): @property def status(self) -> Status: """:class:`Status`: The member's overall status. If the value is unknown, then it will be a :class:`str` instead.""" - return try_enum(Status, self._client_status._status) + return self.client_status.status @property def raw_status(self) -> str: @@ -526,31 +487,36 @@ class Member(discord.abc.Messageable, _UserTag): .. versionadded:: 1.5 """ - return self._client_status._status + return self.client_status._status @status.setter def status(self, value: Status) -> None: # internal use only - self._client_status._status = str(value) + self.client_status._status = str(value) @property def mobile_status(self) -> Status: """:class:`Status`: The member's status on a mobile device, if applicable.""" - return try_enum(Status, self._client_status.mobile or 'offline') + return self.client_status.mobile_status @property def desktop_status(self) -> Status: """:class:`Status`: The member's status on the desktop client, if applicable.""" - return try_enum(Status, self._client_status.desktop or 'offline') + return self.client_status.desktop_status @property def web_status(self) -> Status: """:class:`Status`: The member's status on the web client, if applicable.""" - return try_enum(Status, self._client_status.web or 'offline') + return self.client_status.web_status def is_on_mobile(self) -> bool: - """:class:`bool`: A helper function that determines if a member is active on a mobile device.""" - return self._client_status.mobile is not None + """A helper function that determines if a member is active on a mobile device. + + Returns + ------- + :class:`bool` + """ + return self.client_status.is_on_mobile() @property def colour(self) -> Colour: @@ -595,7 +561,9 @@ class Member(discord.abc.Messageable, _UserTag): role = g.get_role(role_id) if role: result.append(role) - result.append(g.default_role) + default_role = g.default_role + if default_role: + result.append(default_role) result.sort() return result diff --git a/discord/message.py b/discord/message.py index 3d755e314..05b0da183 100644 --- a/discord/message.py +++ b/discord/message.py @@ -610,6 +610,11 @@ class MessageReference: .. versionadded:: 2.5 message_id: Optional[:class:`int`] The id of the message referenced. + This can be ``None`` when this message reference was retrieved from + a system message of one of the following types: + + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` channel_id: :class:`int` The channel id of the message referenced. guild_id: Optional[:class:`int`] @@ -773,7 +778,7 @@ class MessageInteraction(Hashable): self.user: Union[User, Member] = MISSING try: - payload = data['member'] + payload = data['member'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.user = state.create_user(data['user']) else: @@ -2010,9 +2015,16 @@ class Message(PartialMessage, Hashable): The :class:`TextChannel` or :class:`Thread` that the message was sent from. Could be a :class:`DMChannel` or :class:`GroupChannel` if it's a private message. reference: Optional[:class:`~discord.MessageReference`] - The message that this message references. This is only applicable to messages of - type :attr:`MessageType.pins_add`, crossposted messages created by a - followed channel integration, or message replies. + The message that this message references. This is only applicable to + message replies (:attr:`MessageType.reply`), crossposted messages created by + a followed channel integration, forwarded messages, and messages of type: + + - :attr:`MessageType.pins_add` + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` + - :attr:`MessageType.thread_starter_message` + - :attr:`MessageType.poll_result` + - :attr:`MessageType.context_menu_command` .. versionadded:: 1.5 @@ -2200,7 +2212,8 @@ class Message(PartialMessage, Hashable): self.poll: Optional[Poll] = None try: - self.poll = Poll._from_data(data=data['poll'], message=self, state=state) + poll = data['poll'] # pyright: ignore[reportTypedDictNotRequiredAccess] + self.poll = Poll._from_data(data=poll, message=self, state=state) except KeyError: pass @@ -2214,7 +2227,7 @@ class Message(PartialMessage, Hashable): if self.guild is not None: try: - thread = data['thread'] + thread = data['thread'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2229,7 +2242,7 @@ class Message(PartialMessage, Hashable): # deprecated try: - interaction = data['interaction'] + interaction = data['interaction'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2237,20 +2250,20 @@ class Message(PartialMessage, Hashable): self.interaction_metadata: Optional[MessageInteractionMetadata] = None try: - interaction_metadata = data['interaction_metadata'] + interaction_metadata = data['interaction_metadata'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: self.interaction_metadata = MessageInteractionMetadata(state=state, guild=self.guild, data=interaction_metadata) try: - ref = data['message_reference'] + ref = data['message_reference'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.reference = None else: self.reference = ref = MessageReference.with_state(state, ref) try: - resolved = data['referenced_message'] + resolved = data['referenced_message'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2268,9 +2281,16 @@ class Message(PartialMessage, Hashable): # the channel will be the correct type here ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore + if self.type is MessageType.poll_result: + if isinstance(self.reference.resolved, self.__class__): + self._state._update_poll_results(self, self.reference.resolved) + else: + if self.reference.message_id: + self._state._update_poll_results(self, self.reference.message_id) + self.application: Optional[MessageApplication] = None try: - application = data['application'] + application = data['application'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2278,7 +2298,7 @@ class Message(PartialMessage, Hashable): self.role_subscription: Optional[RoleSubscriptionInfo] = None try: - role_subscription = data['role_subscription_data'] + role_subscription = data['role_subscription_data'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2286,7 +2306,7 @@ class Message(PartialMessage, Hashable): self.purchase_notification: Optional[PurchaseNotification] = None try: - purchase_notification = data['purchase_notification'] + purchase_notification = data['purchase_notification'] # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: pass else: @@ -2294,7 +2314,7 @@ class Message(PartialMessage, Hashable): for handler in ('author', 'member', 'mentions', 'mention_roles', 'components', 'call'): try: - getattr(self, f'_handle_{handler}')(data[handler]) + getattr(self, f'_handle_{handler}')(data[handler]) # type: ignore except KeyError: continue @@ -2634,6 +2654,7 @@ class Message(PartialMessage, Hashable): MessageType.chat_input_command, MessageType.context_menu_command, MessageType.thread_starter_message, + MessageType.poll_result, ) @utils.cached_slot_property('_cs_system_content') @@ -2810,6 +2831,14 @@ class Message(PartialMessage, Hashable): if guild_product_purchase is not None: return f'{self.author.name} has purchased {guild_product_purchase.product_name}!' + if self.type is MessageType.poll_result: + embed = self.embeds[0] # Will always have 1 embed + poll_title = utils.get( + embed.fields, + name='poll_question_text', + ) + return f'{self.author.display_name}\'s poll {poll_title.value} has closed.' # type: ignore + # Fallback for unknown message types return '' diff --git a/discord/poll.py b/discord/poll.py index 720f91245..6ab680abd 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -29,7 +29,7 @@ from typing import Optional, List, TYPE_CHECKING, Union, AsyncIterator, Dict import datetime -from .enums import PollLayoutType, try_enum +from .enums import PollLayoutType, try_enum, MessageType from . import utils from .emoji import PartialEmoji, Emoji from .user import User @@ -125,7 +125,16 @@ class PollAnswer: Whether the current user has voted to this answer or not. """ - __slots__ = ('media', 'id', '_state', '_message', '_vote_count', 'self_voted', '_poll') + __slots__ = ( + 'media', + 'id', + '_state', + '_message', + '_vote_count', + 'self_voted', + '_poll', + '_victor', + ) def __init__( self, @@ -141,6 +150,7 @@ class PollAnswer: self._vote_count: int = 0 self.self_voted: bool = False self._poll: Poll = poll + self._victor: bool = False def _handle_vote_event(self, added: bool, self_voted: bool) -> None: if added: @@ -210,6 +220,19 @@ class PollAnswer: 'poll_media': self.media.to_dict(), } + @property + def victor(self) -> bool: + """:class:`bool`: Whether the answer is the one that had the most + votes when the poll ended. + + .. versionadded:: 2.5 + + .. note:: + + If the poll has not ended, this will always return ``False``. + """ + return self._victor + async def voters( self, *, limit: Optional[int] = None, after: Optional[Snowflake] = None ) -> AsyncIterator[Union[User, Member]]: @@ -313,6 +336,15 @@ class Poll: Defaults to ``False``. layout_type: :class:`PollLayoutType` The layout type of the poll. Defaults to :attr:`PollLayoutType.default`. + + Attributes + ----------- + duration: :class:`datetime.timedelta` + The duration of the poll. + multiple: :class:`bool` + Whether users are allowed to select more than one answer. + layout_type: :class:`PollLayoutType` + The layout type of the poll. """ __slots__ = ( @@ -325,6 +357,8 @@ class Poll: '_expiry', '_finalized', '_state', + '_total_votes', + '_victor_answer_id', ) def __init__( @@ -348,6 +382,8 @@ class Poll: self._state: Optional[ConnectionState] = None self._finalized: bool = False self._expiry: Optional[datetime.datetime] = None + self._total_votes: Optional[int] = None + self._victor_answer_id: Optional[int] = None def _update(self, message: Message) -> None: self._state = message._state @@ -360,6 +396,33 @@ class Poll: self._expiry = message.poll.expires_at self._finalized = message.poll._finalized self._answers = message.poll._answers + self._update_results_from_message(message) + + def _update_results_from_message(self, message: Message) -> None: + if message.type != MessageType.poll_result or not message.embeds: + return + + result_embed = message.embeds[0] # Will always have 1 embed + fields: Dict[str, str] = {field.name: field.value for field in result_embed.fields} # type: ignore + + total_votes = fields.get('total_votes') + + if total_votes is not None: + self._total_votes = int(total_votes) + + victor_answer = fields.get('victor_answer_id') + + if victor_answer is None: + return # Can't do anything else without the victor answer + + self._victor_answer_id = int(victor_answer) + + victor_answer_votes = fields['victor_answer_votes'] + + answer = self._answers[self._victor_answer_id] + answer._victor = True + answer._vote_count = int(victor_answer_votes) + self._answers[answer.id] = answer # Ensure update def _update_results(self, data: PollResultPayload) -> None: self._finalized = data['is_finalized'] @@ -432,6 +495,32 @@ class Poll: """List[:class:`PollAnswer`]: Returns a read-only copy of the answers.""" return list(self._answers.values()) + @property + def victor_answer_id(self) -> Optional[int]: + """Optional[:class:`int`]: The victor answer ID. + + .. versionadded:: 2.5 + + .. note:: + + This will **always** be ``None`` for polls that have not yet finished. + """ + return self._victor_answer_id + + @property + def victor_answer(self) -> Optional[PollAnswer]: + """Optional[:class:`PollAnswer`]: The victor answer. + + .. versionadded:: 2.5 + + .. note:: + + This will **always** be ``None`` for polls that have not yet finished. + """ + if self.victor_answer_id is None: + return None + return self.get_answer(self.victor_answer_id) + @property def expires_at(self) -> Optional[datetime.datetime]: """Optional[:class:`datetime.datetime`]: A datetime object representing the poll expiry. @@ -457,12 +546,20 @@ class Poll: @property def message(self) -> Optional[Message]: - """:class:`Message`: The message this poll is from.""" + """Optional[:class:`Message`]: The message this poll is from.""" return self._message @property def total_votes(self) -> int: - """:class:`int`: Returns the sum of all the answer votes.""" + """:class:`int`: Returns the sum of all the answer votes. + + If the poll has not yet finished, this is an approximate vote count. + + .. versionchanged:: 2.5 + This now returns an exact vote count when updated from its poll results message. + """ + if self._total_votes is not None: + return self._total_votes return sum([answer.vote_count for answer in self.answers]) def is_finalised(self) -> bool: diff --git a/discord/presences.py b/discord/presences.py new file mode 100644 index 000000000..7fec2a09d --- /dev/null +++ b/discord/presences.py @@ -0,0 +1,150 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional, Tuple + +from .activity import create_activity +from .enums import Status, try_enum +from .utils import MISSING, _get_as_snowflake, _RawReprMixin + +if TYPE_CHECKING: + from typing_extensions import Self + + from .activity import ActivityTypes + from .guild import Guild + from .state import ConnectionState + from .types.activity import ClientStatus as ClientStatusPayload, PartialPresenceUpdate + + +__all__ = ( + 'RawPresenceUpdateEvent', + 'ClientStatus', +) + + +class ClientStatus: + """Represents the :ddocs:`Client Status Object ` from Discord, + which holds information about the status of the user on various clients/platforms, with additional helpers. + + .. versionadded:: 2.5 + """ + + __slots__ = ('_status', 'desktop', 'mobile', 'web') + + def __init__(self, *, status: str = MISSING, data: ClientStatusPayload = MISSING) -> None: + self._status: str = status or 'offline' + + data = data or {} + self.desktop: Optional[str] = data.get('desktop') + self.mobile: Optional[str] = data.get('mobile') + self.web: Optional[str] = data.get('web') + + def __repr__(self) -> str: + attrs = [ + ('_status', self._status), + ('desktop', self.desktop), + ('mobile', self.mobile), + ('web', self.web), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f'<{self.__class__.__name__} {inner}>' + + def _update(self, status: str, data: ClientStatusPayload, /) -> None: + self._status = status + + self.desktop = data.get('desktop') + self.mobile = data.get('mobile') + self.web = data.get('web') + + @classmethod + def _copy(cls, client_status: Self, /) -> Self: + self = cls.__new__(cls) # bypass __init__ + + self._status = client_status._status + + self.desktop = client_status.desktop + self.mobile = client_status.mobile + self.web = client_status.web + + return self + + @property + def status(self) -> Status: + """:class:`Status`: The user's overall status. If the value is unknown, then it will be a :class:`str` instead.""" + return try_enum(Status, self._status) + + @property + def raw_status(self) -> str: + """:class:`str`: The user's overall status as a string value.""" + return self._status + + @property + def mobile_status(self) -> Status: + """:class:`Status`: The user's status on a mobile device, if applicable.""" + return try_enum(Status, self.mobile or 'offline') + + @property + def desktop_status(self) -> Status: + """:class:`Status`: The user's status on the desktop client, if applicable.""" + return try_enum(Status, self.desktop or 'offline') + + @property + def web_status(self) -> Status: + """:class:`Status`: The user's status on the web client, if applicable.""" + return try_enum(Status, self.web or 'offline') + + def is_on_mobile(self) -> bool: + """:class:`bool`: A helper function that determines if a user is active on a mobile device.""" + return self.mobile is not None + + +class RawPresenceUpdateEvent(_RawReprMixin): + """Represents the payload for a :func:`on_raw_presence_update` event. + + .. versionadded:: 2.5 + + Attributes + ---------- + user_id: :class:`int` + The ID of the user that triggered the presence update. + guild_id: Optional[:class:`int`] + The guild ID for the users presence update. Could be ``None``. + guild: Optional[:class:`Guild`] + The guild associated with the presence update and user. Could be ``None``. + client_status: :class:`ClientStatus` + The :class:`~.ClientStatus` model which holds information about the status of the user on various clients. + activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]] + The activities the user is currently doing. Due to a Discord API limitation, a user's Spotify activity may not appear + if they are listening to a song with a title longer than ``128`` characters. See :issue:`1738` for more information. + """ + + __slots__ = ('user_id', 'guild_id', 'guild', 'client_status', 'activities') + + def __init__(self, *, data: PartialPresenceUpdate, state: ConnectionState) -> None: + self.user_id: int = int(data['user']['id']) + self.client_status: ClientStatus = ClientStatus(status=data['status'], data=data['client_status']) + self.activities: Tuple[ActivityTypes, ...] = tuple(create_activity(d, state) for d in data['activities']) + self.guild_id: Optional[int] = _get_as_snowflake(data, 'guild_id') + self.guild: Optional[Guild] = state._get_guild(self.guild_id) diff --git a/discord/raw_models.py b/discord/raw_models.py index 012b8f07d..8304559a1 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -25,10 +25,10 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Literal, Optional, Set, List, Tuple, Union +from typing import TYPE_CHECKING, Literal, Optional, Set, List, Union from .enums import ChannelType, try_enum, ReactionType -from .utils import _get_as_snowflake +from .utils import _get_as_snowflake, _RawReprMixin from .app_commands import AppCommandPermissions from .colour import Colour @@ -82,14 +82,6 @@ __all__ = ( ) -class _RawReprMixin: - __slots__: Tuple[str, ...] = () - - def __repr__(self) -> str: - value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) - return f'<{self.__class__.__name__} {value}>' - - class RawMessageDeleteEvent(_RawReprMixin): """Represents the event payload for a :func:`on_raw_message_delete` event. @@ -112,7 +104,7 @@ class RawMessageDeleteEvent(_RawReprMixin): self.channel_id: int = int(data['channel_id']) self.cached_message: Optional[Message] = None try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -140,7 +132,7 @@ class RawBulkMessageDeleteEvent(_RawReprMixin): self.cached_messages: List[Message] = [] try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -256,7 +248,7 @@ class RawReactionActionEvent(_RawReprMixin): self.type: ReactionType = try_enum(ReactionType, data['type']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -289,7 +281,7 @@ class RawReactionClearEvent(_RawReprMixin): self.channel_id: int = int(data['channel_id']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -319,7 +311,7 @@ class RawReactionClearEmojiEvent(_RawReprMixin): self.channel_id: int = int(data['channel_id']) try: - self.guild_id: Optional[int] = int(data['guild_id']) + self.guild_id: Optional[int] = int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: self.guild_id: Optional[int] = None @@ -346,7 +338,9 @@ class RawIntegrationDeleteEvent(_RawReprMixin): self.guild_id: int = int(data['guild_id']) try: - self.application_id: Optional[int] = int(data['application_id']) + self.application_id: Optional[int] = int( + data['application_id'] # pyright: ignore[reportTypedDictNotRequiredAccess] + ) except KeyError: self.application_id: Optional[int] = None 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 8dad83a88..0fbeadea2 100644 --- a/discord/state.py +++ b/discord/state.py @@ -62,6 +62,7 @@ from .message import Message from .channel import * from .channel import _channel_factory from .raw_models import * +from .presences import RawPresenceUpdateEvent from .member import Member from .role import Role from .enums import ChannelType, try_enum, Status @@ -261,6 +262,10 @@ class ConnectionState(Generic[ClientT]): if not intents.members or cache_flags._empty: self.store_user = self.store_user_no_intents + self.raw_presence_flag: bool = options.get('enable_raw_presences', utils.MISSING) + if self.raw_presence_flag is utils.MISSING: + self.raw_presence_flag = not intents.members and intents.presences + self.parsers: Dict[str, Callable[[Any], None]] self.parsers = parsers = {} for attr, func in inspect.getmembers(self): @@ -535,7 +540,7 @@ class ConnectionState(Generic[ClientT]): ) -> Tuple[Union[Channel, Thread], Optional[Guild]]: channel_id = int(data['channel_id']) try: - guild_id = guild_id or int(data['guild_id']) + guild_id = guild_id or int(data['guild_id']) # pyright: ignore[reportTypedDictNotRequiredAccess] guild = self._get_guild(guild_id) except KeyError: channel = DMChannel._from_message(self, channel_id) @@ -552,6 +557,27 @@ class ConnectionState(Generic[ClientT]): poll._handle_vote(answer_id, added, self_voted) return poll + def _update_poll_results(self, from_: Message, to: Union[Message, int]) -> None: + if isinstance(to, Message): + cached = self._get_message(to.id) + elif isinstance(to, int): + cached = self._get_message(to) + + if cached is None: + return + + to = cached + else: + return + + if to.poll is None: + return + + to.poll._update_results_from_message(from_) + + if cached is not None and cached.poll: + cached.poll._update_results_from_message(from_) + async def chunker( self, guild_id: int, query: str = '', limit: int = 0, presences: bool = False, *, nonce: Optional[str] = None ) -> None: @@ -710,7 +736,7 @@ class ConnectionState(Generic[ClientT]): if 'components' in data: try: - entity_id = int(data['interaction']['id']) + entity_id = int(data['interaction']['id']) # pyright: ignore[reportTypedDictNotRequiredAccess] except (KeyError, ValueError): entity_id = raw.message_id @@ -806,22 +832,24 @@ class ConnectionState(Generic[ClientT]): self.dispatch('interaction', interaction) def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None: - guild_id = utils._get_as_snowflake(data, 'guild_id') - # guild_id won't be None here - guild = self._get_guild(guild_id) - if guild is None: - _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id) + raw = RawPresenceUpdateEvent(data=data, state=self) + + if self.raw_presence_flag: + self.dispatch('raw_presence_update', raw) + + if raw.guild is None: + _log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', raw.guild_id) return - user = data['user'] - member_id = int(user['id']) - member = guild.get_member(member_id) + member = raw.guild.get_member(raw.user_id) + if member is None: - _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id) + _log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', raw.user_id) return old_member = Member._copy(member) - user_update = member._presence_update(data=data, user=user) + user_update = member._presence_update(raw=raw, user=data['user']) + if user_update: self.dispatch('user_update', user_update[0], user_update[1]) @@ -907,7 +935,7 @@ class ConnectionState(Generic[ClientT]): def parse_channel_pins_update(self, data: gw.ChannelPinsUpdateEvent) -> None: channel_id = int(data['channel_id']) try: - guild = self._get_guild(int(data['guild_id'])) + guild = self._get_guild(int(data['guild_id'])) # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: guild = None channel = self._get_private_channel(channel_id) @@ -989,7 +1017,7 @@ class ConnectionState(Generic[ClientT]): return try: - channel_ids = {int(i) for i in data['channel_ids']} + channel_ids = {int(i) for i in data['channel_ids']} # pyright: ignore[reportTypedDictNotRequiredAccess] except KeyError: # If not provided, then the entire guild is being synced # So all previous thread data should be overwritten @@ -1409,8 +1437,10 @@ class ConnectionState(Generic[ClientT]): user = presence['user'] member_id = user['id'] member = member_dict.get(member_id) + if member is not None: - member._presence_update(presence, user) + raw_presence = RawPresenceUpdateEvent(data=presence, state=self) + member._presence_update(raw_presence, user) complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count') self.process_chunk_requests(guild_id, data.get('nonce'), members, complete) @@ -1523,12 +1553,8 @@ class ConnectionState(Generic[ClientT]): def parse_guild_scheduled_event_delete(self, data: gw.GuildScheduledEventDeleteEvent) -> None: guild = self._get_guild(int(data['guild_id'])) if guild is not None: - try: - scheduled_event = guild._scheduled_events.pop(int(data['id'])) - except KeyError: - pass - else: - self.dispatch('scheduled_event_delete', scheduled_event) + scheduled_event = guild._scheduled_events.pop(int(data['id']), ScheduledEvent(state=self, data=data)) + self.dispatch('scheduled_event_delete', scheduled_event) else: _log.debug('SCHEDULED_EVENT_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id']) diff --git a/discord/subscription.py b/discord/subscription.py index d861615ab..ec6d7c3e5 100644 --- a/discord/subscription.py +++ b/discord/subscription.py @@ -63,6 +63,8 @@ class Subscription(Hashable): canceled_at: Optional[:class:`datetime.datetime`] When the subscription was canceled. This is only available for subscriptions with a :attr:`status` of :attr:`SubscriptionStatus.inactive`. + renewal_sku_ids: List[:class:`int`] + The IDs of the SKUs that the user is going to be subscribed to when renewing. """ __slots__ = ( @@ -75,6 +77,7 @@ class Subscription(Hashable): 'current_period_end', 'status', 'canceled_at', + 'renewal_sku_ids', ) def __init__(self, *, state: ConnectionState, data: SubscriptionPayload): @@ -88,6 +91,7 @@ class Subscription(Hashable): self.current_period_end: datetime.datetime = utils.parse_time(data['current_period_end']) self.status: SubscriptionStatus = try_enum(SubscriptionStatus, data['status']) self.canceled_at: Optional[datetime.datetime] = utils.parse_time(data['canceled_at']) + self.renewal_sku_ids: List[int] = list(map(int, data['renewal_sku_ids'] or [])) def __repr__(self) -> str: return f'' 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 f2f1c5a9f..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): @@ -71,7 +72,7 @@ class EmbedAuthor(TypedDict, total=False): proxy_icon_url: str -EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link'] +EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link', 'poll_result'] class Embed(TypedDict, total=False): @@ -88,3 +89,4 @@ class Embed(TypedDict, total=False): provider: EmbedProvider author: EmbedAuthor fields: List[EmbedField] + flags: int 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/types/message.py b/discord/types/message.py index 1ec86681b..ae38db46f 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -174,6 +174,7 @@ MessageType = Literal[ 38, 39, 44, + 46, ] diff --git a/discord/types/subscription.py b/discord/types/subscription.py index bb707afce..8d4c02070 100644 --- a/discord/types/subscription.py +++ b/discord/types/subscription.py @@ -40,3 +40,4 @@ class Subscription(TypedDict): current_period_end: str status: SubscriptionStatus canceled_at: Optional[str] + renewal_sku_ids: Optional[List[Snowflake]] 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 9b6bd59a2..bcdf922b4 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -108,7 +108,7 @@ __all__ = ( ) DISCORD_EPOCH = 1420070400000 -DEFAULT_FILE_SIZE_LIMIT_BYTES = 26214400 +DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760 class _MissingSentinel: @@ -714,13 +714,13 @@ async def maybe_coroutine(f: MaybeAwaitableFunc[P, T], *args: P.args, **kwargs: if _isawaitable(value): return await value else: - return value # type: ignore + return value async def async_all( gen: Iterable[Union[T, Awaitable[T]]], *, - check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable, + check: Callable[[Union[T, Awaitable[T]]], TypeGuard[Awaitable[T]]] = _isawaitable, # type: ignore ) -> bool: for elem in gen: if check(elem): @@ -1121,7 +1121,7 @@ def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]: literal_cls = type(Literal[0]) for p in parameters: if isinstance(p, literal_cls): - params.extend(p.__args__) + params.extend(p.__args__) # type: ignore else: params.append(p) return tuple(params) @@ -1532,3 +1532,11 @@ def _format_call_duration(duration: datetime.timedelta) -> str: formatted = f"{years} years" return formatted + + +class _RawReprMixin: + __slots__: Tuple[str, ...] = () + + def __repr__(self) -> str: + value = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__) + return f'<{self.__class__.__name__} {value}>' 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 0b4015f78..73e0238fc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -80,6 +80,14 @@ AppInstallParams .. autoclass:: AppInstallParams() :members: +IntegrationTypeConfig +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: IntegrationTypeConfig + +.. autoclass:: IntegrationTypeConfig() + :members: + Team ~~~~~ @@ -916,6 +924,29 @@ Members :param after: The updated member's updated info. :type after: :class:`Member` +.. function:: on_raw_presence_update(payload) + + Called when a :class:`Member` updates their presence. + + This requires :attr:`Intents.presences` to be enabled. + + Unlike :func:`on_presence_update`, when enabled, this is called regardless of the state of internal guild + and member caches, and **does not** provide a comparison between the previous and updated states of the :class:`Member`. + + .. important:: + + By default, this event is only dispatched when :attr:`Intents.presences` is enabled **and** :attr:`Intents.members` + is disabled. + + You can manually override this behaviour by setting the **enable_raw_presences** flag in the :class:`Client`, + however :attr:`Intents.presences` is always required for this event to work. + + .. versionadded:: 2.5 + + :param payload: The raw presence update event model. + :type payload: :class:`RawPresenceUpdateEvent` + + Messages ~~~~~~~~~ @@ -1887,6 +1918,10 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 + .. attribute:: poll_result + + The system message sent when a poll has closed. + .. class:: UserFlags Represents Discord User flags. @@ -3816,17 +3851,25 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 - .. attribute:: reply + .. attribute:: default - A message reply. + A standard reference used by message replies (:attr:`MessageType.reply`), + crossposted messaged created by a followed channel integration, and messages of type: + + - :attr:`MessageType.pins_add` + - :attr:`MessageType.channel_follow_add` + - :attr:`MessageType.thread_created` + - :attr:`MessageType.thread_starter_message` + - :attr:`MessageType.poll_result` + - :attr:`MessageType.context_menu_command` .. attribute:: forward A forwarded message. - .. attribute:: default + .. attribute:: reply - An alias for :attr:`.reply`. + An alias for :attr:`.default`. .. _discord-api-audit-logs: @@ -5360,6 +5403,14 @@ RawPollVoteActionEvent .. autoclass:: RawPollVoteActionEvent() :members: +RawPresenceUpdateEvent +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: RawPresenceUpdateEvent + +.. autoclass:: RawPresenceUpdateEvent() + :members: + PartialWebhookGuild ~~~~~~~~~~~~~~~~~~~~ @@ -5394,6 +5445,14 @@ MessageSnapshot .. autoclass:: MessageSnapshot :members: +ClientStatus +~~~~~~~~~~~~ + +.. attributetable:: ClientStatus + +.. autoclass:: ClientStatus() + :members: + Data Classes -------------- @@ -5665,6 +5724,14 @@ SKUFlags .. autoclass:: SKUFlags() :members: +EmbedFlags +~~~~~~~~~~ + +.. attributetable:: EmbedFlags + +.. autoclass:: EmbedFlags() + :members: + ForumTag ~~~~~~~~~ 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/faq.rst b/docs/faq.rst index 0cd8b8ad6..16d03362a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -439,7 +439,7 @@ How can I disable all items on timeout? This requires three steps. -1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :meth:`Interaction.original_response`. +1. Attach a message to the :class:`~discord.ui.View` using either the return type of :meth:`~abc.Messageable.send` or retrieving it via :attr:`InteractionCallbackResponse.resource`. 2. Inside :meth:`~ui.View.on_timeout`, loop over all items inside the view and mark them disabled. 3. Edit the message we retrieved in step 1 with the newly modified view. @@ -467,7 +467,7 @@ Putting it all together, we can do this in a text command: # Step 1 view.message = await ctx.send('Press me!', view=view) -Application commands do not return a message when you respond with :meth:`InteractionResponse.send_message`, therefore in order to reliably do this we should retrieve the message using :meth:`Interaction.original_response`. +Application commands, when you respond with :meth:`InteractionResponse.send_message`, return an instance of :class:`InteractionCallbackResponse` which contains the message you sent. This is the message you should attach to the view. Putting it all together, using the previous view definition: @@ -477,10 +477,13 @@ Putting it all together, using the previous view definition: async def more_timeout_example(interaction): """Another example to showcase disabling buttons on timing out""" view = MyView() - await interaction.response.send_message('Press me!', view=view) + callback = await interaction.response.send_message('Press me!', view=view) # Step 1 - view.message = await interaction.original_response() + resource = callback.resource + # making sure it's an interaction response message + if isinstance(resource, discord.InteractionMessage): + view.message = resource Application Commands 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 diff --git a/setup.py b/setup.py index 2481afeb4..e3d6d59f4 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup import re + def derive_version() -> str: version = '' with open('discord/__init__.py') as f: