From c8df6a4cda8ef481594a87b597e5454dc7b847e3 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:47:53 +0200 Subject: [PATCH 1/8] Add support for hang statuses --- discord/activity.py | 111 +++++++++++++++++++++++++++++++++++++- discord/enums.py | 13 +++++ discord/types/activity.py | 2 +- docs/api.rst | 53 ++++++++++++++++++ 4 files changed, 176 insertions(+), 3 deletions(-) diff --git a/discord/activity.py b/discord/activity.py index c692443f987a..dc9c57dc91d0 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -28,7 +28,7 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union, overload from .asset import Asset -from .enums import ActivityType, try_enum +from .enums import ActivityType, try_enum, HangStatusType from .colour import Colour from .partial_emoji import PartialEmoji from .utils import _get_as_snowflake @@ -40,6 +40,7 @@ 'Game', 'Spotify', 'CustomActivity', + 'HangStatus', ) """If curious, this is the current schema for an activity. @@ -109,6 +110,7 @@ class BaseActivity: - :class:`Game` - :class:`Streaming` - :class:`CustomActivity` + - :class:`HangStatus` Note that although these types are considered user-settable by the library, Discord typically ignores certain combinations of activity depending on @@ -147,6 +149,7 @@ class Activity(BaseActivity): - :class:`Game` - :class:`Streaming` + - :class:`HangStatus` Attributes ------------ @@ -825,7 +828,109 @@ def __repr__(self) -> str: return f'' -ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify] +class HangStatus(BaseActivity): + """A slimmed down version of :class:`Activity` that represents a Discord hang status. + + This is typically displayed via **Right now, I'm -** on the official Discord client. + + .. container:: operations + + .. describe:: x == y + + Checks if two hang statuses are equal. + + .. describe:: x != y + + Checks if two hang statuses are not equal. + + .. describe:: hash(x) + + Returns the hang status' hash. + + .. describe:: str(x) + + Returns the hang status' name. + + .. versionadded: 2.5 + + Attributes + ----------- + state: :class:`HangStatusType` + The state of hang status. + name: :class:`str` + The name of the hang status. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the hang status if any. + """ + + __slots__ = ('state', 'name', 'emoji') + + def __init__(self, state: str, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any) -> None: + super().__init__(**extra) + self.state: HangStatusType = try_enum(HangStatusType, state) + + self.name: str + if self.state == HangStatusType.custom: + self.name = extra['details'] + else: + self.name = self.state.value + + self.emoji: Optional[PartialEmoji] + if emoji is None: + self.emoji = emoji + elif isinstance(emoji, dict): + self.emoji = PartialEmoji.from_dict(emoji) + elif isinstance(emoji, str): + self.emoji = PartialEmoji(name=emoji) + elif isinstance(emoji, PartialEmoji): + self.emoji = emoji + else: + raise TypeError(f'Expected str, PartialEmoji, or None, received {type(emoji)!r} instead.') + + @property + def type(self) -> ActivityType: + """:class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.hang`. + """ + return ActivityType.hang + + def to_dict(self) -> Dict[str, Any]: + if self.state == HangStatusType.custom: + ret = { + 'type': ActivityType.hang.value, + 'name': 'Hang Status', + 'state': self.state.value, + 'details': self.name, + } + else: + ret = { + 'type': ActivityType.hang.value, + 'name': 'Hang Status', + 'state': self.state.value, + } + + if self.emoji: + ret['emoji'] = self.emoji.to_dict() + return ret + + def __str__(self) -> str: + return str(self.name) + + def __repr__(self) -> str: + return f'' + + def __eq__(self, other: object) -> bool: + return isinstance(other, HangStatus) and other.name == self.name and other.emoji == self.emoji + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __hash__(self) -> int: + return hash((self.name, str(self.emoji))) + + +ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify, HangStatus] @overload @@ -862,6 +967,8 @@ def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> return Activity(**data) elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data: return Spotify(**data) + elif game_type is ActivityType.hang: + return HangStatus(**data) # type: ignore else: ret = Activity(**data) diff --git a/discord/enums.py b/discord/enums.py index eaf8aef5e058..27002befac85 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -74,6 +74,7 @@ 'EntitlementType', 'EntitlementOwnerType', 'PollLayoutType', + 'HangStatusType', ) @@ -525,11 +526,23 @@ class ActivityType(Enum): watching = 3 custom = 4 competing = 5 + hang = 6 def __int__(self) -> int: return self.value +class HangStatusType(Enum): + chilling = 'chilling' + gaming = 'gaming' + focusing = 'focusing' + brb = 'brb' + eating = 'eating' + in_transit = 'in-transit' + watching = 'watching' + custom = 'custom' + + class TeamMembershipState(Enum): invited = 1 accepted = 2 diff --git a/discord/types/activity.py b/discord/types/activity.py index f57334936338..0f2b8dbf63ca 100644 --- a/discord/types/activity.py +++ b/discord/types/activity.py @@ -76,7 +76,7 @@ class ActivityEmoji(TypedDict): animated: NotRequired[bool] -ActivityType = Literal[0, 1, 2, 4, 5] +ActivityType = Literal[0, 1, 2, 4, 5, 6] class SendableActivity(TypedDict): diff --git a/docs/api.rst b/docs/api.rst index 41cf6549d169..e8ac2186e098 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1911,6 +1911,12 @@ of :class:`enum.Enum`. .. versionadded:: 1.5 + .. attribute:: hang + + A hang status activity type. + + .. versionadded:: 2.5 + .. class:: VerificationLevel Specifies a :class:`Guild`\'s verification level, which is the criteria in @@ -3663,6 +3669,45 @@ of :class:`enum.Enum`. A burst reaction, also known as a "super reaction". +.. class:: HangStatusType + + Represents the type of an hang status. + + .. versionadded:: 2.5 + + .. attribute:: chilling + + The default hang status "Chilling". + + .. attribute:: gaming + + The default hang status "GAMING". + + .. attribute:: focusing + + The default hang status "In the zone". + + .. attribute:: brb + + The default hang status "Gonna BRB". + + .. attribute:: eating + + The default hang status "Grubbin". + + .. attribute:: in_transit + + The default hang status "Wandering IRL". + + .. attribute:: watching + + The default hang status "Watchin' stuff". + + .. attribute:: custom + + A custom hang status set by the user. + + .. _discord-api-audit-logs: Audit Log Data @@ -5310,6 +5355,14 @@ CustomActivity .. autoclass:: CustomActivity :members: +HangStatus +~~~~~~~~~~~ + +.. attributetable:: HangStatus + +.. autoclass:: HangStatus + :members: + Permissions ~~~~~~~~~~~~ From 8bc04d53b13f80e51fc2eb0ee596152d3c000beb Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:45:19 +0200 Subject: [PATCH 2/8] Change how data is handled even you can't set it (yet) --- discord/activity.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/discord/activity.py b/discord/activity.py index dc9c57dc91d0..fb1da1b393cc 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -865,13 +865,23 @@ class HangStatus(BaseActivity): __slots__ = ('state', 'name', 'emoji') - def __init__(self, state: str, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any) -> None: + def __init__( + self, + state: HangStatusType, + *, + name: Optional[str] = None, + emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, + **extra: Any, + ) -> None: super().__init__(**extra) - self.state: HangStatusType = try_enum(HangStatusType, state) + self.state: HangStatusType = state self.name: str - if self.state == HangStatusType.custom: - self.name = extra['details'] + if state == HangStatusType.custom: + if name is None: + raise ValueError(f'name must be set if state is custom') + else: + self.name = extra.pop('details', name) else: self.name = self.state.value @@ -968,7 +978,8 @@ def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data: return Spotify(**data) elif game_type is ActivityType.hang: - return HangStatus(**data) # type: ignore + hang_state = try_enum(HangStatusType, data.pop('state')) + return HangStatus(state=hang_state, **data) # type: ignore else: ret = Activity(**data) From 3905151b1ebe36f47a8366e20202108752c66d9a Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:46:31 +0200 Subject: [PATCH 3/8] [docs] Remove HangStatus from user-settable list it's not settable (yet) --- discord/activity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/activity.py b/discord/activity.py index fb1da1b393cc..ce6e9beab20c 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -110,7 +110,6 @@ class BaseActivity: - :class:`Game` - :class:`Streaming` - :class:`CustomActivity` - - :class:`HangStatus` Note that although these types are considered user-settable by the library, Discord typically ignores certain combinations of activity depending on From 517962bff8ec0051f97af2e321ec100e38bfbf10 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:50:20 +0200 Subject: [PATCH 4/8] Revert "Change how data is handled" This reverts commit 8bc04d53b13f80e51fc2eb0ee596152d3c000beb. --- discord/activity.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/discord/activity.py b/discord/activity.py index ce6e9beab20c..b16862a0eead 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -864,23 +864,13 @@ class HangStatus(BaseActivity): __slots__ = ('state', 'name', 'emoji') - def __init__( - self, - state: HangStatusType, - *, - name: Optional[str] = None, - emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, - **extra: Any, - ) -> None: + def __init__(self, state: str, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any) -> None: super().__init__(**extra) - self.state: HangStatusType = state + self.state: HangStatusType = try_enum(HangStatusType, state) self.name: str - if state == HangStatusType.custom: - if name is None: - raise ValueError(f'name must be set if state is custom') - else: - self.name = extra.pop('details', name) + if self.state == HangStatusType.custom: + self.name = extra['details'] else: self.name = self.state.value @@ -977,8 +967,7 @@ def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data: return Spotify(**data) elif game_type is ActivityType.hang: - hang_state = try_enum(HangStatusType, data.pop('state')) - return HangStatus(state=hang_state, **data) # type: ignore + return HangStatus(**data) # type: ignore else: ret = Activity(**data) From f07921f2d8b8e639ac60c72c679b1f090c72e9fd Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:12:10 +0200 Subject: [PATCH 5/8] Remove ability for usage in change_presence --- discord/activity.py | 31 ++++++------------------------- discord/widget.py | 6 +++--- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/discord/activity.py b/discord/activity.py index b16862a0eead..f19fbb3f8aac 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -827,7 +827,7 @@ def __repr__(self) -> str: return f'' -class HangStatus(BaseActivity): +class HangStatus: """A slimmed down version of :class:`Activity` that represents a Discord hang status. This is typically displayed via **Right now, I'm -** on the official Discord client. @@ -864,17 +864,17 @@ class HangStatus(BaseActivity): __slots__ = ('state', 'name', 'emoji') - def __init__(self, state: str, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any) -> None: - super().__init__(**extra) - self.state: HangStatusType = try_enum(HangStatusType, state) + def __init__(self, **data: Any) -> None: + self.state: HangStatusType = try_enum(HangStatusType, data['state']) self.name: str if self.state == HangStatusType.custom: - self.name = extra['details'] + self.name = data['details'] else: self.name = self.state.value self.emoji: Optional[PartialEmoji] + emoji = data.get('emoji') if emoji is None: self.emoji = emoji elif isinstance(emoji, dict): @@ -894,25 +894,6 @@ def type(self) -> ActivityType: """ return ActivityType.hang - def to_dict(self) -> Dict[str, Any]: - if self.state == HangStatusType.custom: - ret = { - 'type': ActivityType.hang.value, - 'name': 'Hang Status', - 'state': self.state.value, - 'details': self.name, - } - else: - ret = { - 'type': ActivityType.hang.value, - 'name': 'Hang Status', - 'state': self.state.value, - } - - if self.emoji: - ret['emoji'] = self.emoji.to_dict() - return ret - def __str__(self) -> str: return str(self.name) @@ -967,7 +948,7 @@ def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data: return Spotify(**data) elif game_type is ActivityType.hang: - return HangStatus(**data) # type: ignore + return HangStatus(**data) else: ret = Activity(**data) diff --git a/discord/widget.py b/discord/widget.py index 8220086652d8..e171a1d80194 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -28,7 +28,7 @@ from .utils import snowflake_time, _get_as_snowflake, resolve_invite from .user import BaseUser -from .activity import BaseActivity, Spotify, create_activity +from .activity import BaseActivity, Spotify, HangStatus, create_activity from .invite import Invite from .enums import Status, try_enum @@ -167,7 +167,7 @@ class WidgetMember(BaseUser): ) if TYPE_CHECKING: - activity: Optional[Union[BaseActivity, Spotify]] + activity: Optional[Union[BaseActivity, Spotify, HangStatus]] def __init__( self, @@ -190,7 +190,7 @@ def __init__( else: activity = create_activity(game, state) - self.activity: Optional[Union[BaseActivity, Spotify]] = activity + self.activity: Optional[Union[BaseActivity, Spotify, HangStatus]] = activity self.connected_channel: Optional[WidgetChannel] = connected_channel From b93f713e8a509e0580acebbfc499a5e5994718bd Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:30:18 +0200 Subject: [PATCH 6/8] Revert "Remove ability for usage in change_presence" This reverts commit f07921f2d8b8e639ac60c72c679b1f090c72e9fd. --- discord/activity.py | 31 +++++++++++++++++++++++++------ discord/widget.py | 6 +++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/discord/activity.py b/discord/activity.py index f19fbb3f8aac..b16862a0eead 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -827,7 +827,7 @@ def __repr__(self) -> str: return f'' -class HangStatus: +class HangStatus(BaseActivity): """A slimmed down version of :class:`Activity` that represents a Discord hang status. This is typically displayed via **Right now, I'm -** on the official Discord client. @@ -864,17 +864,17 @@ class HangStatus: __slots__ = ('state', 'name', 'emoji') - def __init__(self, **data: Any) -> None: - self.state: HangStatusType = try_enum(HangStatusType, data['state']) + def __init__(self, state: str, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any) -> None: + super().__init__(**extra) + self.state: HangStatusType = try_enum(HangStatusType, state) self.name: str if self.state == HangStatusType.custom: - self.name = data['details'] + self.name = extra['details'] else: self.name = self.state.value self.emoji: Optional[PartialEmoji] - emoji = data.get('emoji') if emoji is None: self.emoji = emoji elif isinstance(emoji, dict): @@ -894,6 +894,25 @@ def type(self) -> ActivityType: """ return ActivityType.hang + def to_dict(self) -> Dict[str, Any]: + if self.state == HangStatusType.custom: + ret = { + 'type': ActivityType.hang.value, + 'name': 'Hang Status', + 'state': self.state.value, + 'details': self.name, + } + else: + ret = { + 'type': ActivityType.hang.value, + 'name': 'Hang Status', + 'state': self.state.value, + } + + if self.emoji: + ret['emoji'] = self.emoji.to_dict() + return ret + def __str__(self) -> str: return str(self.name) @@ -948,7 +967,7 @@ def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data: return Spotify(**data) elif game_type is ActivityType.hang: - return HangStatus(**data) + return HangStatus(**data) # type: ignore else: ret = Activity(**data) diff --git a/discord/widget.py b/discord/widget.py index e171a1d80194..8220086652d8 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -28,7 +28,7 @@ from .utils import snowflake_time, _get_as_snowflake, resolve_invite from .user import BaseUser -from .activity import BaseActivity, Spotify, HangStatus, create_activity +from .activity import BaseActivity, Spotify, create_activity from .invite import Invite from .enums import Status, try_enum @@ -167,7 +167,7 @@ class WidgetMember(BaseUser): ) if TYPE_CHECKING: - activity: Optional[Union[BaseActivity, Spotify, HangStatus]] + activity: Optional[Union[BaseActivity, Spotify]] def __init__( self, @@ -190,7 +190,7 @@ def __init__( else: activity = create_activity(game, state) - self.activity: Optional[Union[BaseActivity, Spotify, HangStatus]] = activity + self.activity: Optional[Union[BaseActivity, Spotify]] = activity self.connected_channel: Optional[WidgetChannel] = connected_channel From b47c895ab2e7bdda59eeea93f0252809a5c6aa66 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:49:29 +0200 Subject: [PATCH 7/8] Make default hang statuses settable --- discord/activity.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/discord/activity.py b/discord/activity.py index b16862a0eead..e38cfb65fb7c 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -110,6 +110,7 @@ class BaseActivity: - :class:`Game` - :class:`Streaming` - :class:`CustomActivity` + - :class:`HangStatus` Note that although these types are considered user-settable by the library, Discord typically ignores certain combinations of activity depending on @@ -864,17 +865,22 @@ class HangStatus(BaseActivity): __slots__ = ('state', 'name', 'emoji') - def __init__(self, state: str, emoji: Optional[Union[PartialEmoji, Dict[str, Any], str]] = None, **extra: Any) -> None: + def __init__(self, state: HangStatusType, **extra: Any) -> None: super().__init__(**extra) - self.state: HangStatusType = try_enum(HangStatusType, state) + self.state: HangStatusType = state self.name: str if self.state == HangStatusType.custom: - self.name = extra['details'] + print(state, extra) + details = extra.get('details') + if details is None: + raise ValueError('hang status state cannot be custom') + self.name = details else: self.name = self.state.value self.emoji: Optional[PartialEmoji] + emoji = extra.get('emoji') if emoji is None: self.emoji = emoji elif isinstance(emoji, dict): @@ -895,22 +901,16 @@ def type(self) -> ActivityType: return ActivityType.hang def to_dict(self) -> Dict[str, Any]: + ret = { + 'type': ActivityType.hang.value, + 'name': 'Hang Status', + 'state': self.state.value, + } if self.state == HangStatusType.custom: - ret = { - 'type': ActivityType.hang.value, - 'name': 'Hang Status', - 'state': self.state.value, - 'details': self.name, - } - else: - ret = { - 'type': ActivityType.hang.value, - 'name': 'Hang Status', - 'state': self.state.value, - } - + ret['details'] = self.name if self.emoji: ret['emoji'] = self.emoji.to_dict() + return ret def __str__(self) -> str: @@ -967,7 +967,8 @@ def create_activity(data: Optional[ActivityPayload], state: ConnectionState) -> elif game_type is ActivityType.listening and 'sync_id' in data and 'session_id' in data: return Spotify(**data) elif game_type is ActivityType.hang: - return HangStatus(**data) # type: ignore + hang_state = try_enum(HangStatusType, data.pop('state')) + return HangStatus(state=hang_state, **data) # type: ignore else: ret = Activity(**data) From 9524deb93109a8e9c72484e4734b55e00d3ebe55 Mon Sep 17 00:00:00 2001 From: Andrin <65789180+Puncher1@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:50:32 +0200 Subject: [PATCH 8/8] Remove left-over --- discord/activity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/activity.py b/discord/activity.py index e38cfb65fb7c..58d43984c440 100644 --- a/discord/activity.py +++ b/discord/activity.py @@ -871,7 +871,6 @@ def __init__(self, state: HangStatusType, **extra: Any) -> None: self.name: str if self.state == HangStatusType.custom: - print(state, extra) details = extra.get('details') if details is None: raise ValueError('hang status state cannot be custom')