From 696dcbfa7682d4b435dfc3fda4f4c3ea1e1ed066 Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 6 Feb 2025 09:48:18 +0100 Subject: [PATCH 1/8] :recycle: Rewrite `RoleTags.bot_id` handling --- discord/role.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/discord/role.py b/discord/role.py index 09423b168c..641dd0ec8d 100644 --- a/discord/role.py +++ b/discord/role.py @@ -33,7 +33,13 @@ from .flags import RoleFlags from .mixins import Hashable from .permissions import Permissions -from .utils import MISSING, _bytes_to_base64_data, _get_as_snowflake, snowflake_time +from .utils import ( + MISSING, + _bytes_to_base64_data, + _get_as_snowflake, + cached_slot_property, + snowflake_time, +) __all__ = ( "RoleTags", @@ -79,16 +85,18 @@ class RoleTags: """ __slots__ = ( - "bot_id", "integration_id", "subscription_listing_id", "_premium_subscriber", "_available_for_purchase", "_guild_connections", + "_bot_id", + "_bot_role", + "_data", ) def __init__(self, data: RoleTagPayload): - self.bot_id: int | None = _get_as_snowflake(data, "bot_id") + self._data: RoleTagPayload = data self.integration_id: int | None = _get_as_snowflake(data, "integration_id") self.subscription_listing_id: int | None = _get_as_snowflake( data, "subscription_listing_id" @@ -104,7 +112,13 @@ def __init__(self, data: RoleTagPayload): ) self._guild_connections: Any | None = data.get("guild_connections", MISSING) - def is_bot_managed(self) -> bool: + @cached_slot_property("_bot_id") + def bot_id(self) -> int | None: + """The bot's user ID that manages this role.""" + return int(self._data.get("bot_id", 0) or 0) or None + + @cached_slot_property("_bot_role") + def is_bot_role(self) -> bool: """Whether the role is associated with a bot.""" return self.bot_id is not None From 54d030dde58e67a68c7eb40904a49d3909d7113b Mon Sep 17 00:00:00 2001 From: Paillat Date: Thu, 6 Feb 2025 16:49:17 +0100 Subject: [PATCH 2/8] :recycle: Rewrite this mess --- discord/role.py | 223 ++++++++++++++++++++++++++++++------------------ 1 file changed, 142 insertions(+), 81 deletions(-) diff --git a/discord/role.py b/discord/role.py index 641dd0ec8d..19623b68e0 100644 --- a/discord/role.py +++ b/discord/role.py @@ -25,7 +25,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Final, TypeVar from .asset import Asset from .colour import Colour @@ -36,8 +36,6 @@ from .utils import ( MISSING, _bytes_to_base64_data, - _get_as_snowflake, - cached_slot_property, snowflake_time, ) @@ -57,20 +55,72 @@ from .types.role import RoleTags as RoleTagPayload +def _parse_tag_bool(data: RoleTagPayload, key: str) -> bool | None: + """Parse a boolean from a role tag payload. + + None is returned if the key is not present. + True is returned if the key is present and the value is None. + False is returned if the key is present and the value is not None. + + Parameters + ---------- + data: :class:`RoleTagPayload` + The role tag payload to parse from. + key: :class:`str` + The key to parse from. + + Returns + ------- + :class:`bool` | :class:`None` + The parsed boolean value or None if the key is not present. + """ + try: + # if it is False, False != None -> False + # if it is None, None == None -> True + return data[key] is None + except KeyError: + # if the key is not present, None + return None + + +def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: + """Parse an integer from a role tag payload. + + An integer is returned if the key is present and the value is an integer string. + None is returned if the key is not present or the value is not an integer string. + + Parameters + ---------- + data: :class:`RoleTagPayload` + The role tag payload to parse from. + key: :class:`str` + The key to parse from. + + Returns + ------- + :class:`int` | :class:`None` + The parsed integer value or None if the key is not present or the value is not an integer string. + """ + try: + return int(data[key]) # pyright: ignore[reportUnknownArgumentType] + except (KeyError, ValueError): + # key error means it's not there + # value error means it's not an number string (None or "") + return None + + class RoleTags: """Represents tags on a role. A role tag is a piece of extra information attached to a managed role that gives it context for the reason the role is managed. - While this can be accessed, a useful interface is also provided in the - :class:`Role` and :class:`Guild` classes as well. - Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. We aim to improve the documentation / introduce new attributes in future. For the meantime read `this `_ if you need detailed information about how role tags work. .. versionadded:: 1.6 + .. versionchanged:: 2.7 Attributes ---------- @@ -90,73 +140,123 @@ class RoleTags: "_premium_subscriber", "_available_for_purchase", "_guild_connections", - "_bot_id", - "_bot_role", + "bot_id", "_data", ) def __init__(self, data: RoleTagPayload): self._data: RoleTagPayload = data - self.integration_id: int | None = _get_as_snowflake(data, "integration_id") - self.subscription_listing_id: int | None = _get_as_snowflake( + self.integration_id: int | None = _parse_tag_int(data, "integration_id") + self.subscription_listing_id: int | None = _parse_tag_int( data, "subscription_listing_id" ) - # NOTE: The API returns "null" for each of the following tags if they are True, and omits them if False. - # However, "null" corresponds to None. - # This is different from other fields where "null" means "not there". - # So in this case, a value of None is the same as True. - # Which means we would need a different sentinel. - self._premium_subscriber: Any | None = data.get("premium_subscriber", MISSING) - self._available_for_purchase: Any | None = data.get( - "available_for_purchase", MISSING + self.bot_id: int | None = _parse_tag_int(data, "bot_id") + self._guild_connections: bool | None = _parse_tag_bool( + data, "guild_connections" + ) + self._premium_subscriber: bool | None = _parse_tag_bool( + data, "premium_subscriber" + ) + self._available_for_purchase: bool | None = _parse_tag_bool( + data, "available_for_purchase" ) - self._guild_connections: Any | None = data.get("guild_connections", MISSING) - - @cached_slot_property("_bot_id") - def bot_id(self) -> int | None: - """The bot's user ID that manages this role.""" - return int(self._data.get("bot_id", 0) or 0) or None - @cached_slot_property("_bot_role") + @property def is_bot_role(self) -> bool: - """Whether the role is associated with a bot.""" + """Whether the role is associated with a bot. + .. versionadded:: 2.7 + """ return self.bot_id is not None - def is_premium_subscriber(self) -> bool: - """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" - return self._premium_subscriber is None + @property + def is_booster_role(self) -> bool: + """Whether the role is the "boost", role for the guild. + .. versionadded:: 2.7 + """ + return self._guild_connections is False and self._premium_subscriber is True + + @property + def is_guild_product_role(self) -> bool: + """Whether the role is a guild product role. + + .. versionadded:: 2.7 + """ + return self._guild_connections is False and self._premium_subscriber is False + @property def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. """ return self.integration_id is not None - def is_available_for_purchase(self) -> bool: - """Whether the role is available for purchase. + @property + def is_base_subscription_role(self) -> bool: + """Whether the role is a base subscription role. - Returns ``True`` if the role is available for purchase, and - ``False`` if it is not available for purchase or if the role - is not linked to a guild subscription. + .. versionadded:: 2.7 + """ + return ( + self._guild_connections is False + and self._premium_subscriber is False + and self.integration_id is not None + ) + + @property + def is_subscription_role(self) -> bool: + """Whether the role is a subscription role. .. versionadded:: 2.7 """ - return self._available_for_purchase is None + return ( + self._guild_connections is False + and self._premium_subscriber is None + and self.integration_id is not None + and self.subscription_listing_id is not None + and self._available_for_purchase is True + ) + + @property + def is_draft_subscription_role(self) -> bool: + """Whether the role is a draft subscription role. + + .. versionadded:: 2.7 + """ + return ( + self._guild_connections is False + and self._premium_subscriber is None + and self.subscription_listing_id is not None + and self.integration_id is not None + and self._available_for_purchase is False + ) + @property def is_guild_connections_role(self) -> bool: """Whether the role is a guild connections role. .. versionadded:: 2.7 """ - return self._guild_connections is None + return self._guild_connections is True + + QUALIFIERS: Final = ( + "is_bot_role", + "is_booster_role", + "is_guild_product_role", + "is_integration", + "is_base_subscription_role", + "is_subscription_role", + "is_draft_subscription_role", + "is_guild_connections_role", + ) def __repr__(self) -> str: return ( f"" + + f"subscription_listing_id={self.subscription_listing_id} " + + " ".join( + q.removeprefix("is_") for q in self.QUALIFIERS if getattr(self, q) + ) + + ">" ) @@ -230,7 +330,8 @@ class Role(Hashable): mentionable: :class:`bool` Indicates if the role can be mentioned by users. tags: Optional[:class:`RoleTags`] - The role tags associated with this role. + The role tags associated with this role. Use the tags to determine additional information about the role, + like if it's a bot role, a booster role, etc... unicode_emoji: Optional[:class:`str`] The role's unicode emoji. Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`. @@ -330,28 +431,6 @@ def is_default(self) -> bool: """Checks if the role is the default role.""" return self.guild.id == self.id - def is_bot_managed(self) -> bool: - """Whether the role is associated with a bot. - - .. versionadded:: 1.6 - """ - return self.tags is not None and self.tags.is_bot_managed() - - def is_premium_subscriber(self) -> bool: - """Whether the role is the premium subscriber, AKA "boost", role for the guild. - - .. versionadded:: 1.6 - """ - return self.tags is not None and self.tags.is_premium_subscriber() - - def is_integration(self) -> bool: - """Whether the guild manages the role through some form of - integrations such as Twitch or through guild subscriptions. - - .. versionadded:: 1.6 - """ - return self.tags is not None and self.tags.is_integration() - def is_assignable(self) -> bool: """Whether the role is able to be assigned or removed by the bot. @@ -364,24 +443,6 @@ def is_assignable(self) -> bool: and (me.top_role > self or me.id == self.guild.owner_id) ) - def is_available_for_purchase(self) -> bool: - """Whether the role is available for purchase. - - Returns ``True`` if the role is available for purchase, and - ``False`` if it is not available for purchase or if the - role is not linked to a guild subscription. - - .. versionadded:: 2.7 - """ - return self.tags is not None and self.tags.is_available_for_purchase() - - def is_guild_connections_role(self) -> bool: - """Whether the role is a guild connections role. - - .. versionadded:: 2.7 - """ - return self.tags is not None and self.tags.is_guild_connections_role() - @property def permissions(self) -> Permissions: """Returns the role's permissions.""" From 5810fa32b8e1735a9bce8698610b8fc5c1074fd7 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sat, 15 Feb 2025 14:25:53 +0100 Subject: [PATCH 3/8] :sparkles: Things --- discord/role.py | 225 +++++++++++++++++++++++++++++++----------------- 1 file changed, 145 insertions(+), 80 deletions(-) diff --git a/discord/role.py b/discord/role.py index 19623b68e0..9a79a53ee3 100644 --- a/discord/role.py +++ b/discord/role.py @@ -25,7 +25,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Final, TypeVar +from enum import IntEnum +from typing import TYPE_CHECKING, Any, TypeVar from .asset import Asset from .colour import Colour @@ -36,6 +37,8 @@ from .utils import ( MISSING, _bytes_to_base64_data, + cached_slot_property, + deprecated, snowflake_time, ) @@ -109,6 +112,43 @@ def _parse_tag_int(data: RoleTagPayload, key: str) -> int | None: return None +class RoleType(IntEnum): + """Represents the type of role. + + This is NOT provided by discord but is rather computed by pycord based on the role tags. + + .. versionadded:: 2.7 + + Attributes + ---------- + APPLICATION: :class:`int` + The role is an application role. + BOOSTER: :class:`int` + The role is a guild's booster role. + GUILD_PRODUCT: :class:`int` + The role is a guild product role. + PREMIUM_SUBSCRIPTION_BASE: :class:`int` + The role is a base subscription role. + PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a subscription role. + DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a draft subscription role. + INTEGRATION: :class:`int` + The role is an integration role. + CONNECTION: :class:`int` + The role is a guild connections role. + """ + + APPLICATION = 1 + BOOSTER = 2 + GUILD_PRODUCT = 3 + PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription + PREMIUM_SUBSCRIPTION_TIER = 5 + DRAFT_PREMIUM_SUBSCRIPTION_TIER = 5 + INTEGRATION = 7 + CONNECTION = 8 + + class RoleTags: """Represents tags on a role. @@ -142,6 +182,7 @@ class RoleTags: "_guild_connections", "bot_id", "_data", + "_type", ) def __init__(self, data: RoleTagPayload): @@ -161,102 +202,73 @@ def __init__(self, data: RoleTagPayload): data, "available_for_purchase" ) - @property - def is_bot_role(self) -> bool: - """Whether the role is associated with a bot. - .. versionadded:: 2.7 - """ + @cached_slot_property("_type") + def type(self) -> RoleType: + """Determine the role type based on tag flags.""" + # Bot role + if self.bot_id is not None: + return RoleType.APPLICATION + + # Role connection + if self._guild_connections is True: + return RoleType.CONNECTION + + # Paid roles + if self._guild_connections is False: + if self._premium_subscriber is False: + return RoleType.GUILD_PRODUCT + + if self._premium_subscriber is True: + return RoleType.BOOSTER + + # subscription roles + if self.integration_id is not None: + if ( + self._premium_subscriber is None + and self.subscription_listing_id is not None + ): + if self._available_for_purchase is True: + return RoleType.PREMIUM_SUBSCRIPTION_TIER + return RoleType.DRAFT_PREMIUM_SUBSCRIPTION_TIER + + # integration role (Twitch/YouTube) + if self.integration_id is not None: + return RoleType.INTEGRATION + + raise ValueError("Unable to determine the role type based on provided tags.") + + @deprecated("RoleTags.type", "2.7") + def is_bot_managed(self) -> bool: + """Whether the role is associated with a bot.""" return self.bot_id is not None - @property - def is_booster_role(self) -> bool: - """Whether the role is the "boost", role for the guild. - .. versionadded:: 2.7 - """ - return self._guild_connections is False and self._premium_subscriber is True - - @property - def is_guild_product_role(self) -> bool: - """Whether the role is a guild product role. + @deprecated("RoleTags.type", "2.7") + def is_premium_subscriber(self) -> bool: + """Whether the role is the premium subscriber, AKA "boost", role for the guild.""" + return self._premium_subscriber is None - .. versionadded:: 2.7 - """ - return self._guild_connections is False and self._premium_subscriber is False - - @property + @deprecated("RoleTags.type", "2.7") def is_integration(self) -> bool: """Whether the guild manages the role through some form of integrations such as Twitch or through guild subscriptions. """ return self.integration_id is not None - @property - def is_base_subscription_role(self) -> bool: - """Whether the role is a base subscription role. - - .. versionadded:: 2.7 - """ - return ( - self._guild_connections is False - and self._premium_subscriber is False - and self.integration_id is not None - ) - - @property - def is_subscription_role(self) -> bool: - """Whether the role is a subscription role. - - .. versionadded:: 2.7 - """ - return ( - self._guild_connections is False - and self._premium_subscriber is None - and self.integration_id is not None - and self.subscription_listing_id is not None - and self._available_for_purchase is True - ) - - @property - def is_draft_subscription_role(self) -> bool: - """Whether the role is a draft subscription role. - - .. versionadded:: 2.7 - """ - return ( - self._guild_connections is False - and self._premium_subscriber is None - and self.subscription_listing_id is not None - and self.integration_id is not None - and self._available_for_purchase is False - ) + @deprecated("RoleTags.type", "2.7") + def is_available_for_purchase(self) -> bool: + """Whether the role is available for purchase.""" + return self._available_for_purchase is True - @property + @deprecated("RoleTags.type", "2.7") def is_guild_connections_role(self) -> bool: - """Whether the role is a guild connections role. - - .. versionadded:: 2.7 - """ + """Whether the role is a guild connections role.""" return self._guild_connections is True - QUALIFIERS: Final = ( - "is_bot_role", - "is_booster_role", - "is_guild_product_role", - "is_integration", - "is_base_subscription_role", - "is_subscription_role", - "is_draft_subscription_role", - "is_guild_connections_role", - ) - def __repr__(self) -> str: return ( f"" + + f"type={self.type!r}>" ) @@ -431,6 +443,31 @@ def is_default(self) -> bool: """Checks if the role is the default role.""" return self.guild.id == self.id + @deprecated("Role.type", "2.7") + def is_bot_managed(self) -> bool: + """Whether the role is associated with a bot. + + .. versionadded:: 1.6 + """ + return self.tags is not None and self.tags.is_bot_managed() + + @deprecated("Role.type", "2.7") + def is_premium_subscriber(self) -> bool: + """Whether the role is the premium subscriber, AKA "boost", role for the guild. + + .. versionadded:: 1.6 + """ + return self.tags is not None and self.tags.is_premium_subscriber() + + @deprecated("Role.type", "2.7") + def is_integration(self) -> bool: + """Whether the guild manages the role through some form of + integrations such as Twitch or through guild subscriptions. + + .. versionadded:: 1.6 + """ + return self.tags is not None and self.tags.is_integration() + def is_assignable(self) -> bool: """Whether the role is able to be assigned or removed by the bot. @@ -443,6 +480,26 @@ def is_assignable(self) -> bool: and (me.top_role > self or me.id == self.guild.owner_id) ) + @deprecated("Role.type", "2.7") + def is_available_for_purchase(self) -> bool: + """Whether the role is available for purchase. + + Returns ``True`` if the role is available for purchase, and + ``False`` if it is not available for purchase or if the + role is not linked to a guild subscription. + + .. versionadded:: 2.7 + """ + return self.tags is not None and self.tags.is_available_for_purchase() + + @deprecated("Role.type", "2.7") + def is_guild_connections_role(self) -> bool: + """Whether the role is a guild connections role. + + .. versionadded:: 2.7 + """ + return self.tags is not None and self.tags.is_guild_connections_role() + @property def permissions(self) -> Permissions: """Returns the role's permissions.""" @@ -489,6 +546,14 @@ def icon(self) -> Asset | None: return Asset._from_icon(self._state, self.id, self._icon, "role") + @property + def type(self) -> RoleType: + """The type of the role. + + .. versionadded:: 2.7 + """ + return self.tags.type + async def _move(self, position: int, reason: str | None) -> None: if position <= 0: raise InvalidArgument("Cannot move role to position 0 or below") From 6a918755f37d7b481cf4311694da7fbb58bd7c44 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sat, 15 Feb 2025 14:48:08 +0100 Subject: [PATCH 4/8] :bug: Who tf made this messed up system --- discord/role.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/discord/role.py b/discord/role.py index 9a79a53ee3..96497807f9 100644 --- a/discord/role.py +++ b/discord/role.py @@ -144,9 +144,10 @@ class RoleType(IntEnum): GUILD_PRODUCT = 3 PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription PREMIUM_SUBSCRIPTION_TIER = 5 - DRAFT_PREMIUM_SUBSCRIPTION_TIER = 5 + DRAFT_PREMIUM_SUBSCRIPTION_TIER = 6 INTEGRATION = 7 CONNECTION = 8 + UNKNOWN = 9 class RoleTags: @@ -180,6 +181,7 @@ class RoleTags: "_premium_subscriber", "_available_for_purchase", "_guild_connections", + "_is_guild_product_role", "bot_id", "_data", "_type", @@ -201,6 +203,8 @@ def __init__(self, data: RoleTagPayload): self._available_for_purchase: bool | None = _parse_tag_bool( data, "available_for_purchase" ) + # here discord did things in a normal and logical way for once + self._is_guild_product_role: bool | None = data.get("is_guild_product_role") @cached_slot_property("_type") def type(self) -> RoleType: @@ -214,28 +218,29 @@ def type(self) -> RoleType: return RoleType.CONNECTION # Paid roles - if self._guild_connections is False: - if self._premium_subscriber is False: - return RoleType.GUILD_PRODUCT - - if self._premium_subscriber is True: - return RoleType.BOOSTER - - # subscription roles - if self.integration_id is not None: - if ( - self._premium_subscriber is None - and self.subscription_listing_id is not None - ): - if self._available_for_purchase is True: - return RoleType.PREMIUM_SUBSCRIPTION_TIER - return RoleType.DRAFT_PREMIUM_SUBSCRIPTION_TIER + if self._is_guild_product_role is True: + return RoleType.GUILD_PRODUCT + + # Booster role + if self._premium_subscriber is True: + return RoleType.BOOSTER + + # subscription roles + if ( + self.integration_id is not None + and self._premium_subscriber is None + and self.subscription_listing_id is not None + ): + if self._available_for_purchase is True: + return RoleType.PREMIUM_SUBSCRIPTION_TIER + return RoleType.DRAFT_PREMIUM_SUBSCRIPTION_TIER # integration role (Twitch/YouTube) if self.integration_id is not None: return RoleType.INTEGRATION - raise ValueError("Unable to determine the role type based on provided tags.") + # Seeing how messed up this is it wouldn't be a surprise if this happened + return RoleType.UNKNOWN @deprecated("RoleTags.type", "2.7") def is_bot_managed(self) -> bool: From 54f1baa1efea50bdf9e2b1a76d3a57ef11de8858 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sat, 15 Feb 2025 14:53:04 +0100 Subject: [PATCH 5/8] :memo: Better docs --- discord/role.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/discord/role.py b/discord/role.py index 96497807f9..55854f11a7 100644 --- a/discord/role.py +++ b/discord/role.py @@ -122,21 +122,23 @@ class RoleType(IntEnum): Attributes ---------- APPLICATION: :class:`int` - The role is an application role. + The role is an application (bot) role. BOOSTER: :class:`int` The role is a guild's booster role. GUILD_PRODUCT: :class:`int` The role is a guild product role. PREMIUM_SUBSCRIPTION_BASE: :class:`int` - The role is a base subscription role. + The role is a base subscription role. This is not possible to determine currently, will be INTEGRATION if it's a base subscription. PREMIUM_SUBSCRIPTION_TIER: :class:`int` The role is a subscription role. DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` The role is a draft subscription role. INTEGRATION: :class:`int` - The role is an integration role. + The role is an integration role, such as Twitch or YouTube, or a base subscription role. CONNECTION: :class:`int` The role is a guild connections role. + UNKNOWN: :class:`int` + The role type is unknown. """ APPLICATION = 1 @@ -157,11 +159,13 @@ class RoleTags: that gives it context for the reason the role is managed. Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type. - We aim to improve the documentation / introduce new attributes in future. - For the meantime read `this `_ if you need detailed information about how role tags work. + In order to make your life easier, pycord provides a :attr:`RoleTags.type` attribute that attempts to determine the role type based on the role tags. It's value is not provided by discord but is rather computed by pycord based on the role tags. + If you find an issue, please open an issue on `GitHub `_. + Read `this `_ if you need detailed information about how role tags work. .. versionadded:: 1.6 .. versionchanged:: 2.7 + The type of the role is now determined by the :attr:`RoleTags.type` attribute. Attributes ---------- From 5ac16bbd21681a4627724288d6ed92f91822acaf Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sun, 16 Feb 2025 13:43:24 +0100 Subject: [PATCH 6/8] :adhesive_bandage: Fix error when tags is None --- discord/role.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 55854f11a7..04b545665f 100644 --- a/discord/role.py +++ b/discord/role.py @@ -121,6 +121,8 @@ class RoleType(IntEnum): Attributes ---------- + NORMAL: :class:`int` + The role is a normal role. APPLICATION: :class:`int` The role is an application (bot) role. BOOSTER: :class:`int` @@ -141,6 +143,7 @@ class RoleType(IntEnum): The role type is unknown. """ + NORMAL = 0 APPLICATION = 1 BOOSTER = 2 GUILD_PRODUCT = 3 @@ -561,7 +564,7 @@ def type(self) -> RoleType: .. versionadded:: 2.7 """ - return self.tags.type + return self.tags.type if self.tags is not None else RoleType.NORMAL async def _move(self, position: int, reason: str | None) -> None: if position <= 0: From 304d70bd6370eec9add54c8d0321556e0d2b02c6 Mon Sep 17 00:00:00 2001 From: Paillat-dev Date: Sun, 16 Feb 2025 14:00:04 +0100 Subject: [PATCH 7/8] :memo: CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 647fc311be..2728351144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2579](https://github.com/Pycord-Development/pycord/pull/2579)) - Added new `Subscription` object and related methods/events. ([#2564](https://github.com/Pycord-Development/pycord/pull/2564)) +- Added `RoleType` enum accessible via `Role.type` and `RoleTags.type`. + ([#2708](https://github.com/Pycord-Development/pycord/pull/2708)) ### Fixed @@ -121,6 +123,10 @@ These changes are available on the `master` branch, but have not yet been releas ([#2501](https://github.com/Pycord-Development/pycord/pull/2501)) - Deprecated `Interaction.cached_channel` in favor of `Interaction.channel`. ([#2658](https://github.com/Pycord-Development/pycord/pull/2658)) +- Deprecated for both `Role` and `RoleTags`: `is_bot_managed`, `is_premium_subscriber`, + `is_integration`, `is_available_for_purchase`, and `is_guild_connections_role`, in + favor of `Role.type` and `RoleTags.type`. + ([#2708](https://github.com/Pycord-Development/pycord/pull/2708)) ## [2.6.1] - 2024-09-15 From 3b34892000c3a26be556dcb095ca9f74fbc655fc Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 4 Mar 2025 10:38:00 +0100 Subject: [PATCH 8/8] :label: Add missing typing --- discord/role.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/role.py b/discord/role.py index 7aea7374bb..366f3f615e 100644 --- a/discord/role.py +++ b/discord/role.py @@ -194,6 +194,8 @@ class RoleTags: "_type", ) + _type: RoleType + def __init__(self, data: RoleTagPayload): self._data: RoleTagPayload = data self.integration_id: int | None = _parse_tag_int(data, "integration_id")