Skip to content

Commit 54d030d

Browse files
committed
♻️ Rewrite this mess
1 parent 696dcbf commit 54d030d

File tree

1 file changed

+142
-81
lines changed

1 file changed

+142
-81
lines changed

discord/role.py

Lines changed: 142 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from __future__ import annotations
2727

28-
from typing import TYPE_CHECKING, Any, TypeVar
28+
from typing import TYPE_CHECKING, Any, Final, TypeVar
2929

3030
from .asset import Asset
3131
from .colour import Colour
@@ -36,8 +36,6 @@
3636
from .utils import (
3737
MISSING,
3838
_bytes_to_base64_data,
39-
_get_as_snowflake,
40-
cached_slot_property,
4139
snowflake_time,
4240
)
4341

@@ -57,20 +55,72 @@
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+
60112
class 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

Comments
 (0)