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 diff --git a/discord/guild.py b/discord/guild.py index 6a9d54537a..74692bee8e 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 @@ -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 = ..., @@ -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, @@ -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 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 d1fc2ae10d..ec1c7695d9 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 @@ -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 @@ -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 @@ -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"" + ) + + class Role(Hashable): """Represents a Discord role in a :class:`Guild`. @@ -227,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__ = ( @@ -234,6 +309,7 @@ class Role(Hashable): "name", "_permissions", "_colour", + "colours", "position", "managed", "mentionable", @@ -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) @@ -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: @@ -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, @@ -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 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..09e718e173 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: RoleColours hoist: bool position: int permissions: str 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 ~~~~~~~~~~~~~~~