Add Webhook model and update helpers (#6)

This commit is contained in:
Slipstream 2025-06-10 00:42:34 -06:00 committed by GitHub
parent 8be34bdcbf
commit eb5908682d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 193 additions and 4 deletions

View File

@ -46,6 +46,7 @@ if TYPE_CHECKING:
CategoryChannel,
Thread,
DMChannel,
Webhook,
)
from .ui.view import View
from .enums import ChannelType as EnumChannelType
@ -133,6 +134,7 @@ class Client:
) # Placeholder for User model cache if needed
self._messages: Dict[Snowflake, "Message"] = {}
self._views: Dict[Snowflake, "View"] = {}
self._webhooks: Dict[Snowflake, "Webhook"] = {}
# Default whether replies mention the user
self.mention_replies: bool = mention_replies
@ -648,6 +650,19 @@ class Client:
self._messages[message.id] = message
return message
def parse_webhook(self, data: Union[Dict[str, Any], "Webhook"]) -> "Webhook":
"""Parses webhook data and returns a Webhook object, updating cache."""
from .models import Webhook
if isinstance(data, Webhook):
webhook = data
webhook._client = self # type: ignore[attr-defined]
else:
webhook = Webhook(data, client_instance=self)
self._webhooks[webhook.id] = webhook
return webhook
async def fetch_user(self, user_id: Snowflake) -> Optional["User"]:
"""Fetches a user by ID from Discord."""
if self._closed:
@ -1083,6 +1098,36 @@ class Client:
print(f"Failed to fetch channel {channel_id}: {e}")
return None
async def create_webhook(
self, channel_id: Snowflake, payload: Dict[str, Any]
) -> "Webhook":
"""|coro| Create a webhook in the given channel."""
if self._closed:
raise DisagreementException("Client is closed.")
data = await self._http.create_webhook(channel_id, payload)
return self.parse_webhook(data)
async def edit_webhook(
self, webhook_id: Snowflake, payload: Dict[str, Any]
) -> "Webhook":
"""|coro| Edit an existing webhook."""
if self._closed:
raise DisagreementException("Client is closed.")
data = await self._http.edit_webhook(webhook_id, payload)
return self.parse_webhook(data)
async def delete_webhook(self, webhook_id: Snowflake) -> None:
"""|coro| Delete a webhook by ID."""
if self._closed:
raise DisagreementException("Client is closed.")
await self._http.delete_webhook(webhook_id)
# --- Application Command Methods ---
async def process_interaction(self, interaction: Interaction) -> None:
"""Internal method to process an interaction from the gateway."""

View File

@ -20,7 +20,7 @@ from . import __version__ # For User-Agent
if TYPE_CHECKING:
from .client import Client
from .models import Message
from .models import Message, Webhook
from .interactions import ApplicationCommand, InteractionResponsePayload, Snowflake
# Discord API constants
@ -323,6 +323,33 @@ class HTTPClient:
"""Fetches a channel by ID."""
return await self.request("GET", f"/channels/{channel_id}")
async def create_webhook(
self, channel_id: "Snowflake", payload: Dict[str, Any]
) -> "Webhook":
"""Creates a webhook in the specified channel."""
data = await self.request(
"POST", f"/channels/{channel_id}/webhooks", payload=payload
)
from .models import Webhook
return Webhook(data)
async def edit_webhook(
self, webhook_id: "Snowflake", payload: Dict[str, Any]
) -> "Webhook":
"""Edits an existing webhook."""
data = await self.request("PATCH", f"/webhooks/{webhook_id}", payload=payload)
from .models import Webhook
return Webhook(data)
async def delete_webhook(self, webhook_id: "Snowflake") -> None:
"""Deletes a webhook."""
await self.request("DELETE", f"/webhooks/{webhook_id}")
async def get_user(self, user_id: "Snowflake") -> Dict[str, Any]:
"""Fetches a user object for a given user ID."""
return await self.request("GET", f"/users/{user_id}")

View File

@ -1094,6 +1094,28 @@ class PartialChannel:
return f"<PartialChannel id='{self.id}' name='{self.name}' type='{type_name}'>"
class Webhook:
"""Represents a Discord Webhook."""
def __init__(
self, data: Dict[str, Any], client_instance: Optional["Client"] = None
):
self._client: Optional["Client"] = client_instance
self.id: str = data["id"]
self.type: int = int(data.get("type", 1))
self.guild_id: Optional[str] = data.get("guild_id")
self.channel_id: Optional[str] = data.get("channel_id")
self.name: Optional[str] = data.get("name")
self.avatar: Optional[str] = data.get("avatar")
self.token: Optional[str] = data.get("token")
self.application_id: Optional[str] = data.get("application_id")
self.url: Optional[str] = data.get("url")
self.user: Optional[User] = User(data["user"]) if data.get("user") else None
def __repr__(self) -> str:
return f"<Webhook id='{self.id}' name='{self.name}'>"
# --- Message Components ---

View File

@ -9,7 +9,7 @@ from disagreement.http import HTTPClient
http = HTTPClient(token="TOKEN")
payload = {"name": "My Webhook"}
webhook_data = await http.create_webhook("123", payload)
webhook = await http.create_webhook("123", payload)
```
## Edit a webhook
@ -24,11 +24,10 @@ await http.edit_webhook("456", {"name": "Renamed"})
await http.delete_webhook("456")
```
The methods return the raw webhook JSON. You can construct a `Webhook` model if needed:
The methods now return a `Webhook` object directly:
```python
from disagreement.models import Webhook
webhook = Webhook(webhook_data)
print(webhook.id, webhook.name)
```

View File

@ -42,3 +42,99 @@ async def test_delete_followup_message_calls_request():
f"/webhooks/app_id/token/messages/456",
use_auth_header=False,
)
@pytest.mark.asyncio
async def test_create_webhook_returns_model_and_calls_request():
http = HTTPClient(token="t")
http.request = AsyncMock(return_value={"id": "1"})
payload = {"name": "wh"}
webhook = await http.create_webhook("123", payload)
http.request.assert_called_once_with(
"POST",
"/channels/123/webhooks",
payload=payload,
)
from disagreement.models import Webhook
assert isinstance(webhook, Webhook)
@pytest.mark.asyncio
async def test_edit_webhook_returns_model_and_calls_request():
http = HTTPClient(token="t")
http.request = AsyncMock(return_value={"id": "1"})
payload = {"name": "rename"}
webhook = await http.edit_webhook("1", payload)
http.request.assert_called_once_with(
"PATCH",
"/webhooks/1",
payload=payload,
)
from disagreement.models import Webhook
assert isinstance(webhook, Webhook)
@pytest.mark.asyncio
async def test_delete_webhook_calls_request():
http = HTTPClient(token="t")
http.request = AsyncMock()
await http.delete_webhook("1")
http.request.assert_called_once_with(
"DELETE",
"/webhooks/1",
)
@pytest.mark.asyncio
async def test_client_create_webhook_returns_model():
from types import SimpleNamespace
from disagreement.client import Client
from disagreement.models import Webhook
http = SimpleNamespace(create_webhook=AsyncMock(return_value={"id": "1"}))
client = Client.__new__(Client)
client._http = http
client._closed = False
client._webhooks = {}
webhook = await client.create_webhook("123", {"name": "wh"})
http.create_webhook.assert_awaited_once_with("123", {"name": "wh"})
assert isinstance(webhook, Webhook)
assert client._webhooks["1"] is webhook
@pytest.mark.asyncio
async def test_client_edit_webhook_returns_model():
from types import SimpleNamespace
from disagreement.client import Client
from disagreement.models import Webhook
http = SimpleNamespace(edit_webhook=AsyncMock(return_value={"id": "1"}))
client = Client.__new__(Client)
client._http = http
client._closed = False
client._webhooks = {}
webhook = await client.edit_webhook("1", {"name": "rename"})
http.edit_webhook.assert_awaited_once_with("1", {"name": "rename"})
assert isinstance(webhook, Webhook)
assert client._webhooks["1"] is webhook
@pytest.mark.asyncio
async def test_client_delete_webhook_calls_http():
from types import SimpleNamespace
from disagreement.client import Client
http = SimpleNamespace(delete_webhook=AsyncMock())
client = Client.__new__(Client)
client._http = http
client._closed = False
await client.delete_webhook("1")
http.delete_webhook.assert_awaited_once_with("1")