Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add URL methods and properties for rich presence assets. #961

Merged
merged 9 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 + "/{}"}
davfsa marked this conversation as resolved.
Show resolved Hide resolved


@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)
davfsa marked this conversation as resolved.
Show resolved Hide resolved

large_image: typing.Optional[str] = attr.field(repr=False)
"""The ID of the asset's large image, if set."""
FasterSpeeding marked this conversation as resolved.
Show resolved Hide resolved

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