Skip to content

feat: ✨ Add support for role gradient colors in Role #2818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 25 additions & 4 deletions discord/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -2895,6 +2896,7 @@ async def create_role(
name: str = ...,
permissions: Permissions = ...,
color: Colour | int = ...,
colors: RoleColours = ...,
hoist: bool = ...,
mentionable: bool = ...,
icon: bytes | None = ...,
Expand All @@ -2908,6 +2910,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,
Expand Down Expand Up @@ -2971,11 +2975,28 @@ async def create_role(
else:
fields["permissions"] = "0"

actual_colour = colour or color or Colour.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
Expand Down
1 change: 1 addition & 0 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -2159,6 +2159,7 @@ def edit_role(
"name",
"permissions",
"color",
"colors",
"hoist",
"mentionable",
"icon",
Expand Down
128 changes: 119 additions & 9 deletions discord/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,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
Expand All @@ -48,6 +47,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


Expand Down Expand Up @@ -149,6 +149,76 @@ 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)

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,
}

@classmethod
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"<RoleColours primary={self.primary!r} "
f"secondary={self.secondary!r} "
f"tertiary={self.tertiary!r}>"
)


class Role(Hashable):
"""Represents a Discord role in a :class:`Guild`.

Expand Down Expand Up @@ -227,13 +297,19 @@ class Role(Hashable):
Extra attributes of the role.

.. versionadded:: 2.6

colours: :class:`RoleColours`
The role's colours.

.. versionadded:: 2.7
"""

__slots__ = (
"id",
"name",
"_permissions",
"_colour",
"colours",
"position",
"managed",
"mentionable",
Expand Down Expand Up @@ -299,6 +375,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["colors"])
self.hoist: bool = data.get("hoist", False)
self.managed: bool = data.get("managed", False)
self.mentionable: bool = data.get("mentionable", False)
Expand Down Expand Up @@ -375,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``.

.. versionchanged:: 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:
Expand Down Expand Up @@ -452,6 +545,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,
Expand Down Expand Up @@ -523,8 +618,23 @@ 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 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:
payload["name"] = name

Expand Down
1 change: 1 addition & 0 deletions discord/types/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class UnavailableGuild(TypedDict):
"VERIFIED",
"VIP_REGIONS",
"WELCOME_SCREEN_ENABLED",
"ENHANCED_ROLE_COLORS",
]


Expand Down
7 changes: 7 additions & 0 deletions discord/types/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: RoleColours
hoist: bool
position: int
permissions: str
Expand Down
5 changes: 5 additions & 0 deletions docs/api/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ Role
.. autoclass:: RoleTags()
:members:

.. attributetable:: RoleColours

.. autoclass:: RoleColours
:members:

Scheduled Event
~~~~~~~~~~~~~~~

Expand Down
Loading