25
25
26
26
from __future__ import annotations
27
27
28
- from typing import TYPE_CHECKING , Any , TypeVar
28
+ from typing import TYPE_CHECKING , Any , Final , TypeVar
29
29
30
30
from .asset import Asset
31
31
from .colour import Colour
36
36
from .utils import (
37
37
MISSING ,
38
38
_bytes_to_base64_data ,
39
- _get_as_snowflake ,
40
- cached_slot_property ,
41
39
snowflake_time ,
42
40
)
43
41
57
55
from .types .role import RoleTags as RoleTagPayload
58
56
59
57
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
+
60
112
class RoleTags :
61
113
"""Represents tags on a role.
62
114
63
115
A role tag is a piece of extra information attached to a managed role
64
116
that gives it context for the reason the role is managed.
65
117
66
- While this can be accessed, a useful interface is also provided in the
67
- :class:`Role` and :class:`Guild` classes as well.
68
-
69
118
Role tags are a fairly complex topic, since it's usually hard to determine which role tag combination represents which role type.
70
119
We aim to improve the documentation / introduce new attributes in future.
71
120
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.
72
121
73
122
.. versionadded:: 1.6
123
+ .. versionchanged:: 2.7
74
124
75
125
Attributes
76
126
----------
@@ -90,73 +140,123 @@ class RoleTags:
90
140
"_premium_subscriber" ,
91
141
"_available_for_purchase" ,
92
142
"_guild_connections" ,
93
- "_bot_id" ,
94
- "_bot_role" ,
143
+ "bot_id" ,
95
144
"_data" ,
96
145
)
97
146
98
147
def __init__ (self , data : RoleTagPayload ):
99
148
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 (
102
151
data , "subscription_listing_id"
103
152
)
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"
112
162
)
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
119
163
120
- @cached_slot_property ( "_bot_role" )
164
+ @property
121
165
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
+ """
123
169
return self .bot_id is not None
124
170
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
128
185
186
+ @property
129
187
def is_integration (self ) -> bool :
130
188
"""Whether the guild manages the role through some form of
131
189
integrations such as Twitch or through guild subscriptions.
132
190
"""
133
191
return self .integration_id is not None
134
192
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.
137
196
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.
141
208
142
209
.. versionadded:: 2.7
143
210
"""
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
+ )
145
232
233
+ @property
146
234
def is_guild_connections_role (self ) -> bool :
147
235
"""Whether the role is a guild connections role.
148
236
149
237
.. versionadded:: 2.7
150
238
"""
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
+ )
152
251
153
252
def __repr__ (self ) -> str :
154
253
return (
155
254
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
+ + ">"
160
260
)
161
261
162
262
@@ -230,7 +330,8 @@ class Role(Hashable):
230
330
mentionable: :class:`bool`
231
331
Indicates if the role can be mentioned by users.
232
332
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...
234
335
unicode_emoji: Optional[:class:`str`]
235
336
The role's unicode emoji.
236
337
Only available to guilds that contain ``ROLE_ICONS`` in :attr:`Guild.features`.
@@ -330,28 +431,6 @@ def is_default(self) -> bool:
330
431
"""Checks if the role is the default role."""
331
432
return self .guild .id == self .id
332
433
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
-
355
434
def is_assignable (self ) -> bool :
356
435
"""Whether the role is able to be assigned or removed by the bot.
357
436
@@ -364,24 +443,6 @@ def is_assignable(self) -> bool:
364
443
and (me .top_role > self or me .id == self .guild .owner_id )
365
444
)
366
445
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
-
385
446
@property
386
447
def permissions (self ) -> Permissions :
387
448
"""Returns the role's permissions."""
0 commit comments