Skip to content

Commit bad2a0d

Browse files
feat: implemented message reminders feature
1 parent 514a795 commit bad2a0d

File tree

5 files changed

+524
-0
lines changed

5 files changed

+524
-0
lines changed

stream_chat/async_chat/client.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,83 @@ async def unread_counts(self, user_id: str) -> StreamResponse:
818818
async def unread_counts_batch(self, user_ids: List[str]) -> StreamResponse:
819819
return await self.post("unread_batch", data={"user_ids": user_ids})
820820

821+
async def create_reminder(
822+
self,
823+
message_id: str,
824+
user_id: str,
825+
remind_at: Optional[datetime.datetime] = None,
826+
) -> StreamResponse:
827+
"""
828+
Creates a reminder for a message.
829+
830+
:param message_id: The ID of the message to create a reminder for
831+
:param user_id: The ID of the user creating the reminder
832+
:param remind_at: When to remind the user (optional)
833+
:return: API response
834+
"""
835+
data = {"user_id": user_id}
836+
if remind_at is not None:
837+
if isinstance(remind_at, datetime.datetime):
838+
remind_at = remind_at.isoformat()
839+
data["remind_at"] = remind_at
840+
841+
return await self.post(f"messages/{message_id}/reminders", data=data)
842+
843+
async def update_reminder(
844+
self,
845+
message_id: str,
846+
user_id: str,
847+
remind_at: Optional[datetime.datetime] = None,
848+
) -> StreamResponse:
849+
"""
850+
Updates a reminder for a message.
851+
852+
:param message_id: The ID of the message with the reminder
853+
:param user_id: The ID of the user who owns the reminder
854+
:param remind_at: When to remind the user (optional)
855+
:return: API response
856+
"""
857+
data = {"user_id": user_id}
858+
if remind_at is not None:
859+
if isinstance(remind_at, datetime.datetime):
860+
remind_at = remind_at.isoformat()
861+
data["remind_at"] = remind_at
862+
return await self.patch(f"messages/{message_id}/reminders", data=data)
863+
864+
async def delete_reminder(self, message_id: str, user_id: str) -> StreamResponse:
865+
"""
866+
Deletes a reminder for a message.
867+
868+
:param message_id: The ID of the message with the reminder
869+
:param user_id: The ID of the user who owns the reminder
870+
:return: API response
871+
"""
872+
return await self.delete(
873+
f"messages/{message_id}/reminders", params={"user_id": user_id}
874+
)
875+
876+
async def query_reminders(
877+
self,
878+
user_id: str,
879+
filter_conditions: Dict = None,
880+
sort: List[Dict] = None,
881+
**options: Any,
882+
) -> StreamResponse:
883+
"""
884+
Queries reminders based on filter conditions.
885+
886+
:param user_id: The ID of the user whose reminders to query
887+
:param filter_conditions: Conditions to filter reminders
888+
:param sort: Sort parameters (default: [{ field: 'remind_at', direction: 1 }])
889+
:param options: Additional query options like limit, offset
890+
:return: API response with reminders
891+
"""
892+
params = options.copy()
893+
params["filter_conditions"] = filter_conditions or {}
894+
params["sort"] = sort or [{"field": "remind_at", "direction": 1}]
895+
params["user_id"] = user_id
896+
return await self.post("reminders/query", data=params)
897+
821898
async def close(self) -> None:
822899
await self.session.close()
823900

stream_chat/base/client.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,72 @@ def unread_counts_batch(
13691369
"""
13701370
pass
13711371

1372+
@abc.abstractmethod
1373+
def create_reminder(
1374+
self,
1375+
message_id: str,
1376+
user_id: str,
1377+
remind_at: Optional[datetime.datetime] = None,
1378+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
1379+
"""
1380+
Creates a reminder for a message.
1381+
1382+
:param message_id: The ID of the message to create a reminder for
1383+
:param user_id: The ID of the user creating the reminder
1384+
:param remind_at: When to remind the user (optional)
1385+
:return: API response
1386+
"""
1387+
pass
1388+
1389+
@abc.abstractmethod
1390+
def update_reminder(
1391+
self,
1392+
message_id: str,
1393+
user_id: str,
1394+
remind_at: Optional[datetime.datetime] = None,
1395+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
1396+
"""
1397+
Updates a reminder for a message.
1398+
1399+
:param message_id: The ID of the message with the reminder
1400+
:param user_id: The ID of the user who owns the reminder
1401+
:param remind_at: When to remind the user (optional)
1402+
:return: API response
1403+
"""
1404+
pass
1405+
1406+
@abc.abstractmethod
1407+
def delete_reminder(
1408+
self, message_id: str, user_id: str
1409+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
1410+
"""
1411+
Deletes a reminder for a message.
1412+
1413+
:param message_id: The ID of the message with the reminder
1414+
:param user_id: The ID of the user who owns the reminder
1415+
:return: API response
1416+
"""
1417+
pass
1418+
1419+
@abc.abstractmethod
1420+
def query_reminders(
1421+
self,
1422+
user_id: str,
1423+
filter_conditions: Dict = None,
1424+
sort: List[Dict] = None,
1425+
**options: Any,
1426+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
1427+
"""
1428+
Queries reminders based on filter conditions.
1429+
1430+
:param user_id: The ID of the user whose reminders to query
1431+
:param filter_conditions: Conditions to filter reminders
1432+
:param sort: Sort parameters (default: [{ field: 'remind_at', direction: 1 }])
1433+
:param options: Additional query options like limit, offset
1434+
:return: API response with reminders
1435+
"""
1436+
pass
1437+
13721438
#####################
13731439
# Private methods #
13741440
#####################

stream_chat/client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,3 +777,71 @@ def unread_counts(self, user_id: str) -> StreamResponse:
777777

778778
def unread_counts_batch(self, user_ids: List[str]) -> StreamResponse:
779779
return self.post("unread_batch", data={"user_ids": user_ids})
780+
781+
def create_reminder(
782+
self, message_id: str, user_id: str, remind_at: Optional[datetime.datetime] = None
783+
) -> StreamResponse:
784+
"""
785+
Creates a reminder for a message.
786+
787+
:param message_id: The ID of the message to create a reminder for
788+
:param user_id: The ID of the user creating the reminder
789+
:param remind_at: When to remind the user (optional)
790+
:return: API response
791+
"""
792+
data = {"user_id": user_id}
793+
if remind_at is not None:
794+
# Format as ISO 8601 date string without microseconds
795+
data["remind_at"] = remind_at.strftime("%Y-%m-%dT%H:%M:%SZ")
796+
return self.post(f"messages/{message_id}/reminders", data=data)
797+
798+
def update_reminder(
799+
self, message_id: str, user_id: str, remind_at: Optional[datetime.datetime] = None
800+
) -> StreamResponse:
801+
"""
802+
Updates a reminder for a message.
803+
804+
:param message_id: The ID of the message with the reminder
805+
:param user_id: The ID of the user who owns the reminder
806+
:param remind_at: When to remind the user (optional)
807+
:return: API response
808+
"""
809+
data = {"user_id": user_id}
810+
if remind_at is not None:
811+
# Format as ISO 8601 date string without microseconds
812+
data["remind_at"] = remind_at.strftime("%Y-%m-%dT%H:%M:%SZ")
813+
return self.patch(f"messages/{message_id}/reminders", data=data)
814+
815+
def delete_reminder(self, message_id: str, user_id: str) -> StreamResponse:
816+
"""
817+
Deletes a reminder for a message.
818+
819+
:param message_id: The ID of the message with the reminder
820+
:param user_id: The ID of the user who owns the reminder
821+
:return: API response
822+
"""
823+
return self.delete(
824+
f"messages/{message_id}/reminders", params={"user_id": user_id}
825+
)
826+
827+
def query_reminders(
828+
self,
829+
user_id: str,
830+
filter_conditions: Dict = None,
831+
sort: List[Dict] = None,
832+
**options: Any,
833+
) -> StreamResponse:
834+
"""
835+
Queries reminders based on filter conditions.
836+
837+
:param user_id: The ID of the user whose reminders to query
838+
:param filter_conditions: Conditions to filter reminders
839+
:param sort: Sort parameters (default: [{ field: 'remind_at', direction: 1 }])
840+
:param options: Additional query options like limit, offset
841+
:return: API response with reminders
842+
"""
843+
params = options.copy()
844+
params["filter_conditions"] = filter_conditions or {}
845+
params["sort"] = sort or [{"field": "remind_at", "direction": 1}]
846+
params["user_id"] = user_id
847+
return self.post("reminders/query", data=params)
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import uuid
2+
from datetime import datetime, timedelta, timezone
3+
4+
import pytest
5+
6+
from stream_chat.async_chat import StreamChatAsync
7+
from stream_chat.base.exceptions import StreamAPIException
8+
9+
10+
class TestReminders:
11+
@pytest.mark.asyncio
12+
async def test_create_reminder(self, client: StreamChatAsync, channel, random_user):
13+
# First, send a message to create a reminder for
14+
message_data = {
15+
"text": "This is a test message for reminder",
16+
}
17+
response = await channel.send_message(message_data, random_user["id"])
18+
message_id = response["message"]["id"]
19+
20+
# Create a reminder without remind_at
21+
response = await client.create_reminder(message_id, random_user["id"])
22+
# Verify the response contains the expected data
23+
assert response is not None
24+
assert "reminder" in response
25+
assert response["reminder"]["message_id"] == message_id
26+
assert "user_id" in response["reminder"]
27+
28+
# Clean up - try to delete the reminder
29+
try:
30+
await client.delete_reminder(message_id, random_user["id"])
31+
except StreamAPIException:
32+
pass # It's okay if deletion fails
33+
34+
@pytest.mark.asyncio
35+
async def test_create_reminder_with_remind_at(
36+
self, client: StreamChatAsync, channel, random_user
37+
):
38+
# First, send a message to create a reminder for
39+
message_data = {
40+
"text": "This is a test message for reminder with time",
41+
}
42+
response = await channel.send_message(message_data, random_user["id"])
43+
message_id = response["message"]["id"]
44+
45+
# Create a reminder with remind_at
46+
remind_at = datetime.now(timezone.utc) + timedelta(days=1)
47+
response = await client.create_reminder(
48+
message_id, random_user["id"], remind_at
49+
)
50+
# Verify the response contains the expected data
51+
assert response is not None
52+
assert "reminder" in response
53+
assert response["reminder"]["message_id"] == message_id
54+
assert "user_id" in response["reminder"]
55+
assert "remind_at" in response["reminder"]
56+
57+
# Clean up - try to delete the reminder
58+
try:
59+
await client.delete_reminder(message_id, random_user["id"])
60+
except StreamAPIException:
61+
pass # It's okay if deletion fails
62+
63+
@pytest.mark.asyncio
64+
async def test_update_reminder(self, client: StreamChatAsync, channel, random_user):
65+
# First, send a message to create a reminder for
66+
message_data = {
67+
"text": "This is a test message for updating reminder",
68+
}
69+
response = await channel.send_message(message_data, random_user["id"])
70+
message_id = response["message"]["id"]
71+
72+
# Create a reminder
73+
await client.create_reminder(message_id, random_user["id"])
74+
75+
# Update the reminder with a remind_at time
76+
remind_at = datetime.now(timezone.utc) + timedelta(days=2)
77+
response = await client.update_reminder(
78+
message_id, random_user["id"], remind_at
79+
)
80+
# Verify the response contains the expected data
81+
assert response is not None
82+
assert "reminder" in response
83+
assert response["reminder"]["message_id"] == message_id
84+
assert "user_id" in response["reminder"]
85+
assert "remind_at" in response["reminder"]
86+
87+
# Clean up - try to delete the reminder
88+
try:
89+
await client.delete_reminder(message_id, random_user["id"])
90+
except StreamAPIException:
91+
pass # It's okay if deletion fails
92+
93+
@pytest.mark.asyncio
94+
async def test_delete_reminder(self, client: StreamChatAsync, channel, random_user):
95+
# First, send a message to create a reminder for
96+
message_data = {
97+
"text": "This is a test message for deleting reminder",
98+
}
99+
response = await channel.send_message(message_data, random_user["id"])
100+
message_id = response["message"]["id"]
101+
102+
# Create a reminder
103+
await client.create_reminder(message_id, random_user["id"])
104+
105+
# Delete the reminder
106+
response = await client.delete_reminder(message_id, random_user["id"])
107+
# Verify the response contains the expected data
108+
assert response is not None
109+
# The delete response may not include the reminder object
110+
111+
@pytest.mark.asyncio
112+
async def test_query_reminders(self, client: StreamChatAsync, channel, random_user):
113+
# First, send messages to create reminders for
114+
message_ids = []
115+
channel_cid = channel.cid
116+
117+
for i in range(3):
118+
message_data = {
119+
"text": f"This is test message {i} for querying reminders",
120+
}
121+
response = await channel.send_message(message_data, random_user["id"])
122+
message_id = response["message"]["id"]
123+
message_ids.append(message_id)
124+
125+
# Create a reminder with different remind_at times
126+
remind_at = datetime.now(timezone.utc) + timedelta(hours=i + 1)
127+
await client.create_reminder(message_id, random_user["id"], remind_at)
128+
129+
# Test case 1: Query reminders without filters
130+
response = await client.query_reminders(random_user["id"])
131+
assert response is not None
132+
assert "reminders" in response
133+
# Check that we have at least our 3 reminders
134+
assert len(response["reminders"]) >= 3
135+
136+
# Check that at least some of our message IDs are in the results
137+
found_ids = [
138+
r["message_id"]
139+
for r in response["reminders"]
140+
if r["message_id"] in message_ids
141+
]
142+
assert len(found_ids) > 0
143+
144+
# Test case 2: Query reminders by message ID
145+
if len(message_ids) > 0:
146+
filter_conditions = {"message_id": {"$in": [message_ids[0]]}}
147+
response = await client.query_reminders(
148+
random_user["id"], filter_conditions
149+
)
150+
assert response is not None
151+
152+
# Test case 3: Query reminders by channel CID
153+
filter_conditions = {"channel_cid": channel_cid}
154+
response = await client.query_reminders(random_user["id"], filter_conditions)
155+
assert response is not None
156+
157+
# Clean up - try to delete the reminders
158+
for message_id in message_ids:
159+
try:
160+
await client.delete_reminder(message_id, random_user["id"])
161+
except StreamAPIException:
162+
pass # It's okay if deletion fails

0 commit comments

Comments
 (0)