diff --git a/disagreement/event_dispatcher.py b/disagreement/event_dispatcher.py index b7d9489..b6ea317 100644 --- a/disagreement/event_dispatcher.py +++ b/disagreement/event_dispatcher.py @@ -79,6 +79,10 @@ class EventDispatcher: self._client._messages.pop(message_id, None) return data + def _parse_message_reaction_raw(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Returns the raw reaction payload.""" + return data + def _parse_interaction_create(self, data: Dict[str, Any]) -> "Interaction": """Parses raw INTERACTION_CREATE data into an Interaction object.""" from .interactions import Interaction diff --git a/disagreement/http.py b/disagreement/http.py index c9c3487..410e37b 100644 --- a/disagreement/http.py +++ b/disagreement/http.py @@ -348,6 +348,18 @@ class HTTPClient: f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}", ) + async def bulk_delete_messages( + self, channel_id: "Snowflake", messages: List["Snowflake"] + ) -> List["Snowflake"]: + """Bulk deletes messages in a channel and returns their IDs.""" + + await self.request( + "POST", + f"/channels/{channel_id}/messages/bulk-delete", + payload={"messages": messages}, + ) + return messages + async def delete_channel( self, channel_id: str, reason: Optional[str] = None ) -> None: diff --git a/disagreement/models.py b/disagreement/models.py index cfef621..8ec85e6 100644 --- a/disagreement/models.py +++ b/disagreement/models.py @@ -31,6 +31,7 @@ if TYPE_CHECKING: from .client import Client # For type hinting to avoid circular imports from .enums import OverwriteType # For PermissionOverwrite model from .ui.view import View + from .interactions import Snowflake # Forward reference Message if it were used in type hints before its definition # from .models import Message # Not needed as Message is defined before its use in TextChannel.send etc. @@ -1085,6 +1086,27 @@ class TextChannel(Channel): components=components, ) + async def purge( + self, limit: int, *, before: "Snowflake | None" = None + ) -> List["Snowflake"]: + """Bulk delete messages from this channel.""" + + params: Dict[str, Union[int, str]] = {"limit": limit} + if before is not None: + params["before"] = before + + messages = await self._client._http.request( + "GET", f"/channels/{self.id}/messages", params=params + ) + ids = [m["id"] for m in messages] + if not ids: + return [] + + await self._client._http.bulk_delete_messages(self.id, ids) + for mid in ids: + self._client._messages.pop(mid, None) + return ids + def __repr__(self) -> str: return f"" diff --git a/tests/test_textchannel_purge.py b/tests/test_textchannel_purge.py new file mode 100644 index 0000000..1be072a --- /dev/null +++ b/tests/test_textchannel_purge.py @@ -0,0 +1,48 @@ +import pytest +from types import SimpleNamespace +from unittest.mock import AsyncMock + +from disagreement.client import Client +from disagreement.models import TextChannel + + +@pytest.mark.asyncio +async def test_textchannel_purge_calls_bulk_delete(): + http = SimpleNamespace( + request=AsyncMock(return_value=[{"id": "1"}, {"id": "2"}]), + bulk_delete_messages=AsyncMock(), + ) + client = Client.__new__(Client) + client._http = http + client._messages = {} + + channel = TextChannel({"id": "c", "type": 0}, client) + + deleted = await channel.purge(2) + + http.request.assert_called_once_with( + "GET", "/channels/c/messages", params={"limit": 2} + ) + http.bulk_delete_messages.assert_awaited_once_with("c", ["1", "2"]) + assert deleted == ["1", "2"] + + +@pytest.mark.asyncio +async def test_textchannel_purge_before_param(): + http = SimpleNamespace( + request=AsyncMock(return_value=[]), + bulk_delete_messages=AsyncMock(), + ) + client = Client.__new__(Client) + client._http = http + client._messages = {} + + channel = TextChannel({"id": "c", "type": 0}, client) + + deleted = await channel.purge(1, before="b") + + http.request.assert_called_once_with( + "GET", "/channels/c/messages", params={"limit": 1, "before": "b"} + ) + http.bulk_delete_messages.assert_not_awaited() + assert deleted == []