From b207cdcdd06076a118f18001ab09c23a9c4e03f7 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 10 Sep 2023 15:45:38 +0200 Subject: [PATCH] Add custom status support (#1683) --- changes/1683.feature.md | 4 ++++ hikari/impl/gateway_bot.py | 11 ++------- hikari/impl/shard.py | 14 ++++++++++- hikari/presences.py | 22 ++++++++++------- tests/hikari/impl/test_gateway_bot.py | 16 +------------ tests/hikari/impl/test_shard.py | 34 +++++++++++++++++++++++---- 6 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 changes/1683.feature.md diff --git a/changes/1683.feature.md b/changes/1683.feature.md new file mode 100644 index 0000000000..0fe12cbb1c --- /dev/null +++ b/changes/1683.feature.md @@ -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. diff --git a/hikari/impl/gateway_bot.py b/hikari/impl/gateway_bot.py index 70bf8bcadc..debe57bb42 100644 --- a/hikari/impl/gateway_bot.py +++ b/hikari/impl/gateway_bot.py @@ -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.", diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py index 607eb83162..5738941cf2 100644 --- a/hikari/impl/shard.py +++ b/hikari/impl/shard.py @@ -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]: @@ -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): diff --git a/hikari/presences.py b/hikari/presences.py index 3f07271b44..6d879bfcf6 100644 --- a/hikari/presences.py +++ b/hikari/presences.py @@ -81,13 +81,10 @@ class ActivityType(int, enums.Enum): """Shows up as `Watching `.""" 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 ` `. .. warning:: - Bots **DO NOT** support setting custom statuses. + As of the time of writing, emoji cannot be used by bot accounts. """ COMPETING = 5 @@ -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.""" @@ -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.""" diff --git a/tests/hikari/impl/test_gateway_bot.py b/tests/hikari/impl/test_gateway_bot.py index b854a3f75a..6dd52a2cfd 100644 --- a/tests/hikari/impl/test_gateway_bot.py +++ b/tests/hikari/impl/test_gateway_bot.py @@ -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) @@ -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) diff --git a/tests/hikari/impl/test_shard.py b/tests/hikari/impl/test_shard.py index 943376fd14..5b10ab1b0a 100644 --- a/tests/hikari/impl/test_shard.py +++ b/tests/hikari/impl/test_shard.py @@ -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(): @@ -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