Skip to content

Commit 30e032c

Browse files
committed
feat: ✨ Add support for role gradient colors in Role (Pycord-Development#2818)
* ✨ Add support for enhanced role colors in `Role` * ✨ add RoleColours support to `Role.edit` * ✨ add RoleColours support to role creation in `Guild` and define default colors in `RoleColours` * ✨ add support for RoleColours in role creation and editing * ✨ update role attributes to use RoleColours and enhance color properties * 📝 CHANGELOG.md * 📝 add documentation for RoleColours and its attributes * ✏️ fix version annotation for primary color method in role.py * ✨ add holographic role support in RoleColours * 📝 update tertiary color documentation to specify allowed value * ✨ Finish implementing holographic support * 🗑️ Add deprecation warnings for singular colour properties in Role and Guild (cherry picked from commit 975a0d9)
1 parent 94ec8f9 commit 30e032c

File tree

7 files changed

+198
-14
lines changed

7 files changed

+198
-14
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ These changes are available on the `master` branch, but have not yet been releas
6363
([#2775](https://github.com/Pycord-Development/pycord/pull/2775))
6464
- Added `discord.Interaction.created_at`.
6565
([#2801](https://github.com/Pycord-Development/pycord/pull/2801))
66+
- Added role gradients support with `Role.colours` and the `RoleColours` class.
67+
([#2818](https://github.com/Pycord-Development/pycord/pull/2818))
6668

6769
### Fixed
6870

discord/guild.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
from .monetization import Entitlement
8080
from .onboarding import Onboarding
8181
from .permissions import PermissionOverwrite
82-
from .role import Role
82+
from .role import Role, RoleColours
8383
from .scheduled_events import ScheduledEvent, ScheduledEventLocation
8484
from .stage_instance import StageInstance
8585
from .sticker import GuildSticker
@@ -2802,6 +2802,8 @@ async def create_role(
28022802
name: str = ...,
28032803
permissions: Permissions = ...,
28042804
colour: Colour | int = ...,
2805+
colours: RoleColours = ...,
2806+
holographic: bool = ...,
28052807
hoist: bool = ...,
28062808
mentionable: bool = ...,
28072809
icon: bytes | None | utils.Undefined = MISSING,
@@ -2816,6 +2818,8 @@ async def create_role(
28162818
name: str = ...,
28172819
permissions: Permissions = ...,
28182820
color: Colour | int = ...,
2821+
colors: RoleColours = ...,
2822+
holographic: bool = ...,
28192823
hoist: bool = ...,
28202824
mentionable: bool = ...,
28212825
icon: bytes | None = ...,
@@ -2829,6 +2833,9 @@ async def create_role(
28292833
permissions: Permissions | utils.Undefined = MISSING,
28302834
color: Colour | int | utils.Undefined = MISSING,
28312835
colour: Colour | int | utils.Undefined = MISSING,
2836+
colors: RoleColours | utils.Undefined = MISSING,
2837+
colours: RoleColours | utils.Undefined = MISSING,
2838+
holographic: bool | utils.Undefined = MISSING,
28322839
hoist: bool | utils.Undefined = MISSING,
28332840
mentionable: bool | utils.Undefined = MISSING,
28342841
reason: str | None = None,
@@ -2892,11 +2899,30 @@ async def create_role(
28922899
else:
28932900
fields["permissions"] = "0"
28942901

2895-
actual_colour = colour or color or Colour.default()
2902+
actual_colour = colour if colour not in (MISSING, None) else color
2903+
28962904
if isinstance(actual_colour, int):
2897-
fields["color"] = actual_colour
2905+
actual_colour = Colour(actual_colour)
2906+
2907+
if actual_colour not in (MISSING, None):
2908+
utils.warn_deprecated("colour", "colours", "2.7")
2909+
actual_colours = RoleColours(primary=actual_colour)
2910+
elif holographic:
2911+
actual_colours = RoleColours.holographic()
2912+
else:
2913+
actual_colours = colours or colors or RoleColours.default()
2914+
2915+
if isinstance(actual_colours, RoleColours):
2916+
if "ENHANCED_ROLE_COLORS" not in self.features:
2917+
actual_colours.secondary = None
2918+
actual_colours.tertiary = None
2919+
fields["colors"] = actual_colours._to_dict()
28982920
else:
2899-
fields["color"] = actual_colour.value
2921+
raise InvalidArgument(
2922+
"colours parameter must be of type RoleColours, not {0.__class__.__name__}".format(
2923+
actual_colours
2924+
)
2925+
)
29002926

29012927
if hoist is not MISSING:
29022928
fields["hoist"] = hoist

discord/http.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,6 +2014,7 @@ def edit_role(
20142014
"name",
20152015
"permissions",
20162016
"color",
2017+
"colors",
20172018
"hoist",
20182019
"mentionable",
20192020
"icon",

discord/role.py

Lines changed: 152 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,25 @@
2828
from email import utils
2929
from typing import TYPE_CHECKING, Any, TypeVar
3030

31+
from typing_extensions import Self
32+
3133
from .asset import Asset
3234
from .colour import Colour
3335
from .errors import InvalidArgument
3436
from .flags import RoleFlags
3537
from .mixins import Hashable
3638
from .permissions import Permissions
37-
from .utils import MISSING, _bytes_to_base64_data, _get_as_snowflake, snowflake_time
38-
39-
__all__ = (
40-
"RoleTags",
41-
"Role",
39+
from .utils import (
40+
MISSING,
41+
_bytes_to_base64_data,
42+
_get_as_snowflake,
43+
deprecated,
44+
snowflake_time,
45+
warn_deprecated,
4246
)
4347

48+
__all__ = ("RoleTags", "Role", "RoleColours")
49+
4450
if TYPE_CHECKING:
4551
import datetime
4652

@@ -49,6 +55,7 @@
4955
from .state import ConnectionState
5056
from .types.guild import RolePositionUpdate
5157
from .types.role import Role as RolePayload
58+
from .types.role import RoleColours as RoleColoursPayload
5259
from .types.role import RoleTags as RoleTagPayload
5360

5461

@@ -146,6 +153,96 @@ def __repr__(self) -> str:
146153
R = TypeVar("R", bound="Role")
147154

148155

156+
class RoleColours:
157+
"""Represents a role's gradient colours.
158+
159+
.. versionadded:: 2.7
160+
161+
Attributes
162+
----------
163+
primary: :class:`Colour`
164+
The primary colour of the role.
165+
secondary: Optional[:class:`Colour`]
166+
The secondary colour of the role.
167+
tertiary: Optional[:class:`Colour`]
168+
The tertiary colour of the role. At the moment, only `16761760` is allowed.
169+
"""
170+
171+
def __init__(
172+
self,
173+
primary: Colour,
174+
secondary: Colour | None = None,
175+
tertiary: Colour | None = None,
176+
):
177+
"""Initialises a :class:`RoleColours` object.
178+
179+
.. versionadded:: 2.7
180+
181+
Parameters
182+
----------
183+
primary: :class:`Colour`
184+
The primary colour of the role.
185+
secondary: Optional[:class:`Colour`]
186+
The secondary colour of the role.
187+
tertiary: Optional[:class:`Colour`]
188+
The tertiary colour of the role.
189+
"""
190+
self.primary: Colour = primary
191+
self.secondary: Colour | None = secondary
192+
self.tertiary: Colour | None = tertiary
193+
194+
@classmethod
195+
def _from_payload(cls, data: RoleColoursPayload) -> Self:
196+
primary = Colour(data["primary_color"])
197+
secondary = (
198+
Colour(data["secondary_color"]) if data.get("secondary_color") else None
199+
)
200+
tertiary = (
201+
Colour(data["tertiary_color"]) if data.get("tertiary_color") else None
202+
)
203+
return cls(primary, secondary, tertiary)
204+
205+
def _to_dict(self) -> RoleColoursPayload:
206+
"""Converts the role colours to a dictionary."""
207+
return {
208+
"primary_color": self.primary.value,
209+
"secondary_color": self.secondary.value if self.secondary else None,
210+
"tertiary_color": self.tertiary.value if self.tertiary else None,
211+
} # type: ignore
212+
213+
@classmethod
214+
def default(cls) -> RoleColours:
215+
"""Returns a default :class:`RoleColours` object with no colours set."""
216+
return cls(Colour.default(), None, None)
217+
218+
@classmethod
219+
def holographic(cls) -> RoleColours:
220+
"""Returns a :class:`RoleColours` that makes the role look holographic.
221+
222+
Currently holographic roles are only supported with colours 11127295, 16759788, and 16761760.
223+
"""
224+
return cls(Colour(11127295), Colour(16759788), Colour(16761760))
225+
226+
@property
227+
def is_holographic(self) -> bool:
228+
"""Whether the role is holographic.
229+
230+
Currently roles are holographic when colours are set to 11127295, 16759788, and 16761760.
231+
"""
232+
return (
233+
self.primary.value == 11127295
234+
and self.secondary.value == 16759788
235+
and self.tertiary.value == 16761760
236+
)
237+
238+
def __repr__(self) -> str:
239+
return (
240+
f"<RoleColours primary={self.primary!r} "
241+
f"secondary={self.secondary!r} "
242+
f"tertiary={self.tertiary!r}>"
243+
)
244+
245+
149246
class Role(Hashable):
150247
"""Represents a Discord role in a :class:`Guild`.
151248
@@ -224,13 +321,19 @@ class Role(Hashable):
224321
Extra attributes of the role.
225322
226323
.. versionadded:: 2.6
324+
325+
colours: :class:`RoleColours`
326+
The role's colours.
327+
328+
.. versionadded:: 2.7
227329
"""
228330

229331
__slots__ = (
230332
"id",
231333
"name",
232334
"_permissions",
233335
"_colour",
336+
"colours",
234337
"position",
235338
"managed",
236339
"mentionable",
@@ -296,6 +399,7 @@ def _update(self, data: RolePayload):
296399
self._permissions: int = int(data.get("permissions", 0))
297400
self.position: int = data.get("position", 0)
298401
self._colour: int = data.get("color", 0)
402+
self.colours: RoleColours | None = RoleColours._from_payload(data["colors"])
299403
self.hoist: bool = data.get("hoist", False)
300404
self.managed: bool = data.get("managed", False)
301405
self.mentionable: bool = data.get("mentionable", False)
@@ -367,14 +471,32 @@ def permissions(self) -> Permissions:
367471
return Permissions(self._permissions)
368472

369473
@property
474+
@deprecated("colours.primary", "2.7")
370475
def colour(self) -> Colour:
371-
"""Returns the role colour. An alias exists under ``color``."""
372-
return Colour(self._colour)
476+
"""Returns the role colour. Equivalent to :attr:`colours.primary`.
477+
An alias exists under ``color``.
478+
479+
.. versionchanged:: 2.7
480+
"""
481+
return self.colours.primary
373482

374483
@property
484+
@deprecated("colors.primary", "2.7")
375485
def color(self) -> Colour:
376-
"""Returns the role color. An alias exists under ``colour``."""
377-
return self.colour
486+
"""Returns the role's primary color. Equivalent to :attr:`colors.primary`.
487+
An alias exists under ``colour``.
488+
489+
.. versionchanged:: 2.7
490+
"""
491+
return self.colours.primary
492+
493+
@property
494+
def colors(self) -> RoleColours:
495+
"""Returns the role's colours. Equivalent to :attr:`colours`.
496+
497+
.. versionadded:: 2.7
498+
"""
499+
return self.colours
378500

379501
@property
380502
def created_at(self) -> datetime.datetime:
@@ -437,6 +559,9 @@ async def edit(
437559
permissions: Permissions | utils.Undefined = MISSING,
438560
colour: Colour | int | utils.Undefined = MISSING,
439561
color: Colour | int | utils.Undefined = MISSING,
562+
colours: RoleColours | None | utils.Undefined = MISSING,
563+
colors: RoleColours | None | utils.Undefined = MISSING,
564+
holographic: bool | utils.Undefined = MISSING,
440565
hoist: bool | utils.Undefined = MISSING,
441566
mentionable: bool | utils.Undefined = MISSING,
442567
position: int | utils.Undefined = MISSING,
@@ -508,8 +633,25 @@ async def edit(
508633
if color is not MISSING:
509634
colour = color
510635

636+
if colors is not MISSING:
637+
colours = colors
638+
511639
if colour is not MISSING:
512-
payload["color"] = colour if isinstance(colour, int) else colour.value
640+
warn_deprecated("colour", "colours", "2.7")
641+
if isinstance(colour, int):
642+
colour = Colour(colour)
643+
colours = RoleColours(primary=colour)
644+
if holographic:
645+
colours = RoleColours.holographic()
646+
if colours is not MISSING:
647+
if not isinstance(colours, RoleColours):
648+
raise InvalidArgument("colours must be a RoleColours object")
649+
if "ENHANCED_ROLE_COLORS" not in self.guild.features:
650+
colours.secondary = None
651+
colours.tertiary = None
652+
653+
payload["colors"] = colours._to_dict()
654+
513655
if name is not MISSING:
514656
payload["name"] = name
515657

discord/types/guild.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ class UnavailableGuild(TypedDict):
223223
"VOICE_CHANNEL_EFFECTS",
224224
"VOICE_IN_THREADS",
225225
"WELCOME_SCREEN_ENABLED",
226+
"ENHANCED_ROLE_COLORS",
226227
]
227228

228229

discord/types/role.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,18 @@
3030
from .snowflake import Snowflake
3131

3232

33+
class RoleColours(TypedDict):
34+
primary_color: int
35+
secondary_color: int | None
36+
tertiary_color: int | None
37+
38+
3339
class Role(TypedDict):
3440
tags: NotRequired[RoleTags]
3541
id: Snowflake
3642
name: str
3743
color: int
44+
colors: RoleColours
3845
hoist: bool
3946
position: int
4047
permissions: str

docs/api/models.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ Role
221221
.. autoclass:: RoleTags()
222222
:members:
223223

224+
.. attributetable:: RoleColours
225+
226+
.. autoclass:: RoleColours
227+
:members:
228+
224229
Scheduled Event
225230
~~~~~~~~~~~~~~~
226231

0 commit comments

Comments
 (0)