From 0b3f1dd74682e50670118db62c06968920074887 Mon Sep 17 00:00:00 2001 From: shiftinv <8530778+shiftinv@users.noreply.github.com> Date: Tue, 25 Oct 2022 17:43:19 +0200 Subject: [PATCH] refactor: slightly rework resolved interaction data handling (#814) Co-authored-by: arl --- changelog/814.breaking.rst | 1 + changelog/814.bugfix.rst | 1 + changelog/814.deprecate.rst | 1 + disnake/interactions/application_command.py | 235 ++------------------ disnake/interactions/base.py | 222 ++++++++++++++++-- disnake/types/interactions.py | 40 ++-- docs/api.rst | 16 +- tests/interactions/test_base.py | 87 ++++++++ 8 files changed, 348 insertions(+), 255 deletions(-) create mode 100644 changelog/814.breaking.rst create mode 100644 changelog/814.bugfix.rst create mode 100644 changelog/814.deprecate.rst diff --git a/changelog/814.breaking.rst b/changelog/814.breaking.rst new file mode 100644 index 0000000000..6e151927a6 --- /dev/null +++ b/changelog/814.breaking.rst @@ -0,0 +1 @@ +Rename :meth:`InteractionDataResolved.get` to :meth:`~InteractionDataResolved.get_by_id`. diff --git a/changelog/814.bugfix.rst b/changelog/814.bugfix.rst new file mode 100644 index 0000000000..d5263a0930 --- /dev/null +++ b/changelog/814.bugfix.rst @@ -0,0 +1 @@ +Try to get threads used in interactions (like threads in command arguments) from the cache first, before creating a new instance. diff --git a/changelog/814.deprecate.rst b/changelog/814.deprecate.rst new file mode 100644 index 0000000000..8bd663466c --- /dev/null +++ b/changelog/814.deprecate.rst @@ -0,0 +1 @@ +Rename :class:`ApplicationCommandInteractionDataResolved` to :class:`InteractionDataResolved`. diff --git a/disnake/interactions/application_command.py b/disnake/interactions/application_command.py index 8aab7917c2..1cb7154a10 100644 --- a/disnake/interactions/application_command.py +++ b/disnake/interactions/application_command.py @@ -2,26 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Union from .. import utils -from ..channel import ( - CategoryChannel, - ForumChannel, - PartialMessageable, - StageChannel, - TextChannel, - VoiceChannel, - _threaded_guild_channel_factory, -) -from ..enums import ApplicationCommandType, ChannelType, Locale, OptionType, try_enum +from ..enums import ApplicationCommandType, Locale, OptionType, try_enum from ..guild import Guild from ..member import Member -from ..message import Attachment, Message -from ..object import Object -from ..role import Role +from ..message import Message from ..user import User -from .base import Interaction +from .base import Interaction, InteractionDataResolved __all__ = ( "ApplicationCommandInteraction", @@ -44,27 +33,13 @@ MISSING = utils.MISSING if TYPE_CHECKING: - from ..abc import MessageableChannel from ..ext.commands import InvokableApplicationCommand from ..state import ConnectionState - from ..threads import Thread from ..types.interactions import ( ApplicationCommandInteraction as ApplicationCommandInteractionPayload, ApplicationCommandInteractionData as ApplicationCommandInteractionDataPayload, - ApplicationCommandInteractionDataResolved as ApplicationCommandInteractionDataResolvedPayload, ) - InteractionChannel = Union[ - VoiceChannel, - StageChannel, - TextChannel, - CategoryChannel, - Thread, - PartialMessageable, - VoiceChannel, - ForumChannel, - ] - class ApplicationCommandInteraction(Interaction): """Represents an interaction with an application command. @@ -193,7 +168,7 @@ class ApplicationCommandInteractionData(Dict[str, Any]): The application command name. type: :class:`ApplicationCommandType` The application command type. - resolved: :class:`ApplicationCommandInteractionDataResolved` + resolved: :class:`InteractionDataResolved` All resolved objects related to this interaction. options: List[:class:`ApplicationCommandInteractionDataOption`] A list of options from the API. @@ -224,11 +199,14 @@ def __init__( self.id: int = int(data["id"]) self.name: str = data["name"] self.type: ApplicationCommandType = try_enum(ApplicationCommandType, data["type"]) - self.resolved = ApplicationCommandInteractionDataResolved( + + self.resolved = InteractionDataResolved( data=data.get("resolved", {}), state=state, guild_id=guild_id ) self.target_id: Optional[int] = utils._get_as_snowflake(data, "target_id") - self.target: Optional[Union[User, Member, Message]] = self.resolved.get(self.target_id) # type: ignore + target = self.resolved.get_by_id(self.target_id) + self.target: Optional[Union[User, Member, Message]] = target # type: ignore + self.options: List[ApplicationCommandInteractionDataOption] = [ ApplicationCommandInteractionDataOption(data=d, resolved=self.resolved) for d in data.get("options", []) @@ -294,17 +272,15 @@ class ApplicationCommandInteractionDataOption(Dict[str, Any]): __slots__ = ("name", "type", "value", "options", "focused") - def __init__( - self, *, data: Mapping[str, Any], resolved: ApplicationCommandInteractionDataResolved - ): + def __init__(self, *, data: Mapping[str, Any], resolved: InteractionDataResolved): super().__init__(data) self.name: str = data["name"] self.type: OptionType = try_enum(OptionType, data["type"]) - value = data.get("value") - if value is not None: - self.value: Any = resolved.get_with_type(value, self.type.value, value) - else: - self.value: Any = None + + self.value: Any = None + if (value := data.get("value")) is not None: + self.value: Any = resolved.get_with_type(value, self.type, value) + self.options: List[ApplicationCommandInteractionDataOption] = [ ApplicationCommandInteractionDataOption(data=d, resolved=resolved) for d in data.get("options", []) @@ -343,183 +319,8 @@ def _get_chain_and_kwargs( return chain, {} -class ApplicationCommandInteractionDataResolved(Dict[str, Any]): - """Represents the resolved data related to an interaction with an application command. - - .. versionadded:: 2.1 - - Attributes - ---------- - members: Dict[:class:`int`, :class:`Member`] - A mapping of IDs to partial members (``deaf`` and ``mute`` attributes are missing). - users: Dict[:class:`int`, :class:`User`] - A mapping of IDs to users. - roles: Dict[:class:`int`, :class:`Role`] - A mapping of IDs to roles. - channels: Dict[:class:`int`, Channel] - A mapping of IDs to partial channels (only ``id``, ``name`` and ``permissions`` are included, - threads also have ``thread_metadata`` and ``parent_id``). - messages: Dict[:class:`int`, :class:`Message`] - A mapping of IDs to messages. - attachments: Dict[:class:`int`, :class:`Attachment`] - A mapping of IDs to attachments. - - .. versionadded:: 2.4 - """ - - __slots__ = ("members", "users", "roles", "channels", "messages", "attachments") - - def __init__( - self, - *, - data: ApplicationCommandInteractionDataResolvedPayload, - state: ConnectionState, - guild_id: Optional[int], - ): - data = data or {} - super().__init__(data) - - self.members: Dict[int, Member] = {} - self.users: Dict[int, User] = {} - self.roles: Dict[int, Role] = {} - self.channels: Dict[int, InteractionChannel] = {} - self.messages: Dict[int, Message] = {} - self.attachments: Dict[int, Attachment] = {} - - users = data.get("users", {}) - members = data.get("members", {}) - roles = data.get("roles", {}) - channels = data.get("channels", {}) - messages = data.get("messages", {}) - attachments = data.get("attachments", {}) - - guild: Optional[Guild] = None - # `guild_fallback` is only used in guild contexts, so this `MISSING` value should never be used. - # We need to define it anyway to satisfy the typechecker. - guild_fallback: Union[Guild, Object] = MISSING - if guild_id is not None: - guild = state._get_guild(guild_id) - guild_fallback = guild or Object(id=guild_id) - - for str_id, user in users.items(): - user_id = int(str_id) - member = members.get(str_id) - if member is not None: - self.members[user_id] = ( - guild - and guild.get_member(user_id) - or Member( - data=member, - user_data=user, - guild=guild_fallback, # type: ignore - state=state, - ) - ) - else: - self.users[user_id] = User(state=state, data=user) - - for str_id, role in roles.items(): - self.roles[int(str_id)] = Role( - guild=guild_fallback, # type: ignore - state=state, - data=role, - ) - - for str_id, channel in channels.items(): - channel_id = int(str_id) - factory, _ = _threaded_guild_channel_factory(channel["type"]) - if factory: - channel["position"] = 0 # type: ignore - self.channels[channel_id] = ( - guild - and guild.get_channel(channel_id) - or factory( - guild=guild_fallback, # type: ignore - state=state, - data=channel, # type: ignore - ) - ) - else: - self.channels[channel_id] = PartialMessageable( - state=state, id=channel_id, type=try_enum(ChannelType, channel["type"]) - ) - - for str_id, message in messages.items(): - channel_id = int(message["channel_id"]) - channel = cast( - "Optional[MessageableChannel]", - (guild and guild.get_channel(channel_id) or state.get_channel(channel_id)), - ) - if channel is None: - # The channel is not part of `resolved.channels`, - # so we need to fall back to partials here. - channel = PartialMessageable(state=state, id=channel_id, type=None) - self.messages[int(str_id)] = Message(state=state, channel=channel, data=message) - - for str_id, attachment in attachments.items(): - self.attachments[int(str_id)] = Attachment(data=attachment, state=state) - - def __repr__(self): - return ( - f"" - ) - - def get_with_type(self, key: Any, option_type: OptionType, default: Any = None): - if isinstance(option_type, int): - option_type = try_enum(OptionType, option_type) - if option_type is OptionType.mentionable: - key = int(key) - result = self.members.get(key) - if result is not None: - return result - result = self.users.get(key) - if result is not None: - return result - return self.roles.get(key, default) - - if option_type is OptionType.user: - key = int(key) - member = self.members.get(key) - if member is not None: - return member - return self.users.get(key, default) - - if option_type is OptionType.channel: - return self.channels.get(int(key), default) - - if option_type is OptionType.role: - return self.roles.get(int(key), default) - - if option_type is OptionType.attachment: - return self.attachments.get(int(key), default) - - return default - - def get(self, key: int): - if key is None: - return None - - res = self.members.get(key) - if res is not None: - return res - res = self.users.get(key) - if res is not None: - return res - res = self.roles.get(key) - if res is not None: - return res - res = self.channels.get(key) - if res is not None: - return res - res = self.messages.get(key) - if res is not None: - return res - res = self.attachments.get(key) - if res is not None: - return res - - return None +# backwards compatibility +ApplicationCommandInteractionDataResolved = InteractionDataResolved # People asked about shorter aliases, let's see which one catches on the most diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index c4f08f3783..f2e4040203 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -4,12 +4,32 @@ import asyncio from datetime import timedelta -from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Union, cast, overload +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Mapping, + Optional, + Tuple, + TypeVar, + Union, + cast, + overload, +) from .. import utils from ..app_commands import OptionChoice -from ..channel import ChannelType, PartialMessageable -from ..enums import InteractionResponseType, InteractionType, Locale, WebhookType, try_enum +from ..channel import PartialMessageable, _threaded_guild_channel_factory +from ..enums import ( + ChannelType, + InteractionResponseType, + InteractionType, + Locale, + OptionType, + WebhookType, + try_enum, +) from ..errors import ( HTTPException, InteractionNotEditable, @@ -26,6 +46,7 @@ from ..message import Attachment, Message from ..object import Object from ..permissions import Permissions +from ..role import Role from ..ui.action_row import components_to_dict from ..user import ClientUser, User from ..webhook.async_ import Webhook, async_context, handle_message_parameters @@ -34,6 +55,7 @@ "Interaction", "InteractionMessage", "InteractionResponse", + "InteractionDataResolved", ) if TYPE_CHECKING: @@ -41,13 +63,13 @@ from aiohttp import ClientSession + from ..abc import MessageableChannel from ..app_commands import Choices - from ..channel import CategoryChannel, StageChannel, TextChannel, VoiceChannel from ..client import Client from ..embeds import Embed from ..ext.commands import AutoShardedBot, Bot from ..file import File - from ..guild import GuildMessageable + from ..guild import GuildChannel, GuildMessageable from ..mentions import AllowedMentions from ..state import ConnectionState from ..threads import Thread @@ -55,26 +77,24 @@ from ..types.interactions import ( ApplicationCommandOptionChoice as ApplicationCommandOptionChoicePayload, Interaction as InteractionPayload, + InteractionDataResolved as InteractionDataResolvedPayload, ) + from ..types.snowflake import Snowflake from ..ui.action_row import Components, MessageUIComponent, ModalUIComponent from ..ui.modal import Modal from ..ui.view import View from .message import MessageInteraction from .modal import ModalInteraction - InteractionChannel = Union[ - VoiceChannel, - StageChannel, - TextChannel, - CategoryChannel, - Thread, - PartialMessageable, - ] + InteractionChannel = Union[GuildChannel, Thread, PartialMessageable] AnyBot = Union[Bot, AutoShardedBot] + MISSING: Any = utils.MISSING +T = TypeVar("T") + class Interaction: """A base class representing a user-initiated Discord interaction. @@ -1613,3 +1633,179 @@ async def inner_call(delay: float = delay): asyncio.create_task(inner_call()) else: await self._state._interaction.delete_original_response() + + +class InteractionDataResolved(Dict[str, Any]): + """Represents the resolved data related to an interaction. + + .. versionadded:: 2.1 + + .. versionchanged:: 2.7 + Renamed from ``ApplicationCommandInteractionDataResolved`` to ``InteractionDataResolved``. + + Attributes + ---------- + members: Dict[:class:`int`, :class:`Member`] + A mapping of IDs to partial members (``deaf`` and ``mute`` attributes are missing). + users: Dict[:class:`int`, :class:`User`] + A mapping of IDs to users. + roles: Dict[:class:`int`, :class:`Role`] + A mapping of IDs to roles. + channels: Dict[:class:`int`, Union[:class:`abc.GuildChannel`, :class:`Thread`, :class:`PartialMessageable`]] + A mapping of IDs to partial channels (only ``id``, ``name`` and ``permissions`` are included, + threads also have ``thread_metadata`` and ``parent_id``). + messages: Dict[:class:`int`, :class:`Message`] + A mapping of IDs to messages. + attachments: Dict[:class:`int`, :class:`Attachment`] + A mapping of IDs to attachments. + + .. versionadded:: 2.4 + """ + + __slots__ = ("members", "users", "roles", "channels", "messages", "attachments") + + def __init__( + self, + *, + data: InteractionDataResolvedPayload, + state: ConnectionState, + guild_id: Optional[int], + ): + data = data or {} + super().__init__(data) + + self.members: Dict[int, Member] = {} + self.users: Dict[int, User] = {} + self.roles: Dict[int, Role] = {} + self.channels: Dict[int, InteractionChannel] = {} + self.messages: Dict[int, Message] = {} + self.attachments: Dict[int, Attachment] = {} + + users = data.get("users", {}) + members = data.get("members", {}) + roles = data.get("roles", {}) + channels = data.get("channels", {}) + messages = data.get("messages", {}) + attachments = data.get("attachments", {}) + + guild: Optional[Guild] = None + # `guild_fallback` is only used in guild contexts, so this `MISSING` value should never be used. + # We need to define it anyway to satisfy the typechecker. + guild_fallback: Union[Guild, Object] = MISSING + if guild_id is not None: + guild = state._get_guild(guild_id) + guild_fallback = guild or Object(id=guild_id) + + for str_id, user in users.items(): + user_id = int(str_id) + member = members.get(str_id) + if member is not None: + self.members[user_id] = ( + guild + and guild.get_member(user_id) + or Member( + data=member, + user_data=user, + guild=guild_fallback, # type: ignore + state=state, + ) + ) + else: + self.users[user_id] = User(state=state, data=user) + + for str_id, role in roles.items(): + self.roles[int(str_id)] = Role( + guild=guild_fallback, # type: ignore + state=state, + data=role, + ) + + for str_id, channel in channels.items(): + channel_id = int(str_id) + factory, _ = _threaded_guild_channel_factory(channel["type"]) + if factory: + channel["position"] = 0 # type: ignore + self.channels[channel_id] = ( + guild + and guild.get_channel_or_thread(channel_id) + or factory( + guild=guild_fallback, # type: ignore + state=state, + data=channel, # type: ignore + ) + ) + else: + # TODO: guild_directory is not messageable + self.channels[channel_id] = PartialMessageable( + state=state, id=channel_id, type=try_enum(ChannelType, channel["type"]) + ) + + for str_id, message in messages.items(): + channel_id = int(message["channel_id"]) + channel = cast( + "Optional[MessageableChannel]", + (guild and guild.get_channel(channel_id) or state.get_channel(channel_id)), + ) + if channel is None: + # The channel is not part of `resolved.channels`, + # so we need to fall back to partials here. + channel = PartialMessageable(state=state, id=channel_id, type=None) + self.messages[int(str_id)] = Message(state=state, channel=channel, data=message) + + for str_id, attachment in attachments.items(): + self.attachments[int(str_id)] = Attachment(data=attachment, state=state) + + def __repr__(self): + return ( + f"" + ) + + def get_with_type( + self, key: Snowflake, data_type: OptionType, default: T = None + ) -> Union[Member, User, Role, InteractionChannel, Message, Attachment, T]: + if data_type is OptionType.mentionable: + key = int(key) + if (result := self.members.get(key)) is not None: + return result + if (result := self.users.get(key)) is not None: + return result + return self.roles.get(key, default) + + if data_type is OptionType.user: + key = int(key) + if (member := self.members.get(key)) is not None: + return member + return self.users.get(key, default) + + if data_type is OptionType.channel: + return self.channels.get(int(key), default) + + if data_type is OptionType.role: + return self.roles.get(int(key), default) + + if data_type is OptionType.attachment: + return self.attachments.get(int(key), default) + + return default + + def get_by_id( + self, key: Optional[int] + ) -> Optional[Union[Member, User, Role, InteractionChannel, Message, Attachment]]: + if key is None: + return None + + if (res := self.members.get(key)) is not None: + return res + if (res := self.users.get(key)) is not None: + return res + if (res := self.roles.get(key)) is not None: + return res + if (res := self.channels.get(key)) is not None: + return res + if (res := self.messages.get(key)) is not None: + return res + if (res := self.attachments.get(key)) is not None: + return res + + return None diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index faac07ce9a..9a83737c79 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -12,6 +12,7 @@ from .member import Member, MemberWithUser from .role import Role from .snowflake import Snowflake +from .threads import ThreadMetadata from .user import User if TYPE_CHECKING: @@ -88,6 +89,27 @@ class GuildApplicationCommandPermissions(TypedDict): InteractionType = Literal[1, 2, 3, 4, 5] +class ResolvedPartialChannel(TypedDict): + id: Snowflake + type: ChannelType + permissions: str + name: str + + # only in threads + thread_metadata: NotRequired[ThreadMetadata] + parent_id: NotRequired[Snowflake] + + +class InteractionDataResolved(TypedDict, total=False): + users: Dict[Snowflake, User] + members: Dict[Snowflake, Member] + roles: Dict[Snowflake, Role] + channels: Dict[Snowflake, ResolvedPartialChannel] + # only in application commands + messages: Dict[Snowflake, Message] + attachments: Dict[Snowflake, Attachment] + + class _ApplicationCommandInteractionDataOption(TypedDict): name: str @@ -132,27 +154,11 @@ class _ApplicationCommandInteractionDataOptionNumber(_ApplicationCommandInteract ] -class ApplicationCommandResolvedPartialChannel(TypedDict): - id: Snowflake - type: ChannelType - permissions: str - name: str - - -class ApplicationCommandInteractionDataResolved(TypedDict, total=False): - users: Dict[Snowflake, User] - members: Dict[Snowflake, Member] - roles: Dict[Snowflake, Role] - channels: Dict[Snowflake, ApplicationCommandResolvedPartialChannel] - messages: Dict[Snowflake, Message] - attachments: Dict[Snowflake, Attachment] - - class ApplicationCommandInteractionData(TypedDict): id: Snowflake name: str type: ApplicationCommandType - resolved: NotRequired[ApplicationCommandInteractionDataResolved] + resolved: NotRequired[InteractionDataResolved] options: NotRequired[List[ApplicationCommandInteractionDataOption]] # this is the guild the command is registered to, not the guild the command was invoked in (see interaction.guild_id) guild_id: NotRequired[Snowflake] diff --git a/docs/api.rst b/docs/api.rst index 9a7c8d5776..11d7ad8659 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5098,6 +5098,14 @@ InteractionMessage :members: :inherited-members: +InteractionDataResolved +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: InteractionDataResolved + +.. autoclass:: InteractionDataResolved() + :members: + ApplicationCommandInteractionData ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -5114,14 +5122,6 @@ ApplicationCommandInteractionDataOption .. autoclass:: ApplicationCommandInteractionDataOption() :members: -ApplicationCommandInteractionDataResolved -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. attributetable:: ApplicationCommandInteractionDataResolved - -.. autoclass:: ApplicationCommandInteractionDataResolved() - :members: - MessageInteractionData ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/interactions/test_base.py b/tests/interactions/test_base.py index eefea54d2c..29140b65f7 100644 --- a/tests/interactions/test_base.py +++ b/tests/interactions/test_base.py @@ -1,13 +1,22 @@ # SPDX-License-Identifier: MIT +from __future__ import annotations + +from typing import TYPE_CHECKING from unittest import mock import pytest import disnake from disnake import InteractionResponseType as ResponseType # shortcut +from disnake.state import ConnectionState from disnake.utils import MISSING +if TYPE_CHECKING: + from disnake.types.interactions import ResolvedPartialChannel as ResolvedPartialChannelPayload + from disnake.types.member import Member as MemberPayload + from disnake.types.user import User as UserPayload + @pytest.mark.asyncio class TestInteractionResponse: @@ -116,3 +125,81 @@ async def test_defer_invalid_parent(self, response: disnake.InteractionResponse, with pytest.raises(TypeError, match="This interaction must be of type"): await response.defer() adapter.create_interaction_response.assert_not_called() + + +class TestInteractionDataResolved: + # TODO: use proper mock models once we have state/guild mocks + @pytest.fixture() + def state(self): + s = mock.Mock(spec_set=ConnectionState) + s._get_guild.return_value = None + return s + + def test_init_member(self, state): + member_payload: MemberPayload = { + "roles": [], + "joined_at": "2022-09-02T22:00:55.069000+00:00", + "deaf": False, + "mute": False, + } + + user_payload: UserPayload = { + "id": "1234", + "discriminator": "1111", + "username": "h", + "avatar": None, + } + + # user only, should deserialize user object + resolved = disnake.InteractionDataResolved( + data={"users": {"1234": user_payload}}, + state=state, + guild_id=1234, + ) + assert len(resolved.members) == 0 + assert len(resolved.users) == 1 + + # member only, shouldn't deserialize anything + resolved = disnake.InteractionDataResolved( + data={"members": {"1234": member_payload}}, + state=state, + guild_id=1234, + ) + assert len(resolved.members) == 0 + assert len(resolved.users) == 0 + + # user + member, should deserialize member object only + resolved = disnake.InteractionDataResolved( + data={"users": {"1234": user_payload}, "members": {"1234": member_payload}}, + state=state, + guild_id=1234, + ) + assert len(resolved.members) == 1 + assert len(resolved.users) == 0 + + @pytest.mark.parametrize("channel_type", [t.value for t in disnake.ChannelType]) + def test_channel(self, state, channel_type): + channel_data: ResolvedPartialChannelPayload = { + "id": "42", + "type": channel_type, + "permissions": "7", + "name": "a-channel", + } + if channel_type in (10, 11, 12): # thread + channel_data["parent_id"] = "123123" + channel_data["thread_metadata"] = { + "archived": False, + "auto_archive_duration": 60, + "archive_timestamp": "2022-09-02T22:00:55.069000+00:00", + "locked": False, + } + + resolved = disnake.InteractionDataResolved( + data={"channels": {"42": channel_data}}, state=state, guild_id=1234 + ) + assert len(resolved.channels) == 1 + + channel = next(iter(resolved.channels.values())) + # should be partial if and only if it's a dm/group + # TODO: currently includes directory channels (14), see `InteractionDataResolved.__init__` + assert isinstance(channel, disnake.PartialMessageable) == (channel_type in (1, 3, 14))