From 0e11543f7f798114bc6e012967c607a0d2bc7272 Mon Sep 17 00:00:00 2001 From: Lennart <1247198+totalimmersion@users.noreply.github.com> Date: Sat, 7 Dec 2024 09:44:32 +0100 Subject: [PATCH] [CHA-15] Added pin, archive and partial member update functions (#179) --- .github/workflows/ci.yml | 4 -- stream_chat/async_chat/channel.py | 38 ++++++++++ stream_chat/base/channel.py | 46 ++++++++++++ stream_chat/channel.py | 38 ++++++++++ stream_chat/tests/async_chat/test_channel.py | 73 ++++++++++++++++++++ stream_chat/tests/test_channel.py | 71 +++++++++++++++++++ 6 files changed, 266 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5fcc3e..69f9b37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,6 @@ jobs: with: fetch-depth: 0 # gives the commit message linter access to all previous commits - - name: Commit lint - if: ${{ matrix.python == '3.8' && github.ref == 'refs/heads/master' }} - uses: wagoid/commitlint-github-action@v4 - - uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} diff --git a/stream_chat/async_chat/channel.py b/stream_chat/async_chat/channel.py index 764a19f..18342f0 100644 --- a/stream_chat/async_chat/channel.py +++ b/stream_chat/async_chat/channel.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterable, List, Union from stream_chat.base.channel import ChannelInterface, add_user_id +from stream_chat.base.exceptions import StreamChannelException from stream_chat.types.stream_response import StreamResponse @@ -209,3 +210,40 @@ async def unmute(self, user_id: str) -> StreamResponse: "channel_cid": self.cid, } return await self.client.post("moderation/unmute/channel", data=params) + + async def pin(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"pinned": True}} + return await self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + async def unpin(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"pinned": False}} + return await self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + async def archive(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"archived": True}} + return await self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + async def unarchive(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"archived": False}} + return await self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + async def update_member_partial( + self, user_id: str, to_set: Dict = None, to_unset: Iterable[str] = None + ) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": to_set or {}, "unset": to_unset or []} + return await self.client.patch(f"{self.url}/member/{user_id}", data=payload) diff --git a/stream_chat/base/channel.py b/stream_chat/base/channel.py index 16cc87e..a00d329 100644 --- a/stream_chat/base/channel.py +++ b/stream_chat/base/channel.py @@ -440,6 +440,52 @@ def unmute(self, user_id: str) -> Union[StreamResponse, Awaitable[StreamResponse """ pass + @abc.abstractmethod + def pin(self, user_id: str) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Pins a channel + Allows a user to pin the channel (only for themselves) + """ + pass + + @abc.abstractmethod + def unpin(self, user_id: str) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Unpins a channel + Allows a user to unpin the channel (only for themselves) + """ + pass + + @abc.abstractmethod + def archive(self, user_id: str) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Pins a channel + Allows a user to archive the channel (only for themselves) + """ + pass + + @abc.abstractmethod + def unarchive( + self, user_id: str + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Unpins a channel + Allows a user to unpin the channel (only for themselves) + """ + pass + + @abc.abstractmethod + def update_member_partial( + self, user_id: str, to_set: Dict = None, to_unset: Iterable[str] = None + ) -> Union[StreamResponse, Awaitable[StreamResponse]]: + """ + Update channel member partially + + :param to_set: a dictionary of key/value pairs to set or to override + :param to_unset: a list of keys to clear + """ + pass + def add_user_id(payload: Dict, user_id: str) -> Dict: return {**payload, "user": {"id": user_id}} diff --git a/stream_chat/channel.py b/stream_chat/channel.py index 776c892..cacfdd6 100644 --- a/stream_chat/channel.py +++ b/stream_chat/channel.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterable, List, Union from stream_chat.base.channel import ChannelInterface, add_user_id +from stream_chat.base.exceptions import StreamChannelException from stream_chat.types.stream_response import StreamResponse @@ -210,3 +211,40 @@ def unmute(self, user_id: str) -> StreamResponse: "channel_cid": self.cid, } return self.client.post("moderation/unmute/channel", data=params) + + def pin(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"pinned": True}} + return self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + def unpin(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"pinned": False}} + return self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + def archive(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"archived": True}} + return self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + def unarchive(self, user_id: str) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": {"archived": False}} + return self.client.patch(f"{self.url}/member/{user_id}", data=payload) + + def update_member_partial( + self, user_id: str, to_set: Dict = None, to_unset: Iterable[str] = None + ) -> StreamResponse: + if not user_id: + raise StreamChannelException("user_id must not be empty") + + payload = {"set": to_set or {}, "unset": to_unset or []} + return self.client.patch(f"{self.url}/member/{user_id}", data=payload) diff --git a/stream_chat/tests/async_chat/test_channel.py b/stream_chat/tests/async_chat/test_channel.py index 148f94e..a266601 100644 --- a/stream_chat/tests/async_chat/test_channel.py +++ b/stream_chat/tests/async_chat/test_channel.py @@ -380,3 +380,76 @@ async def test_export_channel( assert "error" not in resp break time.sleep(0.5) + + async def test_pin_channel( + self, client: StreamChatAsync, channel: Channel, random_users: List[Dict] + ): + user_id = random_users[0]["id"] + await channel.add_members([user_id]) + + # Pin the channel + response = await channel.pin(user_id) + assert response is not None + + # Query for pinned channels + response = await client.query_channels( + {"pinned": True, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + # Unpin the channel + response = await channel.unpin(user_id) + assert response is not None + + # Query for pinned channels + response = await client.query_channels( + {"pinned": False, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + async def test_archive_channel( + self, client: StreamChatAsync, channel: Channel, random_users: List[Dict] + ): + user_id = random_users[0]["id"] + await channel.add_members([user_id]) + + # Archive the channel + response = await channel.archive(user_id) + assert response is not None + + # Query for archived channels + response = await client.query_channels( + {"archived": True, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + # Unarchive the channel + response = await channel.unarchive(user_id) + assert response is not None + + # Query for archived channels + response = await client.query_channels( + {"archived": False, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + async def test_update_member_partial( + self, channel: Channel, random_users: List[Dict] + ): + user_id = random_users[0]["id"] + await channel.add_members([user_id]) + + # Test setting a custom field + response = await channel.update_member_partial(user_id, to_set={"hat": "blue"}) + assert response["channel_member"]["hat"] == "blue" + + # Test setting a new field while unsetting the previous one + response = await channel.update_member_partial( + user_id, to_set={"color": "red"}, to_unset=["hat"] + ) + assert response["channel_member"]["color"] == "red" + assert "hat" not in response["channel_member"] diff --git a/stream_chat/tests/test_channel.py b/stream_chat/tests/test_channel.py index ceb1911..522ea46 100644 --- a/stream_chat/tests/test_channel.py +++ b/stream_chat/tests/test_channel.py @@ -377,3 +377,74 @@ def test_export_channel( assert "error" not in resp break time.sleep(0.5) + + def test_pin_channel( + self, client: StreamChat, channel: Channel, random_users: List[Dict] + ): + user_id = random_users[0]["id"] + channel.add_members([user_id]) + + # Pin the channel + response = channel.pin(user_id) + assert response is not None + + # Query for pinned channels + response = client.query_channels( + {"pinned": True, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + # Unpin the channel + response = channel.unpin(user_id) + assert response is not None + + # Query for pinned channels + response = client.query_channels( + {"pinned": False, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + def test_archive_channel( + self, client: StreamChat, channel: Channel, random_users: List[Dict] + ): + user_id = random_users[0]["id"] + channel.add_members([user_id]) + + # Archive the channel + response = channel.archive(user_id) + assert response is not None + + # Query for archived channels + response = client.query_channels( + {"archived": True, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + # Unarchive the channel + response = channel.unarchive(user_id) + assert response is not None + + # Query for archhived channels + response = client.query_channels( + {"archived": False, "cid": channel.cid}, user_id=user_id + ) + assert len(response["channels"]) == 1 + assert response["channels"][0]["channel"]["cid"] == channel.cid + + def test_update_member_partial(self, channel: Channel, random_users: List[Dict]): + user_id = random_users[0]["id"] + channel.add_members([user_id]) + + # Test setting a custom field + response = channel.update_member_partial(user_id, to_set={"hat": "blue"}) + assert response["channel_member"]["hat"] == "blue" + + # Test setting a new field while unsetting the previous one + response = channel.update_member_partial( + user_id, to_set={"color": "red"}, to_unset=["hat"] + ) + assert response["channel_member"]["color"] == "red" + assert "hat" not in response["channel_member"]