2525
2626from __future__ import annotations
2727
28- from typing import TYPE_CHECKING , Any , TypeVar
28+ from typing import TYPE_CHECKING , Any , Final , TypeVar
2929
3030from .asset import Asset
3131from .colour import Colour
3636from .utils import (
3737 MISSING ,
3838 _bytes_to_base64_data ,
39- _get_as_snowflake ,
40- cached_slot_property ,
4139 snowflake_time ,
4240)
4341
5755 from .types .role import RoleTags as RoleTagPayload
5856
5957
58+ def _parse_tag_bool (data : RoleTagPayload , key : str ) -> bool | None :
59+ """Parse a boolean from a role tag payload.
60+
61+ None is returned if the key is not present.
62+ True is returned if the key is present and the value is None.
63+ False is returned if the key is present and the value is not None.
64+
65+ Parameters
66+ ----------
67+ data: :class:`RoleTagPayload`
68+ The role tag payload to parse from.
69+ key: :class:`str`
70+ The key to parse from.
71+
72+ Returns
73+ -------
74+ :class:`bool` | :class:`None`
75+ The parsed boolean value or None if the key is not present.
76+ """
77+ try :
78+ # if it is False, False != None -> False
79+ # if it is None, None == None -> True
80+ return data [key ] is None
81+ except KeyError :
82+ # if the key is not present, None
83+ return None
84+
85+
86+ def _parse_tag_int (data : RoleTagPayload , key : str ) -> int | None :
87+ """Parse an integer from a role tag payload.
88+
89+ An integer is returned if the key is present and the value is an integer string.
90+ None is returned if the key is not present or the value is not an integer string.
91+
92+ Parameters
93+ ----------
94+ data: :class:`RoleTagPayload`
95+ The role tag payload to parse from.
96+ key: :class:`str`
97+ The key to parse from.
98+
99+ Returns
100+ -------
101+ :class:`int` | :class:`None`
102+ The parsed integer value or None if the key is not present or the value is not an integer string.
103+ """
104+ try :
105+ return int (data [key ]) # pyright: ignore[reportUnknownArgumentType]
106+ except (KeyError , ValueError ):
107+ # key error means it's not there
108+ # value error means it's not an number string (None or "")
109+ return None
110+
111+
60112class RoleTags :
61113 """Represents tags on a role.
62114
63115 A role tag is a piece of extra information attached to a managed role
64116 that gives it context for the reason the role is managed.
65117
66- While this can be accessed, a useful interface is also provided in the
67- :class:`Role` and :class:`Guild` classes as well.
68-
69118 Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type.
70119 We aim to improve the documentation / introduce new attributes in future.
71120 For the meantime read `this <https://lulalaby.notion.site/Special-Roles-Documentation-17411d3839e680abbb1eff63c51bd7a7?pvs=4>`_ if you need detailed information about how role tags work.
72121
73122 .. versionadded:: 1.6
123+ .. versionchanged:: 2.7
74124
75125 Attributes
76126 ----------
@@ -90,73 +140,123 @@ class RoleTags:
90140 "_premium_subscriber" ,
91141 "_available_for_purchase" ,
92142 "_guild_connections" ,
93- "_bot_id" ,
94- "_bot_role" ,
143+ "bot_id" ,
95144 "_data" ,
96145 )
97146
98147 def __init__ (self , data : RoleTagPayload ):
99148 self ._data : RoleTagPayload = data
100- self .integration_id : int | None = _get_as_snowflake (data , "integration_id" )
101- self .subscription_listing_id : int | None = _get_as_snowflake (
149+ self .integration_id : int | None = _parse_tag_int (data , "integration_id" )
150+ self .subscription_listing_id : int | None = _parse_tag_int (
102151 data , "subscription_listing_id"
103152 )
104- # NOTE: The API returns "null" for each of the following tags if they are True, and omits them if False.
105- # However, "null" corresponds to None.
106- # This is different from other fields where "null" means "not there".
107- # So in this case, a value of None is the same as True.
108- # Which means we would need a different sentinel.
109- self ._premium_subscriber : Any | None = data .get ("premium_subscriber" , MISSING )
110- self ._available_for_purchase : Any | None = data .get (
111- "available_for_purchase" , MISSING
153+ self .bot_id : int | None = _parse_tag_int (data , "bot_id" )
154+ self ._guild_connections : bool | None = _parse_tag_bool (
155+ data , "guild_connections"
156+ )
157+ self ._premium_subscriber : bool | None = _parse_tag_bool (
158+ data , "premium_subscriber"
159+ )
160+ self ._available_for_purchase : bool | None = _parse_tag_bool (
161+ data , "available_for_purchase"
112162 )
113- self ._guild_connections : Any | None = data .get ("guild_connections" , MISSING )
114-
115- @cached_slot_property ("_bot_id" )
116- def bot_id (self ) -> int | None :
117- """The bot's user ID that manages this role."""
118- return int (self ._data .get ("bot_id" , 0 ) or 0 ) or None
119163
120- @cached_slot_property ( "_bot_role" )
164+ @property
121165 def is_bot_role (self ) -> bool :
122- """Whether the role is associated with a bot."""
166+ """Whether the role is associated with a bot.
167+ .. versionadded:: 2.7
168+ """
123169 return self .bot_id is not None
124170
125- def is_premium_subscriber (self ) -> bool :
126- """Whether the role is the premium subscriber, AKA "boost", role for the guild."""
127- return self ._premium_subscriber is None
171+ @property
172+ def is_booster_role (self ) -> bool :
173+ """Whether the role is the "boost", role for the guild.
174+ .. versionadded:: 2.7
175+ """
176+ return self ._guild_connections is False and self ._premium_subscriber is True
177+
178+ @property
179+ def is_guild_product_role (self ) -> bool :
180+ """Whether the role is a guild product role.
181+
182+ .. versionadded:: 2.7
183+ """
184+ return self ._guild_connections is False and self ._premium_subscriber is False
128185
186+ @property
129187 def is_integration (self ) -> bool :
130188 """Whether the guild manages the role through some form of
131189 integrations such as Twitch or through guild subscriptions.
132190 """
133191 return self .integration_id is not None
134192
135- def is_available_for_purchase (self ) -> bool :
136- """Whether the role is available for purchase.
193+ @property
194+ def is_base_subscription_role (self ) -> bool :
195+ """Whether the role is a base subscription role.
137196
138- Returns ``True`` if the role is available for purchase, and
139- ``False`` if it is not available for purchase or if the role
140- is not linked to a guild subscription.
197+ .. versionadded:: 2.7
198+ """
199+ return (
200+ self ._guild_connections is False
201+ and self ._premium_subscriber is False
202+ and self .integration_id is not None
203+ )
204+
205+ @property
206+ def is_subscription_role (self ) -> bool :
207+ """Whether the role is a subscription role.
141208
142209 .. versionadded:: 2.7
143210 """
144- return self ._available_for_purchase is None
211+ return (
212+ self ._guild_connections is False
213+ and self ._premium_subscriber is None
214+ and self .integration_id is not None
215+ and self .subscription_listing_id is not None
216+ and self ._available_for_purchase is True
217+ )
218+
219+ @property
220+ def is_draft_subscription_role (self ) -> bool :
221+ """Whether the role is a draft subscription role.
222+
223+ .. versionadded:: 2.7
224+ """
225+ return (
226+ self ._guild_connections is False
227+ and self ._premium_subscriber is None
228+ and self .subscription_listing_id is not None
229+ and self .integration_id is not None
230+ and self ._available_for_purchase is False
231+ )
145232
233+ @property
146234 def is_guild_connections_role (self ) -> bool :
147235 """Whether the role is a guild connections role.
148236
149237 .. versionadded:: 2.7
150238 """
151- return self ._guild_connections is None
239+ return self ._guild_connections is True
240+
241+ QUALIFIERS : Final = (
242+ "is_bot_role" ,
243+ "is_booster_role" ,
244+ "is_guild_product_role" ,
245+ "is_integration" ,
246+ "is_base_subscription_role" ,
247+ "is_subscription_role" ,
248+ "is_draft_subscription_role" ,
249+ "is_guild_connections_role" ,
250+ )
152251
153252 def __repr__ (self ) -> str :
154253 return (
155254 f"<RoleTags bot_id={ self .bot_id } integration_id={ self .integration_id } "
156- f"subscription_listing_id={ self .subscription_listing_id } "
157- f"premium_subscriber={ self .is_premium_subscriber ()} "
158- f"available_for_purchase={ self .is_available_for_purchase ()} "
159- f"guild_connections={ self .is_guild_connections_role ()} >"
255+ + f"subscription_listing_id={ self .subscription_listing_id } "
256+ + " " .join (
257+ q .removeprefix ("is_" ) for q in self .QUALIFIERS if getattr (self , q )
258+ )
259+ + ">"
160260 )
161261
162262
@@ -230,7 +330,8 @@ class Role(Hashable):
230330 mentionable: :class:`bool`
231331 Indicates if the role can be mentioned by users.
232332 tags: Optional[:class:`RoleTags`]
233- The role tags associated with this role.
333+ The role tags associated with this role. Use the tags to determine additional information about the role,
334+ like if it's a bot role, a booster role, etc...
234335 unicode_emoji: Optional[:class:`str`]
235336 The role's unicode emoji.
236337 Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`.
@@ -330,28 +431,6 @@ def is_default(self) -> bool:
330431 """Checks if the role is the default role."""
331432 return self .guild .id == self .id
332433
333- def is_bot_managed (self ) -> bool :
334- """Whether the role is associated with a bot.
335-
336- .. versionadded:: 1.6
337- """
338- return self .tags is not None and self .tags .is_bot_managed ()
339-
340- def is_premium_subscriber (self ) -> bool :
341- """Whether the role is the premium subscriber, AKA "boost", role for the guild.
342-
343- .. versionadded:: 1.6
344- """
345- return self .tags is not None and self .tags .is_premium_subscriber ()
346-
347- def is_integration (self ) -> bool :
348- """Whether the guild manages the role through some form of
349- integrations such as Twitch or through guild subscriptions.
350-
351- .. versionadded:: 1.6
352- """
353- return self .tags is not None and self .tags .is_integration ()
354-
355434 def is_assignable (self ) -> bool :
356435 """Whether the role is able to be assigned or removed by the bot.
357436
@@ -364,24 +443,6 @@ def is_assignable(self) -> bool:
364443 and (me .top_role > self or me .id == self .guild .owner_id )
365444 )
366445
367- def is_available_for_purchase (self ) -> bool :
368- """Whether the role is available for purchase.
369-
370- Returns ``True`` if the role is available for purchase, and
371- ``False`` if it is not available for purchase or if the
372- role is not linked to a guild subscription.
373-
374- .. versionadded:: 2.7
375- """
376- return self .tags is not None and self .tags .is_available_for_purchase ()
377-
378- def is_guild_connections_role (self ) -> bool :
379- """Whether the role is a guild connections role.
380-
381- .. versionadded:: 2.7
382- """
383- return self .tags is not None and self .tags .is_guild_connections_role ()
384-
385446 @property
386447 def permissions (self ) -> Permissions :
387448 """Returns the role's permissions."""
0 commit comments