Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/1235.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:attr:`Interaction.guild` is now a :class:`Guild` instance if the guild could not be found in cache.
28 changes: 28 additions & 0 deletions disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)


Expand Down
65 changes: 45 additions & 20 deletions disnake/interactions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -222,6 +222,7 @@
"_state",
"_session",
"_original_response",
"_guild",
"_cs_response",
"_cs_followup",
"_cs_me",
Expand All @@ -242,6 +243,7 @@
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")
Expand All @@ -254,19 +256,26 @@
# 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)
Expand Down Expand Up @@ -322,8 +331,19 @@

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]:
Expand Down Expand Up @@ -394,6 +414,12 @@
"""
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.

Expand Down Expand Up @@ -2040,10 +2066,8 @@
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)
Expand All @@ -2052,22 +2076,23 @@
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

Check failure on line 2079 in disnake/interactions/base.py

View workflow job for this annotation

GitHub Actions / pyright (3.8, false)

Unnecessary "# type: ignore" comment
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
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():
Expand Down
2 changes: 1 addition & 1 deletion disnake/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2215,10 +2215,10 @@
) -> MessageableChannel: ...

# note: this resolves unknown types to `PartialMessageable`
def _get_partial_interaction_channel(

Check failure on line 2218 in disnake/state.py

View workflow job for this annotation

GitHub Actions / pyright (3.8, false)

Overloaded implementation is not consistent with signature of overload 2   Type "(self: Self@ConnectionState, data: InteractionChannel, guild: Guild | None, *, return_messageable: bool = False) -> AnyChannel" cannot be assigned to type "(self: Self@ConnectionState, data: InteractionChannel, guild: Guild | Object | None, *, return_messageable: Literal[True]) -> MessageableChannel"     Parameter 3: type "Guild | Object | None" cannot be assigned to type "Guild | None" (reportGeneralTypeIssues)

Check failure on line 2218 in disnake/state.py

View workflow job for this annotation

GitHub Actions / pyright (3.8, false)

Overloaded implementation is not consistent with signature of overload 1   Type "(self: Self@ConnectionState, data: InteractionChannel, guild: Guild | None, *, return_messageable: bool = False) -> AnyChannel" cannot be assigned to type "(self: Self@ConnectionState, data: InteractionChannel, guild: Guild | Object | None, *, return_messageable: Literal[False] = False) -> AnyChannel"     Parameter 3: type "Guild | Object | None" cannot be assigned to type "Guild | None" (reportGeneralTypeIssues)
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,
Expand Down
4 changes: 4 additions & 0 deletions disnake/types/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions disnake/types/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
Loading