diff --git a/stream_chat/async_chat/client.py b/stream_chat/async_chat/client.py index ce07f6d..fddfb1d 100644 --- a/stream_chat/async_chat/client.py +++ b/stream_chat/async_chat/client.py @@ -986,6 +986,48 @@ async def update_user_location( params = {"user_id": user_id, **options} return await self.put("users/live_locations", data=data, params=params) + async def mark_delivered(self, data: Dict[str, Any]) -> Optional[StreamResponse]: + """ + Send the mark delivered event for this user, only works if the `delivery_receipts` setting is enabled + + :param data: MarkDeliveredOptions containing latest_delivered_messages and other optional fields + :return: The server response or None if delivery receipts are disabled + """ + # Validate required fields + if not data.get("latest_delivered_messages"): + raise ValueError("latest_delivered_messages must not be empty") + + # Ensure either user or user_id is provided + if not data.get("user") and not data.get("user_id"): + raise ValueError("either user or user_id must be provided") + + return await self.post("channels/delivered", data=data) + + async def mark_delivered_simple( + self, user_id: str, message_id: str, channel_cid: str + ) -> Optional[StreamResponse]: + """ + Convenience method to mark a message as delivered for a specific user. + + :param user_id: The user ID + :param message_id: The message ID + :param channel_cid: The channel CID (channel_type:channel_id) + :return: The server response or None if delivery receipts are disabled + """ + if not user_id: + raise ValueError("user ID must not be empty") + if not message_id: + raise ValueError("message ID must not be empty") + if not channel_cid: + raise ValueError("channel CID must not be empty") + + data = { + "latest_delivered_messages": [{"cid": channel_cid, "id": message_id}], + "user_id": user_id, + } + + return await self.mark_delivered(data) + async def close(self) -> None: await self.session.close() diff --git a/stream_chat/base/client.py b/stream_chat/base/client.py index c6f2d94..d64c6f9 100644 --- a/stream_chat/base/client.py +++ b/stream_chat/base/client.py @@ -1537,6 +1537,32 @@ def update_user_location( """ pass + @abc.abstractmethod + def mark_delivered( + self, data: Dict[str, Any] + ) -> Union[StreamResponse, Awaitable[StreamResponse], None]: + """ + Send the mark delivered event for this user, only works if the `delivery_receipts` setting is enabled + + :param data: MarkDeliveredOptions containing latest_delivered_messages and other optional fields + :return: The server response or None if delivery receipts are disabled + """ + pass + + @abc.abstractmethod + def mark_delivered_simple( + self, user_id: str, message_id: str, channel_cid: str + ) -> Union[StreamResponse, Awaitable[StreamResponse], None]: + """ + Convenience method to mark a message as delivered for a specific user. + + :param user_id: The user ID + :param message_id: The message ID + :param channel_cid: The channel CID (channel_type:channel_id) + :return: The server response or None if delivery receipts are disabled + """ + pass + ##################### # Private methods # ##################### diff --git a/stream_chat/client.py b/stream_chat/client.py index b9572b1..7d57c95 100644 --- a/stream_chat/client.py +++ b/stream_chat/client.py @@ -939,3 +939,49 @@ def update_user_location( data.update(cast(dict, options)) params = {"user_id": user_id, **options} return self.put("users/live_locations", data=data, params=params) + + def mark_delivered(self, data: Dict[str, Any]) -> Optional[StreamResponse]: + """ + Send the mark delivered event for this user, only works if the `delivery_receipts` setting is enabled + + :param data: MarkDeliveredOptions containing latest_delivered_messages and other optional fields + :return: The server response or None if delivery receipts are disabled + """ + # Validate required fields + if not data.get("latest_delivered_messages"): + raise ValueError("latest_delivered_messages must not be empty") + + # Ensure either user or user_id is provided + if not data.get("user") and not data.get("user_id"): + raise ValueError("either user or user_id must be provided") + + # Extract user_id from data + user_id = data.get("user_id") or data.get("user", {}).get("id") + if not user_id: + raise ValueError("user_id must be provided") + + params = {"user_id": user_id} + return self.post("channels/delivered", data=data, params=params) + + def mark_delivered_simple( + self, user_id: str, message_id: str, channel_cid: str + ) -> Optional[StreamResponse]: + """ + Convenience method to mark a message as delivered for a specific user. + + :param user_id: The user ID + :param message_id: The message ID + :param channel_cid: The channel CID (channel_type:channel_id) + :return: The server response or None if delivery receipts are disabled + """ + if not user_id: + raise ValueError("user ID must not be empty") + if not message_id: + raise ValueError("message ID must not be empty") + if not channel_cid: + raise ValueError("channel CID must not be empty") + data = { + "latest_delivered_messages": [{"cid": channel_cid, "id": message_id}], + "user_id": user_id, + } + return self.mark_delivered(data=data) diff --git a/stream_chat/tests/test_client.py b/stream_chat/tests/test_client.py index ae9c751..981baa9 100644 --- a/stream_chat/tests/test_client.py +++ b/stream_chat/tests/test_client.py @@ -1043,3 +1043,44 @@ def test_query_message_history( assert len(response_next["message_history"]) == 1 assert response_next["message_history"][0]["text"] == "helloworld-2" + + def test_mark_delivered( + self, client: StreamChat, channel: Channel, random_user: Dict + ): + delivery_data = { + "latest_delivered_messages": [ + {"cid": channel.cid, "id": "test-message-id"} + ], + "user_id": random_user["id"], + } + response = client.mark_delivered(delivery_data) + assert response is not None + delivery_data_multiple = { + "latest_delivered_messages": [ + {"cid": channel.cid, "id": "test-message-id-1"}, + {"cid": channel.cid, "id": "test-message-id-2"}, + ], + "user_id": random_user["id"], + } + response = client.mark_delivered(delivery_data_multiple) + assert response is not None + + def test_mark_delivered_simple( + self, client: StreamChat, channel: Channel, random_user: Dict + ): + response = client.mark_delivered_simple( + user_id=random_user["id"], + message_id="test-message-id", + channel_cid=channel.cid, + ) + assert response is not None + + def test_mark_delivered_validation(self, client: StreamChat, random_user: Dict): + with pytest.raises( + ValueError, match="latest_delivered_messages must not be empty" + ): + client.mark_delivered({"user_id": random_user["id"]}) + with pytest.raises(ValueError, match="either user or user_id must be provided"): + client.mark_delivered( + {"latest_delivered_messages": [{"cid": "test:channel", "id": "test"}]} + ) diff --git a/stream_chat/types/delivery_receipts.py b/stream_chat/types/delivery_receipts.py new file mode 100644 index 0000000..47f3b80 --- /dev/null +++ b/stream_chat/types/delivery_receipts.py @@ -0,0 +1,58 @@ +import sys +from typing import Dict, List, Optional + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + + +class DeliveredMessageConfirmation(TypedDict): + """ + Confirmation of a delivered message. + + Parameters: + cid: Channel CID (channel_type:channel_id) + id: Message ID + """ + + cid: str + id: str + + +class MarkDeliveredOptions(TypedDict, total=False): + """ + Options for marking messages as delivered. + + Parameters: + latest_delivered_messages: List of delivered message confirmations + user: Optional user object + user_id: Optional user ID + """ + + latest_delivered_messages: List[DeliveredMessageConfirmation] + user: Optional[Dict] # UserResponse equivalent + user_id: Optional[str] + + +class ChannelReadStatus(TypedDict, total=False): + """ + Channel read status information. + + Parameters: + last_read: Last read timestamp + unread_messages: Number of unread messages + user: User information + first_unread_message_id: ID of first unread message + last_read_message_id: ID of last read message + last_delivered_at: Last delivered timestamp + last_delivered_message_id: ID of last delivered message + """ + + last_read: str # ISO format string for timestamp + unread_messages: int + user: Dict # UserResponse equivalent + first_unread_message_id: Optional[str] + last_read_message_id: Optional[str] + last_delivered_at: Optional[str] # ISO format string for timestamp + last_delivered_message_id: Optional[str]