diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 43521b531c..1536b8abef 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -34,6 +34,7 @@ from hikari import audit_logs as audit_log_models from hikari import auto_mod as auto_mod_models from hikari import channels as channel_models + from hikari import colors as color_models from hikari import commands from hikari import embeds as embed_models from hikari import emojis as emoji_models @@ -125,6 +126,21 @@ class EntityFactory(abc.ABC): __slots__: typing.Sequence[str] = () + @abc.abstractmethod + def serialize_color_gradient(self, gradient: color_models.ColorGradient) -> data_binding.JSONObject: + """Serialize a color gradient into json. + + Parameters + ---------- + gradient + The color gradient to serialize. + + Returns + ------- + hikari.internal.data_binding.JSONObject + The serialized representation of the gradient object. + """ + ###################### # APPLICATION MODELS # ###################### diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 06d8e53c96..0b4bc108f7 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -38,7 +38,7 @@ from hikari import audit_logs from hikari import auto_mod from hikari import channels as channels_ - from hikari import colors + from hikari import colors as colors_ from hikari import commands from hikari import embeds as embeds_ from hikari import emojis @@ -6395,8 +6395,8 @@ async def create_role( *, name: undefined.UndefinedOr[str] = undefined.UNDEFINED, permissions: undefined.UndefinedOr[permissions_.Permissions] = permissions_.Permissions.NONE, - color: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, - colour: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, + color: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, + colour: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, hoist: undefined.UndefinedOr[bool] = undefined.UNDEFINED, icon: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED, unicode_emoji: undefined.UndefinedOr[str] = undefined.UNDEFINED, @@ -6501,8 +6501,8 @@ async def edit_role( *, name: undefined.UndefinedOr[str] = undefined.UNDEFINED, permissions: undefined.UndefinedOr[permissions_.Permissions] = undefined.UNDEFINED, - color: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, - colour: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, + color: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, + colour: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, hoist: undefined.UndefinedOr[bool] = undefined.UNDEFINED, icon: undefined.UndefinedNoneOr[files.Resourceish] = undefined.UNDEFINED, unicode_emoji: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED, diff --git a/hikari/colors.py b/hikari/colors.py index 4311ed6052..fd26be8379 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -22,14 +22,19 @@ from __future__ import annotations -__all__: typing.Sequence[str] = ("Color", "Colorish") +__all__: typing.Sequence[str] = ("Color", "ColorGradient", "Colorish") import re import string import typing +import attrs + from hikari.internal import typing_extensions +if typing.TYPE_CHECKING: + from typing_extensions import Self + def _to_rgb_int(value: str, name: str) -> int: # Heavy validation that is user-friendly and doesn't allow exploiting overflows, etc easily. @@ -585,3 +590,79 @@ def to_bytes( Web-safe colours are three hex-digits wide, `XYZ` becomes `XXYYZZ` in full-form. """ + + +@attrs.define(unsafe_hash=True, kw_only=True, weakref_slot=False) +class ColorGradient: + """Represents a role colors object.""" + + _primary_color: Colorish = attrs.field(hash=True, repr=True, alias="primary_color") + _secondary_color: Colorish | None = attrs.field(hash=True, repr=True, alias="secondary_color", default=None) + _tertiary_color: Colorish | None = attrs.field(hash=True, repr=True, alias="tertiary_color", default=None) + + @property + def primary_color(self) -> Color: + """The primary color of the role.""" + return Color.of(self._primary_color) + + def set_primary_color(self, primary_color: Colorish) -> Self: + """Set the primary color of the role. + + Parameters + ---------- + primary_color + The new color to set + + Returns + ------- + RoleColors + The role colors obj. + """ + self._primary_color = primary_color + return self + + @property + def secondary_color(self) -> Color | None: + """The secondary color of the role.""" + if self._secondary_color is None: + return None + return Color.of(self._secondary_color) + + def set_secondary_color(self, secondary_color: Colorish | None) -> Self: + """Set the secondary color of the role. + + Parameters + ---------- + secondary_color + The new color to set + + Returns + ------- + RoleColors + The role colors obj. + """ + self._secondary_color = secondary_color + return self + + @property + def tertiary_color(self) -> Color | None: + """The tertiary color of the role.""" + if self._secondary_color is None: + return None + return Color.of(self._secondary_color) + + def set_tertiary_color(self, tertiary_color: Colorish | None) -> Self: + """Set the secondary color of the role. + + Parameters + ---------- + tertiary_color + The new color to set + + Returns + ------- + RoleColors + The role colors obj. + """ + self._tertiary_color = tertiary_color + return self diff --git a/hikari/colours.py b/hikari/colours.py index 9931e6c382..13c1031f05 100644 --- a/hikari/colours.py +++ b/hikari/colours.py @@ -22,7 +22,7 @@ from __future__ import annotations -__all__: typing.Sequence[str] = ("Colour", "Colourish") +__all__: typing.Sequence[str] = ("Colour", "ColourGradient", "Colourish") import typing @@ -33,3 +33,6 @@ Colourish = colors.Colorish """An alias for [`hikari.colors.Colorish`][].""" + +ColourGradient = colors.ColorGradient +"""An alias for [`hikari.colors.ColorGradient`][].""" diff --git a/hikari/guilds.py b/hikari/guilds.py index 6dabad3cea..45b736e938 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -59,6 +59,7 @@ import attrs from hikari import channels as channels_ +from hikari import colors from hikari import snowflakes from hikari import stickers from hikari import traits @@ -75,7 +76,6 @@ if typing.TYPE_CHECKING: import datetime - from hikari import colors from hikari import colours from hikari import emojis as emojis_ from hikari import files @@ -191,6 +191,9 @@ class GuildFeature(str, enums.Enum): RAID_ALERTS_DISABLED = "RAID_ALERTS_DISABLED" """Guild has disabled alerts for join raids in the configured safety alerts channel.""" + ENHANCED_ROLE_COLORS = "ENHANCED_ROLE_COLORS" + """Guild is able to set gradient colors to roles.""" + @typing.final class GuildMessageNotificationsLevel(int, enums.Enum): @@ -1217,6 +1220,9 @@ class Role(PartialRole): This will be applied to a member's name in chat if it's their top coloured role. """ + colors: colors.ColorGradient = attrs.field(eq=False, hash=False, repr=False) + """The colors object of this role.""" + guild_id: snowflakes.Snowflake = attrs.field(eq=False, hash=False, repr=True) """The ID of the guild this role belongs to.""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index f6b9b47178..bd1ce9ec40 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -637,6 +637,14 @@ def app(self) -> traits.RESTAware: """Object of the application this entity factory is bound to.""" return self._app + @typing_extensions.override + def serialize_color_gradient(self, gradient: color_models.ColorGradient) -> data_binding.JSONObject: + return { + "primary_color": gradient.primary_color, + "secondary_color": gradient.secondary_color, + "tertiary_color": gradient.tertiary_color, + } + ###################### # APPLICATION MODELS # ###################### @@ -2100,6 +2108,18 @@ def deserialize_role( if "guild_connections" in tags_payload: is_guild_linked_role = True + colors_payload = payload["colors"] + primary_color = color_models.Color(colors_payload["primary_color"]) + secondary_color: color_models.Color | None = None + if (raw_secondary_color := colors_payload.get("secondary_color")) is not None: + secondary_color = color_models.Color(raw_secondary_color) + tertiary_color: color_models.Color | None = None + if (raw_tertiary_color := colors_payload.get("tertiary_color")) is not None: + tertiary_color = color_models.Color(raw_tertiary_color) + colors = color_models.ColorGradient( + primary_color=primary_color, secondary_color=secondary_color, tertiary_color=tertiary_color + ) + emoji: emoji_models.UnicodeEmoji | None = None if (raw_emoji := payload.get("unicode_emoji")) is not None: emoji = emoji_models.UnicodeEmoji(raw_emoji) @@ -2109,7 +2129,8 @@ def deserialize_role( id=snowflakes.Snowflake(payload["id"]), guild_id=guild_id, name=payload["name"], - color=color_models.Color(payload["color"]), + color=primary_color, + colors=colors, is_hoisted=payload["hoist"], icon_hash=payload.get("icon"), unicode_emoji=emoji, diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 569370cf3f..74e74c28ff 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -47,7 +47,7 @@ from hikari import _about as about from hikari import applications from hikari import channels as channels_ -from hikari import colors +from hikari import colors as colors_ from hikari import commands from hikari import components as components_ from hikari import embeds as embeds_ @@ -3813,8 +3813,8 @@ async def create_role( *, name: undefined.UndefinedOr[str] = undefined.UNDEFINED, permissions: undefined.UndefinedOr[permissions_.Permissions] = permissions_.Permissions.NONE, - color: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, - colour: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, + color: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, + colour: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, hoist: undefined.UndefinedOr[bool] = undefined.UNDEFINED, icon: undefined.UndefinedOr[files.Resourceish] = undefined.UNDEFINED, unicode_emoji: undefined.UndefinedOr[str] = undefined.UNDEFINED, @@ -3833,8 +3833,14 @@ async def create_role( body = data_binding.JSONObjectBuilder() body.put("name", name) body.put("permissions", permissions) - body.put("color", color, conversion=colors.Color.of) - body.put("color", colour, conversion=colors.Color.of) + if isinstance(color, colors_.ColorGradient): + body.put("colors", color, conversion=self._entity_factory.serialize_color_gradient) + else: + body.put("color", color, conversion=colors_.Color.of) + if isinstance(colour, colors_.ColorGradient): + body.put("colors", colour, conversion=self._entity_factory.serialize_color_gradient) + else: + body.put("color", colour, conversion=colors_.Color.of) body.put("hoist", hoist) body.put("unicode_emoji", unicode_emoji) body.put("mentionable", mentionable) @@ -3867,8 +3873,8 @@ async def edit_role( *, name: undefined.UndefinedOr[str] = undefined.UNDEFINED, permissions: undefined.UndefinedOr[permissions_.Permissions] = undefined.UNDEFINED, - color: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, - colour: undefined.UndefinedOr[colors.Colorish] = undefined.UNDEFINED, + color: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, + colour: undefined.UndefinedOr[colors_.Colorish | colors_.ColorGradient] = undefined.UNDEFINED, hoist: undefined.UndefinedOr[bool] = undefined.UNDEFINED, icon: undefined.UndefinedNoneOr[files.Resourceish] = undefined.UNDEFINED, unicode_emoji: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED, @@ -3888,8 +3894,14 @@ async def edit_role( body = data_binding.JSONObjectBuilder() body.put("name", name) body.put("permissions", permissions) - body.put("color", color, conversion=colors.Color.of) - body.put("color", colour, conversion=colors.Color.of) + if isinstance(color, colors_.ColorGradient): + body.put("colors", color, conversion=self._entity_factory.serialize_color_gradient) + else: + body.put("color", color, conversion=colors_.Color.of) + if isinstance(colour, colors_.ColorGradient): + body.put("colors", colour, conversion=self._entity_factory.serialize_color_gradient) + else: + body.put("color", colour, conversion=colors_.Color.of) body.put("hoist", hoist) body.put("unicode_emoji", unicode_emoji) body.put("mentionable", mentionable) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index ca634e1179..467c3a9925 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -308,6 +308,7 @@ def guild_role_payload(): "id": "41771983423143936", "name": "WE DEM BOYZZ!!!!!!", "color": 3_447_003, + "colors": {"primary_color": 3_447_003, "secondary_color": 3_447_003, "tertiary_color": None}, "hoist": True, "unicode_emoji": "\N{OK HAND SIGN}", "icon": "abc123hash", @@ -3443,6 +3444,11 @@ def test_deserialize_role(self, entity_factory_impl, mock_app, guild_role_payloa assert guild_role.unicode_emoji == emoji_models.UnicodeEmoji("\N{OK HAND SIGN}") assert isinstance(guild_role.unicode_emoji, emoji_models.UnicodeEmoji) assert guild_role.color == color_models.Color(3_447_003) + assert guild_role.colors == color_models.ColorGradient( + primary_color=color_models.Color(3_447_003), + secondary_color=color_models.Color(3_447_003), + tertiary_color=None, + ) assert guild_role.is_hoisted is True assert guild_role.position == 0 assert guild_role.permissions == permission_models.Permissions(66_321_471) diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 5db4f89f60..c13a8861e3 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -138,6 +138,9 @@ def model(self, mock_app): id=snowflakes.Snowflake(979899100), name="@everyone", color=colors.Color(0x1A2B3C), + colors=colors.ColorGradient( + primary_color=colors.Color(0x1A2B3C), secondary_color=colors.Color(0x1A2B3C), tertiary_color=None + ), guild_id=snowflakes.Snowflake(112233), is_hoisted=False, icon_hash="icon_hash", @@ -156,6 +159,9 @@ def model(self, mock_app): def test_colour_property(self, model): assert model.colour == colors.Color(0x1A2B3C) + assert model.colors == colors.ColorGradient( + primary_color=colors.Color(0x1A2B3C), secondary_color=colors.Color(0x1A2B3C), tertiary_color=None + ) def test_make_icon_url_format_set_to_deprecated_ext_argument_if_provided(self, model): with mock.patch.object(