Skip to content

Commit

Permalink
Add custom status support (#1683)
Browse files Browse the repository at this point in the history
  • Loading branch information
davfsa authored Sep 10, 2023
1 parent 96edf79 commit b207cdc
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 38 deletions.
4 changes: 4 additions & 0 deletions changes/1683.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Bots can now utilize `Activity.state`
- When used with `type` set to `ActivityType.CUSTOM`, it will show as the text for the custom status.
Syntactic sugar also exists to support simply using `name` instead of `state`.
- Can be used with other activity types to provide additional information on the activity.
11 changes: 2 additions & 9 deletions hikari/impl/gateway_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,12 @@
def _validate_activity(activity: undefined.UndefinedNoneOr[presences.Activity]) -> None:
# This seems to cause confusion for a lot of people, so lets add some warnings into the mix.

if activity is undefined.UNDEFINED or activity is None:
if not activity:
return

# If you ever change where this is called from, make sure to check the stacklevels are correct
# or the code preview in the warning will be wrong...
if activity.type is presences.ActivityType.CUSTOM:
warnings.warn(
"The CUSTOM activity type is not supported by bots at the time of writing, and may therefore not have "
"any effect if used.",
category=errors.HikariWarning,
stacklevel=3,
)
elif activity.type is presences.ActivityType.STREAMING and activity.url is None:
if activity.type is presences.ActivityType.STREAMING and activity.url is None:
warnings.warn(
"The STREAMING activity type requires a 'url' parameter pointing to a valid Twitch or YouTube video "
"URL to be specified on the activity for the presence update to have any effect.",
Expand Down
14 changes: 13 additions & 1 deletion hikari/impl/shard.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@
errors.ShardCloseCode.RATE_LIMITED,
)
)
# Default value used by the client
_CUSTOM_STATUS_NAME = "Custom Status"


def _log_filterer(token: str) -> typing.Callable[[str], str]:
Expand Down Expand Up @@ -352,7 +354,17 @@ def _serialize_activity(activity: typing.Optional[presences.Activity]) -> data_b
if activity is None:
return None

return {"name": activity.name, "type": int(activity.type), "url": activity.url}
# Syntactic sugar, treat `name` as state if using `CUSTOM` and `state` is not passed.
state: typing.Optional[str]
if activity.type is presences.ActivityType.CUSTOM and activity.name and not activity.state:
name = _CUSTOM_STATUS_NAME
state = activity.name
else:
name = activity.name
state = activity.state

payload = {"name": name, "state": state, "type": int(activity.type), "url": activity.url}
return payload


class GatewayShardImpl(shard.GatewayShard):
Expand Down
22 changes: 13 additions & 9 deletions hikari/presences.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,10 @@ class ActivityType(int, enums.Enum):
"""Shows up as `Watching <name>`."""

CUSTOM = 4
"""A custom status.
To set an emoji with the status, place a unicode emoji or Discord emoji
(`:smiley:`) as the first part of the status activity name.
"""Shows up as `<emoji> <name>`.
.. warning::
Bots **DO NOT** support setting custom statuses.
As of the time of writing, emoji cannot be used by bot accounts.
"""

COMPETING = 5
Expand Down Expand Up @@ -306,8 +303,18 @@ class Activity:
name: str = attrs.field()
"""The activity name."""

state: typing.Optional[str] = attrs.field(default=None)
"""The activities state, if set.
This field can be use to set a custom status or provide more information
on the activity.
"""

url: typing.Optional[str] = attrs.field(default=None, repr=False)
"""The activity URL. Only valid for `STREAMING` activities."""
"""The activity URL, if set.
Only valid for `STREAMING` activities.
"""

type: typing.Union[ActivityType, int] = attrs.field(converter=ActivityType, default=ActivityType.PLAYING)
"""The activity type."""
Expand All @@ -332,9 +339,6 @@ class RichActivity(Activity):
details: typing.Optional[str] = attrs.field(repr=False)
"""The text that describes what the activity's target is doing, if set."""

state: typing.Optional[str] = attrs.field(repr=False)
"""The current status of this activity's target, if set."""

emoji: typing.Optional[emojis_.Emoji] = attrs.field(repr=False)
"""The emoji of this activity, if it is a custom status and set."""

Expand Down
16 changes: 1 addition & 15 deletions tests/hikari/impl/test_gateway_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,6 @@ def test_validate_activity_when_no_activity(activity):
warn.assert_not_called()


def test_validate_activity_when_type_is_custom():
activity = presences.Activity(name="test", type=presences.ActivityType.CUSTOM)

with mock.patch.object(warnings, "warn") as warn:
bot_impl._validate_activity(activity)

warn.assert_called_once_with(
"The CUSTOM activity type is not supported by bots at the time of writing, and may therefore not have "
"any effect if used.",
category=errors.HikariWarning,
stacklevel=3,
)


def test_validate_activity_when_type_is_streaming_but_no_url():
activity = presences.Activity(name="test", url=None, type=presences.ActivityType.STREAMING)

Expand All @@ -84,7 +70,7 @@ def test_validate_activity_when_type_is_streaming_but_no_url():


def test_validate_activity_when_no_warning():
activity = presences.Activity(name="test", type=presences.ActivityType.PLAYING)
activity = presences.Activity(name="test", state="Hello!", type=presences.ActivityType.CUSTOM)

with mock.patch.object(warnings, "warn") as warn:
bot_impl._validate_activity(activity)
Expand Down
34 changes: 30 additions & 4 deletions tests/hikari/impl/test_shard.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,30 @@ def test__serialize_activity_when_activity_is_None():


def test__serialize_activity_when_activity_is_not_None():
activity = mock.Mock(type="0", url="https://some.url")
activity.name = "some name" # This has to be set separate because if not, its set as the mock's name
assert shard._serialize_activity(activity) == {"name": "some name", "type": 0, "url": "https://some.url"}
activity = presences.Activity(name="some name", type=0, state="blah", url="https://some.url")
assert shard._serialize_activity(activity) == {
"name": "some name",
"type": 0,
"state": "blah",
"url": "https://some.url",
}


@pytest.mark.parametrize(
("activity_name", "activity_state", "expected_name", "expected_state"),
[("Testing!", None, "Custom Status", "Testing!"), ("Blah name!", "Testing!", "Blah name!", "Testing!")],
)
def test__serialize_activity_custom_activity_syntactic_sugar(
activity_name, activity_state, expected_name, expected_state
):
activity = presences.Activity(name=activity_name, state=activity_state, type=presences.ActivityType.CUSTOM)

assert shard._serialize_activity(activity) == {
"type": 4,
"name": expected_name,
"state": expected_state,
"url": None,
}


def test__serialize_datetime_when_datetime_is_None():
Expand Down Expand Up @@ -598,7 +619,12 @@ def test__serialize_and_store_presence_payload_when_all_args_undefined(
actual_result = client._serialize_and_store_presence_payload()

if activity is not None:
expected_activity = {"name": activity.name, "type": activity.type, "url": activity.url}
expected_activity = {
"name": activity.name,
"state": activity.state,
"type": activity.type,
"url": activity.url,
}
else:
expected_activity = None

Expand Down

0 comments on commit b207cdc

Please sign in to comment.