Skip to content

Commit

Permalink
Add URL methods and properties for rich presence assets. (#961)
Browse files Browse the repository at this point in the history
  • Loading branch information
FasterSpeeding authored Jan 31, 2022
1 parent e5c9f38 commit ba3b821
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 11 deletions.
1 change: 1 addition & 0 deletions changes/961.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add URL methods and properties for rich presence assets.
1 change: 1 addition & 0 deletions hikari/impl/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2610,6 +2610,7 @@ def deserialize_member_presence( # noqa: CFQ001 - Max function length
if "assets" in activity_payload:
assets_payload = activity_payload["assets"]
assets = presence_models.ActivityAssets(
application_id=application_id,
large_image=assets_payload.get("large_image"),
large_text=assets_payload.get("large_text"),
small_image=assets_payload.get("small_image"),
Expand Down
1 change: 1 addition & 0 deletions hikari/internal/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ def compile_to_file(

CDN_APPLICATION_ICON: typing.Final[CDNRoute] = CDNRoute("/app-icons/{application_id}/{hash}", {PNG, *JPEG_JPG, WEBP})
CDN_APPLICATION_COVER: typing.Final[CDNRoute] = CDNRoute("/app-assets/{application_id}/{hash}", {PNG, *JPEG_JPG, WEBP})
CDN_APPLICATION_ASSET: typing.Final[CDNRoute] = CDNRoute("/app-assets/{application_id}/{hash}", {PNG, *JPEG_JPG, WEBP})
CDN_ACHIEVEMENT_ICON: typing.Final[CDNRoute] = CDNRoute(
"/app-assets/{application_id}/achievements/{achievement_id}/icons/{hash}", {PNG, *JPEG_JPG, WEBP}
)
Expand Down
115 changes: 115 additions & 0 deletions hikari/presences.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@

import attr

from hikari import files
from hikari import snowflakes
from hikari import urls
from hikari.internal import attr_extensions
from hikari.internal import enums
from hikari.internal import routes

if typing.TYPE_CHECKING:
import datetime
Expand Down Expand Up @@ -118,11 +121,16 @@ class ActivityParty:
"""Maximum size of this party, if applicable."""


_DYNAMIC_URLS = {"mp": urls.MEDIA_PROXY_URL + "/{}"}


@attr_extensions.with_copy
@attr.define(hash=False, kw_only=True, weakref_slot=False)
class ActivityAssets:
"""Used to represent possible assets for an activity."""

_application_id: typing.Optional[snowflakes.Snowflake] = attr.field(repr=False)

large_image: typing.Optional[str] = attr.field(repr=False)
"""The ID of the asset's large image, if set."""

Expand All @@ -135,6 +143,113 @@ class ActivityAssets:
small_text: typing.Optional[str] = attr.field(repr=True)
"""The text that'll appear when hovering over the small image, if set."""

def _make_asset_url(self, asset: typing.Optional[str], ext: str, size: int) -> typing.Optional[files.URL]:
if asset is None:
return None

try:
resource, identifier = asset.split(":", 1)
return files.URL(url=_DYNAMIC_URLS[resource].format(identifier))

except KeyError:
raise RuntimeError("Unknown asset type") from None

except ValueError:
assert self._application_id is not None
return routes.CDN_APPLICATION_ASSET.compile_to_file(
urls.CDN_URL,
application_id=self._application_id,
hash=asset,
size=size,
file_format=ext,
)

@property
def large_image_url(self) -> typing.Optional[files.URL]:
"""Large image asset URL.
!!! note
This will be `builtins.None` if no large image asset exists or if the
asset's dymamic URL (indicated by a `{name}:` prefix) is not known.
"""
try:
return self.make_large_image_url()

except RuntimeError:
return None

def make_large_image_url(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[files.URL]:
"""Generate the large image asset URL for this application.
!!! note
`ext` and `size` are ignored for images hosted outside of Discord
or on Discord's media proxy.
Parameters
----------
ext : builtins.str
The extension to use for this URL, defaults to `png`.
Supports `png`, `jpeg`, `jpg` and `webp`.
size : builtins.int
The size to set for the URL, defaults to `4096`.
Can be any power of two between 16 and 4096.
Returns
-------
typing.Optional[hikari.files.URL]
The URL, or `builtins.None` if no icon exists.
Raises
------
builtins.ValueError
If the size is not an integer power of 2 between 16 and 4096
(inclusive).
builtins.RuntimeError
If `ActivityAssets.large_image` points towards an unknown asset type.
"""
return self._make_asset_url(self.large_image, ext, size)

@property
def small_image_url(self) -> typing.Optional[files.URL]:
"""Small image asset URL.
!!! note
This will be `builtins.None` if no large image asset exists or if the
asset's dymamic URL (indicated by a `{name}:` prefix) is not known.
"""
try:
return self.make_small_image_url()

except RuntimeError:
return None

def make_small_image_url(self, *, ext: str = "png", size: int = 4096) -> typing.Optional[files.URL]:
"""Generate the small image asset URL for this application.
Parameters
----------
ext : builtins.str
The extension to use for this URL, defaults to `png`.
Supports `png`, `jpeg`, `jpg` and `webp`.
size : builtins.int
The size to set for the URL, defaults to `4096`.
Can be any power of two between 16 and 4096.
Returns
-------
typing.Optional[hikari.files.URL]
The URL, or `builtins.None` if no icon exists.
Raises
------
builtins.ValueError
If the size is not an integer power of 2 between 16 and 4096
(inclusive).
builtins.RuntimeError
If `ActivityAssets.small_image` points towards an unknown asset type.
"""
return self._make_asset_url(self.small_image, ext, size)


@attr_extensions.with_copy
@attr.define(hash=False, kw_only=True, weakref_slot=False)
Expand Down
3 changes: 3 additions & 0 deletions hikari/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@

CDN_URL: typing.Final[str] = "https://cdn.discordapp.com"
"""The CDN URL."""

MEDIA_PROXY_URL: typing.Final[str] = "https://media.discordapp.net"
"""The media proxy URL."""
11 changes: 3 additions & 8 deletions tests/hikari/hikari_test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,19 @@
# condition, and thus acceptable to terminate the test and fail it.
REASONABLE_TIMEOUT_AFTER = 10

_stubbed_classes = {}


def _stub_init(self, kwargs: typing.Mapping[str, typing.Any]):
for attr, value in kwargs.items():
setattr(self, attr, value)
_T = typing.TypeVar("_T")


def mock_class_namespace(
klass,
klass: typing.Type[_T],
/,
*,
init_: bool = True,
slots_: typing.Optional[bool] = None,
implement_abstract_methods_: bool = True,
rename_impl_: bool = True,
**namespace: typing.Any,
):
) -> typing.Type[_T]:
"""Get a version of a class with the provided namespace fields set as class attributes."""
if slots_ or slots_ is None and hasattr(klass, "__slots__"):
namespace["__slots__"] = ()
Expand Down
1 change: 1 addition & 0 deletions tests/hikari/impl/test_entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4634,6 +4634,7 @@ def test_deserialize_member_presence(
assert isinstance(activity.party, presence_models.ActivityParty)
# ActivityAssets
assert activity.assets is not None
assert activity.assets._application_id is activity.application_id
assert activity.assets.large_image == "34234234234243"
assert activity.assets.large_text == "LARGE TEXT"
assert activity.assets.small_image == "3939393"
Expand Down
Loading

0 comments on commit ba3b821

Please sign in to comment.