diff --git a/changelog/1235.breaking.rst b/changelog/1235.breaking.rst new file mode 100644 index 0000000000..602504b8e6 --- /dev/null +++ b/changelog/1235.breaking.rst @@ -0,0 +1 @@ +:attr:`Interaction.guild` is now a :class:`Guild` instance if the guild could not be found in cache. diff --git a/disnake/guild.py b/disnake/guild.py index 97d45ef19e..0ef33ad5c7 100644 --- a/disnake/guild.py +++ b/disnake/guild.py @@ -96,6 +96,7 @@ from .app_commands import APIApplicationCommand from .asset import AssetBytes from .automod import AutoModTriggerMetadata + from .interactions import Interaction from .permissions import Permissions from .state import ConnectionState from .template import Template @@ -111,6 +112,7 @@ Guild as GuildPayload, GuildFeature, MFALevel, + PartialGuild as PartialGuildPayload, ) from .types.integration import Integration as IntegrationPayload, IntegrationType from .types.role import CreateRole as CreateRolePayload @@ -5211,6 +5213,32 @@ async def fetch_soundboard_sounds(self) -> List[GuildSoundboardSound]: ] +class PartialInteractionGuild(Guild): + """Reimplementation of :class:`Guild` for guilds interactions.""" + + def __init__( + self, + *, + state: ConnectionState, + data: PartialGuildPayload, + interaction: Interaction, + ) -> None: + super().__init__(state=state, data=data) + # init the fake data + self._add_role( + Role( + state=state, + guild=self, + data={"id": self.id, "name": "@everyone"}, # type: ignore + ) + ) + self._add_channel(interaction.channel) # type: ignore + + @property + def me(self) -> Any: + return self._state.user + + PlaceholderID = NewType("PlaceholderID", int) diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index e644a6fad7..a92013ac19 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -43,12 +43,11 @@ NotFound, ) from ..flags import InteractionContextTypes, MessageFlags -from ..guild import Guild +from ..guild import Guild, PartialInteractionGuild from ..http import HTTPClient from ..i18n import Localized from ..member import Member from ..message import Attachment, AuthorizingIntegrationOwners, Message -from ..object import Object from ..permissions import Permissions from ..role import Role from ..ui.action_row import normalize_components, normalize_components_to_dict @@ -80,6 +79,7 @@ Modal as ModalPayload, ModalTopLevelComponent as ModalTopLevelComponentPayload, ) + from ..types.guild import PartialGuild as PartialGuildPayload from ..types.interactions import ( ApplicationCommandOptionChoice as ApplicationCommandOptionChoicePayload, Interaction as InteractionPayload, @@ -222,6 +222,7 @@ class Interaction(Generic[ClientT]): "_state", "_session", "_original_response", + "_guild", "_cs_response", "_cs_followup", "_cs_me", @@ -242,6 +243,7 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState) -> None: self.version: int = data["version"] self.application_id: int = int(data["application_id"]) self.guild_id: Optional[int] = utils._get_as_snowflake(data, "guild_id") + self._guild: Optional[PartialGuildPayload] = data.get("guild") self.locale: Locale = try_enum(Locale, data["locale"]) guild_locale = data.get("guild_locale") @@ -254,19 +256,26 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState) -> None: # one of user and member will always exist self.author: Union[User, Member] = MISSING - guild_fallback: Optional[Union[Guild, Object]] = None - if self.guild_id: - guild_fallback = self.guild or Object(self.guild_id) + guild_fallback: Optional[Guild] = None + if self.guild_id is not None: + guild_fallback = self._state._get_guild(self.guild_id) or self._fallback_guild if guild_fallback and (member := data.get("member")): - self.author = ( - isinstance(guild_fallback, Guild) - and guild_fallback.get_member(int(member["user"]["id"])) - ) or Member( - state=self._state, - guild=guild_fallback, # type: ignore # may be `Object` - data=member, - ) + if isinstance(guild_fallback, PartialInteractionGuild): + author = Member( + state=self._state, + guild=guild_fallback, + data=member, + ) + guild_fallback._add_member(author) + else: + author = guild_fallback.get_member(int(member["user"]["id"])) or Member( + state=self._state, + guild=guild_fallback, + data=member, + ) + self._permissions = int(member.get("permissions", 0)) + self.author = author self._permissions = int(member.get("permissions", 0)) elif user := data.get("user"): self.author = self._state.store_user(user) @@ -322,8 +331,19 @@ def guild(self) -> Optional[Guild]: To check whether an interaction was sent from a guild, consider using :attr:`guild_id` or :attr:`context` instead. + + .. versionchanged:: 2.12 + Returns a :class:`Guild` object when the guild could not be resolved from cache. + This object is created from the data provided by Discord, but it is not complete. + The only populated attributes are: + - :attr:`Guild.id` + - :attr:`Guild.preferred_locale` + - :attr:`Guild.features` """ - return self._state._get_guild(self.guild_id) + if self.guild_id is None: + return None + + return self._state._get_guild(self.guild_id) or self._fallback_guild @utils.cached_slot_property("_cs_me") def me(self) -> Union[Member, ClientUser]: @@ -394,6 +414,12 @@ def expires_at(self) -> datetime: """ return self.created_at + timedelta(minutes=15) + @utils.cached_slot_property("_cs_fallback_guild") + def _fallback_guild(self) -> Optional[PartialInteractionGuild]: + if self._guild is None: + return None + return PartialInteractionGuild(data=self._guild, state=self._state, interaction=self) + def is_expired(self) -> bool: """Whether the interaction can still be used to make requests to Discord. @@ -2040,10 +2066,8 @@ def __init__( 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) + guild = parent.guild for str_id, user in users.items(): user_id = int(str_id) @@ -2052,7 +2076,7 @@ def __init__( self.members[user_id] = (guild and guild.get_member(user_id)) or Member( data=member, user_data=user, - guild=guild_fallback, # type: ignore + guild=guild, # type: ignore state=state, ) else: @@ -2060,14 +2084,15 @@ def __init__( for str_id, role in roles.items(): self.roles[int(str_id)] = Role( - guild=guild_fallback, # type: ignore + guild=guild, # type: ignore state=state, data=role, ) for str_id, channel_data in channels.items(): self.channels[int(str_id)] = state._get_partial_interaction_channel( - channel_data, guild_fallback + channel_data, + guild, ) for str_id, message in messages.items(): diff --git a/disnake/state.py b/disnake/state.py index 7a934fbff2..7214830864 100644 --- a/disnake/state.py +++ b/disnake/state.py @@ -2218,7 +2218,7 @@ def _get_partial_interaction_channel( def _get_partial_interaction_channel( self, data: InteractionChannelPayload, - guild: Optional[Union[Guild, Object]], + guild: Optional[Guild], *, # this param is purely for type-checking, it has no effect on runtime behavior. return_messageable: bool = False, diff --git a/disnake/types/guild.py b/disnake/types/guild.py index 462a0aa597..39ec1155cc 100644 --- a/disnake/types/guild.py +++ b/disnake/types/guild.py @@ -153,6 +153,10 @@ class Guild(_BaseGuildPreview): soundboard_sounds: NotRequired[List[GuildSoundboardSound]] +class PartialGuild(Guild, total=False): + pass + + class InviteGuild(Guild, total=False): welcome_screen: WelcomeScreen diff --git a/disnake/types/interactions.py b/disnake/types/interactions.py index ee11200fce..ce37951301 100644 --- a/disnake/types/interactions.py +++ b/disnake/types/interactions.py @@ -9,6 +9,7 @@ from .components import MessageTopLevelComponent, Modal from .embed import Embed from .entitlement import Entitlement +from .guild import PartialGuild from .i18n import LocalizationDict from .member import Member, MemberWithUser from .role import Role @@ -326,6 +327,7 @@ class _BaseUserInteraction(_BaseInteraction): channel: InteractionChannel locale: str guild_id: NotRequired[Snowflake] + guild: NotRequired[PartialGuild] guild_locale: NotRequired[str] entitlements: NotRequired[List[Entitlement]] authorizing_integration_owners: NotRequired[AuthorizingIntegrationOwners]