feat: add Activity presence models (#60)

This commit is contained in:
Slipstream 2025-06-11 14:26:47 -06:00 committed by GitHub
parent c47a7e49f8
commit 39162b6543
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 26 deletions

View File

@ -36,6 +36,7 @@ from .ext import loader as ext_loader
from .interactions import Interaction, Snowflake from .interactions import Interaction, Snowflake
from .error_handler import setup_global_error_handler from .error_handler import setup_global_error_handler
from .voice_client import VoiceClient from .voice_client import VoiceClient
from .models import Activity
if TYPE_CHECKING: if TYPE_CHECKING:
from .models import ( from .models import (
@ -439,8 +440,7 @@ class Client:
async def change_presence( async def change_presence(
self, self,
status: str, status: str,
activity_name: Optional[str] = None, activity: Optional[Activity] = None,
activity_type: int = 0,
since: int = 0, since: int = 0,
afk: bool = False, afk: bool = False,
): ):
@ -449,8 +449,7 @@ class Client:
Args: Args:
status (str): The new status for the client (e.g., "online", "idle", "dnd", "invisible"). status (str): The new status for the client (e.g., "online", "idle", "dnd", "invisible").
activity_name (Optional[str]): The name of the activity. activity (Optional[Activity]): Activity instance describing what the bot is doing.
activity_type (int): The type of the activity.
since (int): The timestamp (in milliseconds) of when the client went idle. since (int): The timestamp (in milliseconds) of when the client went idle.
afk (bool): Whether the client is AFK. afk (bool): Whether the client is AFK.
""" """
@ -460,8 +459,7 @@ class Client:
if self._gateway: if self._gateway:
await self._gateway.update_presence( await self._gateway.update_presence(
status=status, status=status,
activity_name=activity_name, activity=activity,
activity_type=activity_type,
since=since, since=since,
afk=afk, afk=afk,
) )

View File

@ -14,6 +14,8 @@ import time
import random import random
from typing import Optional, TYPE_CHECKING, Any, Dict from typing import Optional, TYPE_CHECKING, Any, Dict
from .models import Activity
from .enums import GatewayOpcode, GatewayIntent from .enums import GatewayOpcode, GatewayIntent
from .errors import GatewayException, DisagreementException, AuthenticationError from .errors import GatewayException, DisagreementException, AuthenticationError
from .interactions import Interaction from .interactions import Interaction
@ -213,26 +215,17 @@ class GatewayClient:
async def update_presence( async def update_presence(
self, self,
status: str, status: str,
activity_name: Optional[str] = None, activity: Optional[Activity] = None,
activity_type: int = 0, *,
since: int = 0, since: int = 0,
afk: bool = False, afk: bool = False,
): ) -> None:
"""Sends the presence update payload to the Gateway.""" """Sends the presence update payload to the Gateway."""
payload = { payload = {
"op": GatewayOpcode.PRESENCE_UPDATE, "op": GatewayOpcode.PRESENCE_UPDATE,
"d": { "d": {
"since": since, "since": since,
"activities": ( "activities": [activity.to_dict()] if activity else [],
[
{
"name": activity_name,
"type": activity_type,
}
]
if activity_name
else []
),
"status": status, "status": status,
"afk": afk, "afk": afk,
}, },
@ -353,7 +346,10 @@ class GatewayClient:
future._members.extend(raw_event_d_payload.get("members", [])) # type: ignore future._members.extend(raw_event_d_payload.get("members", [])) # type: ignore
# If this is the last chunk, resolve the future # If this is the last chunk, resolve the future
if raw_event_d_payload.get("chunk_index") == raw_event_d_payload.get("chunk_count", 1) - 1: if (
raw_event_d_payload.get("chunk_index")
== raw_event_d_payload.get("chunk_count", 1) - 1
):
future.set_result(future._members) # type: ignore future.set_result(future._members) # type: ignore
del self._member_chunk_requests[nonce] del self._member_chunk_requests[nonce]

View File

@ -2361,6 +2361,37 @@ class ThreadMember:
return f"<ThreadMember user_id='{self.user_id}' thread_id='{self.id}'>" return f"<ThreadMember user_id='{self.user_id}' thread_id='{self.id}'>"
class Activity:
"""Represents a user's presence activity."""
def __init__(self, name: str, type: int) -> None:
self.name = name
self.type = type
def to_dict(self) -> Dict[str, Any]:
return {"name": self.name, "type": self.type}
class Game(Activity):
"""Represents a playing activity."""
def __init__(self, name: str) -> None:
super().__init__(name, 0)
class Streaming(Activity):
"""Represents a streaming activity."""
def __init__(self, name: str, url: str) -> None:
super().__init__(name, 1)
self.url = url
def to_dict(self) -> Dict[str, Any]:
payload = super().to_dict()
payload["url"] = self.url
return payload
class PresenceUpdate: class PresenceUpdate:
"""Represents a PRESENCE_UPDATE event.""" """Represents a PRESENCE_UPDATE event."""
@ -2371,7 +2402,17 @@ class PresenceUpdate:
self.user = User(data["user"]) self.user = User(data["user"])
self.guild_id: Optional[str] = data.get("guild_id") self.guild_id: Optional[str] = data.get("guild_id")
self.status: Optional[str] = data.get("status") self.status: Optional[str] = data.get("status")
self.activities: List[Dict[str, Any]] = data.get("activities", []) self.activities: List[Activity] = []
for activity in data.get("activities", []):
act_type = activity.get("type", 0)
name = activity.get("name", "")
if act_type == 0:
obj = Game(name)
elif act_type == 1:
obj = Streaming(name, activity.get("url", ""))
else:
obj = Activity(name, act_type)
self.activities.append(obj)
self.client_status: Dict[str, Any] = data.get("client_status", {}) self.client_status: Dict[str, Any] = data.get("client_status", {})
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@ -1,6 +1,7 @@
# Updating Presence # Updating Presence
The `Client.change_presence` method allows you to update the bot's status and displayed activity. The `Client.change_presence` method allows you to update the bot's status and displayed activity.
Pass an :class:`~disagreement.models.Activity` (such as :class:`~disagreement.models.Game` or :class:`~disagreement.models.Streaming`) to describe what your bot is doing.
## Status Strings ## Status Strings
@ -22,8 +23,18 @@ An activity dictionary must include a `name` and a `type` field. The type value
| `4` | Custom | | `4` | Custom |
| `5` | Competing | | `5` | Competing |
Example: Example using the provided activity classes:
```python ```python
await client.change_presence(status="idle", activity={"name": "with Discord", "type": 0}) from disagreement.models import Game
await client.change_presence(status="idle", activity=Game("with Discord"))
```
You can also specify a streaming URL:
```python
from disagreement.models import Streaming
await client.change_presence(status="online", activity=Streaming("My Stream", "https://twitch.tv/someone"))
``` ```

View File

@ -2,6 +2,7 @@ import pytest
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from disagreement.client import Client from disagreement.client import Client
from disagreement.models import Game
from disagreement.errors import DisagreementException from disagreement.errors import DisagreementException
@ -18,11 +19,11 @@ class DummyGateway(MagicMock):
async def test_change_presence_passes_arguments(): async def test_change_presence_passes_arguments():
client = Client(token="t") client = Client(token="t")
client._gateway = DummyGateway() client._gateway = DummyGateway()
game = Game("hi")
await client.change_presence(status="idle", activity_name="hi", activity_type=0) await client.change_presence(status="idle", activity=game)
client._gateway.update_presence.assert_awaited_once_with( client._gateway.update_presence.assert_awaited_once_with(
status="idle", activity_name="hi", activity_type=0, since=0, afk=False status="idle", activity=game, since=0, afk=False
) )