diff --git a/changelog/1142.bugfix.rst b/changelog/1142.bugfix.rst new file mode 100644 index 0000000000..4f10261ea3 --- /dev/null +++ b/changelog/1142.bugfix.rst @@ -0,0 +1 @@ +Support fetching invites with ``null`` channel (e.g. friend invites). diff --git a/changelog/1142.feature.rst b/changelog/1142.feature.rst new file mode 100644 index 0000000000..44d87009f1 --- /dev/null +++ b/changelog/1142.feature.rst @@ -0,0 +1 @@ +Add :attr:`Invite.type`. diff --git a/disnake/audit_logs.py b/disnake/audit_logs.py index c5ec6caf79..cc2948f9a3 100644 --- a/disnake/audit_logs.py +++ b/disnake/audit_logs.py @@ -64,6 +64,7 @@ DefaultReaction as DefaultReactionPayload, PermissionOverwrite as PermissionOverwritePayload, ) + from .types.invite import Invite as InvitePayload from .types.role import Role as RolePayload from .types.snowflake import Snowflake from .types.threads import ForumTag as ForumTagPayload @@ -799,15 +800,19 @@ def _convert_target_invite(self, target_id: int) -> Invite: # so figure out which change has the full invite data changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after - fake_payload = { + fake_payload: InvitePayload = { "max_age": changeset.max_age, "max_uses": changeset.max_uses, "code": changeset.code, "temporary": changeset.temporary, "uses": changeset.uses, + "type": 0, + "channel": None, } - obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel) # type: ignore + obj = Invite( + state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel + ) try: obj.inviter = changeset.inviter except AttributeError: diff --git a/disnake/enums.py b/disnake/enums.py index 0597343ff7..56b06ca5a2 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -41,6 +41,7 @@ "ExpireBehavior", "StickerType", "StickerFormatType", + "InviteType", "InviteTarget", "VideoQualityMode", "ComponentType", @@ -606,6 +607,12 @@ def file_extension(self) -> str: } +class InviteType(Enum): + guild = 0 + group_dm = 1 + friend = 2 + + class InviteTarget(Enum): unknown = 0 stream = 1 diff --git a/disnake/guild.py b/disnake/guild.py index 301d926876..97ea1e80ac 100644 --- a/disnake/guild.py +++ b/disnake/guild.py @@ -3185,7 +3185,10 @@ async def invites(self) -> List[Invite]: data = await self._state.http.invites_from(self.id) result = [] for invite in data: - channel = self.get_channel(int(invite["channel"]["id"])) + if channel_data := invite.get("channel"): + channel = self.get_channel(int(channel_data["id"])) + else: + channel = None result.append(Invite(state=self._state, data=invite, guild=self, channel=channel)) return result @@ -4130,11 +4133,15 @@ async def vanity_invite(self, *, use_cached: bool = False) -> Optional[Invite]: # reliable or a thing anymore data = await self._state.http.get_invite(payload["code"]) - channel = self.get_channel(int(data["channel"]["id"])) + if channel_data := data.get("channel"): + channel = self.get_channel(int(channel_data["id"])) + else: + channel = None payload["temporary"] = False payload["max_uses"] = 0 payload["max_age"] = 0 payload["uses"] = payload.get("uses", 0) + payload["type"] = 0 return Invite(state=self._state, data=payload, guild=self, channel=channel) # TODO: use MISSING when async iterators get refactored diff --git a/disnake/invite.py b/disnake/invite.py index a936c832b1..545159dfb8 100644 --- a/disnake/invite.py +++ b/disnake/invite.py @@ -6,7 +6,7 @@ from .appinfo import PartialAppInfo from .asset import Asset -from .enums import ChannelType, InviteTarget, NSFWLevel, VerificationLevel, try_enum +from .enums import ChannelType, InviteTarget, InviteType, NSFWLevel, VerificationLevel, try_enum from .guild_scheduled_event import GuildScheduledEvent from .mixins import Hashable from .object import Object @@ -307,8 +307,13 @@ class Invite(Hashable): ---------- code: :class:`str` The URL fragment used for the invite. + type: :class:`InviteType` + The type of the invite. + + .. versionadded:: 2.10 + guild: Optional[Union[:class:`Guild`, :class:`Object`, :class:`PartialInviteGuild`]] - The guild the invite is for. Can be ``None`` if it's from a group direct message. + The guild the invite is for. Can be ``None`` if it's not a guild invite (see :attr:`type`). max_age: Optional[:class:`int`] How long before the invite expires in seconds. A value of ``0`` indicates that it doesn't expire. @@ -382,6 +387,7 @@ class Invite(Hashable): __slots__ = ( "max_age", "code", + "type", "guild", "created_at", "uses", @@ -412,6 +418,7 @@ def __init__( ) -> None: self._state: ConnectionState = state self.code: str = data["code"] + self.type: InviteType = try_enum(InviteType, data.get("type", 0)) self.guild: Optional[InviteGuildType] = self._resolve_guild(data.get("guild"), guild) self.max_age: Optional[int] = data.get("max_age") @@ -481,15 +488,12 @@ def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self # If it's not cached, then it has to be a partial guild guild = PartialInviteGuild(state, guild_data, guild_id) - # todo: this is no longer true - # As far as I know, invites always need a channel - # So this should never raise. - channel: Union[PartialInviteChannel, GuildChannel] = PartialInviteChannel( - data=data["channel"], state=state - ) - if guild is not None and not isinstance(guild, PartialInviteGuild): - # Upgrade the partial data if applicable - channel = guild.get_channel(channel.id) or channel + channel: Optional[Union[PartialInviteChannel, GuildChannel]] = None + if channel_data := data.get("channel"): + channel = PartialInviteChannel(data=channel_data, state=state) + if guild is not None and not isinstance(guild, PartialInviteGuild): + # Upgrade the partial data if applicable + channel = guild.get_channel(channel.id) or channel return cls(state=state, data=data, guild=guild, channel=channel) @@ -543,11 +547,13 @@ def __str__(self) -> str: return self.url def __repr__(self) -> str: - return ( - f"" - ) + s = f" int: return hash(self.code) diff --git a/disnake/types/gateway.py b/disnake/types/gateway.py index e2494848b7..9e81523d29 100644 --- a/disnake/types/gateway.py +++ b/disnake/types/gateway.py @@ -17,7 +17,7 @@ from .guild_scheduled_event import GuildScheduledEvent from .integration import BaseIntegration from .interactions import BaseInteraction, GuildApplicationCommandPermissions -from .invite import InviteTargetType +from .invite import InviteTargetType, InviteType from .member import MemberWithUser from .message import Message from .role import Role @@ -348,6 +348,7 @@ class InviteCreateEvent(TypedDict): target_user: NotRequired[User] target_application: NotRequired[PartialAppInfo] temporary: bool + type: InviteType uses: int # always 0 diff --git a/disnake/types/invite.py b/disnake/types/invite.py index 93e573cfa5..b1f4ac4b63 100644 --- a/disnake/types/invite.py +++ b/disnake/types/invite.py @@ -12,6 +12,7 @@ from .guild_scheduled_event import GuildScheduledEvent from .user import PartialUser +InviteType = Literal[0, 1, 2] InviteTargetType = Literal[1, 2] @@ -30,8 +31,9 @@ class _InviteMetadata(TypedDict, total=False): class Invite(_InviteMetadata): code: str + type: InviteType guild: NotRequired[InviteGuild] - channel: InviteChannel + channel: Optional[InviteChannel] inviter: NotRequired[PartialUser] target_type: NotRequired[InviteTargetType] target_user: NotRequired[PartialUser] diff --git a/docs/api/invites.rst b/docs/api/invites.rst index 729f91272a..aa58074c2f 100644 --- a/docs/api/invites.rst +++ b/docs/api/invites.rst @@ -37,6 +37,27 @@ PartialInviteChannel Enumerations ------------ +InviteType +~~~~~~~~~~ + +.. class:: InviteType + + Represents the type of an invite. + + .. versionadded:: 2.10 + + .. attribute:: guild + + Represents an invite to a guild. + + .. attribute:: group_dm + + Represents an invite to a group channel. + + .. attribute:: friend + + Represents a friend invite. + InviteTarget ~~~~~~~~~~~~