diff --git a/docs/channels/channel_management/deleting.md b/docs/channels/channel_management/deleting.md index 3eb2f5d..66fc938 100644 --- a/docs/channels/channel_management/deleting.md +++ b/docs/channels/channel_management/deleting.md @@ -12,6 +12,17 @@ channel.delete() > If you recreate this channel, it will show up empty. Recovering old messages is not supported. Use the disable method if you want a reversible change. +### Keeping the messages + +Pass `skip_truncate=True` to keep the messages of a soft deleted channel, so recreating the channel with the same id restores its history. It cannot be combined with a hard delete, and only distinct channels are eligible. + +```python +channel.delete(skip_truncate=True) + +# same option on the batch endpoint +response = client.delete_channels([cid1, cid2], skip_truncate=True) +``` + ## Deleting Many Channels You can delete up to 100 channels and optionally all of their messages using this method. This can be a large amount of data to delete, so this endpoint processes asynchronously, meaning responses contain a `task ID` which can be polled using the [getTask endpoint](/chat/docs/python#tasks-gettask) to check status of the deletions. Channels will be soft-deleted immediately so that channels no longer return from queries, but permanently deleting the channel and deleting messages takes longer to process. diff --git a/stream_chat/async_chat/channel.py b/stream_chat/async_chat/channel.py index 0f0fcf7..5b2708d 100644 --- a/stream_chat/async_chat/channel.py +++ b/stream_chat/async_chat/channel.py @@ -85,8 +85,13 @@ async def update_partial( payload = {"set": to_set or {}, "unset": to_unset or []} return await self.client.patch(self.url, data=payload) - async def delete(self, hard: bool = False) -> StreamResponse: - return await self.client.delete(self.url, {"hard_delete": hard}) + async def delete( + self, hard: bool = False, skip_truncate: bool = False + ) -> StreamResponse: + params = {"hard_delete": hard} + if skip_truncate: + params["skip_truncate"] = True + return await self.client.delete(self.url, params) async def truncate(self, **options: Any) -> StreamResponse: return await self.client.post(f"{self.url}/truncate", data=options) diff --git a/stream_chat/base/channel.py b/stream_chat/base/channel.py index c6287f8..0ab74d6 100644 --- a/stream_chat/base/channel.py +++ b/stream_chat/base/channel.py @@ -166,11 +166,14 @@ def update_partial( @abc.abstractmethod def delete( - self, hard: bool = False + self, hard: bool = False, skip_truncate: bool = False ) -> Union[StreamResponse, Awaitable[StreamResponse]]: """ - Delete the channel. Messages are permanently removed. + Delete the channel. + :param hard: hard delete the channel and its messages + :param skip_truncate: keep the messages of a soft deleted channel, so + recreating it with the same id restores the history :return: The server response """ pass diff --git a/stream_chat/channel.py b/stream_chat/channel.py index b3400b4..b3006d9 100644 --- a/stream_chat/channel.py +++ b/stream_chat/channel.py @@ -83,8 +83,11 @@ def update_partial( payload = {"set": to_set or {}, "unset": to_unset or []} return self.client.patch(self.url, data=payload) - def delete(self, hard: bool = False) -> StreamResponse: - return self.client.delete(self.url, params={"hard_delete": hard}) + def delete(self, hard: bool = False, skip_truncate: bool = False) -> StreamResponse: + params = {"hard_delete": hard} + if skip_truncate: + params["skip_truncate"] = True + return self.client.delete(self.url, params=params) def truncate(self, **options: Any) -> StreamResponse: return self.client.post(f"{self.url}/truncate", data=options) diff --git a/stream_chat/tests/test_channel_delete.py b/stream_chat/tests/test_channel_delete.py new file mode 100644 index 0000000..4fd0cc6 --- /dev/null +++ b/stream_chat/tests/test_channel_delete.py @@ -0,0 +1,42 @@ +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from stream_chat.async_chat.channel import Channel as AsyncChannel +from stream_chat.channel import Channel + + +def test_delete_omits_skip_truncate_by_default() -> None: + client = MagicMock() + Channel(client, "messaging", "chan").delete() + client.delete.assert_called_once_with( + "channels/messaging/chan", params={"hard_delete": False} + ) + + +def test_delete_sends_skip_truncate() -> None: + client = MagicMock() + Channel(client, "messaging", "chan").delete(skip_truncate=True) + client.delete.assert_called_once_with( + "channels/messaging/chan", params={"hard_delete": False, "skip_truncate": True} + ) + + +@pytest.mark.asyncio +async def test_async_delete_omits_skip_truncate_by_default() -> None: + client = MagicMock() + client.delete = AsyncMock() + await AsyncChannel(client, "messaging", "chan").delete() + client.delete.assert_called_once_with( + "channels/messaging/chan", {"hard_delete": False} + ) + + +@pytest.mark.asyncio +async def test_async_delete_sends_skip_truncate() -> None: + client = MagicMock() + client.delete = AsyncMock() + await AsyncChannel(client, "messaging", "chan").delete(skip_truncate=True) + client.delete.assert_called_once_with( + "channels/messaging/chan", {"hard_delete": False, "skip_truncate": True} + )