From d8fbbeb6c7e9e5fe55f705fa423dc1b7758ac299 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 2 Jan 2024 16:09:57 +0100 Subject: [PATCH 1/8] feat: add `Invite.type` --- disnake/enums.py | 7 +++++++ disnake/invite.py | 24 +++++++++++++++++------- disnake/types/gateway.py | 3 ++- disnake/types/invite.py | 2 ++ docs/api/invites.rst | 21 +++++++++++++++++++++ 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/disnake/enums.py b/disnake/enums.py index 912cb36183..63b02d34eb 100644 --- a/disnake/enums.py +++ b/disnake/enums.py @@ -41,6 +41,7 @@ "ExpireBehavior", "StickerType", "StickerFormatType", + "InviteType", "InviteTarget", "VideoQualityMode", "ComponentType", @@ -599,6 +600,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/invite.py b/disnake/invite.py index a936c832b1..a171aff1a8 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,12 @@ 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 +386,7 @@ class Invite(Hashable): __slots__ = ( "max_age", "code", + "type", "guild", "created_at", "uses", @@ -412,6 +417,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") @@ -543,11 +549,15 @@ 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 d71af27ab6..dd727d6eac 100644 --- a/disnake/types/gateway.py +++ b/disnake/types/gateway.py @@ -16,7 +16,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 @@ -347,6 +347,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..98b1bcafbf 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,6 +31,7 @@ class _InviteMetadata(TypedDict, total=False): class Invite(_InviteMetadata): code: str + type: InviteType guild: NotRequired[InviteGuild] channel: InviteChannel inviter: 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 ~~~~~~~~~~~~ From fb549512ccb98c26fbcb63adfdf50f5d67aa7b3e Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 2 Jan 2024 16:10:26 +0100 Subject: [PATCH 2/8] fix: add missing fields to raw payloads This is technically not required since both of these fields have fallbacks, but still good practice nonetheless. --- disnake/audit_logs.py | 2 ++ disnake/guild.py | 1 + 2 files changed, 3 insertions(+) diff --git a/disnake/audit_logs.py b/disnake/audit_logs.py index 256aaa04dc..b8691bd162 100644 --- a/disnake/audit_logs.py +++ b/disnake/audit_logs.py @@ -805,6 +805,8 @@ def _convert_target_invite(self, target_id: int) -> Invite: "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 diff --git a/disnake/guild.py b/disnake/guild.py index 449f303c27..f21bff3fcd 100644 --- a/disnake/guild.py +++ b/disnake/guild.py @@ -3921,6 +3921,7 @@ async def vanity_invite(self, *, use_cached: bool = False) -> Optional[Invite]: 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 From c46940b83297ccf38d9538aa7e1b25c3745e6f58 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 2 Jan 2024 16:13:04 +0100 Subject: [PATCH 3/8] fix: support null channel in `Invite.from_incomplete` --- disnake/invite.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/disnake/invite.py b/disnake/invite.py index a171aff1a8..bda20d8858 100644 --- a/disnake/invite.py +++ b/disnake/invite.py @@ -487,15 +487,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) From 4a2a8557eb70385a6e5d53d14afc5543981c6d74 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Tue, 2 Jan 2024 16:15:10 +0100 Subject: [PATCH 4/8] docs: add changelog entry --- changelog/1141.bugfix.rst | 1 + changelog/1141.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/1141.bugfix.rst create mode 100644 changelog/1141.feature.rst diff --git a/changelog/1141.bugfix.rst b/changelog/1141.bugfix.rst new file mode 100644 index 0000000000..4f10261ea3 --- /dev/null +++ b/changelog/1141.bugfix.rst @@ -0,0 +1 @@ +Support fetching invites with ``null`` channel (e.g. friend invites). diff --git a/changelog/1141.feature.rst b/changelog/1141.feature.rst new file mode 100644 index 0000000000..44d87009f1 --- /dev/null +++ b/changelog/1141.feature.rst @@ -0,0 +1 @@ +Add :attr:`Invite.type`. From 5da391e960bda919f5c41af63af71da7aa0e92ac Mon Sep 17 00:00:00 2001 From: shiftinv <8530778+shiftinv@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:59:14 +0100 Subject: [PATCH 5/8] chore: docs spacing Co-authored-by: Victor <67214928+Victorsitou@users.noreply.github.com> Signed-off-by: shiftinv <8530778+shiftinv@users.noreply.github.com> --- disnake/invite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/disnake/invite.py b/disnake/invite.py index bda20d8858..ed7e8e3091 100644 --- a/disnake/invite.py +++ b/disnake/invite.py @@ -311,6 +311,7 @@ class Invite(Hashable): 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 not a guild invite (see :attr:`type`). max_age: Optional[:class:`int`] From 93b205010502c1b25836b3e6eac4b6fd3dd1c6c4 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Sat, 18 May 2024 14:25:32 +0200 Subject: [PATCH 6/8] feat: adjust repr for `group_dm` invites --- disnake/invite.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/disnake/invite.py b/disnake/invite.py index ed7e8e3091..545159dfb8 100644 --- a/disnake/invite.py +++ b/disnake/invite.py @@ -548,12 +548,10 @@ def __str__(self) -> str: def __repr__(self) -> str: s = f" Date: Sun, 19 May 2024 17:13:22 +0200 Subject: [PATCH 7/8] fix(typing): mark `invite["channel"]` as nullable --- disnake/audit_logs.py | 7 +++++-- disnake/guild.py | 10 ++++++++-- disnake/types/invite.py | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/disnake/audit_logs.py b/disnake/audit_logs.py index 0930623ed3..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,7 +800,7 @@ 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, @@ -809,7 +810,9 @@ def _convert_target_invite(self, target_id: int) -> Invite: "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/guild.py b/disnake/guild.py index d06978025a..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,7 +4133,10 @@ 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 diff --git a/disnake/types/invite.py b/disnake/types/invite.py index 98b1bcafbf..b1f4ac4b63 100644 --- a/disnake/types/invite.py +++ b/disnake/types/invite.py @@ -33,7 +33,7 @@ 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] From 7af4118a90cbdeacddcbcfa2de8727bfd047eab0 Mon Sep 17 00:00:00 2001 From: shiftinv Date: Fri, 9 Aug 2024 17:39:12 +0200 Subject: [PATCH 8/8] fix(changelog): update issue number because i somehow messed up --- changelog/{1141.bugfix.rst => 1142.bugfix.rst} | 0 changelog/{1141.feature.rst => 1142.feature.rst} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename changelog/{1141.bugfix.rst => 1142.bugfix.rst} (100%) rename changelog/{1141.feature.rst => 1142.feature.rst} (100%) diff --git a/changelog/1141.bugfix.rst b/changelog/1142.bugfix.rst similarity index 100% rename from changelog/1141.bugfix.rst rename to changelog/1142.bugfix.rst diff --git a/changelog/1141.feature.rst b/changelog/1142.feature.rst similarity index 100% rename from changelog/1141.feature.rst rename to changelog/1142.feature.rst