diff --git a/CHANGELOG.md b/CHANGELOG.md index 700cb771d9..651cd525d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,8 @@ These changes are available on the `master` branch, but have not yet been releas ([#2775](https://github.com/Pycord-Development/pycord/pull/2775)) - 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 fd0e21e0ea..488f4aa074 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 @@ -2916,6 +2916,8 @@ async def create_role( name: str = ..., permissions: Permissions = ..., colour: Colour | int = ..., + colours: RoleColours = ..., + holographic: bool = ..., hoist: bool = ..., mentionable: bool = ..., icon: bytes | None = MISSING, @@ -2930,6 +2932,8 @@ async def create_role( name: str = ..., permissions: Permissions = ..., color: Colour | int = ..., + colors: RoleColours = ..., + holographic: bool = ..., hoist: bool = ..., mentionable: bool = ..., icon: bytes | None = ..., @@ -2943,6 +2947,9 @@ async def create_role( permissions: Permissions = MISSING, color: Colour | int = MISSING, colour: Colour | int = MISSING, + colors: RoleColours = MISSING, + colours: RoleColours = MISSING, + holographic: bool = MISSING, hoist: bool = MISSING, mentionable: bool = MISSING, reason: str | None = None, @@ -3006,11 +3013,30 @@ 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): + utils.warn_deprecated("colour", "colours", "2.7") + actual_colours = RoleColours(primary=actual_colour) + elif holographic: + actual_colours = RoleColours.holographic() + 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 a6eec1a82a..145a3c411b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2163,6 +2163,7 @@ def edit_role( "name", "permissions", "color", + "colors", "hoist", "mentionable", "icon", diff --git a/discord/role.py b/discord/role.py index d1fc2ae10d..60016f1aec 100644 --- a/discord/role.py +++ b/discord/role.py @@ -27,19 +27,25 @@ from typing import TYPE_CHECKING, Any, TypeVar +from typing_extensions import Self + from .asset import Asset from .colour import Colour from .errors import InvalidArgument 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 - -__all__ = ( - "RoleTags", - "Role", +from .utils import ( + MISSING, + _bytes_to_base64_data, + _get_as_snowflake, + deprecated, + snowflake_time, + warn_deprecated, ) +__all__ = ("RoleTags", "Role", "RoleColours") + if TYPE_CHECKING: import datetime @@ -48,6 +54,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 +156,96 @@ 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. At the moment, only `16761760` is allowed. + """ + + 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, + } # type: ignore + + @classmethod + 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"" + ) + + class Role(Hashable): """Represents a Discord role in a :class:`Guild`. @@ -227,6 +324,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 +336,7 @@ class Role(Hashable): "name", "_permissions", "_colour", + "colours", "position", "managed", "mentionable", @@ -299,6 +402,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) @@ -374,14 +478,32 @@ def permissions(self) -> Permissions: return Permissions(self._permissions) @property + @deprecated("colours.primary", "2.7") 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 + @deprecated("colors.primary", "2.7") 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 +574,9 @@ async def edit( permissions: Permissions = MISSING, colour: Colour | int = MISSING, color: Colour | int = MISSING, + colours: RoleColours | None = MISSING, + colors: RoleColours | None = MISSING, + holographic: bool = MISSING, hoist: bool = MISSING, mentionable: bool = MISSING, position: int = MISSING, @@ -523,8 +648,25 @@ 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 + warn_deprecated("colour", "colours", "2.7") + 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") + 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 f0fd5078ac..77ff8d0e0c 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -101,6 +101,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 ed8452ec1c..7bee315b9c 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 ~~~~~~~~~~~~~~~