Skip to content

Commit

Permalink
feat(activity): improve custom asset image support (#687)
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftinv authored Aug 18, 2023
1 parent f08edf8 commit a491421
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 33 deletions.
1 change: 1 addition & 0 deletions changelog/687.feature.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support activity assets with ``mp:`` prefix in :attr:`Activity.large_image_url` and :attr:`Activity.small_image_url`, now returning the correct url.
1 change: 1 addition & 0 deletions changelog/687.feature.1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Move asset properties from :class:`Activity` to all activity types: :attr:`~Game.large_image_url`, :attr:`~Game.small_image_url`, :attr:`~Game.large_image_text`, :attr:`~Game.small_image_text`.
108 changes: 75 additions & 33 deletions disnake/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,63 @@ def end(self) -> Optional[datetime.datetime]:
def to_dict(self) -> ActivityPayload:
raise NotImplementedError

def _create_image_url(self, asset: str) -> Optional[str]:
# `asset` can be a simple ID (see `Activity._create_image_url`),
# or a string of the format `<prefix>:<id>`
prefix, _, asset_id = asset.partition(":")

if asset_id and (url_fmt := _ACTIVITY_URLS.get(prefix)):
return url_fmt.format(asset_id)
return None

@property
def large_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
Additionally, supports dynamic asset urls using the ``mp:`` prefix now.
"""
try:
large_image = self.assets["large_image"]
except KeyError:
return None
else:
return self._create_image_url(large_image)

@property
def small_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
Additionally, supports dynamic asset urls using the ``mp:`` prefix now.
"""
try:
small_image = self.assets["small_image"]
except KeyError:
return None
else:
return self._create_image_url(small_image)

@property
def large_image_text(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
"""
return self.assets.get("large_text", None)

@property
def small_image_text(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity, if applicable.
.. versionchanged:: 2.10
Moved from :class:`Activity` to base type, making this available to all activity types.
"""
return self.assets.get("small_text", None)


# tag type for user-settable activities
class BaseActivity(_BaseActivity):
Expand All @@ -160,6 +217,15 @@ class BaseActivity(_BaseActivity):
__slots__ = ()


# There are additional urls for twitch/youtube/spotify, however
# it appears that Discord does not want to document those:
# https://github.com/discord/discord-api-docs/pull/4617
# They are partially supported by different properties, e.g. `Spotify.album_cover_url`.
_ACTIVITY_URLS = {
"mp": "https://media.discordapp.net/{}",
}


class Activity(BaseActivity):
"""Represents an activity in Discord.
Expand Down Expand Up @@ -320,41 +386,17 @@ def to_dict(self) -> Dict[str, Any]:
ret["timestamps"] = self._timestamps
return ret

@property
def large_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable."""
if self.application_id is None:
return None

try:
large_image = self.assets["large_image"]
except KeyError:
return None
else:
return f"{Asset.BASE}/app-assets/{self.application_id}/{large_image}.png"

@property
def small_image_url(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable."""
if self.application_id is None:
return None

try:
small_image = self.assets["small_image"]
except KeyError:
return None
else:
return f"{Asset.BASE}/app-assets/{self.application_id}/{small_image}.png"
def _create_image_url(self, asset: str) -> Optional[str]:
# if parent method already returns valid url, use that
if url := super()._create_image_url(asset):
return url

@property
def large_image_text(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the large image asset hover text of this activity, if applicable."""
return self.assets.get("large_text", None)
# if it's not a `<prefix>:<id>` asset and we have an application ID, create url
if ":" not in asset and self.application_id:
return f"{Asset.BASE}/app-assets/{self.application_id}/{asset}.png"

@property
def small_image_text(self) -> Optional[str]:
"""Optional[:class:`str`]: Returns the small image asset hover text of this activity, if applicable."""
return self.assets.get("small_text", None)
# else, it's an unknown asset url
return None


class Game(BaseActivity):
Expand Down
1 change: 1 addition & 0 deletions docs/api/activities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ CustomActivity
.. autoclass:: CustomActivity
:members:
:inherited-members:
:exclude-members: large_image_url, large_image_text, small_image_url, small_image_text

Enumerations
------------
Expand Down
97 changes: 97 additions & 0 deletions tests/test_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# SPDX-License-Identifier: MIT

from typing import TYPE_CHECKING

import pytest

from disnake import activity as _activity

if TYPE_CHECKING:
from disnake.types.activity import ActivityAssets


@pytest.fixture
def activity():
return _activity.Activity()


@pytest.fixture
def game():
return _activity.Game(name="Celeste")


@pytest.fixture
def custom_activity():
return _activity.CustomActivity(name="custom")


@pytest.fixture
def streaming():
return _activity.Streaming(name="me", url="https://disnake.dev")


@pytest.fixture
def spotify():
return _activity.Spotify()


@pytest.fixture(params=["activity", "game", "custom_activity", "streaming", "spotify"])
def any_activity(request):
return request.getfixturevalue(request.param)


class TestAssets:
def test_none(self, any_activity: _activity.ActivityTypes) -> None:
assert any_activity.large_image_url is None
assert any_activity.small_image_url is None
assert any_activity.large_image_text is None
assert any_activity.small_image_text is None

def test_text(self, any_activity: _activity.ActivityTypes) -> None:
assets: ActivityAssets = {"large_text": "hi", "small_text": "hello"}
any_activity.assets = assets

assert any_activity.large_image_url is None
assert any_activity.small_image_url is None
assert any_activity.large_image_text == "hi"
assert any_activity.small_image_text == "hello"

def test_mp(self, any_activity: _activity.ActivityTypes) -> None:
assets: ActivityAssets = {
"large_image": "mp:external/stuff/large",
"small_image": "mp:external/stuff/small",
}
any_activity.assets = assets

assert any_activity.large_image_url == "https://media.discordapp.net/external/stuff/large"
assert any_activity.small_image_url == "https://media.discordapp.net/external/stuff/small"

def test_unknown_prefix(self, any_activity: _activity.ActivityTypes) -> None:
assets: ActivityAssets = {"large_image": "unknown:a", "small_image": "unknown:b"}
any_activity.assets = assets

assert any_activity.large_image_url is None
assert any_activity.small_image_url is None

def test_asset_id(self, any_activity: _activity.ActivityTypes) -> None:
assets: ActivityAssets = {"large_image": "1234", "small_image": "5678"}
any_activity.assets = assets

assert any_activity.large_image_url is None
assert any_activity.small_image_url is None

# test `Activity` with application_id separately;
# without application_id, it should behave like the other types (see previous test)
def test_asset_id_activity(self, activity: _activity.Activity) -> None:
activity.application_id = 1010

assets: ActivityAssets = {"large_image": "1234", "small_image": "5678"}
activity.assets = assets
assert activity.large_image_url == "https://cdn.discordapp.com/app-assets/1010/1234.png"
assert activity.small_image_url == "https://cdn.discordapp.com/app-assets/1010/5678.png"

# if it's a prefixed asset, it's should return `None` again
assets: ActivityAssets = {"large_image": "unknown:1234", "small_image": "unknown:5678"}
activity.assets = assets
assert activity.large_image_url is None
assert activity.small_image_url is None

0 comments on commit a491421

Please sign in to comment.