From bbb8e45352b9f7788b0440091dfa9112990a89f4 Mon Sep 17 00:00:00 2001 From: vmphase Date: Tue, 21 Jul 2026 18:29:57 +0300 Subject: [PATCH 01/15] fix(invite): guard against None code --- discord/invite.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discord/invite.py b/discord/invite.py index 198f0987bf..f020ea0e08 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -545,7 +545,7 @@ def __init__( ): self._state: ConnectionState = state self.max_age: int | None = data.get("max_age") - self.code: str = data["code"] + self.code: str | None = data.get("code") self.guild: InviteGuildType | None = self._resolve_guild( data.get("guild"), guild ) @@ -689,7 +689,7 @@ def _resolve_roles( return [Object(role_id) for role_id in role_ids] def __str__(self) -> str: - return self.url + return self.code or "" def __repr__(self) -> str: return ( @@ -712,6 +712,8 @@ def id(self) -> str: @property def url(self) -> str: """A property that retrieves the invite URL.""" + if self.code is None: + raise ValueError("Cannot build an invite URL when code is None") return f"{self.BASE}/{self.code}{f'?event={self.scheduled_event.id}' if self.scheduled_event else ''}" @property From 2bd7aceeacbd455af685137a375e328c68b9e08b Mon Sep 17 00:00:00 2001 From: vmphase Date: Tue, 21 Jul 2026 18:33:02 +0300 Subject: [PATCH 02/15] chore: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7647529eee..0892805efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ These changes are available on the `master` branch, but have not yet been releas ### Fixed +- Fix `Invite.url` producing `"https://discord.gg/None"` when `code` is `None` + (from `VanityInvitePayload`). Now raises `ValueError` on `.url` and returns`""` + from `__str__`. + ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) - Fix an attribute error in `RoleColours.is_holographic()` when `secondary` or `tertiary` is `None`. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) From 43af42c129cb94678bac71042492b0a5cd8b5cef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:33:38 +0000 Subject: [PATCH 03/15] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0892805efb..fb7be1ad27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,10 +20,9 @@ These changes are available on the `master` branch, but have not yet been releas ### Fixed -- Fix `Invite.url` producing `"https://discord.gg/None"` when `code` is `None` - (from `VanityInvitePayload`). Now raises `ValueError` on `.url` and returns`""` - from `__str__`. - ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) +- Fix `Invite.url` producing `"https://discord.gg/None"` when `code` is `None` (from + `VanityInvitePayload`). Now raises `ValueError` on `.url` and returns`""` from + `__str__`. ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) - Fix an attribute error in `RoleColours.is_holographic()` when `secondary` or `tertiary` is `None`. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) From ed05041a7f683323fcf6654343cf30c03901e8a9 Mon Sep 17 00:00:00 2001 From: vmphase Date: Tue, 21 Jul 2026 18:38:40 +0300 Subject: [PATCH 04/15] fix(invite): return "" on id property when code is None --- discord/invite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/invite.py b/discord/invite.py index f020ea0e08..de31f12bcf 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -705,9 +705,9 @@ def __hash__(self) -> int: return hash(self.code) @property - def id(self) -> str: + def id(self) -> str: # type: ignore[override] """Returns the proper code portion of the invite.""" - return self.code + return self.code or "" @property def url(self) -> str: From d348ffcd27358a20e0a5d995f7504b164872b25a Mon Sep 17 00:00:00 2001 From: vmphase Date: Tue, 21 Jul 2026 18:46:34 +0300 Subject: [PATCH 05/15] refactor(types/invite): do not inherit VanityInvite from _InviteMetadata --- discord/types/invite.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/types/invite.py b/discord/types/invite.py index 584b573094..a7ed6a2fe7 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -49,8 +49,9 @@ class _InviteMetadata(TypedDict, total=False): expires_at: str | None -class VanityInvite(_InviteMetadata): +class VanityInvite(TypedDict): code: str | None + uses: int class IncompleteInvite(_InviteMetadata): From 70099ecac285f20b84cb2642c0a21ec61114ba1d Mon Sep 17 00:00:00 2001 From: vmphase Date: Tue, 21 Jul 2026 18:52:23 +0300 Subject: [PATCH 06/15] fix(invite): raise ValueError when code is None --- discord/invite.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/discord/invite.py b/discord/invite.py index de31f12bcf..f0737dc145 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -721,7 +721,14 @@ def target_users(self) -> InviteTargetUsers: """An :class:`InviteTargetUsers` object for managing the target users list for this invite. .. versionadded:: 2.8 + + Raises + ------ + ValueError + The invite has no code. """ + if self.code is None: + raise ValueError("Cannot manage target users for an invite with no code") return InviteTargetUsers(invite_code=self.code, state=self._state) async def edit_target_users(self, target_users_file: File) -> None: @@ -740,6 +747,8 @@ async def edit_target_users(self, target_users_file: File) -> None: Raises ------ + ValueError + The invite has no code. HTTPException Updating the target users failed. Forbidden @@ -747,6 +756,8 @@ async def edit_target_users(self, target_users_file: File) -> None: NotFound The invite is invalid or expired. """ + if self.code is None: + raise ValueError("Cannot edit target users for an invite with no code") await self._state.http.update_invite_target_users( self.code, target_users_file=target_users_file ) @@ -766,6 +777,8 @@ async def fetch_target_users_job_status(self) -> InviteTargetUsersJobStatus: Raises ------ + ValueError + The invite has no code. HTTPException Fetching the job status failed. NotFound @@ -773,6 +786,10 @@ async def fetch_target_users_job_status(self) -> InviteTargetUsersJobStatus: Forbidden You do not have permission to view the target users. """ + if self.code is None: + raise ValueError( + "Cannot fetch target users job status for an invite with no code" + ) r = await self._state.http.get_invite_target_users_job_status(self.code) return InviteTargetUsersJobStatus(data=r) @@ -790,6 +807,8 @@ async def delete(self, *, reason: str | None = None): Raises ------ + ValueError + The invite has no code. Forbidden You do not have permissions to revoke invites. NotFound @@ -798,6 +817,8 @@ async def delete(self, *, reason: str | None = None): Revoking the invite failed. """ + if self.code is None: + raise ValueError("Cannot delete an invite with no code") await self._state.http.delete_invite(self.code, reason=reason) def set_scheduled_event(self, event: ScheduledEvent) -> None: From 6a5cfa1965e76b21854622cb5dcbecf9166a8a38 Mon Sep 17 00:00:00 2001 From: vmphase Date: Tue, 21 Jul 2026 18:56:39 +0300 Subject: [PATCH 07/15] refactor(types/invite): inline _InviteMetadata into IncompleteInvite --- discord/types/invite.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/discord/types/invite.py b/discord/types/invite.py index a7ed6a2fe7..6d2b511b69 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -40,23 +40,20 @@ InviteTargetType = Literal[1, 2] -class _InviteMetadata(TypedDict, total=False): - uses: int - max_uses: int - max_age: int - temporary: bool - created_at: str - expires_at: str | None - - class VanityInvite(TypedDict): code: str | None uses: int -class IncompleteInvite(_InviteMetadata): +class IncompleteInvite(TypedDict): code: str channel: PartialChannel + uses: NotRequired[int] + max_uses: NotRequired[int] + max_age: NotRequired[int] + temporary: NotRequired[bool] + created_at: NotRequired[str] + expires_at: NotRequired[str | None] class Invite(IncompleteInvite): From 1812204b26a00a9bd59f87b34ba9c4a134ad7671 Mon Sep 17 00:00:00 2001 From: vmphase Date: Tue, 21 Jul 2026 18:58:27 +0300 Subject: [PATCH 08/15] chore: update changelog --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb7be1ad27..16603379cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,16 @@ These changes are available on the `master` branch, but have not yet been releas ### Changed +- Inline `_InviteMetadata` fields into `IncompleteInvite` and remove the now-unused + `_InviteMetadata` TypedDict. + ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) + ### Fixed -- Fix `Invite.url` producing `"https://discord.gg/None"` when `code` is `None` (from - `VanityInvitePayload`). Now raises `ValueError` on `.url` and returns`""` from - `__str__`. ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) +- Fix `Invite.code` handling when `None` (from `VanityInvitePayload`). `__str__` + falls back to `""`, `.url` and all code-dependent methods raise `ValueError`, + and `Invite.code` is typed as `str | None`. + ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) - Fix an attribute error in `RoleColours.is_holographic()` when `secondary` or `tertiary` is `None`. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) From bc000829d1edbef84d10570b44f06cad5faf26ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:59:00 +0000 Subject: [PATCH 09/15] style(pre-commit): auto fixes from pre-commit.com hooks --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16603379cf..f461265343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,9 @@ These changes are available on the `master` branch, but have not yet been releas ### Fixed -- Fix `Invite.code` handling when `None` (from `VanityInvitePayload`). `__str__` - falls back to `""`, `.url` and all code-dependent methods raise `ValueError`, - and `Invite.code` is typed as `str | None`. +- Fix `Invite.code` handling when `None` (from `VanityInvitePayload`). `__str__` falls + back to `""`, `.url` and all code-dependent methods raise `ValueError`, and + `Invite.code` is typed as `str | None`. ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) - Fix an attribute error in `RoleColours.is_holographic()` when `secondary` or `tertiary` is `None`. From a665bcf6581f320ddca66018ff38b2a24bc0b4e3 Mon Sep 17 00:00:00 2001 From: vmphase Date: Mon, 27 Jul 2026 10:00:49 +0300 Subject: [PATCH 10/15] chore: simplify changelog --- CHANGELOG.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca980de048..0fda3d29ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ These changes are available on the `master` branch, but have not yet been releas ### Changed +- Inline `_InviteMetadata` fields into `IncompleteInvite`. + ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) + ### Fixed -- Inline `_InviteMetadata` fields into `IncompleteInvite` and remove the now-unused - `_InviteMetadata` TypedDict. +- Fix `Invite.code` handling when `None` (from `VanityInvitePayload`). ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) ### Deprecated @@ -34,10 +36,6 @@ These changes are available on the `master` branch, but have not yet been releas ### Fixed -- Fix `Invite.code` handling when `None` (from `VanityInvitePayload`). `__str__` falls - back to `""`, `.url` and all code-dependent methods raise `ValueError`, and - `Invite.code` is typed as `str | None`. - ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) - Fix an attribute error in `RoleColours.is_holographic()` when `secondary` or `tertiary` is `None`. ([#3268](https://github.com/Pycord-Development/pycord/pull/3268)) From a1c109e088bfbb04f10da2408d2eaf51f064be15 Mon Sep 17 00:00:00 2001 From: vmphase Date: Mon, 27 Jul 2026 10:04:18 +0300 Subject: [PATCH 11/15] fix(invite): preserve `__str__` return value for invites with a code --- discord/invite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/invite.py b/discord/invite.py index f0737dc145..569d8a07ac 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -689,7 +689,7 @@ def _resolve_roles( return [Object(role_id) for role_id in role_ids] def __str__(self) -> str: - return self.code or "" + return self.url if self.code is not None else "" def __repr__(self) -> str: return ( From 442ccbc0f35e8121ac00d8f3e133c3b4a6d39a27 Mon Sep 17 00:00:00 2001 From: vmphase Date: Mon, 27 Jul 2026 12:15:19 +0300 Subject: [PATCH 12/15] feat(invite): deprecate Invite.id --- CHANGELOG.md | 3 +++ discord/invite.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a12063cc3..0f42109892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ These changes are available on the `master` branch, but have not yet been releas ### Deprecated +- Deprecated `Invite.code` in favor of `Invite.code`. + ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) + ### Removed ## [2.8.1] - 2026-07-25 diff --git a/discord/invite.py b/discord/invite.py index 569d8a07ac..7fa8c3efb5 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -30,7 +30,7 @@ import os from typing import TYPE_CHECKING, TypeVar, Union -from typing_extensions import override +from typing_extensions import deprecated, override from .appinfo import PartialAppInfo from .asset import Asset @@ -705,6 +705,9 @@ def __hash__(self) -> int: return hash(self.code) @property + @deprecated( + "Invite.id is deprecated since version 2.9, consider using Invite.code instead." + ) def id(self) -> str: # type: ignore[override] """Returns the proper code portion of the invite.""" return self.code or "" From c921a591bfaf3d0396c11e31784bcb9791f9b98f Mon Sep 17 00:00:00 2001 From: vmphase Date: Mon, 27 Jul 2026 16:09:33 +0300 Subject: [PATCH 13/15] chore: fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f42109892..5085ee5cce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ These changes are available on the `master` branch, but have not yet been releas ### Deprecated -- Deprecated `Invite.code` in favor of `Invite.code`. +- Deprecated `Invite.id` in favor of `Invite.code`. ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) ### Removed From 7ab228a9901a94fa10209e5a8a8532f1d2486a6f Mon Sep 17 00:00:00 2001 From: vmphase Date: Mon, 27 Jul 2026 17:24:15 +0300 Subject: [PATCH 14/15] feat(invite; guild): revert guards, cast data to IncompleteInvite --- discord/guild.py | 10 ++++++++- discord/invite.py | 56 ++++++++++++++--------------------------------- 2 files changed, 26 insertions(+), 40 deletions(-) mode change 100644 => 100755 discord/invite.py diff --git a/discord/guild.py b/discord/guild.py index 0053cbfd61..a4661d0131 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -39,6 +39,7 @@ Tuple, TypeVar, Union, + cast, overload, ) @@ -121,6 +122,7 @@ from .types.guild import Guild as GuildPayload from .types.guild import GuildFeature, MFALevel from .types.guild import ModifyIncidents as ModifyIncidentsPayload + from .types.invite import IncompleteInvite from .types.member import Member as MemberPayload from .types.threads import Thread as ThreadPayload from .types.voice import VoiceState as GuildVoiceState @@ -3786,7 +3788,13 @@ async def vanity_invite(self) -> Invite | None: payload["max_uses"] = 0 payload["max_age"] = 0 payload["uses"] = payload.get("uses", 0) - return Invite(state=self._state, data=payload, guild=self, channel=channel) + + return Invite( + state=self._state, + data=cast("IncompleteInvite", payload), + guild=self, + channel=channel, + ) # TODO: use MISSING when async iterators get refactored def audit_logs( diff --git a/discord/invite.py b/discord/invite.py old mode 100644 new mode 100755 index 7fa8c3efb5..89aebae948 --- a/discord/invite.py +++ b/discord/invite.py @@ -62,12 +62,12 @@ from .state import ConnectionState from .types.channel import PartialChannel as InviteChannelPayload from .types.invite import GatewayInvite as GatewayInvitePayload + from .types.invite import IncompleteInvite as IncompleteInvitePayload from .types.invite import Invite as InvitePayload from .types.invite import InviteGuild as InviteGuildPayload from .types.invite import ( InviteTargetUsersJobStatus as InviteTargetUsersJobStatusPayload, ) - from .types.invite import VanityInvite as VanityInvitePayload from .types.role import Role as RolePayload from .types.scheduled_events import ScheduledEvent as ScheduledEventPayload from .types.snowflake import Snowflake @@ -512,25 +512,25 @@ class Invite(Hashable): """ __slots__ = ( - "max_age", - "code", - "guild", - "revoked", - "created_at", - "uses", - "temporary", - "max_uses", - "inviter", - "channel", - "target_user", - "target_type", "_state", "approximate_member_count", "approximate_presence_count", - "scheduled_event", - "target_application", + "channel", + "code", + "created_at", "expires_at", + "guild", + "inviter", + "max_age", + "max_uses", + "revoked", "roles", + "scheduled_event", + "target_application", + "target_type", + "target_user", + "temporary", + "uses", ) BASE = "https://discord.gg" @@ -539,13 +539,13 @@ def __init__( self, *, state: ConnectionState, - data: InvitePayload | VanityInvitePayload, + data: InvitePayload | IncompleteInvitePayload, guild: PartialInviteGuild | Guild | None = None, channel: PartialInviteChannel | GuildChannel | None = None, ): self._state: ConnectionState = state self.max_age: int | None = data.get("max_age") - self.code: str | None = data.get("code") + self.code: str = data["code"] self.guild: InviteGuildType | None = self._resolve_guild( data.get("guild"), guild ) @@ -724,14 +724,7 @@ def target_users(self) -> InviteTargetUsers: """An :class:`InviteTargetUsers` object for managing the target users list for this invite. .. versionadded:: 2.8 - - Raises - ------ - ValueError - The invite has no code. """ - if self.code is None: - raise ValueError("Cannot manage target users for an invite with no code") return InviteTargetUsers(invite_code=self.code, state=self._state) async def edit_target_users(self, target_users_file: File) -> None: @@ -750,8 +743,6 @@ async def edit_target_users(self, target_users_file: File) -> None: Raises ------ - ValueError - The invite has no code. HTTPException Updating the target users failed. Forbidden @@ -759,8 +750,6 @@ async def edit_target_users(self, target_users_file: File) -> None: NotFound The invite is invalid or expired. """ - if self.code is None: - raise ValueError("Cannot edit target users for an invite with no code") await self._state.http.update_invite_target_users( self.code, target_users_file=target_users_file ) @@ -780,8 +769,6 @@ async def fetch_target_users_job_status(self) -> InviteTargetUsersJobStatus: Raises ------ - ValueError - The invite has no code. HTTPException Fetching the job status failed. NotFound @@ -789,10 +776,6 @@ async def fetch_target_users_job_status(self) -> InviteTargetUsersJobStatus: Forbidden You do not have permission to view the target users. """ - if self.code is None: - raise ValueError( - "Cannot fetch target users job status for an invite with no code" - ) r = await self._state.http.get_invite_target_users_job_status(self.code) return InviteTargetUsersJobStatus(data=r) @@ -810,8 +793,6 @@ async def delete(self, *, reason: str | None = None): Raises ------ - ValueError - The invite has no code. Forbidden You do not have permissions to revoke invites. NotFound @@ -819,9 +800,6 @@ async def delete(self, *, reason: str | None = None): HTTPException Revoking the invite failed. """ - - if self.code is None: - raise ValueError("Cannot delete an invite with no code") await self._state.http.delete_invite(self.code, reason=reason) def set_scheduled_event(self, event: ScheduledEvent) -> None: From 6a51a9bed1f1a8dbab0e98d5b93871f9193261b0 Mon Sep 17 00:00:00 2001 From: vmphase Date: Mon, 27 Jul 2026 17:31:03 +0300 Subject: [PATCH 15/15] chore: update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5085ee5cce..e362e9c00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,11 @@ These changes are available on the `master` branch, but have not yet been releas - Inline `_InviteMetadata` fields into `IncompleteInvite`. ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) +- Accept `IncompleteInvite` instead of `VanityInvite` in `Invite.__init__`. + ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) ### Fixed -- Fix `Invite.code` handling when `None` (from `VanityInvitePayload`). - ([#3313](https://github.com/Pycord-Development/pycord/pull/3313)) - Fix `TypeError` when accessing `ApplicationCommand.guild_only` or `SlashCommandGroup.guild_only` when `contexts` is `None`. ([#3320](https://github.com/Pycord-Development/pycord/pull/3320))