From fec9a2d734315d1234271ef049a7c0c8296e9108 Mon Sep 17 00:00:00 2001 From: SassGuard Date: Thu, 16 Apr 2026 12:27:00 +0200 Subject: [PATCH 1/3] Add invite roles support (PartialInviteRole + target_role_ids) --- discord/abc.py | 5 ++++ discord/http.py | 4 ++++ discord/invite.py | 52 +++++++++++++++++++++++++++++++++++++++++ discord/types/invite.py | 4 +++- discord/types/role.py | 10 ++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/discord/abc.py b/discord/abc.py index 95ccfd67b..724337519 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1282,6 +1282,7 @@ class GuildChannel: target_type: Optional[InviteTarget] = None, target_user: Optional[User] = None, target_application_id: Optional[int] = None, + target_role_ids: Optional[List[int]] = None, guest: bool = False, ) -> Invite: """|coro| @@ -1321,6 +1322,9 @@ class GuildChannel: The id of the embedded application for the invite, required if ``target_type`` is :attr:`.InviteTarget.embedded_application`. .. versionadded:: 2.0 + target_role_ids: Optional[List[:class:`int`]] + A list of role IDs to assign to the user upon accepting this invite. + guest: :class:`bool` Whether the invite is a guest invite. @@ -1357,6 +1361,7 @@ class GuildChannel: target_type=target_type.value if target_type else None, target_user_id=target_user.id if target_user else None, target_application_id=target_application_id, + target_role_ids=target_role_ids, flags=flags.value if flags else None, ) return Invite.from_incomplete(data=data, state=self._state) diff --git a/discord/http.py b/discord/http.py index 05c1e69fc..ae7e7f38c 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1854,6 +1854,7 @@ class HTTPClient: target_type: Optional[invite.InviteTargetType] = None, target_user_id: Optional[Snowflake] = None, target_application_id: Optional[Snowflake] = None, + target_role_ids: Optional[List[Snowflake]] = None, flags: Optional[int] = None, ) -> Response[invite.Invite]: r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) @@ -1873,6 +1874,9 @@ class HTTPClient: if target_application_id: payload['target_application_id'] = str(target_application_id) + if target_role_ids is not None: + payload['target_role_ids'] = [str(r) for r in target_role_ids] + if flags: payload['flags'] = flags diff --git a/discord/invite.py b/discord/invite.py index 8b5088a89..390ce84d0 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -37,6 +37,7 @@ from .flags import InviteFlags __all__ = ( 'PartialInviteChannel', 'PartialInviteGuild', + 'PartialInviteRole', 'Invite', ) @@ -48,6 +49,7 @@ if TYPE_CHECKING: InviteGuild as InviteGuildPayload, GatewayInvite as GatewayInvitePayload, ) + from .types.role import PartialInviteRole as PartialInviteRolePayload from .types.guild import GuildFeature from .types.channel import ( PartialChannel as InviteChannelPayload, @@ -64,6 +66,47 @@ if TYPE_CHECKING: import datetime +class PartialInviteRole: + """Represents a partial role that is assigned to a user upon accepting an invite. + + .. versionadded:: 2.5 + + Attributes + ----------- + id: :class:`int` + The role's ID. + name: :class:`str` + The role's name. + position: :class:`int` + The role's position in the role hierarchy. + color: :class:`int` + The role's color as an integer. + icon: Optional[:class:`str`] + The role's icon hash, if any. + unicode_emoji: Optional[:class:`str`] + The role's unicode emoji, if any. + """ + + __slots__ = ('id', 'name', 'position', 'color', 'icon', 'unicode_emoji') + + def __init__(self, data: PartialInviteRolePayload) -> None: + self.id: int = int(data['id']) + self.name: str = data['name'] + self.position: int = data['position'] + self.color: int = data['color'] + self.icon: Optional[str] = data.get('icon') + self.unicode_emoji: Optional[str] = data.get('unicode_emoji') + + def __repr__(self) -> str: + return f'' + + def __eq__(self, other: object) -> bool: + return isinstance(other, PartialInviteRole) and other.id == self.id + + def __hash__(self) -> int: + return hash(self.id) + + class PartialInviteChannel: """Represents a "partial" invite channel. @@ -358,6 +401,11 @@ class Invite(Hashable): The ID of the scheduled event associated with this invite, if any. .. versionadded:: 2.0 + roles: List[:class:`PartialInviteRole`] + The roles that will be assigned to the user upon accepting this invite. + Empty list if no roles are assigned. + + .. versionadded:: 2.5 """ __slots__ = ( @@ -382,6 +430,7 @@ class Invite(Hashable): 'scheduled_event_id', 'type', '_flags', + 'roles', ) BASE = 'https://discord.gg' @@ -436,6 +485,9 @@ class Invite(Hashable): ) self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None self._flags: int = data.get('flags', 0) + self.roles: List[PartialInviteRole] = [ + PartialInviteRole(r) for r in data.get('roles', []) + ] @classmethod def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self: diff --git a/discord/types/invite.py b/discord/types/invite.py index 38e28f959..64eec8433 100644 --- a/discord/types/invite.py +++ b/discord/types/invite.py @@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import Literal, Optional, TypedDict, Union +from typing import List, Literal, Optional, TypedDict, Union from typing_extensions import NotRequired from .scheduled_event import GuildScheduledEvent @@ -33,6 +33,7 @@ from .guild import InviteGuild, _GuildPreviewUnique from .channel import PartialChannel from .user import PartialUser from .appinfo import PartialAppInfo +from .role import PartialInviteRole InviteTargetType = Literal[1, 2] InviteType = Literal[0, 1, 2] @@ -66,6 +67,7 @@ class Invite(IncompleteInvite, total=False): type: InviteType flags: NotRequired[int] expires_at: Optional[str] + roles: List[PartialInviteRole] class InviteWithCounts(Invite, _GuildPreviewUnique): ... diff --git a/discord/types/role.py b/discord/types/role.py index dabd1c1cf..f811aa021 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -59,3 +59,13 @@ class RoleTags(TypedDict, total=False): premium_subscriber: None available_for_purchase: None guild_connections: None + + +class PartialInviteRole(TypedDict): + id: Snowflake + name: str + position: int + color: int + colors: RoleColours + icon: Optional[str] + unicode_emoji: Optional[str] From 71f175dc648265c5024f56f1d5bbc55887e71a61 Mon Sep 17 00:00:00 2001 From: SassGuard Date: Thu, 16 Apr 2026 12:48:49 +0200 Subject: [PATCH 2/3] Fix target_role_ids type to use Sequence[Snowflake] --- discord/abc.py | 4 ++-- discord/http.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 724337519..67dc28014 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1282,7 +1282,7 @@ class GuildChannel: target_type: Optional[InviteTarget] = None, target_user: Optional[User] = None, target_application_id: Optional[int] = None, - target_role_ids: Optional[List[int]] = None, + target_role_ids: Optional[Sequence[Snowflake]] = None, guest: bool = False, ) -> Invite: """|coro| @@ -1322,7 +1322,7 @@ class GuildChannel: The id of the embedded application for the invite, required if ``target_type`` is :attr:`.InviteTarget.embedded_application`. .. versionadded:: 2.0 - target_role_ids: Optional[List[:class:`int`]] + target_role_ids: Optional[Sequence[:class:`abc.Snowflake`]] A list of role IDs to assign to the user upon accepting this invite. guest: :class:`bool` diff --git a/discord/http.py b/discord/http.py index ae7e7f38c..d0e83f34b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1854,7 +1854,7 @@ class HTTPClient: target_type: Optional[invite.InviteTargetType] = None, target_user_id: Optional[Snowflake] = None, target_application_id: Optional[Snowflake] = None, - target_role_ids: Optional[List[Snowflake]] = None, + target_role_ids: Optional[Sequence[Snowflake]] = None, flags: Optional[int] = None, ) -> Response[invite.Invite]: r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) From a100530956b15a892bf0d6de03a85b31c6d0e250 Mon Sep 17 00:00:00 2001 From: SassGuard Date: Thu, 16 Apr 2026 12:55:13 +0200 Subject: [PATCH 3/3] Add support for invite roles --- discord/abc.py | 2 +- discord/http.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 67dc28014..d60abc796 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1361,7 +1361,7 @@ class GuildChannel: target_type=target_type.value if target_type else None, target_user_id=target_user.id if target_user else None, target_application_id=target_application_id, - target_role_ids=target_role_ids, + target_role_ids=[r.id for r in target_role_ids] if target_role_ids is not None else None, flags=flags.value if flags else None, ) return Invite.from_incomplete(data=data, state=self._state) diff --git a/discord/http.py b/discord/http.py index d0e83f34b..d08f8b9f0 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1854,7 +1854,7 @@ class HTTPClient: target_type: Optional[invite.InviteTargetType] = None, target_user_id: Optional[Snowflake] = None, target_application_id: Optional[Snowflake] = None, - target_role_ids: Optional[Sequence[Snowflake]] = None, + target_role_ids: Optional[SnowflakeList] = None, flags: Optional[int] = None, ) -> Response[invite.Invite]: r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id)