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

View File

@ -14,6 +14,8 @@ import time
import random
from typing import Optional, TYPE_CHECKING, Any, Dict
from .models import Activity
from .enums import GatewayOpcode, GatewayIntent
from .errors import GatewayException, DisagreementException, AuthenticationError
from .interactions import Interaction
@ -213,26 +215,17 @@ class GatewayClient:
async def update_presence(
self,
status: str,
activity_name: Optional[str] = None,
activity_type: int = 0,
activity: Optional[Activity] = None,
*,
since: int = 0,
afk: bool = False,
):
) -> None:
"""Sends the presence update payload to the Gateway."""
payload = {
"op": GatewayOpcode.PRESENCE_UPDATE,
"d": {
"since": since,
"activities": (
[
{
"name": activity_name,
"type": activity_type,
}
]
if activity_name
else []
),
"activities": [activity.to_dict()] if activity else [],
"status": status,
"afk": afk,
},
@ -353,7 +346,10 @@ class GatewayClient:
future._members.extend(raw_event_d_payload.get("members", [])) # type: ignore
# 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
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}'>"
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:
"""Represents a PRESENCE_UPDATE event."""
@ -2371,7 +2402,17 @@ class PresenceUpdate:
self.user = User(data["user"])
self.guild_id: Optional[str] = data.get("guild_id")
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", {})
def __repr__(self) -> str:

View File

@ -1,6 +1,7 @@
# Updating Presence
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
@ -22,8 +23,18 @@ An activity dictionary must include a `name` and a `type` field. The type value
| `4` | Custom |
| `5` | Competing |
Example:
Example using the provided activity classes:
```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 disagreement.client import Client
from disagreement.models import Game
from disagreement.errors import DisagreementException
@ -18,11 +19,11 @@ class DummyGateway(MagicMock):
async def test_change_presence_passes_arguments():
client = Client(token="t")
client._gateway = DummyGateway()
await client.change_presence(status="idle", activity_name="hi", activity_type=0)
game = Game("hi")
await client.change_presence(status="idle", activity=game)
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
)