From 6ae6032141a4d0bc5b753d3a95c617e3cebec0b2 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 14:07:59 +0200 Subject: [PATCH 01/12] :sparkles: Add support for enhanced role colors in `Role` --- discord/role.py | 54 ++++++++++++++++++++++++++++++++++++++++++ discord/types/guild.py | 1 + discord/types/role.py | 7 ++++++ 3 files changed, 62 insertions(+) diff --git a/discord/role.py b/discord/role.py index d1fc2ae10d..174e52808b 100644 --- a/discord/role.py +++ b/discord/role.py @@ -27,6 +27,8 @@ from typing import TYPE_CHECKING, Any, TypeVar +from typing_extensions import Self + from .asset import Asset from .colour import Colour from .errors import InvalidArgument @@ -48,6 +50,7 @@ from .state import ConnectionState from .types.guild import RolePositionUpdate from .types.role import Role as RolePayload + from .types.role import RoleColours as RoleColoursPayload from .types.role import RoleTags as RoleTagPayload @@ -149,6 +152,56 @@ def __repr__(self) -> str: R = TypeVar("R", bound="Role") +class RoleColours: + """Represents a role's gradient colours. + + .. versionadded:: 2.7 + + Attributes + ---------- + primary: :class:`Colour` + The primary colour of the role. + secondary: Optional[:class:`Colour`] + The secondary colour of the role. + tertiary: Optional[:class:`Colour`] + The tertiary colour of the role. + """ + + def __init__( + self, + primary: Colour, + secondary: Colour | None = None, + tertiary: Colour | None = None, + ): + """Initialises a :class:`RoleColours` object. + + .. versionadded:: 2.7 + + Parameters + ---------- + primary: :class:`Colour` + The primary colour of the role. + secondary: Optional[:class:`Colour`] + The secondary colour of the role. + tertiary: Optional[:class:`Colour`] + The tertiary colour of the role. + """ + self.primary: Colour = primary + self.secondary: Colour | None = secondary + self.tertiary: Colour | None = tertiary + + @classmethod + def _from_payload(cls, data: RoleColoursPayload) -> Self: + primary = Colour(data["primary_color"]) + secondary = ( + Colour(data["secondary_color"]) if data.get("secondary_color") else None + ) + tertiary = ( + Colour(data["tertiary_color"]) if data.get("tertiary_color") else None + ) + return cls(primary, secondary, tertiary) + + class Role(Hashable): """Represents a Discord role in a :class:`Guild`. @@ -299,6 +352,7 @@ def _update(self, data: RolePayload): self._permissions: int = int(data.get("permissions", 0)) self.position: int = data.get("position", 0) self._colour: int = data.get("color", 0) + self.colours: RoleColours | None = RoleColours._from_payload(data) self.hoist: bool = data.get("hoist", False) self.managed: bool = data.get("managed", False) self.mentionable: bool = data.get("mentionable", False) diff --git a/discord/types/guild.py b/discord/types/guild.py index 9ada5e194e..a563ea4019 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -98,6 +98,7 @@ class UnavailableGuild(TypedDict): "VERIFIED", "VIP_REGIONS", "WELCOME_SCREEN_ENABLED", + "ENHANCED_ROLE_COLORS", ] diff --git a/discord/types/role.py b/discord/types/role.py index c1354f1f0f..2bdba707b1 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -30,11 +30,18 @@ from .snowflake import Snowflake +class RoleColours(TypedDict): + primary_color: int + secondary_color: int | None + tertiary_color: int | None + + class Role(TypedDict): tags: NotRequired[RoleTags] id: Snowflake name: str color: int + colors: NotRequired[RoleColours] hoist: bool position: int permissions: str From 1266115db2b40e22e1e9c8eeec7164dfd31966fa Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 14:25:02 +0200 Subject: [PATCH 02/12] :sparkles: add RoleColours support to `Role.edit` --- discord/role.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/discord/role.py b/discord/role.py index 174e52808b..25352088f9 100644 --- a/discord/role.py +++ b/discord/role.py @@ -201,6 +201,14 @@ def _from_payload(cls, data: RoleColoursPayload) -> Self: ) return cls(primary, secondary, tertiary) + def _to_dict(self) -> RoleColoursPayload: + """Converts the role colours to a dictionary.""" + return { + "primary_color": self.primary.value, + "secondary_color": self.secondary.value if self.secondary else None, + "tertiary_color": self.tertiary.value if self.tertiary else None, + } + class Role(Hashable): """Represents a Discord role in a :class:`Guild`. @@ -506,6 +514,8 @@ async def edit( permissions: Permissions = MISSING, colour: Colour | int = MISSING, color: Colour | int = MISSING, + colours: RoleColours | None = MISSING, + colors: RoleColours | None = MISSING, hoist: bool = MISSING, mentionable: bool = MISSING, position: int = MISSING, @@ -577,8 +587,17 @@ async def edit( if color is not MISSING: colour = color + if colors is not MISSING: + colours = colors + if colour is not MISSING: payload["color"] = colour if isinstance(colour, int) else colour.value + + if colours is not MISSING: + if not isinstance(colours, RoleColours): + raise InvalidArgument("colours must be a RoleColours object") + payload["colors"] = colours._to_dict() + if name is not MISSING: payload["name"] = name From 2e67e40696b59e7598619471a79078d78677c972 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 14:28:57 +0200 Subject: [PATCH 03/12] :sparkles: add RoleColours support to role creation in `Guild` and define default colors in `RoleColours` --- discord/guild.py | 6 +++++- discord/role.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 6a9d54537a..68132cfce5 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -79,7 +79,7 @@ from .monetization import Entitlement from .onboarding import Onboarding from .permissions import PermissionOverwrite -from .role import Role +from .role import Role, RoleColours from .scheduled_events import ScheduledEvent, ScheduledEventLocation from .stage_instance import StageInstance from .sticker import GuildSticker @@ -2908,6 +2908,8 @@ async def create_role( permissions: Permissions = MISSING, color: Colour | int = MISSING, colour: Colour | int = MISSING, + colors: RoleColours = MISSING, + colours: RoleColours = MISSING, hoist: bool = MISSING, mentionable: bool = MISSING, reason: str | None = None, @@ -2972,6 +2974,8 @@ async def create_role( fields["permissions"] = "0" actual_colour = colour or color or Colour.default() + colours or colors or RoleColours.default() + if isinstance(actual_colour, int): fields["color"] = actual_colour else: diff --git a/discord/role.py b/discord/role.py index 25352088f9..67682ff9b0 100644 --- a/discord/role.py +++ b/discord/role.py @@ -209,6 +209,11 @@ def _to_dict(self) -> RoleColoursPayload: "tertiary_color": self.tertiary.value if self.tertiary else None, } + @classmethod + def default(cls) -> RoleColours: + """Returns a default :class:`RoleColours` object with no colours set.""" + return cls(Colour.default(), None, None) + class Role(Hashable): """Represents a Discord role in a :class:`Guild`. From a7bec525a229d251bd88c19b465bc6ddfc9c4b0e Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 15:18:17 +0200 Subject: [PATCH 04/12] :sparkles: add support for RoleColours in role creation and editing --- discord/guild.py | 25 +++++++++++++++++++++---- discord/http.py | 1 + discord/role.py | 23 +++++++++++++++++------ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 68132cfce5..74692bee8e 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2881,6 +2881,7 @@ async def create_role( name: str = ..., permissions: Permissions = ..., colour: Colour | int = ..., + colours: RoleColours = ..., hoist: bool = ..., mentionable: bool = ..., icon: bytes | None = MISSING, @@ -2895,6 +2896,7 @@ async def create_role( name: str = ..., permissions: Permissions = ..., color: Colour | int = ..., + colors: RoleColours = ..., hoist: bool = ..., mentionable: bool = ..., icon: bytes | None = ..., @@ -2973,13 +2975,28 @@ async def create_role( else: fields["permissions"] = "0" - actual_colour = colour or color or Colour.default() - colours or colors or RoleColours.default() + actual_colour = colour if colour not in (MISSING, None) else color if isinstance(actual_colour, int): - fields["color"] = actual_colour + actual_colour = Colour(actual_colour) + + if actual_colour not in (MISSING, None): + actual_colours = RoleColours(primary=actual_colour) + else: + actual_colours = colours or colors or RoleColours.default() + + if isinstance(actual_colours, RoleColours): + if "ENHANCED_ROLE_COLORS" not in self.features: + actual_colours.secondary = None + actual_colours.tertiary = None + fields["colors"] = actual_colours._to_dict() + else: - fields["color"] = actual_colour.value + raise InvalidArgument( + "colours parameter must be of type RoleColours, not {0.__class__.__name__}".format( + actual_colours + ) + ) if hoist is not MISSING: fields["hoist"] = hoist diff --git a/discord/http.py b/discord/http.py index 2db704b268..45b101290d 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2159,6 +2159,7 @@ def edit_role( "name", "permissions", "color", + "colors", "hoist", "mentionable", "icon", diff --git a/discord/role.py b/discord/role.py index 67682ff9b0..90e2655183 100644 --- a/discord/role.py +++ b/discord/role.py @@ -37,10 +37,7 @@ from .permissions import Permissions from .utils import MISSING, _bytes_to_base64_data, _get_as_snowflake, snowflake_time -__all__ = ( - "RoleTags", - "Role", -) +__all__ = ("RoleTags", "Role", "RoleColours") if TYPE_CHECKING: import datetime @@ -214,6 +211,13 @@ def default(cls) -> RoleColours: """Returns a default :class:`RoleColours` object with no colours set.""" return cls(Colour.default(), None, None) + def __repr__(self) -> str: + return ( + f"" + ) + class Role(Hashable): """Represents a Discord role in a :class:`Guild`. @@ -300,6 +304,7 @@ class Role(Hashable): "name", "_permissions", "_colour", + "colours", "position", "managed", "mentionable", @@ -365,7 +370,7 @@ def _update(self, data: RolePayload): self._permissions: int = int(data.get("permissions", 0)) self.position: int = data.get("position", 0) self._colour: int = data.get("color", 0) - self.colours: RoleColours | None = RoleColours._from_payload(data) + self.colours: RoleColours | None = RoleColours._from_payload(data["colors"]) self.hoist: bool = data.get("hoist", False) self.managed: bool = data.get("managed", False) self.mentionable: bool = data.get("mentionable", False) @@ -596,11 +601,17 @@ async def edit( colours = colors if colour is not MISSING: - payload["color"] = colour if isinstance(colour, int) else colour.value + if isinstance(colour, int): + colour = Colour(colour) + colours = RoleColours(primary=colour) if colours is not MISSING: if not isinstance(colours, RoleColours): raise InvalidArgument("colours must be a RoleColours object") + if "ENHANCED_ROLE_COLORS" not in self.guild.features: + colours.secondary = None + colours.tertiary = None + payload["colors"] = colours._to_dict() if name is not MISSING: From 7abf3ad321679dde251eb291fc50e617515383cf Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 15:22:52 +0200 Subject: [PATCH 05/12] :sparkles: update role attributes to use RoleColours and enhance color properties --- discord/role.py | 29 +++++++++++++++++++++++++---- discord/types/role.py | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/discord/role.py b/discord/role.py index 90e2655183..6fb9e62edd 100644 --- a/discord/role.py +++ b/discord/role.py @@ -297,6 +297,11 @@ class Role(Hashable): Extra attributes of the role. .. versionadded:: 2.6 + + colours: :class:`RoleColours` + The role's colours. + + .. versionadded:: 2.7 """ __slots__ = ( @@ -447,13 +452,29 @@ def permissions(self) -> Permissions: @property def colour(self) -> Colour: - """Returns the role colour. An alias exists under ``color``.""" - return Colour(self._colour) + """Returns the role colour. Equivalent to :attr:`colours.primary`. + An alias exists under ``color``. + + .. versionchanged:: 2.7 + """ + return self.colours.primary @property def color(self) -> Colour: - """Returns the role color. An alias exists under ``colour``.""" - return self.colour + """Returns the role's primary color. Equivalent to :attr:`colors.primary`. + An alias exists under ``colour``. + + .. versionadded:: 2.7 + """ + return self.colours.primary + + @property + def colors(self) -> RoleColours: + """Returns the role's colours. Equivalent to :attr:`colours`. + + .. versionadded:: 2.7 + """ + return self.colours @property def created_at(self) -> datetime.datetime: diff --git a/discord/types/role.py b/discord/types/role.py index 2bdba707b1..09e718e173 100644 --- a/discord/types/role.py +++ b/discord/types/role.py @@ -41,7 +41,7 @@ class Role(TypedDict): id: Snowflake name: str color: int - colors: NotRequired[RoleColours] + colors: RoleColours hoist: bool position: int permissions: str From 5bb6951b0053c47a4baae05ebef5da0ab9f8dcaa Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 15:25:05 +0200 Subject: [PATCH 06/12] :memo: CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdfdc6d896..75cdd24132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2747](https://github.com/Pycord-Development/pycord/pull/2747)) - Added `discord.Interaction.created_at`. ([#2801](https://github.com/Pycord-Development/pycord/pull/2801)) +- Added role gradients support with `Role.colours` and the `RoleColours` class. + ([#2818](https://github.com/Pycord-Development/pycord/pull/2818)) ### Fixed From 8fcee291f54e791c06d3203e0ff1cacf9444c370 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 15:26:07 +0200 Subject: [PATCH 07/12] :memo: add documentation for RoleColours and its attributes --- docs/api/models.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api/models.rst b/docs/api/models.rst index cb702b2c38..8353d92794 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -221,6 +221,11 @@ Role .. autoclass:: RoleTags() :members: +.. attributetable:: RoleColours + +.. autoclass:: RoleColours + :members: + Scheduled Event ~~~~~~~~~~~~~~~ From c02877534fca438b6bd84e277d613331630aee27 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 22 Jun 2025 15:30:09 +0200 Subject: [PATCH 08/12] :pencil2: fix version annotation for primary color method in role.py --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 6fb9e62edd..ec1c7695d9 100644 --- a/discord/role.py +++ b/discord/role.py @@ -464,7 +464,7 @@ def color(self) -> Colour: """Returns the role's primary color. Equivalent to :attr:`colors.primary`. An alias exists under ``colour``. - .. versionadded:: 2.7 + .. versionchanged:: 2.7 """ return self.colours.primary From 3aa69d4fc7999a01658a6e53f12c4782c1d846cf Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 8 Jul 2025 17:08:21 +0200 Subject: [PATCH 09/12] :sparkles: add holographic role support in RoleColours --- discord/guild.py | 4 +++- discord/role.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 74692bee8e..3e6c27dc76 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2882,6 +2882,7 @@ async def create_role( permissions: Permissions = ..., colour: Colour | int = ..., colours: RoleColours = ..., + holographic: bool = ..., hoist: bool = ..., mentionable: bool = ..., icon: bytes | None = MISSING, @@ -2897,6 +2898,7 @@ async def create_role( permissions: Permissions = ..., color: Colour | int = ..., colors: RoleColours = ..., + holographic: bool = ..., hoist: bool = ..., mentionable: bool = ..., icon: bytes | None = ..., @@ -2912,6 +2914,7 @@ async def create_role( colour: Colour | int = MISSING, colors: RoleColours = MISSING, colours: RoleColours = MISSING, + holographic: bool = MISSING, hoist: bool = MISSING, mentionable: bool = MISSING, reason: str | None = None, @@ -2990,7 +2993,6 @@ async def create_role( actual_colours.secondary = None actual_colours.tertiary = None fields["colors"] = actual_colours._to_dict() - else: raise InvalidArgument( "colours parameter must be of type RoleColours, not {0.__class__.__name__}".format( diff --git a/discord/role.py b/discord/role.py index ec1c7695d9..068e0f7d7a 100644 --- a/discord/role.py +++ b/discord/role.py @@ -211,6 +211,26 @@ def default(cls) -> RoleColours: """Returns a default :class:`RoleColours` object with no colours set.""" return cls(Colour.default(), None, None) + @classmethod + def holographic(cls) -> RoleColours: + """Returns a :class:`RoleColours` that makes the role look holographic. + + Currently holographic roles are only supported with colours 11127295, 16759788, and 16761760. + """ + return cls(Colour(11127295), Colour(16759788), Colour(16761760)) + + @property + def is_holographic(self) -> bool: + """Whether the role is holographic. + + Currently roles are holographic when colours are set to 11127295, 16759788, and 16761760. + """ + return ( + self.primary.value == 11127295 + and self.secondary.value == 16759788 + and self.tertiary.value == 16761760 + ) + def __repr__(self) -> str: return ( f" Date: Tue, 8 Jul 2025 18:07:49 +0200 Subject: [PATCH 10/12] :memo: update tertiary color documentation to specify allowed value --- discord/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 068e0f7d7a..849e377115 100644 --- a/discord/role.py +++ b/discord/role.py @@ -161,7 +161,7 @@ class RoleColours: secondary: Optional[:class:`Colour`] The secondary colour of the role. tertiary: Optional[:class:`Colour`] - The tertiary colour of the role. + The tertiary colour of the role. At the moment, only `16761760` is allowed. """ def __init__( From 81d6542caa792f7e33cf3919c2211d3128880649 Mon Sep 17 00:00:00 2001 From: Paillat Date: Tue, 8 Jul 2025 18:36:47 +0200 Subject: [PATCH 11/12] :sparkles: Finish implementing holographic support --- discord/guild.py | 2 ++ discord/role.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 3e6c27dc76..50d71e97e7 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2985,6 +2985,8 @@ async def create_role( if actual_colour not in (MISSING, None): actual_colours = RoleColours(primary=actual_colour) + elif holographic: + actual_colours = RoleColours.holographic() else: actual_colours = colours or colors or RoleColours.default() diff --git a/discord/role.py b/discord/role.py index 849e377115..d4b7b9d647 100644 --- a/discord/role.py +++ b/discord/role.py @@ -567,6 +567,7 @@ async def edit( color: Colour | int = MISSING, colours: RoleColours | None = MISSING, colors: RoleColours | None = MISSING, + holographic: bool = MISSING, hoist: bool = MISSING, mentionable: bool = MISSING, position: int = MISSING, @@ -645,7 +646,8 @@ async def edit( if isinstance(colour, int): colour = Colour(colour) colours = RoleColours(primary=colour) - + if holographic: + colours = RoleColours.holographic() if colours is not MISSING: if not isinstance(colours, RoleColours): raise InvalidArgument("colours must be a RoleColours object") From d60b2befca7c6375ff1a3bdf974b83763f21f6e3 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 2 Aug 2025 18:04:44 +0200 Subject: [PATCH 12/12] :wastebasket: Add deprecation warnings for singular colour properties in Role and Guild --- discord/guild.py | 1 + discord/role.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 50d71e97e7..01467838e5 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -2984,6 +2984,7 @@ async def create_role( actual_colour = Colour(actual_colour) if actual_colour not in (MISSING, None): + utils.warn_deprecated("colour", "colours", "2.7") actual_colours = RoleColours(primary=actual_colour) elif holographic: actual_colours = RoleColours.holographic() diff --git a/discord/role.py b/discord/role.py index d4b7b9d647..60016f1aec 100644 --- a/discord/role.py +++ b/discord/role.py @@ -35,7 +35,14 @@ 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, + deprecated, + snowflake_time, + warn_deprecated, +) __all__ = ("RoleTags", "Role", "RoleColours") @@ -204,7 +211,7 @@ def _to_dict(self) -> RoleColoursPayload: "primary_color": self.primary.value, "secondary_color": self.secondary.value if self.secondary else None, "tertiary_color": self.tertiary.value if self.tertiary else None, - } + } # type: ignore @classmethod def default(cls) -> RoleColours: @@ -471,6 +478,7 @@ def permissions(self) -> Permissions: return Permissions(self._permissions) @property + @deprecated("colours.primary", "2.7") def colour(self) -> Colour: """Returns the role colour. Equivalent to :attr:`colours.primary`. An alias exists under ``color``. @@ -480,6 +488,7 @@ def colour(self) -> Colour: return self.colours.primary @property + @deprecated("colors.primary", "2.7") def color(self) -> Colour: """Returns the role's primary color. Equivalent to :attr:`colors.primary`. An alias exists under ``colour``. @@ -643,6 +652,7 @@ async def edit( colours = colors if colour is not MISSING: + warn_deprecated("colour", "colours", "2.7") if isinstance(colour, int): colour = Colour(colour) colours = RoleColours(primary=colour)