Repository: hikari-py/hikari
Pull request #2295 (2025-04-05 04:44:39)

Implement avatar decorations.
### Summary
Adds support for global and guild specific avatar decorations per [resources/user#user-object](https://discord.com/developers/docs/resources/user#user-object), [resources/guild#guild-member-object](https://discord.com/developers/docs/resources/guild#guild-member-object), and [resources/user#avatar-decoration-data-object](https://discord.com/developers/docs/resources/user#avatar-decoration-data-object).

In testing the avatar decoration payload seems to always have an `expires_at` key which is a unix timestamp, although this is not documented.

### Checklist
- [✓] I have run `nox` and all the pipelines have passed.
- [✓] I have made unittests according to the code I have added/modified/deleted.


Patch:
diff --git a/changes/2295.feature.md b/changes/2295.feature.md
new file mode 100644
index 00000000..b8e2fdeb
--- /dev/null
+++ b/changes/2295.feature.md
@@ -0,0 +1,3 @@
+Added support for global and guild specific avatar decorations.
+  - New `AvatarDecoration` class.
+  - New optional `avatar_decoration`, `display_avatar_decoration`, and `guild_avatar_decoration` attributes on `User` and `Member` as applicable.
diff --git a/hikari/applications.py b/hikari/applications.py
index 6c586b1a..8eae05fe 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -380,6 +380,11 @@ def app(self) -> traits.RESTAware:
         """Return the app that is bound to the user object."""
         return self.user.app
 
+    @property
+    @typing_extensions.override
+    def avatar_decoration(self) -> users.AvatarDecoration | None:
+        return self.user.avatar_decoration
+
     @property
     @typing_extensions.override
     def avatar_hash(self) -> str | None:
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 174b28c7..3f74af0b 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -461,18 +461,25 @@ class Member(users.User):
     user: users.User = attrs.field(repr=True)
     """This member's corresponding user object."""
 
+    guild_avatar_decoration: users.AvatarDecoration | None = attrs.field(eq=False, hash=False, repr=False)
+    """Hash of the member's guild avatar decoration if set, else [`None`][].
+
+    !!! note
+        This takes precedence over [`hikari.guilds.Member.avatar_decoration`][] in the client.
+    """
+
     guild_avatar_hash: str | None = attrs.field(eq=False, hash=False, repr=False)
     """Hash of the member's guild avatar if set, else [`None`][].
 
     !!! note
-        This takes precedence over [`hikari.guilds.Member.avatar_hash`][].
+        This takes precedence over [`hikari.guilds.Member.avatar_hash`][] in the client.
     """
 
     guild_banner_hash: str | None = attrs.field(eq=False, hash=False, repr=False)
     """Hash of the member's guild banner if set, else [`None`][].
 
     !!! note
-        This takes precedence over [`hikari.guilds.Member.banner_hash`][].
+        This takes precedence over [`hikari.guilds.Member.banner_hash`][] in the client.
     """
 
     guild_flags: GuildMemberFlags | int = attrs.field(eq=False, hash=False, repr=False)
@@ -484,6 +491,11 @@ def app(self) -> traits.RESTAware:
         """Return the app that is bound to the user object."""
         return self.user.app
 
+    @property
+    @typing_extensions.override
+    def avatar_decoration(self) -> users.AvatarDecoration | None:
+        return self.user.avatar_decoration
+
     @property
     @typing_extensions.override
     def avatar_hash(self) -> str | None:
@@ -508,6 +520,11 @@ def guild_avatar_url(self) -> files.URL | None:
     def default_avatar_url(self) -> files.URL:
         return self.user.default_avatar_url
 
+    @property
+    @typing_extensions.override
+    def display_avatar_decoration(self) -> users.AvatarDecoration | None:
+        return self.guild_avatar_decoration or super().display_avatar_decoration
+
     @property
     @typing_extensions.override
     def display_avatar_url(self) -> files.URL:
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 8034a9d2..536c9df7 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -233,6 +233,7 @@ class _UserFields:
     discriminator: str = attrs.field()
     username: str = attrs.field()
     global_name: str | None = attrs.field()
+    avatar_decoration: user_models.AvatarDecoration | None = attrs.field()
     avatar_hash: str = attrs.field()
     banner_hash: str | None = attrs.field()
     accent_color: color_models.Color | None = attrs.field()
@@ -1911,12 +1912,15 @@ def deserialize_member(
         if raw_communication_disabled_until := payload.get("communication_disabled_until"):
             communication_disabled_until = time.iso8601_datetime_string_to_datetime(raw_communication_disabled_until)
 
+        avatar_decoration = self._deserialize_avatar_decoration(payload.get("avatar_decoration_data"))
+
         return guild_models.Member(
             user=user,
             guild_id=guild_id,
             role_ids=role_ids,
             joined_at=joined_at,
             nickname=payload.get("nick"),
+            guild_avatar_decoration=avatar_decoration,
             guild_avatar_hash=payload.get("avatar"),
             guild_banner_hash=payload.get("banner"),
             premium_since=premium_since,
@@ -2592,6 +2596,8 @@ def _deserialize_interaction_member(
 
         guild_flags = guild_models.GuildMemberFlags(payload.get("flags") or guild_models.GuildMemberFlags.NONE)
 
+        avatar_decoration = self._deserialize_avatar_decoration(payload.get("avatar_decoration_data"))
+
         # TODO: deduplicate member unmarshalling logic
         return base_interactions.InteractionMember(
             user=user,
@@ -2599,6 +2605,7 @@ def _deserialize_interaction_member(
             role_ids=role_ids,
             joined_at=time.iso8601_datetime_string_to_datetime(payload["joined_at"]),
             premium_since=premium_since,
+            guild_avatar_decoration=avatar_decoration,
             guild_avatar_hash=payload.get("avatar"),
             guild_banner_hash=payload.get("banner"),
             nickname=payload.get("nick"),
@@ -3826,14 +3833,28 @@ def deserialize_template(self, payload: data_binding.JSONObject) -> template_mod
     # USER MODELS #
     ###############
 
-    @staticmethod
-    def _set_user_attributes(payload: data_binding.JSONObject) -> _UserFields:
+    def _deserialize_avatar_decoration(
+        self, payload: data_binding.JSONObject | None
+    ) -> user_models.AvatarDecoration | None:
+        if not payload:
+            return None
+
+        expires_at = (
+            time.unix_epoch_to_datetime(payload["expires_at"], is_millis=False) if payload.get("expires_at") else None
+        )
+        return user_models.AvatarDecoration(
+            asset_hash=payload["asset"], sku_id=snowflakes.Snowflake(payload["sku_id"]), expires_at=expires_at
+        )
+
+    def _set_user_attributes(self, payload: data_binding.JSONObject) -> _UserFields:
         accent_color = payload.get("accent_color")
+        avatar_decoration = self._deserialize_avatar_decoration(payload.get("avatar_decoration_data"))
         return _UserFields(
             id=snowflakes.Snowflake(payload["id"]),
             discriminator=payload["discriminator"],
             username=payload["username"],
             global_name=payload.get("global_name"),
+            avatar_decoration=avatar_decoration,
             avatar_hash=payload["avatar"],
             banner_hash=payload.get("banner", None),
             accent_color=color_models.Color(accent_color) if accent_color is not None else None,
@@ -3853,6 +3874,7 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User
             discriminator=user_fields.discriminator,
             username=user_fields.username,
             global_name=payload.get("global_name"),
+            avatar_decoration=user_fields.avatar_decoration,
             avatar_hash=user_fields.avatar_hash,
             banner_hash=user_fields.banner_hash,
             accent_color=user_fields.accent_color,
@@ -3870,6 +3892,7 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.O
             discriminator=user_fields.discriminator,
             username=user_fields.username,
             global_name=payload.get("global_name"),
+            avatar_decoration=user_fields.avatar_decoration,
             avatar_hash=user_fields.avatar_hash,
             banner_hash=user_fields.banner_hash,
             accent_color=user_fields.accent_color,
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index c45a5d6f..bfea359b 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -508,6 +508,7 @@ def deserialize_presence_update_event(
                 discriminator=discriminator,
                 username=user_payload.get("username", undefined.UNDEFINED),
                 global_name=user_payload.get("global_name", undefined.UNDEFINED),
+                avatar_decoration=None,
                 avatar_hash=user_payload.get("avatar", undefined.UNDEFINED),
                 banner_hash=user_payload.get("banner", undefined.UNDEFINED),
                 accent_color=accent_color,
diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py
index 66205dc5..9b73d63b 100644
--- a/hikari/internal/cache.py
+++ b/hikari/internal/cache.py
@@ -413,6 +413,7 @@ class MemberData(BaseData[guilds.Member]):
     user: RefCell[users_.User] = attrs.field()
     guild_id: snowflakes.Snowflake = attrs.field()
     nickname: str | None = attrs.field()
+    guild_avatar_decoration: users_.AvatarDecoration | None = attrs.field()
     guild_avatar_hash: str | None = attrs.field()
     guild_banner_hash: str | None = attrs.field()
     role_ids: tuple[snowflakes.Snowflake, ...] = attrs.field()
@@ -434,6 +435,7 @@ def build_from_entity(cls, member: guilds.Member, /, *, user: RefCell[users_.Use
             nickname=member.nickname,
             joined_at=member.joined_at,
             premium_since=member.premium_since,
+            guild_avatar_decoration=member.guild_avatar_decoration,
             guild_avatar_hash=member.guild_avatar_hash,
             guild_banner_hash=member.guild_banner_hash,
             is_deaf=member.is_deaf,
@@ -454,6 +456,7 @@ def build_entity(self, _: traits.RESTAware, /) -> guilds.Member:
             nickname=self.nickname,
             role_ids=self.role_ids,
             joined_at=self.joined_at,
+            guild_avatar_decoration=self.guild_avatar_decoration,
             guild_avatar_hash=self.guild_avatar_hash,
             guild_banner_hash=self.guild_banner_hash,
             premium_since=self.premium_since,
diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py
index 5ad9cc96..6503f4d9 100644
--- a/hikari/internal/routes.py
+++ b/hikari/internal/routes.py
@@ -607,6 +607,7 @@ def compile_to_file(
 )
 CDN_GUILD_BANNER: typing.Final[CDNRoute] = CDNRoute("/banners/{guild_id}/{hash}", {PNG, *JPEG_JPG, WEBP, GIF})
 
+CDN_AVATAR_DECORATION: typing.Final[CDNRoute] = CDNRoute("/avatar-decoration-presets/{hash}", {PNG})
 CDN_DEFAULT_USER_AVATAR: typing.Final[CDNRoute] = CDNRoute("/embed/avatars/{style}", {PNG}, is_sizable=False)
 CDN_USER_AVATAR: typing.Final[CDNRoute] = CDNRoute("/avatars/{user_id}/{hash}", {PNG, *JPEG_JPG, WEBP, GIF})
 CDN_USER_BANNER: typing.Final[CDNRoute] = CDNRoute("/banners/{user_id}/{hash}", {PNG, *JPEG_JPG, WEBP, GIF})
diff --git a/hikari/users.py b/hikari/users.py
index 176fbb92..7f16c14b 100644
--- a/hikari/users.py
+++ b/hikari/users.py
@@ -42,6 +42,8 @@
     from hikari.internal import typing_extensions
 
 if typing.TYPE_CHECKING:
+    import datetime
+
     import typing_extensions  # noqa: TC004
 
     from hikari import channels
@@ -127,6 +129,48 @@ class PremiumType(int, enums.Enum):
     """Premium tier including basic perks (e.g. animated emojis and avatars)."""
 
 
+@attrs.define(kw_only=True, weakref_slot=False)
+class AvatarDecoration:
+    """Data for an avatar decoration."""
+
+    asset_hash: str = attrs.field(repr=True)
+    """The hash of the asset."""
+
+    sku_id: snowflakes.Snowflake = attrs.field(repr=True)
+    """The ID of the asset's SKU."""
+
+    expires_at: datetime.datetime | None = attrs.field(repr=True)
+    """The datetime at which the user will no longer have access to the avatar decoration."""
+
+    @property
+    def url(self) -> files.URL:
+        """Return the URL for this avatar decoration."""
+        return self.make_url()
+
+    def make_url(self, size: int = 4096) -> files.URL:
+        """Generate the url for this avatar decoration.
+
+        Parameters
+        ----------
+        size
+            The size to set for the URL.
+            Can be any power of two between `16` and `4096`.
+
+        Returns
+        -------
+        hikari.files.URL
+            The URL to the avatar decoration.
+
+        Raises
+        ------
+        ValueError
+            If `size` is not a power of two or not between 16 and 4096.
+        """
+        return routes.CDN_AVATAR_DECORATION.compile_to_file(
+            urls.CDN_URL, hash=self.asset_hash, size=size, file_format="png"
+        )
+
+
 class PartialUser(snowflakes.Unique, abc.ABC):
     """A partial interface for a user.
 
@@ -147,6 +191,11 @@ class PartialUser(snowflakes.Unique, abc.ABC):
     def app(self) -> traits.RESTAware:
         """Client application that models may use for procedures."""
 
+    @property
+    @abc.abstractmethod
+    def avatar_decoration(self) -> undefined.UndefinedNoneOr[AvatarDecoration]:
+        """Avatar decoration for the user, if they have one, otherwise [`None`][]."""
+
     @property
     @abc.abstractmethod
     def avatar_hash(self) -> undefined.UndefinedNoneOr[str]:
@@ -478,6 +527,12 @@ def accent_colour(self) -> colors.Color | None:
         """Alias for the [`hikari.users.User.accent_color`][] field."""
         return self.accent_color
 
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def avatar_decoration(self) -> AvatarDecoration | None:
+        """Avatar decoration for the user, if they have one, otherwise [`None`][]."""
+
     @property
     @abc.abstractmethod
     @typing_extensions.override
@@ -488,7 +543,7 @@ def avatar_hash(self) -> str | None:
     def avatar_url(self) -> files.URL | None:
         """Avatar URL for the user, if they have one set.
 
-        May be [`None`][] if no custom avatar is set. In this case, you
+        Will be [`None`][] if no custom avatar is set. In this case, you
         should use [`hikari.User.default_avatar_url`][] instead.
         """
         return self.make_avatar_url()
@@ -503,7 +558,7 @@ def banner_hash(self) -> str | None:
     def banner_url(self) -> files.URL | None:
         """Banner URL for the user, if they have one set.
 
-        May be [`None`][] if no custom banner is set.
+        Will be [`None`][] if no custom banner is set.
         """
         return self.make_banner_url()
 
@@ -519,6 +574,14 @@ def default_avatar_url(self) -> files.URL:
             urls.CDN_URL, style=int(self.discriminator) % 5, file_format="png"
         )
 
+    @property
+    def display_avatar_decoration(self) -> AvatarDecoration | None:
+        """Display avatar decoration for the user, if they have one set.
+
+        Will be [`None`][] if no avatar decoration is set.
+        """
+        return self.avatar_decoration
+
     @property
     def display_avatar_url(self) -> files.URL:
         """Display avatar URL for this user."""
@@ -526,7 +589,10 @@ def display_avatar_url(self) -> files.URL:
 
     @property
     def display_banner_url(self) -> files.URL | None:
-        """Display banner URL for this user."""
+        """Display banner URL for this user, if they have one set.
+
+        Will be [`None`][] if no custom banner is set.
+        """
         return self.make_banner_url()
 
     @property
@@ -698,6 +764,9 @@ class PartialUserImpl(PartialUser):
     global_name: undefined.UndefinedNoneOr[str] = attrs.field(eq=False, hash=False, repr=True)
     """Global name of the user."""
 
+    avatar_decoration: undefined.UndefinedNoneOr[AvatarDecoration] = attrs.field(eq=False, hash=False, repr=False)
+    """Avatar decoration of the user, if an avatar decoration is set."""
+
     avatar_hash: undefined.UndefinedNoneOr[str] = attrs.field(eq=False, hash=False, repr=False)
     """Avatar hash of the user, if a custom avatar is set."""
 
@@ -761,6 +830,9 @@ class UserImpl(PartialUserImpl, User):
     global_name: str | None = attrs.field(eq=False, hash=False, repr=True)
     """The user's global name."""
 
+    avatar_decoration: AvatarDecoration | None = attrs.field(eq=False, hash=False, repr=False)
+    """Avatar decoration of the user, if they have one."""
+
     avatar_hash: str | None = attrs.field(eq=False, hash=False, repr=False)
     """The user's avatar hash, if they have one, otherwise [`None`][]."""
 
diff --git a/pyproject.toml b/pyproject.toml
index f03760df..4b5fad79 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -105,7 +105,7 @@ pytest = [
     "mock==5.2.0",
     "pytest==8.3.5",
     "pytest-asyncio==0.26.0",
-    "pytest-cov==6.1.0",
+    "pytest-cov==6.1.1",
     "pytest-randomly==3.16.0",
     "async-timeout==5.0.1",
     { include-group = "coverage" },
diff --git a/uv.lock b/uv.lock
index 0adce3cb..da74372d 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1061,7 +1061,7 @@ pytest = [
     { name = "mock", specifier = "==5.2.0" },
     { name = "pytest", specifier = "==8.3.5" },
     { name = "pytest-asyncio", specifier = "==0.26.0" },
-    { name = "pytest-cov", specifier = "==6.1.0" },
+    { name = "pytest-cov", specifier = "==6.1.1" },
     { name = "pytest-randomly", specifier = "==3.16.0" },
 ]
 release = [{ name = "twine", specifier = "==6.1.0" }]
@@ -2404,15 +2404,15 @@ wheels = [
 
 [[package]]
 name = "pytest-cov"
-version = "6.1.0"
+version = "6.1.1"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "coverage", extra = ["toml"] },
     { name = "pytest" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/34/8c/039a7793f23f5cb666c834da9e944123f498ccc0753bed5fbfb2e2c11f87/pytest_cov-6.1.0.tar.gz", hash = "sha256:ec55e828c66755e5b74a21bd7cc03c303a9f928389c0563e50ba454a6dbe71db", size = 66651 }
+sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/e1/c5/8d6ffe9fc8f7f57b3662156ae8a34f2b8e7a754c73b48e689ce43145e98c/pytest_cov-6.1.0-py3-none-any.whl", hash = "sha256:cd7e1d54981d5185ef2b8d64b50172ce97e6f357e6df5cb103e828c7f993e201", size = 23743 },
+    { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 },
 ]
 
 [[package]]


Test patch:
diff --git a/tests/hikari/impl/test_cache.py b/tests/hikari/impl/test_cache.py
index e578456d..fc022b45 100644
--- a/tests/hikari/impl/test_cache.py
+++ b/tests/hikari/impl/test_cache.py
@@ -1861,6 +1861,7 @@ def test__build_member(self, cache_impl):
             user=cache_utilities.RefCell(mock_user),
             guild_id=snowflakes.Snowflake(6434435234),
             nickname="NICK",
+            guild_avatar_decoration=None,
             guild_avatar_hash="only slightly gay",
             guild_banner_hash="ok maybe alotta gay",
             role_ids=(snowflakes.Snowflake(65234), snowflakes.Snowflake(654234123)),
@@ -2219,6 +2220,7 @@ def test_set_member(self, cache_impl):
             joined_at=datetime.datetime(2020, 7, 15, 23, 30, 59, 501602, tzinfo=datetime.timezone.utc),
             premium_since=datetime.datetime(2020, 7, 1, 2, 0, 12, 501602, tzinfo=datetime.timezone.utc),
             is_deaf=True,
+            guild_avatar_decoration=None,
             guild_avatar_hash="gay",
             guild_banner_hash="gayge",
             is_mute=False,
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 55f614a8..3fb384a9 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -7100,6 +7100,34 @@ def test_deserialize_template_with_null_fields(self, entity_factory_impl, templa
     # USER MODELS #
     ###############
 
+    @pytest.fixture
+    def avatar_decoration_payload(self) -> dict[str, typing.Any]:
+        return {"asset": "ahhhhhhvatardecoration", "sku_id": "789", "expires_at": 1743753661}
+
+    def test_deserialize_avatar_decoration(
+        self, entity_factory_impl, mock_app: mock.Mock, avatar_decoration_payload: dict[str, typing.Any]
+    ):
+        decoration = entity_factory_impl._deserialize_avatar_decoration(avatar_decoration_payload)
+        assert decoration.asset_hash == "ahhhhhhvatardecoration"
+        assert decoration.sku_id == 789
+        assert decoration.expires_at == datetime.datetime(2025, 4, 4, 8, 1, 1, tzinfo=datetime.timezone.utc)
+
+    def test_deserialize_avatar_decoration_with_no_expiry(
+        self, entity_factory_impl, mock_app: mock.Mock, avatar_decoration_payload: dict[str, typing.Any]
+    ):
+        decoration = entity_factory_impl._deserialize_avatar_decoration(
+            {**avatar_decoration_payload, "expires_at": None}
+        )
+        assert decoration.asset_hash == "ahhhhhhvatardecoration"
+        assert decoration.sku_id == 789
+        assert decoration.expires_at is None
+
+    def test_deserialize_avatar_decoration_with_empty_payload(
+        self, entity_factory_impl, mock_app: mock.Mock, avatar_decoration_payload: dict[str, typing.Any]
+    ):
+        decoration = entity_factory_impl._deserialize_avatar_decoration(None)
+        assert decoration is None
+
     def test_deserialize_user(self, entity_factory_impl, mock_app, user_payload):
         user = entity_factory_impl.deserialize_user(user_payload)
         assert user.app is mock_app
diff --git a/tests/hikari/integration/test_equality_comparisons.py b/tests/hikari/integration/test_equality_comparisons.py
index 1744e5b1..8ee4f6d2 100644
--- a/tests/hikari/integration/test_equality_comparisons.py
+++ b/tests/hikari/integration/test_equality_comparisons.py
@@ -38,6 +38,7 @@ def make_user(user_id):
         discriminator="0001",
         username="testing",
         global_name=None,
+        avatar_decoration=None,
         avatar_hash=None,
         banner_hash=None,
         accent_color=None,
@@ -66,6 +67,7 @@ def make_guild_member(user_id):
         joined_at=datetime.datetime.now(),
         nickname=user.username,
         premium_since=None,
+        guild_avatar_decoration=None,
         guild_avatar_hash="no",
         guild_banner_hash="yes",
         is_deaf=False,
diff --git a/tests/hikari/test_applications.py b/tests/hikari/test_applications.py
index fa3237bd..5e94eeb6 100644
--- a/tests/hikari/test_applications.py
+++ b/tests/hikari/test_applications.py
@@ -42,6 +42,9 @@ def model(self):
     def test_app_property(self, model):
         assert model.app is model.user.app
 
+    def test_avatar_decoration_property(self, model):
+        assert model.avatar_decoration is model.user.avatar_decoration
+
     def test_avatar_hash_property(self, model):
         assert model.avatar_hash is model.user.avatar_hash
 
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index 8ddd6269..2f3eb513 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -225,6 +225,11 @@ def model(self, mock_user):
             is_pending=False,
             joined_at=datetime.datetime.now().astimezone(),
             nickname="davb",
+            guild_avatar_decoration=users.AvatarDecoration(
+                asset_hash="dimmadab",
+                sku_id=snowflakes.Snowflake(123456789),
+                expires_at=datetime.datetime(2025, 4, 4, 1, 1, 1),
+            ),
             guild_avatar_hash="dab",
             guild_banner_hash="dimma",
             premium_since=None,
@@ -261,6 +266,13 @@ def test_is_system_property(self, model, mock_user):
     def test_flags_property(self, model, mock_user):
         assert model.flags is mock_user.flags
 
+    def test_display_avatar_decoration_property_when_guild_avatar_decoration_is_set(self, model, mock_user):
+        assert model.display_avatar_decoration is model.guild_avatar_decoration
+
+    def test_display_avatar_decoration_property_when_guild_avatar_decoration_is_None(self, model, mock_user):
+        model.guild_avatar_decoration = None
+        assert model.display_avatar_decoration is mock_user.avatar_decoration
+
     def test_avatar_url_property(self, model, mock_user):
         assert model.avatar_url is mock_user.avatar_url
 
diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py
index e019e0b2..ffad4e85 100644
--- a/tests/hikari/test_users.py
+++ b/tests/hikari/test_users.py
@@ -193,6 +193,28 @@ def test_accent_colour_alias_property(self, obj):
 
         assert obj.accent_colour is obj.accent_color
 
+    def test_avatar_decoration_property(self, obj):
+        obj.avatar_decoration = users.AvatarDecoration(
+            asset_hash="18dnf8dfbakfdh", sku_id=snowflakes.Snowflake(123), expires_at=None
+        )
+
+        with mock.patch.object(users.AvatarDecoration, "make_url") as make_url:
+            assert obj.avatar_decoration.url is make_url.return_value
+
+    def test_avatar_decoration_make_url_with_all_args(self, obj):
+        obj.avatar_decoration = users.AvatarDecoration(
+            asset_hash="18dnf8dfbakfdh", sku_id=snowflakes.Snowflake(123), expires_at=None
+        )
+
+        with mock.patch.object(
+            routes, "CDN_AVATAR_DECORATION", new=mock.Mock(compile_to_file=mock.Mock(return_value="file"))
+        ) as route:
+            assert obj.avatar_decoration.make_url(size=4096) == "file"
+
+        route.compile_to_file.assert_called_once_with(
+            urls.CDN_URL, hash=obj.avatar_decoration.asset_hash, size=4096, file_format="png"
+        )
+
     def test_avatar_url_property(self, obj):
         with mock.patch.object(users.User, "make_avatar_url") as make_avatar_url:
             assert obj.avatar_url is make_avatar_url.return_value
@@ -335,6 +357,7 @@ def obj(self):
             discriminator="8637",
             username="thomm.o",
             global_name=None,
+            avatar_decoration=None,
             avatar_hash=None,
             banner_hash=None,
             accent_color=None,
@@ -379,6 +402,7 @@ def obj(self):
             discriminator="1234",
             username="foobar",
             global_name=None,
+            avatar_decoration=None,
             avatar_hash="69420",
             banner_hash="42069",
             accent_color=123456,