From fd240be3957e68010926ee95e418b025524e9c07 Mon Sep 17 00:00:00 2001 From: dolfies Date: Sat, 9 Sep 2023 21:01:52 -0400 Subject: [PATCH] Add missing types --- discord/abc.py | 8 +- discord/calls.py | 10 +-- discord/commands.py | 127 ++++++++++++++++++---------- discord/components.py | 81 +++++++++++------- discord/file.py | 19 +++-- discord/http.py | 58 +++++++------ discord/interactions.py | 3 +- discord/member.py | 6 +- discord/message.py | 9 +- discord/modal.py | 11 +-- discord/settings.py | 81 +++++++++--------- discord/state.py | 90 +++++++++++++------- discord/types/activity.py | 7 +- discord/types/channel.py | 4 + discord/types/command.py | 40 +++++++-- discord/types/components.py | 16 +++- discord/types/gateway.py | 153 +++++++++++++++++++++++++++++----- discord/types/interactions.py | 121 +++++++++------------------ discord/types/invite.py | 6 +- discord/types/member.py | 6 ++ discord/types/message.py | 11 ++- discord/types/user.py | 65 ++++++++++++++- 22 files changed, 605 insertions(+), 327 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index e284f0a1a..03abde4ae 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -262,7 +262,7 @@ async def _handle_commands( prev_cursor = cursor cursor = data['cursor'].get('next') cmds = data['application_commands'] - apps: Dict[int, dict] = {int(app['id']): app for app in data.get('applications') or []} + apps = {int(app['id']): app for app in data.get('applications') or []} for cmd in cmds: # Handle faked parameters @@ -280,8 +280,10 @@ async def _handle_commands( except ValueError: pass - cmd['application'] = apps.get(int(cmd['application_id'])) - yield cls(state=state, data=cmd, channel=channel, target=target) + application_data = apps.get(int(cmd['application_id'])) + application = state.create_integration_application(application_data) if application_data else None + + yield cls(state=state, data=cmd, channel=channel, target=target, application=application) cmd_ids = None if application_id or len(cmds) < min(limit if limit else 25, 25) or len(cmds) == limit == 25: diff --git a/discord/calls.py b/discord/calls.py index 5814c46c8..69b63a1da 100644 --- a/discord/calls.py +++ b/discord/calls.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations import datetime -from typing import Callable, Dict, List, Optional, Tuple, TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union from . import utils from .errors import ClientException @@ -39,6 +39,7 @@ if TYPE_CHECKING: from .member import VoiceState from .message import Message from .state import ConnectionState + from .types.gateway import CallCreateEvent, CallUpdateEvent from .user import BaseUser, User _PrivateChannel = Union[abc.DMChannel, abc.GroupChannel] @@ -144,7 +145,7 @@ class PrivateCall: def __init__( self, *, - data: dict, + data: Union[CallCreateEvent, CallUpdateEvent], state: ConnectionState, message: Optional[Message], channel: abc.PrivateChannel, @@ -153,7 +154,6 @@ class PrivateCall: self._cs_message = message self.channel = channel # type: ignore # Will always be a DMChannel here self._ended: bool = False - self._update(data) def _delete(self) -> None: @@ -168,7 +168,7 @@ class PrivateCall: state = self.voice_state_for(user) return bool(state and state.channel and state.channel.id == self.channel.id) - def _update(self, data) -> None: + def _update(self, data: Union[CallCreateEvent, CallUpdateEvent]) -> None: self._message_id = int(data['message_id']) self.unavailable = data.get('unavailable', False) try: @@ -179,7 +179,7 @@ class PrivateCall: channel = self.channel recipients = self._get_recipients() lookup = {u.id: u for u in recipients} - self._ringing = tuple(filter(None, map(lookup.get, data.get('ringing', [])))) + self._ringing = tuple(filter(None, map(lookup.get, [int(x) for x in data.get('ringing', [])]))) for vs in data.get('voice_states', []): self._state._update_voice_state(vs, channel.id) diff --git a/discord/commands.py b/discord/commands.py index 4604f7c1f..68200a7d4 100644 --- a/discord/commands.py +++ b/discord/commands.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Tuple, Type, Union, runtime_checkable +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, runtime_checkable from .enums import ApplicationCommandOptionType, ApplicationCommandType, ChannelType, InteractionType, try_enum from .interactions import _wrapped_interaction @@ -38,8 +38,23 @@ if TYPE_CHECKING: from .file import _FileBase from .guild import Guild from .interactions import Interaction - from .message import Attachment, Message + from .message import Message from .state import ConnectionState + from .types.command import ( + ApplicationCommand as ApplicationCommandPayload, + ApplicationCommandOption, + ApplicationCommandOptionChoice as OptionChoicePayload, + SubCommand as SubCommandPayload, + _ValueApplicationCommandOption as OptionPayload, + ) + from .types.interactions import ( + ApplicationCommandInteractionData, + ChatInputCommandInteractionData, + MessageCommandInteractionData, + UserCommandInteractionData, + ApplicationCommandInteractionDataOption as InteractionDataOption, + ) + from .types.message import PartialAttachment as PartialAttachmentPayload __all__ = ( @@ -113,7 +128,10 @@ class ApplicationCommand(Protocol): return self.name async def __call__( - self, data: dict, files: Optional[List[_FileBase]] = None, channel: Optional[Messageable] = None + self, + data: ApplicationCommandInteractionData, + files: Optional[List[_FileBase]] = None, + channel: Optional[Messageable] = None, ) -> Interaction: channel = channel or self.target_channel if channel is None: @@ -174,7 +192,6 @@ class BaseCommand(ApplicationCommand, Hashable): 'description', 'id', 'version', - 'type', 'application', 'application_id', 'dm_permission', @@ -186,21 +203,27 @@ class BaseCommand(ApplicationCommand, Hashable): '_default_member_permissions', ) + if TYPE_CHECKING: + type: ApplicationCommandType + def __init__( - self, *, state: ConnectionState, data: Dict[str, Any], channel: Optional[Messageable] = None, **kwargs + self, + *, + state: ConnectionState, + data: ApplicationCommandPayload, + channel: Optional[Messageable] = None, + application: Optional[IntegrationApplication] = None, + **kwargs, ) -> None: self._state = state self._data = data + self.application = application self.name = data['name'] self.description = data['description'] self._channel = channel self.application_id: int = int(data['application_id']) self.id: int = int(data['id']) self.version = int(data['version']) - self.type = try_enum(ApplicationCommandType, data['type']) - - application = data.get('application') - self.application = state.create_integration_application(application) if application else None self._default_member_permissions = _get_as_snowflake(data, 'default_member_permissions') dm_permission = data.get('dm_permission') # Null means true? @@ -225,28 +248,29 @@ class SlashMixin(ApplicationCommand, Protocol): async def __call__( self, - options: List[dict], + options: List[InteractionDataOption], files: Optional[List[_FileBase]], - attachments: List[Attachment], + attachments: List[PartialAttachmentPayload], channel: Optional[Messageable] = None, ) -> Interaction: obj = self._parent command = obj._data - command['name_localized'] = command['name'] - data = { + data: ChatInputCommandInteractionData = { 'application_command': command, 'attachments': attachments, 'id': str(obj.id), 'name': obj.name, 'options': options, - 'type': obj.type.value, + 'type': ApplicationCommandType.chat_input.value, 'version': str(obj.version), } if self.guild_id: data['guild_id'] = str(self.guild_id) return await super().__call__(data, files, channel) - def _parse_kwargs(self, kwargs: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[_FileBase], List[Attachment]]: + def _parse_kwargs( + self, kwargs: Dict[str, Any] + ) -> Tuple[List[InteractionDataOption], List[_FileBase], List[PartialAttachmentPayload]]: possible_options = {o.name: o for o in self.options} kwargs = {k: v for k, v in kwargs.items() if k in possible_options} options = [] @@ -286,18 +310,18 @@ class SlashMixin(ApplicationCommand, Protocol): return options, files, attachments - def _unwrap_options(self, data: List[Dict[str, Any]]) -> None: + def _unwrap_options(self, data: List[ApplicationCommandOption]) -> None: options = [] children = [] for option in data: type = try_enum(ApplicationCommandOptionType, option['type']) - if type in { + if type in ( ApplicationCommandOptionType.sub_command, ApplicationCommandOptionType.sub_command_group, - }: - children.append(SubCommand(parent=self, data=option)) + ): + children.append(SubCommand(parent=self, data=option)) # type: ignore else: - options.append(Option(option)) + options.append(Option(option)) # type: ignore self.options = options self.children = children @@ -340,8 +364,6 @@ class UserCommand(BaseCommand): The command's name. description: :class:`str` The command's description, if any. - type: :class:`ApplicationCommandType` - The type of application command. This will always be :attr:`ApplicationCommandType.user`. dm_permission: :class:`bool` Whether the command is enabled in DMs. nsfw: :class:`bool` @@ -386,18 +408,23 @@ class UserCommand(BaseCommand): raise TypeError('__call__() missing 1 required positional argument: \'user\'') command = self._data - data = { + data: UserCommandInteractionData = { 'application_command': command, 'attachments': [], 'id': str(self.id), 'name': self.name, 'options': [], 'target_id': str(user.id), - 'type': self.type.value, + 'type': ApplicationCommandType.user.value, 'version': str(self.version), } return await super().__call__(data, None, channel) + @property + def type(self) -> Literal[ApplicationCommandType.user]: + """:class:`ApplicationCommandType`: The type of application command. This is always :attr:`ApplicationCommandType.user`.""" + return ApplicationCommandType.user + @property def target_user(self) -> Optional[Snowflake]: """Optional[:class:`~abc.Snowflake`]: The user this application command will be used on. @@ -452,8 +479,6 @@ class MessageCommand(BaseCommand): The command's name. description: :class:`str` The command's description, if any. - type: :class:`ApplicationCommandType` - The type of application command. This will always be :attr:`ApplicationCommandType.message`. dm_permission: :class:`bool` Whether the command is enabled in DMs. nsfw: :class:`bool` @@ -498,8 +523,7 @@ class MessageCommand(BaseCommand): raise TypeError('__call__() missing 1 required positional argument: \'message\'') command = self._data - command['name_localized'] = command['name'] - data = { + data: MessageCommandInteractionData = { 'application_command': command, 'attachments': [], 'id': str(self.id), @@ -511,6 +535,11 @@ class MessageCommand(BaseCommand): } return await super().__call__(data, None, channel) + @property + def type(self) -> Literal[ApplicationCommandType.message]: + """:class:`ApplicationCommandType`: The type of application command. This is always :attr:`ApplicationCommandType.message`.""" + return ApplicationCommandType.message + @property def target_message(self) -> Optional[Message]: """Optional[:class:`Message`]: The message this application command will be used on. @@ -565,8 +594,6 @@ class SlashCommand(BaseCommand, SlashMixin): The command's name. description: :class:`str` The command's description, if any. - type: :class:`ApplicationCommandType` - The type of application command. dm_permission: :class:`bool` Whether the command is enabled in DMs. nsfw: :class:`bool` @@ -587,7 +614,7 @@ class SlashCommand(BaseCommand, SlashMixin): __slots__ = ('_parent', 'options', 'children') - def __init__(self, *, data: Dict[str, Any], **kwargs) -> None: + def __init__(self, *, data: ApplicationCommandPayload, **kwargs) -> None: super().__init__(data=data, **kwargs) self._parent = self self._unwrap_options(data.get('options', [])) @@ -629,6 +656,11 @@ class SlashCommand(BaseCommand, SlashMixin): BASE += f' children={len(self.children)}' return BASE + '>' + @property + def type(self) -> Literal[ApplicationCommandType.chat_input]: + """:class:`ApplicationCommandType`: The type of application command. This is always :attr:`ApplicationCommandType.chat_input`.""" + return ApplicationCommandType.chat_input + def is_group(self) -> bool: """Query whether this command is a group. @@ -663,8 +695,6 @@ class SubCommand(SlashMixin): The subcommand's name. description: :class:`str` The subcommand's description, if any. - type: :class:`ApplicationCommandType` - The type of application command. Always :attr:`ApplicationCommandType.chat_input`. parent: Union[:class:`SlashCommand`, :class:`SubCommand`] The parent command. options: List[:class:`Option`] @@ -680,18 +710,20 @@ class SubCommand(SlashMixin): 'parent', 'options', 'children', - 'type', ) - def __init__(self, *, parent, data): + def __init__(self, *, parent: Union[SlashCommand, SubCommand], data: SubCommandPayload): self.name = data['name'] self.description = data.get('description') self._state = parent._state - self.parent: Union[SlashCommand, SubCommand] = parent + self.parent = parent self._parent: SlashCommand = getattr(parent, 'parent', parent) # type: ignore - self.type = ApplicationCommandType.chat_input # Avoid confusion I guess - self._type: ApplicationCommandOptionType = try_enum(ApplicationCommandOptionType, data['type']) - self._unwrap_options(data.get('options', [])) + self._type: Literal[ + ApplicationCommandOptionType.sub_command, ApplicationCommandOptionType.sub_command_group + ] = try_enum( + ApplicationCommandOptionType, data['type'] + ) # type: ignore + self._unwrap_options(data.get('options', [])) # type: ignore # ??? def __str__(self) -> str: return self.name @@ -733,8 +765,7 @@ class SubCommand(SlashMixin): raise TypeError('Cannot use a group') options, files, attachments = self._parse_kwargs(kwargs) - - options = [ + options: List[InteractionDataOption] = [ { 'type': self._type.value, 'name': self.name, @@ -742,7 +773,7 @@ class SubCommand(SlashMixin): } ] for parent in self._walk_parents(): - options = [ + options: List[InteractionDataOption] = [ { 'type': parent._type.value, 'name': parent.name, @@ -760,6 +791,12 @@ class SubCommand(SlashMixin): BASE += f' children={len(self.children)}' return BASE + '>' + @property + def type(self) -> Literal[ApplicationCommandType.chat_input]: + """:class:`ApplicationCommandType`: The type of application command. Always :attr:`ApplicationCommandType.chat_input`.""" + # Avoid confusion I guess + return ApplicationCommandType.chat_input + @property def qualified_name(self) -> str: """:class:`str`: Returns the fully qualified command name. @@ -889,13 +926,13 @@ class Option: 'autocomplete', ) - def __init__(self, data): + def __init__(self, data: OptionPayload): self.name: str = data['name'] self.description: str = data['description'] self.type: ApplicationCommandOptionType = try_enum(ApplicationCommandOptionType, data['type']) self.required: bool = data.get('required', False) self.min_value: Optional[Union[int, float]] = data.get('min_value') - self.max_value: Optional[int] = data.get('max_value') + self.max_value: Optional[Union[int, float]] = data.get('max_value') self.choices = [OptionChoice(choice, self.type) for choice in data.get('choices', [])] self.channel_types: List[ChannelType] = [try_enum(ChannelType, c) for c in data.get('channel_types', [])] self.autocomplete: bool = data.get('autocomplete', False) @@ -934,7 +971,7 @@ class OptionChoice: __slots__ = ('name', 'value') - def __init__(self, data: Dict[str, str], type: ApplicationCommandOptionType): + def __init__(self, data: OptionChoicePayload, type: ApplicationCommandOptionType): self.name: str = data['name'] self.value: Union[str, int, float] if type is ApplicationCommandOptionType.string: diff --git a/discord/components.py b/discord/components.py index e36818485..bec1bf50b 100644 --- a/discord/components.py +++ b/discord/components.py @@ -24,30 +24,40 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import ClassVar, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload +from typing import TYPE_CHECKING, ClassVar, List, Literal, Optional, Tuple, Union, overload -from .enums import try_enum, ComponentType, ButtonStyle, TextStyle, InteractionType +from .enums import ButtonStyle, ComponentType, InteractionType, TextStyle, try_enum from .interactions import _wrapped_interaction -from .utils import _generate_nonce, get_slots, MISSING from .partial_emoji import PartialEmoji, _EmojiTag +from .utils import MISSING, _generate_nonce, get_slots if TYPE_CHECKING: from typing_extensions import Self + from .emoji import Emoji + from .interactions import Interaction + from .message import Message from .types.components import ( ActionRow as ActionRowPayload, - Component as ComponentPayload, + ActionRowChildComponent, ButtonComponent as ButtonComponentPayload, + Component as ComponentPayload, + MessageChildComponent, + ModalChildComponent, SelectMenu as SelectMenuPayload, SelectOption as SelectOptionPayload, TextInput as TextInputPayload, - ActionRowChildComponent as ActionRowChildComponentPayload, ) - from .emoji import Emoji - from .interactions import Interaction - from .message import Message + from .types.interactions import ( + ActionRowInteractionData, + ButtonInteractionData, + ComponentInteractionData, + SelectInteractionData, + TextInputInteractionData, + ) - ActionRowChildComponentType = Union['Button', 'SelectMenu', 'TextInput'] + MessageChildComponentType = Union['Button', 'SelectMenu'] + ActionRowChildComponentType = Union[MessageChildComponentType, 'TextInput'] __all__ = ( @@ -99,7 +109,7 @@ class Component: setattr(self, slot, value) return self - def to_dict(self) -> ComponentPayload: + def to_dict(self) -> Union[ActionRowInteractionData, ComponentInteractionData]: raise NotImplementedError @@ -124,13 +134,12 @@ class ActionRow(Component): __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ - def __init__(self, data: ComponentPayload, message: Message): + def __init__(self, data: ActionRowPayload, message: Message): self.message = message self.children: List[ActionRowChildComponentType] = [] for component_data in data.get('components', []): component = _component_factory(component_data, message) - if component is not None: self.children.append(component) @@ -139,12 +148,11 @@ class ActionRow(Component): """:class:`ComponentType`: The type of component.""" return ComponentType.action_row - def to_dict(self) -> ActionRowPayload: - # NOTE: This will have to be changed for the inevitable selects in modals + def to_dict(self) -> ActionRowInteractionData: return { - 'type': self.type.value, - 'components': [c.to_dict() for c in self.children], # type: ignore - } + 'type': ComponentType.action_row.value, + 'components': [c.to_dict() for c in self.children], + } # type: ignore class Button(Component): @@ -202,10 +210,10 @@ class Button(Component): """:class:`ComponentType`: The type of component.""" return ComponentType.button - def to_dict(self) -> dict: + def to_dict(self) -> ButtonInteractionData: return { 'component_type': self.type.value, - 'custom_id': self.custom_id, + 'custom_id': self.custom_id or '', } async def click(self) -> Union[str, Interaction]: @@ -237,7 +245,7 @@ class Button(Component): _generate_nonce(), InteractionType.component, None, - message.channel, # type: ignore # acc_channel is always correct here + message.channel, # type: ignore # channel is always correct here self.to_dict(), message=message, ) @@ -296,7 +304,7 @@ class SelectMenu(Component): """:class:`ComponentType`: The type of component.""" return ComponentType.select - def to_dict(self, options: Optional[Tuple[SelectOption]] = None) -> dict: + def to_dict(self, options: Optional[Tuple[SelectOption]] = None) -> SelectInteractionData: return { 'component_type': self.type.value, 'custom_id': self.custom_id, @@ -524,31 +532,40 @@ class TextInput(Component): """ self.value = value - def to_dict(self) -> dict: + def to_dict(self) -> TextInputInteractionData: return { 'type': self.type.value, 'custom_id': self.custom_id, - 'value': self.value, + 'value': self.value or '', } @overload -def _component_factory( - data: ActionRowChildComponentPayload, message: Message = ... -) -> Optional[ActionRowChildComponentType]: +def _component_factory(data: ActionRowPayload, message: Message = ...) -> ActionRow: + ... + + +@overload +def _component_factory(data: MessageChildComponent, message: Message = ...) -> Optional[MessageChildComponentType]: + ... + + +@overload +def _component_factory(data: ModalChildComponent, message: Message = ...) -> Optional[TextInput]: + ... + + +@overload +def _component_factory(data: ActionRowChildComponent, message: Message = ...) -> Optional[ActionRowChildComponentType]: ... @overload -def _component_factory( - data: ComponentPayload, message: Message = ... -) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: +def _component_factory(data: ComponentPayload, message: Message = ...) -> Optional[Component]: ... -def _component_factory( - data: ComponentPayload, message: Message = MISSING -) -> Optional[Union[ActionRow, ActionRowChildComponentType]]: +def _component_factory(data: ComponentPayload, message: Message = MISSING) -> Optional[Component]: if data['type'] == 1: return ActionRow(data, message) elif data['type'] == 2: diff --git a/discord/file.py b/discord/file.py index 9bd386720..1fff047e5 100644 --- a/discord/file.py +++ b/discord/file.py @@ -24,12 +24,13 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from base64 import b64encode -from hashlib import md5 import io import os +from base64 import b64encode +from hashlib import md5 +from typing import TYPE_CHECKING, Any, Optional, Tuple, Union + import yarl -from typing import Any, Dict, Optional, Tuple, Union, TYPE_CHECKING from .utils import MISSING, cached_slot_property @@ -37,7 +38,11 @@ if TYPE_CHECKING: from typing_extensions import Self from .state import ConnectionState - from .types.message import CloudAttachment as CloudAttachmentPayload, UploadedAttachment as UploadedAttachmentPayload + from .types.message import ( + CloudAttachment as CloudAttachmentPayload, + PartialAttachment as PartialAttachmentPayload, + UploadedAttachment as UploadedAttachmentPayload, + ) __all__ = ( 'File', @@ -76,8 +81,8 @@ class _FileBase: def filename(self, value: str) -> None: self._filename, self.spoiler = _strip_spoiler(value) - def to_dict(self, index: int) -> Dict[str, Any]: - payload = { + def to_dict(self, index: int) -> PartialAttachmentPayload: + payload: PartialAttachmentPayload = { 'id': str(index), 'filename': self.filename, } @@ -264,7 +269,7 @@ class CloudFile(_FileBase): url = yarl.URL(self.url) return url.query['upload_id'] - def to_dict(self, index: int) -> Dict[str, Any]: + def to_dict(self, index: int) -> PartialAttachmentPayload: payload = super().to_dict(index) payload['uploaded_filename'] = self.upload_filename return payload diff --git a/discord/http.py b/discord/http.py index bde7f0c2a..19adad036 100644 --- a/discord/http.py +++ b/discord/http.py @@ -96,6 +96,7 @@ if TYPE_CHECKING: guild, hub, integration, + interactions, invite, library, member, @@ -1179,7 +1180,6 @@ class HTTPClient: flags: Optional[int] = None, last_viewed: Optional[int] = None, ) -> None: - r = Route('POST', '/channels/{channel_id}/messages/{message_id}/ack', channel_id=channel_id, message_id=message_id) payload = {} if manual: payload['manual'] = True @@ -1192,7 +1192,10 @@ class HTTPClient: if last_viewed is not None: payload['last_viewed'] = last_viewed - data: read_state.AcknowledgementToken = await self.request(r, json=payload) + data: read_state.AcknowledgementToken = await self.request( + Route('POST', '/channels/{channel_id}/messages/{message_id}/ack', channel_id=channel_id, message_id=message_id), + json=payload, + ) self.ack_token = data.get('token') if data else None def ack_guild_feature( @@ -1699,8 +1702,8 @@ class HTTPClient: params: MultipartParameters, reason: Optional[str] = None, ) -> Response[threads.ForumThread]: - query = {'use_nested_fields': 1} r = Route('POST', '/channels/{channel_id}/threads', channel_id=channel_id) + query = {'use_nested_fields': 'true'} if params.files: return self.request(r, files=params.files, form=params.multipart, params=query, reason=reason) else: @@ -1925,8 +1928,8 @@ class HTTPClient: payload = {'level': mfa_level} return self.request(Route('POST', '/guilds/{guild_id}/mfa', guild_id=guild_id), json=payload, reason=reason) - def edit_guild_settings(self, guild_id: Snowflake, fields): # TODO: type - return self.request(Route('PATCH', '/users/@me/guilds/{guild_id}/settings', guild_id=guild_id), json=fields) + def edit_guild_settings(self, guild_id: Snowflake, payload: Dict[str, Any]) -> Response[user.UserGuildSettings]: + return self.request(Route('PATCH', '/users/@me/guilds/{guild_id}/settings', guild_id=guild_id), json=payload) def get_template(self, code: str) -> Response[template.Template]: return self.request(Route('GET', '/guilds/templates/{code}', code=code)) @@ -2041,15 +2044,16 @@ class HTTPClient: return self.request(Route('GET', '/sticker-packs'), params=params) - def get_sticker_pack(self, pack_id: Snowflake): + def get_sticker_pack(self, pack_id: Snowflake) -> Response[sticker.StickerPack]: return self.request(Route('GET', '/sticker-packs/{pack_id}', pack_id=pack_id)) def get_all_guild_stickers(self, guild_id: Snowflake) -> Response[List[sticker.GuildSticker]]: return self.request(Route('GET', '/guilds/{guild_id}/stickers', guild_id=guild_id)) def get_guild_sticker(self, guild_id: Snowflake, sticker_id: Snowflake) -> Response[sticker.GuildSticker]: - r = Route('GET', '/guilds/{guild_id}/stickers/{sticker_id}', guild_id=guild_id, sticker_id=sticker_id) - return self.request(r) + return self.request( + Route('GET', '/guilds/{guild_id}/stickers/{sticker_id}', guild_id=guild_id, sticker_id=sticker_id) + ) def create_guild_sticker( self, guild_id: Snowflake, payload: Dict[str, Any], file: File, reason: Optional[str] @@ -2285,7 +2289,7 @@ class HTTPClient: channel_id: Snowflake = MISSING, channel_type: ChannelType = MISSING, message: Optional[Message] = None, - ) -> Response[invite.PartialInvite]: + ) -> Response[invite.AcceptedInvite]: if message: # Invite Button Embed props = ContextProperties.from_invite_button_embed( guild_id=getattr(message.guild, 'id', None), @@ -2523,7 +2527,7 @@ class HTTPClient: ) -> Response[member.MemberWithUser]: return self.edit_member(guild_id, user_id, channel_id=channel_id, reason=reason) - def get_ringability(self, channel_id: Snowflake): + def get_ringability(self, channel_id: Snowflake) -> Response[channel.CallEligibility]: return self.request(Route('GET', '/channels/{channel_id}/call', channel_id=channel_id)) def ring(self, channel_id: Snowflake, *recipients: Snowflake) -> Response[None]: @@ -2534,7 +2538,7 @@ class HTTPClient: payload = {'recipients': recipients} return self.request(Route('POST', '/channels/{channel_id}/call/stop-ringing', channel_id=channel_id), json=payload) - def change_call_voice_region(self, channel_id: int, voice_region: str): # TODO: return type + def change_call_voice_region(self, channel_id: int, voice_region: str) -> Response[None]: payload = {'region': voice_region} return self.request(Route('PATCH', '/channels/{channel_id}/call', channel_id=channel_id), json=payload) @@ -2864,7 +2868,9 @@ class HTTPClient: else: props = ContextProperties.empty() - return self.request(Route('PUT', '/users/@me/relationships/{user_id}', user_id=user_id), context_properties=props, json=payload) + return self.request( + Route('PUT', '/users/@me/relationships/{user_id}', user_id=user_id), context_properties=props, json=payload + ) def send_friend_request(self, username: str, discriminator: Snowflake) -> Response[None]: payload = {'username': username, 'discriminator': int(discriminator) or None} @@ -3165,7 +3171,7 @@ class HTTPClient: super_properties_to_track=True, ) - def delete_app_whitelist(self, app_id: Snowflake, user_id: Snowflake): + def delete_app_whitelist(self, app_id: Snowflake, user_id: Snowflake) -> Response[None]: return self.request( Route('DELETE', '/oauth2/applications/{app_id}/allowlist/{user_id}', app_id=app_id, user_id=user_id), super_properties_to_track=True, @@ -3223,7 +3229,7 @@ class HTTPClient: super_properties_to_track=True, ) - def create_team(self, name: str): + def create_team(self, name: str) -> Response[team.Team]: payload = {'name': name} return self.request(Route('POST', '/teams'), json=payload, super_properties_to_track=True) @@ -3251,13 +3257,15 @@ class HTTPClient: def get_team_members(self, team_id: Snowflake) -> Response[List[team.TeamMember]]: return self.request(Route('GET', '/teams/{team_id}/members', team_id=team_id), super_properties_to_track=True) - def invite_team_member(self, team_id: Snowflake, username: str, discriminator: Snowflake): + def invite_team_member( + self, team_id: Snowflake, username: str, discriminator: Optional[Snowflake] = None + ) -> Response[team.TeamMember]: payload = {'username': username, 'discriminator': str(discriminator) or None} return self.request( Route('POST', '/teams/{team_id}/members', team_id=team_id), json=payload, super_properties_to_track=True ) - def remove_team_member(self, team_id: Snowflake, user_id: Snowflake): + def remove_team_member(self, team_id: Snowflake, user_id: Snowflake) -> Response[None]: return self.request( Route('DELETE', '/teams/{team_id}/members/{user_id}', team_id=team_id, user_id=user_id), super_properties_to_track=True, @@ -4380,27 +4388,27 @@ class HTTPClient: return self.request(Route('PATCH', '/users/@me/settings-proto/{type}', type=type), json=payload) - def get_settings(self): # TODO: return type + def get_settings(self): return self.request(Route('GET', '/users/@me/settings')) - def edit_settings(self, **payload): # TODO: return type, is this cheating? + def edit_settings(self, **payload): return self.request(Route('PATCH', '/users/@me/settings'), json=payload) - def get_tracking(self): # TODO: return type + def get_tracking(self) -> Response[user.ConsentSettings]: return self.request(Route('GET', '/users/@me/consent')) - def edit_tracking(self, payload): + def edit_tracking(self, payload) -> Response[user.ConsentSettings]: return self.request(Route('POST', '/users/@me/consent'), json=payload) - def get_email_settings(self): + def get_email_settings(self) -> Response[user.EmailSettings]: return self.request(Route('GET', '/users/@me/email-settings')) - def edit_email_settings(self, **payload): + def edit_email_settings(self, **payload) -> Response[user.EmailSettings]: return self.request(Route('PATCH', '/users/@me/email-settings'), json={'settings': payload}) def mobile_report( # Report v1 self, guild_id: Snowflake, channel_id: Snowflake, message_id: Snowflake, reason: str - ): # TODO: return type + ) -> Response[user.Report]: payload = {'guild_id': guild_id, 'channel_id': channel_id, 'message_id': message_id, 'reason': reason} return self.request(Route('POST', '/report'), json=payload) @@ -4418,7 +4426,7 @@ class HTTPClient: command_ids: Optional[List[Snowflake]] = None, application_id: Optional[Snowflake] = None, include_applications: Optional[bool] = None, - ): + ) -> Response[command.ApplicationCommandSearch]: params: Dict[str, Any] = { 'type': type, } @@ -4442,7 +4450,7 @@ class HTTPClient: def interact( self, type: InteractionType, - data: dict, + data: interactions.InteractionData, channel: MessageableChannel, message: Optional[Message] = None, *, diff --git a/discord/interactions.py b/discord/interactions.py index 5c1c66832..79d6a137c 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -39,6 +39,7 @@ if TYPE_CHECKING: from .modal import Modal from .state import ConnectionState from .threads import Thread + from .types.interactions import InteractionData from .types.snowflake import Snowflake from .types.user import User as UserPayload from .user import BaseUser, ClientUser @@ -193,7 +194,7 @@ async def _wrapped_interaction( type: InteractionType, name: Optional[str], channel: MessageableChannel, - data: dict, + data: InteractionData, **kwargs, ) -> Interaction: state._interaction_cache[nonce] = (type.value, name, channel) diff --git a/discord/member.py b/discord/member.py index 935b5eb4f..2378cb970 100644 --- a/discord/member.py +++ b/discord/member.py @@ -60,7 +60,7 @@ if TYPE_CHECKING: from .guild import Guild from .profile import MemberProfile from .types.activity import ( - PartialPresenceUpdate, + BasePresenceUpdate, ) from .types.member import ( MemberWithUser as MemberWithUserPayload, @@ -389,7 +389,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): self._user = member._user return self - def _update(self, data: GuildMemberUpdateEvent) -> Optional[Member]: + def _update(self, data: Union[GuildMemberUpdateEvent, MemberWithUserPayload]) -> Optional[Member]: old = Member._copy(self) # Some changes are optional @@ -416,7 +416,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): return old def _presence_update( - self, data: PartialPresenceUpdate, user: Union[PartialUserPayload, Tuple[()]] + self, data: BasePresenceUpdate, user: Union[PartialUserPayload, Tuple[()]] ) -> Optional[Tuple[User, User]]: self._presence = self._state.create_presence(data) return self._user._update_self(user) diff --git a/discord/message.py b/discord/message.py index a2e94f942..88fdae226 100644 --- a/discord/message.py +++ b/discord/message.py @@ -87,7 +87,7 @@ if TYPE_CHECKING: from .types.interactions import MessageInteraction as MessageInteractionPayload - from .types.components import Component as ComponentPayload + from .types.components import MessageActionRow as ComponentPayload from .types.threads import ThreadArchiveDuration from .types.member import ( Member as MemberPayload, @@ -98,7 +98,7 @@ if TYPE_CHECKING: from .types.gateway import MessageReactionRemoveEvent, MessageUpdateEvent from .abc import Snowflake from .abc import GuildChannel, MessageableChannel - from .components import ActionRow, ActionRowChildComponentType + from .components import ActionRow from .file import _FileBase from .state import ConnectionState from .mentions import AllowedMentions @@ -107,7 +107,6 @@ if TYPE_CHECKING: from .role import Role EmojiInputType = Union[Emoji, PartialEmoji, str] - MessageComponentType = Union[ActionRow, ActionRowChildComponentType] __all__ = ( @@ -1573,7 +1572,7 @@ class Message(PartialMessage, Hashable): mentions: List[Union[User, Member]] author: Union[User, Member] role_mentions: List[Role] - components: List[MessageComponentType] + components: List[ActionRow] def __init__( self, @@ -1865,10 +1864,8 @@ class Message(PartialMessage, Hashable): def _handle_components(self, data: List[ComponentPayload]) -> None: self.components = [] - for component_data in data: component = _component_factory(component_data, self) - if component is not None: self.components.append(component) diff --git a/discord/modal.py b/discord/modal.py index 3124e5f8a..218d6ce9c 100644 --- a/discord/modal.py +++ b/discord/modal.py @@ -35,6 +35,7 @@ if TYPE_CHECKING: from .application import IntegrationApplication from .components import ActionRow from .interactions import Interaction + from .types.interactions import Modal as ModalPayload, ModalSubmitInteractionData # fmt: off __all__ = ( @@ -69,7 +70,7 @@ class Modal(Hashable): Attributes ----------- id: :class:`int` - The modal's ID. This is the same as the interaction ID. + The interaction ID. nonce: Optional[Union[:class:`int`, :class:`str`]] The modal's nonce. May not be present. title: :class:`str` @@ -84,24 +85,24 @@ class Modal(Hashable): __slots__ = ('_state', 'interaction', 'id', 'nonce', 'title', 'custom_id', 'components', 'application') - def __init__(self, *, data: dict, interaction: Interaction): + def __init__(self, *, data: ModalPayload, interaction: Interaction): self._state = interaction._state self.interaction = interaction self.id = int(data['id']) self.nonce: Optional[Union[int, str]] = data.get('nonce') self.title: str = data.get('title', '') self.custom_id: str = data.get('custom_id', '') - self.components: List[ActionRow] = [_component_factory(d) for d in data.get('components', [])] # type: ignore # Will always be rows here + self.components: List[ActionRow] = [_component_factory(d) for d in data.get('components', [])] self.application: IntegrationApplication = interaction._state.create_integration_application(data['application']) def __str__(self) -> str: return self.title - def to_dict(self) -> dict: + def to_dict(self) -> ModalSubmitInteractionData: return { 'id': str(self.id), 'custom_id': self.custom_id, - 'components': [c.to_dict() for c in self.components], + 'components': [c.to_dict() for c in self.components], # type: ignore } async def submit(self): diff --git a/discord/settings.py b/discord/settings.py index e943713de..d39edabb7 100644 --- a/discord/settings.py +++ b/discord/settings.py @@ -25,13 +25,13 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations import base64 -from datetime import datetime, timezone -import struct import logging +import struct +from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Collection, Dict, List, Literal, Optional, Sequence, Tuple, Type, Union, overload -from google.protobuf.json_format import MessageToDict, ParseDict from discord_protos import PreloadedUserSettings # , FrecencyUserSettings +from google.protobuf.json_format import MessageToDict, ParseDict from .activity import CustomActivity from .colour import Colour @@ -41,8 +41,8 @@ from .enums import ( InboxTab, Locale, NotificationLevel, - Status, SpoilerRenderOptions, + Status, StickerAnimationOptions, StickerPickerSection, Theme, @@ -51,7 +51,7 @@ from .enums import ( ) from .flags import FriendDiscoveryFlags, FriendSourceFlags, HubProgressFlags, OnboardingProgressFlags from .object import Object -from .utils import MISSING, _get_as_snowflake, _ocast, parse_time, parse_timestamp, utcnow, find +from .utils import MISSING, _get_as_snowflake, _ocast, find, parse_time, parse_timestamp, utcnow if TYPE_CHECKING: from google.protobuf.message import Message @@ -61,6 +61,14 @@ if TYPE_CHECKING: from .channel import DMChannel, GroupChannel from .guild import Guild from .state import ConnectionState + from .types.user import ( + ConsentSettings as ConsentSettingsPayload, + EmailSettings as EmailSettingsPayload, + PartialConsentSettings as PartialConsentSettingsPayload, + UserGuildSettings as UserGuildSettingsPayload, + ChannelOverride as ChannelOverridePayload, + MuteConfig as MuteConfigPayload, + ) from .user import ClientUser, User PrivateChannel = Union[DMChannel, GroupChannel] @@ -1954,8 +1962,8 @@ class MuteConfig: When the mute will expire. """ - def __init__(self, muted: bool, config: Dict[str, str]) -> None: - until = parse_time(config.get('end_time')) + def __init__(self, muted: bool, config: Optional[MuteConfigPayload] = None) -> None: + until = parse_time(config.get('end_time') if config else None) if until is not None: if until <= utcnow(): muted = False @@ -2002,7 +2010,7 @@ class ChannelSettings: muted: MuteConfig collapsed: bool - def __init__(self, guild_id: Optional[int] = None, *, data: Dict[str, Any], state: ConnectionState) -> None: + def __init__(self, guild_id: Optional[int] = None, *, data: ChannelOverridePayload, state: ConnectionState) -> None: self._guild_id = guild_id self._state = state self._update(data) @@ -2010,14 +2018,14 @@ class ChannelSettings: def __repr__(self) -> str: return f'' - def _update(self, data: Dict[str, Any]) -> None: + def _update(self, data: ChannelOverridePayload) -> None: # We consider everything optional because this class can be constructed with no data # to represent the default settings self._channel_id = int(data['channel_id']) self.collapsed = data.get('collapsed', False) self.level = try_enum(NotificationLevel, data.get('message_notifications', 3)) - self.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {}) + self.muted = MuteConfig(data.get('muted', False), data.get('mute_config')) @property def channel(self) -> Union[GuildChannel, PrivateChannel]: @@ -2103,7 +2111,7 @@ class ChannelSettings: override = find(lambda x: x.get('channel_id') == str(channel_id), data['channel_overrides']) or { 'channel_id': channel_id } - return ChannelSettings(guild_id, data=override, state=state) + return ChannelSettings(guild_id, data=override, state=state) # type: ignore class GuildSettings: @@ -2146,14 +2154,15 @@ class GuildSettings: notify_highlights: HighlightLevel version: int - def __init__(self, *, data: Dict[str, Any], state: ConnectionState) -> None: + def __init__(self, *, data: UserGuildSettingsPayload, state: ConnectionState) -> None: self._state = state + self.version = -1 # Overriden by real data self._update(data) def __repr__(self) -> str: return f'' - def _update(self, data: Dict[str, Any]) -> None: + def _update(self, data: UserGuildSettingsPayload) -> None: # We consider everything optional because this class can be constructed with no data # to represent the default settings self._guild_id = guild_id = _get_as_snowflake(data, 'guild_id') @@ -2164,9 +2173,9 @@ class GuildSettings: self.mobile_push = data.get('mobile_push', True) self.mute_scheduled_events = data.get('mute_scheduled_events', False) self.notify_highlights = try_enum(HighlightLevel, data.get('notify_highlights', 0)) - self.version = data.get('version', -1) # Overriden by real data + self.version = data.get('version', self.version) - self.muted = MuteConfig(data.get('muted', False), data.get('mute_config') or {}) + self.muted = MuteConfig(data.get('muted', False), data.get('mute_config')) self._channel_overrides = overrides = {} state = self._state for override in data.get('channel_overrides', []): @@ -2244,9 +2253,7 @@ class GuildSettings: payload['muted'] = False else: payload['muted'] = True - if muted_until is True: - payload['mute_config'] = {'selected_time_window': -1, 'end_time': None} - else: + if muted_until is not True: if muted_until.tzinfo is None: raise TypeError( 'muted_until must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.' @@ -2257,25 +2264,18 @@ class GuildSettings: 'end_time': muted_until.isoformat(), } payload['mute_config'] = mute_config - if level is not MISSING: payload['message_notifications'] = level.value - if suppress_everyone is not MISSING: payload['suppress_everyone'] = suppress_everyone - if suppress_roles is not MISSING: payload['suppress_roles'] = suppress_roles - if mobile_push is not MISSING: payload['mobile_push'] = mobile_push - if hide_muted_channels is not MISSING: payload['hide_muted_channels'] = hide_muted_channels - if mute_scheduled_events is not MISSING: payload['mute_scheduled_events'] = mute_scheduled_events - if notify_highlights is not MISSING: payload['notify_highlights'] = notify_highlights.value @@ -2304,7 +2304,9 @@ class TrackingSettings: __slots__ = ('_state', 'personalization', 'usage_statistics') - def __init__(self, *, data: Dict[str, Dict[str, bool]], state: ConnectionState) -> None: + def __init__( + self, *, data: Union[PartialConsentSettingsPayload, ConsentSettingsPayload], state: ConnectionState + ) -> None: self._state = state self._update(data) @@ -2312,9 +2314,9 @@ class TrackingSettings: return f'' def __bool__(self) -> bool: - return any({self.personalization, self.usage_statistics}) + return any((self.personalization, self.usage_statistics)) - def _update(self, data: Dict[str, Dict[str, bool]]): + def _update(self, data: Union[PartialConsentSettingsPayload, ConsentSettingsPayload]): self.personalization = data.get('personalization', {}).get('consented', False) self.usage_statistics = data.get('usage_statistics', {}).get('consented', False) @@ -2387,22 +2389,22 @@ class EmailSettings: 'family_center_digest', ) - def __init__(self, *, data: dict, state: ConnectionState): + def __init__(self, *, data: EmailSettingsPayload, state: ConnectionState): self._state = state self._update(data) def __repr__(self) -> str: return f'' - def _update(self, data: dict): - self.initialized = data.get('initialized', False) + def _update(self, data: EmailSettingsPayload): + self.initialized: bool = data.get('initialized', False) categories = data.get('categories', {}) - self.communication = categories.get('communication', False) - self.social = categories.get('social', False) - self.recommendations_and_events = categories.get('recommendations_and_events', False) - self.tips = categories.get('tips', False) - self.updates_and_announcements = categories.get('updates_and_announcements', False) - self.family_center_digest = categories.get('family_center_digest', False) + self.communication: bool = categories.get('communication', False) + self.social: bool = categories.get('social', False) + self.recommendations_and_events: bool = categories.get('recommendations_and_events', False) + self.tips: bool = categories.get('tips', False) + self.updates_and_announcements: bool = categories.get('updates_and_announcements', False) + self.family_center_digest: bool = categories.get('family_center_digest', False) @overload async def edit(self) -> None: @@ -2412,6 +2414,7 @@ class EmailSettings: async def edit( self, *, + initialized: bool = MISSING, communication: bool = MISSING, social: bool = MISSING, recommendations_and_events: bool = MISSING, @@ -2449,8 +2452,8 @@ class EmailSettings: # It seems that initialized is settable, but it doesn't do anything # So we support just in case but leave it undocumented - initialized = kwargs.pop('initialized', None) - if initialized is not None: + initialized = kwargs.pop('initialized', MISSING) + if initialized is not MISSING: payload['initialized'] = initialized if kwargs: payload['categories'] = kwargs diff --git a/discord/state.py b/discord/state.py index 635b65e47..e2aa7d230 100644 --- a/discord/state.py +++ b/discord/state.py @@ -128,7 +128,7 @@ if TYPE_CHECKING: PartialMessage as PartialMessagePayload, ) from .types import gateway as gw - from .types.voice import GuildVoiceState + from .types.voice import VoiceState as VoiceStatePayload from .types.activity import ClientStatus as ClientStatusPayload T = TypeVar('T') @@ -496,7 +496,7 @@ class ClientStatus: class Presence: __slots__ = ('client_status', 'activities', 'last_modified') - def __init__(self, data: gw.PresenceUpdateEvent, state: ConnectionState, /) -> None: + def __init__(self, data: gw.BasePresenceUpdate, state: ConnectionState, /) -> None: self.client_status: ClientStatus = ClientStatus(data['status'], data.get('client_status')) self.activities: Tuple[ActivityTypes, ...] = tuple(create_activity(d, state) for d in data['activities']) self.last_modified: Optional[datetime.datetime] = utils.parse_timestamp(data.get('last_modified')) @@ -520,7 +520,7 @@ class Presence: return True return self.client_status != other.client_status or self.activities != other.activities - def _update(self, data: gw.PresenceUpdateEvent, state: ConnectionState, /) -> None: + def _update(self, data: gw.BasePresenceUpdate, state: ConnectionState, /) -> None: self.client_status._update(data['status'], data.get('client_status')) self.activities = tuple(create_activity(d, state) for d in data['activities']) self.last_modified = utils.parse_timestamp(data.get('last_modified')) or utils.utcnow() @@ -653,7 +653,6 @@ class ConnectionState: self.user: Optional[ClientUser] = None self._users: weakref.WeakValueDictionary[int, User] = weakref.WeakValueDictionary() self.settings: Optional[UserSettings] = None - self.guild_settings: Dict[Optional[int], GuildSettings] = {} self.consents: Optional[TrackingSettings] = None self.connections: Dict[str, Connection] = {} self.pending_payments: Dict[int, Payment] = {} @@ -674,6 +673,9 @@ class ConnectionState: self._read_states: Dict[int, Dict[int, ReadState]] = {} self.read_state_version: int = 0 + self.guild_settings: Dict[Optional[int], GuildSettings] = {} + self.guild_settings_version: int = 0 + self._calls: Dict[int, Call] = {} self._call_message_cache: Dict[int, Message] = {} # Hopefully this won't be a memory leak self._voice_clients: Dict[int, VoiceProtocol] = {} @@ -756,7 +758,7 @@ class ConnectionState: return list(self._voice_clients.values()) def _update_voice_state( - self, data: GuildVoiceState, channel_id: Optional[int] + self, data: VoiceStatePayload, channel_id: Optional[int] ) -> Tuple[Optional[User], VoiceState, VoiceState]: user_id = int(data['user_id']) user = self.get_user(user_id) @@ -1079,6 +1081,14 @@ class ConnectionState: self.store_read_state(item) self.read_state_version = read_states.get('version', 0) + # Guild settings parsing + guild_settings = data.get('user_guild_settings', {}) + self.guild_settings = { + utils._get_as_snowflake(entry, 'guild_id'): GuildSettings(data=entry, state=self) + for entry in guild_settings.get('entries', []) + } + self.guild_settings_version = guild_settings.get('version', 0) + # Experiments self.experiments = {exp[0]: UserExperiment(state=self, data=exp) for exp in data.get('experiments', [])} self.guild_experiments = {exp[0]: GuildExperiment(state=self, data=exp) for exp in data.get('guild_experiments', [])} @@ -1087,10 +1097,6 @@ class ConnectionState: self.analytics_token = data.get('analytics_token') self.preferred_rtc_regions = data.get('geo_ordered_rtc_regions', ['us-central']) self.settings = UserSettings(self, data.get('user_settings_proto', '')) - self.guild_settings = { - utils._get_as_snowflake(entry, 'guild_id'): GuildSettings(data=entry, state=self) - for entry in data.get('user_guild_settings', {}).get('entries', []) - } self.consents = TrackingSettings(data=data.get('consents', {}), state=self) self.country_code = data.get('country_code', 'US') self.api_code_version = data.get('api_code_version', 1) @@ -1479,7 +1485,7 @@ class ConnectionState: else: _log.warning('Unknown user settings proto type: %s', type.value) - def parse_user_guild_settings_update(self, data) -> None: + def parse_user_guild_settings_update(self, data: gw.UserGuildSettingsEvent) -> None: guild_id = utils._get_as_snowflake(data, 'guild_id') settings = self.guild_settings.get(guild_id) @@ -1489,6 +1495,7 @@ class ConnectionState: else: old_settings = None settings = GuildSettings(data=data, state=self) + self.guild_settings_version = data.get('version', self.guild_settings_version) self.dispatch('guild_settings_update', old_settings, settings) def parse_user_required_action_update(self, data: gw.RequiredActionEvent) -> None: @@ -1779,14 +1786,22 @@ class ConnectionState: else: self.dispatch('guild_channel_pins_ack', channel, last_pin) - def parse_channel_recipient_add(self, data) -> None: + def parse_channel_recipient_add(self, data: gw.ChannelRecipientEvent) -> None: channel = self._get_private_channel(int(data['channel_id'])) + if channel is None: + _log.debug('CHANNEL_RECIPIENT_ADD referencing an unknown channel ID: %s. Discarding.', data['channel_id']) + return + user = self.store_user(data['user']) channel.recipients.append(user) # type: ignore self.dispatch('group_join', channel, user) - def parse_channel_recipient_remove(self, data) -> None: + def parse_channel_recipient_remove(self, data: gw.ChannelRecipientEvent) -> None: channel = self._get_private_channel(int(data['channel_id'])) + if channel is None: + _log.debug('CHANNEL_RECIPIENT_REMOVE referencing an unknown channel ID: %s. Discarding.', data['channel_id']) + return + user = self.store_user(data['user']) try: channel.recipients.remove(user) # type: ignore @@ -2011,7 +2026,7 @@ class ConnectionState: 'I noticed you triggered a `GUILD_SYNC`.\nIf you want to share your secrets, please feel free to open an issue.' ) - def parse_guild_member_list_update(self, data) -> None: + def parse_guild_member_list_update(self, data: gw.GuildMemberListUpdateEvent) -> None: self.dispatch('raw_member_list_update', data) guild = self._get_guild(int(data['guild_id'])) if guild is None: @@ -2042,7 +2057,6 @@ class ConnectionState: ops = data['ops'] for opdata in ops: - op = opdata['op'] # The OPs are as follows: # SYNC: Provides member/presence data for a 100 member range of the member list # UPDATE: Dispatched when a member is updated and stays in the same range @@ -2050,7 +2064,7 @@ class ConnectionState: # DELETE: Dispatched when a member is removed from a range # INVALIDATE: Sent when you're unsubscribed from a range - if op == 'SYNC': + if opdata['op'] == 'SYNC': for item in opdata['items']: if 'group' in item: # Hoisted role guild._member_list.append(None) if should_parse else None # Insert blank so indexes don't fuck up @@ -2064,7 +2078,7 @@ class ConnectionState: members.append(member) guild._member_list.append(member) if should_parse else None - elif op == 'INSERT': + elif opdata['op'] == 'INSERT': index = opdata['index'] item = opdata['item'] if 'group' in item: # Hoisted role @@ -2111,7 +2125,7 @@ class ConnectionState: guild._member_list.insert(index, member) if should_parse else None - elif op == 'UPDATE' and should_parse: + elif opdata['op'] == 'UPDATE' and should_parse: item = opdata['item'] if 'group' in item: # Hoisted role continue @@ -2159,7 +2173,7 @@ class ConnectionState: guild._member_list.insert(opdata['index'], member) # Race condition? - elif op == 'DELETE' and should_parse: + elif opdata['op'] == 'DELETE' and should_parse: index = opdata['index'] try: item = guild._member_list.pop(index) @@ -2213,7 +2227,6 @@ class ConnectionState: before_emojis = guild.emojis for emoji in before_emojis: self._emojis.pop(emoji.id, None) - # guild won't be None here guild.emojis = tuple(map(lambda d: self.store_emoji(guild, d), data['emojis'])) self.dispatch('guild_emojis_update', guild, before_emojis, guild.emojis) @@ -2226,7 +2239,6 @@ class ConnectionState: before_stickers = guild.stickers for emoji in before_stickers: self._stickers.pop(emoji.id, None) - guild.stickers = tuple(map(lambda d: self.store_sticker(guild, d), data['stickers'])) self.dispatch('guild_stickers_update', guild, before_stickers, guild.stickers) @@ -2243,7 +2255,6 @@ class ConnectionState: data=data, guild=guild, ) - self.dispatch('audit_log_entry_create', entry) def parse_auto_moderation_rule_create(self, data: AutoModerationRule) -> None: @@ -2753,17 +2764,26 @@ class ConnectionState: else: _log.debug('SCHEDULED_EVENT_USER_REMOVE referencing unknown guild ID: %s. Discarding.', data['guild_id']) - def parse_call_create(self, data) -> None: - channel = self._get_private_channel(int(data['channel_id'])) + def parse_call_create(self, data: gw.CallCreateEvent) -> None: + channel_id = int(data['channel_id']) + channel = self._get_private_channel(channel_id) if channel is None: _log.debug('CALL_CREATE referencing unknown channel ID: %s. Discarding.', data['channel_id']) return + + call = self._calls.get(channel_id) + if call is not None: + # Should only happen for unavailable calls + old_call = copy.copy(call) + call._update(data) + self.dispatch('call_update', old_call, call) + message = self._get_message(int(data['message_id'])) call = channel._add_call(data=data, state=self, message=message, channel=channel) self._calls[channel.id] = call self.dispatch('call_create', call) - def parse_call_update(self, data) -> None: + def parse_call_update(self, data: gw.CallUpdateEvent) -> None: call = self._calls.get(int(data['channel_id'])) if call is None: _log.debug('CALL_UPDATE referencing unknown call (channel ID: %s). Discarding.', data['channel_id']) @@ -2772,9 +2792,15 @@ class ConnectionState: call._update(data) self.dispatch('call_update', old_call, call) - def parse_call_delete(self, data) -> None: + def parse_call_delete(self, data: gw.CallDeleteEvent) -> None: call = self._calls.pop(int(data['channel_id']), None) if call is not None: + if data.get('unavailable'): + old_call = copy.copy(call) + call.unavailable = True + self.dispatch('call_update', old_call, call) + return + call._delete() self._call_message_cache.pop(call._message_id, None) self.dispatch('call_delete', call) @@ -2884,7 +2910,7 @@ class ConnectionState: self.dispatch('friend_suggestion_remove', user) self.dispatch('raw_friend_suggestion_remove', user_id) - def parse_interaction_create(self, data) -> None: + def parse_interaction_create(self, data: gw.InteractionEvent) -> None: if 'nonce' not in data: # Sometimes interactions seem to be missing the nonce return @@ -2893,7 +2919,7 @@ class ConnectionState: self._interactions[i.id] = i self.dispatch('interaction', i) - def parse_interaction_success(self, data) -> None: + def parse_interaction_success(self, data: gw.InteractionEvent) -> None: id = int(data['id']) i = self._interactions.get(id, None) if i is None: @@ -2903,7 +2929,7 @@ class ConnectionState: i.successful = True self.dispatch('interaction_finish', i) - def parse_interaction_failed(self, data) -> None: + def parse_interaction_failed(self, data: gw.InteractionEvent) -> None: id = int(data['id']) i = self._interactions.pop(id, None) if i is None: @@ -2913,7 +2939,7 @@ class ConnectionState: i.successful = False self.dispatch('interaction_finish', i) - def parse_interaction_modal_create(self, data) -> None: + def parse_interaction_modal_create(self, data: gw.InteractionModalCreateEvent) -> None: id = int(data['id']) interaction = self._interactions.pop(id, None) if interaction is not None: @@ -3000,10 +3026,10 @@ class ConnectionState: return IntegrationApplication(state=self, data=data) def default_guild_settings(self, guild_id: Optional[int]) -> GuildSettings: - return GuildSettings(data={'guild_id': guild_id}, state=self) + return GuildSettings(data={'guild_id': guild_id}, state=self) # type: ignore def default_channel_settings(self, guild_id: Optional[int], channel_id: int) -> ChannelSettings: - return ChannelSettings(guild_id, data={'channel_id': channel_id}, state=self) + return ChannelSettings(guild_id, data={'channel_id': channel_id}, state=self) # type: ignore def create_implicit_relationship(self, user: User) -> Relationship: relationship = self._relationships.get(user.id) @@ -3027,7 +3053,7 @@ class ConnectionState: def client_presence(self) -> FakeClientPresence: return FakeClientPresence(self) - def create_presence(self, data: gw.PresenceUpdateEvent) -> Presence: + def create_presence(self, data: gw.BasePresenceUpdate) -> Presence: return Presence(data, self) def create_offline_presence(self) -> Presence: diff --git a/discord/types/activity.py b/discord/types/activity.py index 5fd20ba1c..5784e7d40 100644 --- a/discord/types/activity.py +++ b/discord/types/activity.py @@ -33,14 +33,17 @@ from .snowflake import Snowflake StatusType = Literal['idle', 'dnd', 'online', 'offline'] -class PartialPresenceUpdate(TypedDict): +class BasePresenceUpdate(TypedDict): user: PartialUser - guild_id: Optional[Snowflake] status: StatusType activities: List[Activity] client_status: ClientStatus +class PartialPresenceUpdate(BasePresenceUpdate): + guild_id: NotRequired[Snowflake] + + class ClientStatus(TypedDict, total=False): desktop: StatusType mobile: StatusType diff --git a/discord/types/channel.py b/discord/types/channel.py index dcf18ede1..dd36980c1 100644 --- a/discord/types/channel.py +++ b/discord/types/channel.py @@ -205,3 +205,7 @@ class InviteStageInstance(TypedDict): participant_count: int speaker_count: int topic: str + + +class CallEligibility(TypedDict): + ringable: bool diff --git a/discord/types/command.py b/discord/types/command.py index 77700d7f6..1569c0e98 100644 --- a/discord/types/command.py +++ b/discord/types/command.py @@ -24,9 +24,10 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import List, Literal, TypedDict, Union +from typing import List, Literal, Optional, TypedDict, Union from typing_extensions import NotRequired, Required +from .application import IntegrationApplication from .channel import ChannelType from .snowflake import Snowflake @@ -109,6 +110,8 @@ class _NumberApplicationCommandOption(_BaseValueApplicationCommandOption, total= autocomplete: bool +SubCommand = Union[_SubCommandCommandOption, _SubCommandGroupCommandOption] + _ValueApplicationCommandOption = Union[ _StringApplicationCommandOption, _IntegerApplicationCommandOption, @@ -137,13 +140,10 @@ class _BaseApplicationCommand(TypedDict): version: Snowflake -class _ChatInputApplicationCommand(_BaseApplicationCommand, total=False): - description: Required[str] +class _ChatInputApplicationCommand(_BaseApplicationCommand): + description: str type: Literal[1] - options: Union[ - List[_ValueApplicationCommandOption], - List[Union[_SubCommandCommandOption, _SubCommandGroupCommandOption]], - ] + options: NotRequired[List[ApplicationCommandOption]] class _BaseContextMenuApplicationCommand(_BaseApplicationCommand): @@ -177,13 +177,23 @@ class _GuildMessageApplicationCommand(_MessageApplicationCommand): guild_id: Snowflake +ChatInputCommand = Union[ + _ChatInputApplicationCommand, + _GuildChatInputApplicationCommand, +] +UserCommand = Union[ + _UserApplicationCommand, + _GuildUserApplicationCommand, +] +MessageCommand = Union[ + _MessageApplicationCommand, + _GuildMessageApplicationCommand, +] GuildApplicationCommand = Union[ _GuildChatInputApplicationCommand, _GuildUserApplicationCommand, _GuildMessageApplicationCommand, ] - - ApplicationCommand = Union[ GlobalApplicationCommand, GuildApplicationCommand, @@ -204,3 +214,15 @@ class GuildApplicationCommandPermissions(TypedDict): application_id: Snowflake guild_id: Snowflake permissions: List[ApplicationCommandPermissions] + + +class ApplicationCommandCursor(TypedDict): + next: Optional[str] + previous: Optional[str] + repaired: Optional[str] + + +class ApplicationCommandSearch(TypedDict): + application_commands: List[ApplicationCommand] + applications: Optional[List[IntegrationApplication]] + cursor: ApplicationCommandCursor diff --git a/discord/types/components.py b/discord/types/components.py index 697490bd6..87e76a1af 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -34,9 +34,17 @@ ButtonStyle = Literal[1, 2, 3, 4, 5] TextStyle = Literal[1, 2] -class ActionRow(TypedDict): +class MessageActionRow(TypedDict): type: Literal[1] - components: List[ActionRowChildComponent] + components: List[MessageChildComponent] + + +class ModalActionRow(TypedDict): + type: Literal[1] + components: List[ModalChildComponent] + + +ActionRow = Union[MessageActionRow, ModalActionRow] class ButtonComponent(TypedDict): @@ -79,5 +87,7 @@ class TextInput(TypedDict): max_length: NotRequired[int] -ActionRowChildComponent = Union[ButtonComponent, SelectMenu, TextInput] +MessageChildComponent = Union[ButtonComponent, SelectMenu] +ModalChildComponent = TextInput +ActionRowChildComponent = Union[MessageChildComponent, ModalChildComponent] Component = Union[ActionRow, ActionRowChildComponent] diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 5f74cf059..d64b08eed 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -24,11 +24,10 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import List, Literal, Optional, TypedDict, Union - +from typing import Generic, List, Literal, Optional, TypedDict, TypeVar, Union from typing_extensions import NotRequired, Required -from .activity import Activity, ClientStatus, PartialPresenceUpdate, StatusType +from .activity import Activity, BasePresenceUpdate, PartialPresenceUpdate, StatusType from .application import BaseAchievement from .audit_log import AuditLogEntry from .automod import AutoModerationAction, AutoModerationRuleTriggerType @@ -38,10 +37,10 @@ from .entitlements import Entitlement, GatewayGift from .experiment import GuildExperiment, UserExperiment from .guild import ApplicationCommandCounts, Guild, SupplementalGuild, UnavailableGuild from .integration import BaseIntegration, IntegrationApplication -from .interactions import Interaction +from .interactions import Modal from .invite import _InviteTargetType from .library import LibraryApplication -from .member import MemberWithUser +from .member import MemberWithPresence, MemberWithUser from .message import Message from .payments import Payment from .read_state import ReadState, ReadStateType @@ -51,15 +50,23 @@ from .snowflake import Snowflake from .sticker import GuildSticker from .subscriptions import PremiumGuildSubscriptionSlot from .threads import Thread, ThreadMember -from .user import Connection, FriendSuggestion, PartialUser, ProtoSettingsType, Relationship, RelationshipType, User -from .voice import GuildVoiceState - - -class UserPresenceUpdateEvent(TypedDict): - user: PartialUser - status: StatusType - activities: List[Activity] - client_status: ClientStatus +from .user import ( + Connection, + FriendSuggestion, + PartialConsentSettings, + PartialUser, + ProtoSettingsType, + Relationship, + RelationshipType, + User, + UserGuildSettings, +) +from .voice import GuildVoiceState, VoiceState + +T = TypeVar('T') + + +class UserPresenceUpdateEvent(BasePresenceUpdate): last_modified: int @@ -86,6 +93,7 @@ class ReadyEvent(ResumedEvent): auth_session_id_hash: str auth_token: NotRequired[str] connected_accounts: List[Connection] + consents: PartialConsentSettings country_code: str experiments: List[UserExperiment] friend_suggestion_count: int @@ -95,17 +103,17 @@ class ReadyEvent(ResumedEvent): merged_members: List[List[MemberWithUser]] pending_payments: NotRequired[List[Payment]] private_channels: List[Union[DMChannel, GroupDMChannel]] - read_state: VersionedReadState + read_state: Versioned[ReadState] relationships: List[Relationship] resume_gateway_url: str required_action: NotRequired[str] sessions: List[Session] session_id: str - session_type: str + session_type: Literal['normal'] shard: NotRequired[ShardInfo] tutorial: Optional[Tutorial] user: User - user_guild_settings: dict + user_guild_settings: Versioned[UserGuildSettings] user_settings_proto: NotRequired[str] users: List[PartialUser] v: int @@ -138,8 +146,8 @@ class ReadySupplementalEvent(TypedDict): disclose: List[str] -class VersionedReadState(TypedDict): - entries: List[ReadState] +class Versioned(TypedDict, Generic[T]): + entries: List[T] version: int partial: bool @@ -205,9 +213,6 @@ class MessageReactionRemoveEmojiEvent(TypedDict): guild_id: NotRequired[Snowflake] -InteractionCreateEvent = Interaction - - UserUpdateEvent = User @@ -240,6 +245,11 @@ class _ChannelEvent(TypedDict): ChannelCreateEvent = ChannelUpdateEvent = ChannelDeleteEvent = _ChannelEvent +class ChannelRecipientEvent(TypedDict): + channel_id: Snowflake + user: PartialUser + + class ChannelPinsUpdateEvent(TypedDict): channel_id: Snowflake guild_id: NotRequired[Snowflake] @@ -561,3 +571,102 @@ class GuildApplicationCommandIndexUpdateEvent(TypedDict): class UserNoteUpdateEvent(TypedDict): id: Snowflake note: str + + +UserGuildSettingsEvent = UserGuildSettings + + +class InteractionEvent(TypedDict): + id: Snowflake + nonce: NotRequired[Snowflake] + + +InteractionModalCreateEvent = Modal + + +class CallCreateEvent(TypedDict): + channel_id: Snowflake + message_id: Snowflake + embedded_activities: List[dict] + region: str + ringing: List[Snowflake] + voice_states: List[VoiceState] + unavailable: NotRequired[bool] + + +class CallUpdateEvent(TypedDict): + channel_id: Snowflake + guild_id: Optional[Snowflake] # ??? + message_id: Snowflake + region: str + ringing: List[Snowflake] + + +class CallDeleteEvent(TypedDict): + channel_id: Snowflake + unavailable: NotRequired[bool] + + +class _GuildMemberListGroup(TypedDict): + id: Union[Snowflake, Literal['online', 'offline']] + + +class GuildMemberListGroup(_GuildMemberListGroup): + count: int + + +class _GuildMemberListGroupItem(TypedDict): + group: _GuildMemberListGroup + + +class _GuildMemberListMemberItem(TypedDict): + member: MemberWithPresence + + +GuildMemberListItem = Union[_GuildMemberListGroupItem, _GuildMemberListMemberItem] + + +class GuildMemberListSyncOP(TypedDict): + op: Literal['SYNC'] + range: tuple[int, int] + items: List[GuildMemberListItem] + + +class GuildMemberListUpdateOP(TypedDict): + op: Literal['UPDATE'] + index: int + item: _GuildMemberListMemberItem + + +class GuildMemberListInsertOP(TypedDict): + op: Literal['INSERT'] + index: int + item: _GuildMemberListMemberItem + + +class GuildMemberListDeleteOP(TypedDict): + op: Literal['DELETE'] + index: int + + +class GuildMemberListInvalidateOP(TypedDict): + op: Literal['INVALIDATE'] + range: tuple[int, int] + + +GuildMemberListOP = Union[ + GuildMemberListSyncOP, + GuildMemberListUpdateOP, + GuildMemberListInsertOP, + GuildMemberListDeleteOP, + GuildMemberListInvalidateOP, +] + + +class GuildMemberListUpdateEvent(TypedDict): + id: Union[Snowflake, Literal['everyone']] + guild_id: Snowflake + member_count: int + online_count: int + groups: List[GuildMemberListGroup] + ops: List[GuildMemberListOP] diff --git a/discord/types/interactions.py b/discord/types/interactions.py index b5ab32cac..40f3d8780 100644 --- a/discord/types/interactions.py +++ b/discord/types/interactions.py @@ -24,49 +24,20 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import TYPE_CHECKING, Dict, List, Literal, TypedDict, Union +from typing import List, Literal, TypedDict, Union from typing_extensions import NotRequired -from .channel import ChannelTypeWithoutThread, ThreadMetadata -from .threads import ThreadType +from .application import IntegrationApplication +from .command import ApplicationCommand +from .components import ModalActionRow from .member import Member -from .message import Attachment -from .role import Role +from .message import PartialAttachment from .snowflake import Snowflake from .user import User -if TYPE_CHECKING: - from .message import Message - - InteractionType = Literal[1, 2, 3, 4, 5] -class _BasePartialChannel(TypedDict): - id: Snowflake - name: str - permissions: str - - -class PartialChannel(_BasePartialChannel): - type: ChannelTypeWithoutThread - - -class PartialThread(_BasePartialChannel): - type: ThreadType - thread_metadata: ThreadMetadata - parent_id: Snowflake - - -class ResolvedData(TypedDict, total=False): - users: Dict[str, User] - members: Dict[str, Member] - roles: Dict[str, Role] - channels: Dict[str, Union[PartialChannel, PartialThread]] - messages: Dict[str, Message] - attachments: Dict[str, Attachment] - - class _BaseApplicationCommandInteractionDataOption(TypedDict): name: str @@ -123,31 +94,33 @@ ApplicationCommandInteractionDataOption = Union[ class _BaseApplicationCommandInteractionData(TypedDict): id: Snowflake name: str - resolved: NotRequired[ResolvedData] + version: Snowflake guild_id: NotRequired[Snowflake] + application_command: ApplicationCommand + attachments: List[PartialAttachment] + options: List[ApplicationCommandInteractionDataOption] -class ChatInputApplicationCommandInteractionData(_BaseApplicationCommandInteractionData, total=False): +class ChatInputCommandInteractionData(_BaseApplicationCommandInteractionData, total=False): type: Literal[1] - options: List[ApplicationCommandInteractionDataOption] class _BaseNonChatInputApplicationCommandInteractionData(_BaseApplicationCommandInteractionData): target_id: Snowflake -class UserApplicationCommandInteractionData(_BaseNonChatInputApplicationCommandInteractionData): +class UserCommandInteractionData(_BaseNonChatInputApplicationCommandInteractionData): type: Literal[2] -class MessageApplicationCommandInteractionData(_BaseNonChatInputApplicationCommandInteractionData): +class MessageCommandInteractionData(_BaseNonChatInputApplicationCommandInteractionData): type: Literal[3] ApplicationCommandInteractionData = Union[ - ChatInputApplicationCommandInteractionData, - UserApplicationCommandInteractionData, - MessageApplicationCommandInteractionData, + ChatInputCommandInteractionData, + UserCommandInteractionData, + MessageCommandInteractionData, ] @@ -155,40 +128,45 @@ class _BaseMessageComponentInteractionData(TypedDict): custom_id: str -class ButtonMessageComponentInteractionData(_BaseMessageComponentInteractionData): +class ButtonInteractionData(_BaseMessageComponentInteractionData): component_type: Literal[2] -class SelectMessageComponentInteractionData(_BaseMessageComponentInteractionData): +class SelectInteractionData(_BaseMessageComponentInteractionData): component_type: Literal[3] values: List[str] -MessageComponentInteractionData = Union[ButtonMessageComponentInteractionData, SelectMessageComponentInteractionData] +MessageComponentInteractionData = Union[ButtonInteractionData, SelectInteractionData] -class ModalSubmitTextInputInteractionData(TypedDict): +class TextInputInteractionData(TypedDict): type: Literal[4] custom_id: str value: str -ModalSubmitComponentItemInteractionData = ModalSubmitTextInputInteractionData +ModalSubmitComponentInteractionData = TextInputInteractionData -class ModalSubmitActionRowInteractionData(TypedDict): +class MessageActionRowData(TypedDict): type: Literal[1] - components: List[ModalSubmitComponentItemInteractionData] + components: List[MessageComponentInteractionData] -ModalSubmitComponentInteractionData = Union[ModalSubmitActionRowInteractionData, ModalSubmitComponentItemInteractionData] +class ModalSubmitActionRowData(TypedDict): + type: Literal[1] + components: List[ModalSubmitComponentInteractionData] class ModalSubmitInteractionData(TypedDict): + id: Snowflake custom_id: str components: List[ModalSubmitComponentInteractionData] +ActionRowInteractionData = Union[MessageActionRowData, ModalSubmitActionRowData] +ComponentInteractionData = Union[MessageComponentInteractionData, ModalSubmitComponentInteractionData] InteractionData = Union[ ApplicationCommandInteractionData, MessageComponentInteractionData, @@ -196,42 +174,19 @@ InteractionData = Union[ ] -class _BaseInteraction(TypedDict): - id: Snowflake - application_id: Snowflake - token: str - version: Literal[1] - guild_id: NotRequired[Snowflake] - channel_id: NotRequired[Snowflake] - locale: NotRequired[str] - guild_locale: NotRequired[str] - - -class PingInteraction(_BaseInteraction): - type: Literal[1] - - -class ApplicationCommandInteraction(_BaseInteraction): - type: Literal[2, 4] - data: ApplicationCommandInteractionData - - -class MessageComponentInteraction(_BaseInteraction): - type: Literal[3] - data: MessageComponentInteractionData - - -class ModalSubmitInteraction(_BaseInteraction): - type: Literal[5] - data: ModalSubmitInteractionData - - -Interaction = Union[PingInteraction, ApplicationCommandInteraction, MessageComponentInteraction, ModalSubmitInteraction] - - class MessageInteraction(TypedDict): id: Snowflake type: InteractionType name: str user: User member: NotRequired[Member] + + +class Modal(TypedDict): + id: int + nonce: NotRequired[Snowflake] + channel_id: Snowflake + title: str + custom_id: str + application: IntegrationApplication + components: List[ModalActionRow] diff --git a/discord/types/invite.py b/discord/types/invite.py index 192399954..75eeeaf8f 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -78,8 +78,10 @@ class InviteWithMetadata(PartialInvite, _InviteMetadata): ... -Invite = Union[PartialInvite, InviteWithCounts, InviteWithMetadata] - +class AcceptedInvite(InviteWithCounts): + new_member: bool + show_verification_form: bool +Invite = Union[PartialInvite, InviteWithCounts, InviteWithMetadata, AcceptedInvite] GatewayInvite = Union[InviteCreateEvent, InviteDeleteEvent] diff --git a/discord/types/member.py b/discord/types/member.py index 94297132e..654a96158 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -23,6 +23,8 @@ DEALINGS IN THE SOFTWARE. """ from typing import Optional, TypedDict + +from .activity import BasePresenceUpdate from .snowflake import SnowflakeList from .user import PartialUser @@ -60,6 +62,10 @@ class MemberWithUser(_OptionalMemberWithUser): user: PartialUser +class MemberWithPresence(MemberWithUser): + presence: BasePresenceUpdate + + class PrivateMember(MemberWithUser): bio: str banner: Optional[str] diff --git a/discord/types/message.py b/discord/types/message.py index dbee9782a..7de75da80 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -33,7 +33,7 @@ from .user import User from .emoji import PartialEmoji from .embed import Embed from .channel import ChannelType -from .components import Component +from .components import MessageActionRow from .interactions import MessageInteraction from .application import BaseApplication from .sticker import StickerItem @@ -134,7 +134,7 @@ class Message(PartialMessage): sticker_items: NotRequired[List[StickerItem]] referenced_message: NotRequired[Optional[Message]] interaction: NotRequired[MessageInteraction] - components: NotRequired[List[Component]] + components: NotRequired[List[MessageActionRow]] position: NotRequired[int] call: NotRequired[Call] role_subscription_data: NotRequired[RoleSubscriptionData] @@ -190,6 +190,13 @@ MessageSearchSortType = Literal['timestamp', 'relevance'] MessageSearchSortOrder = Literal['desc', 'asc'] +class PartialAttachment(TypedDict): + id: NotRequired[Snowflake] + filename: str + description: NotRequired[str] + uploaded_filename: NotRequired[str] + + class UploadedAttachment(TypedDict): id: NotRequired[Snowflake] filename: str diff --git a/discord/types/user.py b/discord/types/user.py index 064054947..c8379dc8c 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -136,11 +136,70 @@ class Relationship(TypedDict): since: NotRequired[str] +ProtoSettingsType = Literal[1, 2, 3] + + class ProtoSettings(TypedDict): settings: str -ProtoSettingsType = Literal[1, 2, 3] +class _ConsentSettings(TypedDict): + consented: bool + + +class PartialConsentSettings(TypedDict): + personalization: _ConsentSettings + + +class ConsentSettings(PartialConsentSettings): + usage_statistics: _ConsentSettings + + +class _EmailSettingsCategories(TypedDict): + communication: bool + social: bool + recommendations_and_events: bool + tips: bool + updates_and_announcements: bool + family_center_digest: bool + + +class EmailSettings(TypedDict): + initialized: bool + categories: _EmailSettingsCategories + + +MessageNotificationLevel = Literal[0, 1, 2, 3] +HighlightLevel = Literal[0, 1, 2] + + +class MuteConfig(TypedDict): + end_time: Optional[str] + selected_time_window: Optional[int] + + +class ChannelOverride(TypedDict): + channel_id: Snowflake + collapsed: bool + message_notifications: MessageNotificationLevel + muted: bool + mute_config: Optional[MuteConfig] + + +class UserGuildSettings(TypedDict): + guild_id: Optional[Snowflake] + channel_overrides: List[ChannelOverride] + flags: int + message_notifications: MessageNotificationLevel + notify_highlights: HighlightLevel + hide_muted_channels: bool + mobile_push: bool + muted: bool + mute_config: Optional[MuteConfig] + mute_scheduled_events: bool + suppress_everyone: bool + suppress_roles: bool + version: int class UserAffinity(TypedDict): @@ -177,3 +236,7 @@ class FriendSuggestionReason(TypedDict): class FriendSuggestion(TypedDict): suggested_user: PartialUser reasons: List[FriendSuggestionReason] + + +class Report(TypedDict): + report_id: Snowflake