Skip to content

Commit f23705d

Browse files
rafaelmf3Rafael Marinho
andauthored
[CHA-648]: feat(CHA-769) Shared location support (#201)
* feat(cha-769): add support to live locations * test(cha-769): add tests * feat(cha-769): support shared locations * refact(cha-769): reformat * refact(cha-769): lint * fix(cha-769): mypy fix apply * fix(cha-769): lint * lint(cha-648) fix lint * tests(cha-648) fix unit tests * tests(cha-648) fix unit tests * fix lint --------- Co-authored-by: Rafael Marinho <[email protected]>
1 parent a63b949 commit f23705d

File tree

6 files changed

+227
-4
lines changed

6 files changed

+227
-4
lines changed

stream_chat/async_chat/client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
SegmentType,
3030
SegmentUpdatableFields,
3131
)
32+
from stream_chat.types.shared_locations import SharedLocationsOptions
3233

3334
if sys.version_info >= (3, 8):
3435
from typing import Literal
@@ -859,16 +860,12 @@ async def query_drafts(
859860
data: Dict[str, Union[str, Dict[str, Any], List[SortParam]]] = {
860861
"user_id": user_id
861862
}
862-
863863
if filter is not None:
864864
data["filter"] = cast(dict, filter)
865-
866865
if sort is not None:
867866
data["sort"] = cast(dict, sort)
868-
869867
if options is not None:
870868
data.update(cast(dict, options))
871-
872869
return await self.post("drafts/query", data=data)
873870

874871
async def create_reminder(
@@ -956,6 +953,22 @@ async def query_reminders(
956953
params["user_id"] = user_id
957954
return await self.post("reminders/query", data=params)
958955

956+
async def get_user_locations(self, user_id: str, **options: Any) -> StreamResponse:
957+
params = {"user_id": user_id, **options}
958+
return await self.get("users/live_locations", params=params)
959+
960+
async def update_user_location(
961+
self,
962+
user_id: str,
963+
message_id: str,
964+
options: Optional[SharedLocationsOptions] = None,
965+
) -> StreamResponse:
966+
data = {"message_id": message_id}
967+
if options is not None:
968+
data.update(cast(dict, options))
969+
params = {"user_id": user_id, **options}
970+
return await self.put("users/live_locations", data=data, params=params)
971+
959972
async def close(self) -> None:
960973
await self.session.close()
961974

stream_chat/base/client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
SegmentType,
1818
SegmentUpdatableFields,
1919
)
20+
from stream_chat.types.shared_locations import SharedLocationsOptions
2021

2122
if sys.version_info >= (3, 8):
2223
from typing import Literal
@@ -1505,6 +1506,27 @@ def query_reminders(
15051506
"""
15061507
pass
15071508

1509+
@abc.abstractmethod
1510+
def get_user_locations(
1511+
self, user_id: str, **options: Any
1512+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
1513+
"""
1514+
Get the locations of a user.
1515+
"""
1516+
pass
1517+
1518+
@abc.abstractmethod
1519+
def update_user_location(
1520+
self,
1521+
user_id: str,
1522+
message_id: str,
1523+
options: Optional[SharedLocationsOptions] = None,
1524+
) -> Union[StreamResponse, Awaitable[StreamResponse]]:
1525+
"""
1526+
Update the location of a user.
1527+
"""
1528+
pass
1529+
15081530
#####################
15091531
# Private methods #
15101532
#####################

stream_chat/client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
SegmentType,
1919
SegmentUpdatableFields,
2020
)
21+
from stream_chat.types.shared_locations import SharedLocationsOptions
2122

2223
if sys.version_info >= (3, 8):
2324
from typing import Literal
@@ -898,3 +899,19 @@ def query_reminders(
898899
params["sort"] = sort or [{"field": "remind_at", "direction": 1}]
899900
params["user_id"] = user_id
900901
return self.post("reminders/query", data=params)
902+
903+
def get_user_locations(self, user_id: str, **options: Any) -> StreamResponse:
904+
params = {"user_id": user_id, **options}
905+
return self.get("users/live_locations", params=params)
906+
907+
def update_user_location(
908+
self,
909+
user_id: str,
910+
message_id: str,
911+
options: Optional[SharedLocationsOptions] = None,
912+
) -> StreamResponse:
913+
data = {"message_id": message_id}
914+
if options is not None:
915+
data.update(cast(dict, options))
916+
params = {"user_id": user_id, **options}
917+
return self.put("users/live_locations", data=data, params=params)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import datetime
2+
from typing import Dict
3+
4+
import pytest
5+
6+
from stream_chat.async_chat.client import StreamChatAsync
7+
8+
9+
@pytest.mark.incremental
10+
class TestLiveLocations:
11+
@pytest.fixture(autouse=True)
12+
@pytest.mark.asyncio
13+
async def setup_channel_for_shared_locations(self, channel):
14+
await channel.update_partial(
15+
{"config_overrides": {"shared_locations": True}},
16+
)
17+
yield
18+
await channel.update_partial(
19+
{"config_overrides": {"shared_locations": False}},
20+
)
21+
22+
async def test_get_user_locations(
23+
self, client: StreamChatAsync, channel, random_user: Dict
24+
):
25+
# Create a message to attach location to
26+
now = datetime.datetime.now(datetime.timezone.utc)
27+
one_hour_later = now + datetime.timedelta(hours=1)
28+
shared_location = {
29+
"created_by_device_id": "test_device_id",
30+
"latitude": 37.7749,
31+
"longitude": -122.4194,
32+
"end_at": one_hour_later.isoformat(),
33+
}
34+
35+
channel.send_message(
36+
{"text": "Message with location", "shared_location": shared_location},
37+
random_user["id"],
38+
)
39+
40+
# Get user locations
41+
response = await client.get_user_locations(random_user["id"])
42+
43+
assert "active_live_locations" in response
44+
assert isinstance(response["active_live_locations"], list)
45+
46+
async def test_update_user_location(
47+
self, client: StreamChatAsync, channel, random_user: Dict
48+
):
49+
# Create a message to attach location to
50+
now = datetime.datetime.now(datetime.timezone.utc)
51+
one_hour_later = now + datetime.timedelta(hours=1)
52+
shared_location = {
53+
"created_by_device_id": "test_device_id",
54+
"latitude": 37.7749,
55+
"longitude": -122.4194,
56+
"end_at": one_hour_later.isoformat(),
57+
}
58+
59+
msg = await channel.send_message(
60+
{"text": "Message with location", "shared_location": shared_location},
61+
random_user["id"],
62+
)
63+
message_id = msg["message"]["id"]
64+
65+
# Update user location
66+
location_data = {
67+
"created_by_device_id": "test_device_id",
68+
"latitude": 37.7749,
69+
"longitude": -122.4194,
70+
}
71+
response = await client.update_user_location(
72+
random_user["id"], message_id, location_data
73+
)
74+
75+
assert response["latitude"] == location_data["latitude"]
76+
assert response["longitude"] == location_data["longitude"]
77+
78+
# Get user locations to verify
79+
locations_response = await client.get_user_locations(random_user["id"])
80+
assert "active_live_locations" in locations_response
81+
assert len(locations_response["active_live_locations"]) > 0
82+
location = locations_response["active_live_locations"][0]
83+
assert location["latitude"] == location_data["latitude"]
84+
assert location["longitude"] == location_data["longitude"]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import datetime
2+
from typing import Dict
3+
4+
import pytest
5+
6+
from stream_chat import StreamChat
7+
8+
9+
@pytest.mark.incremental
10+
class TestLiveLocations:
11+
@pytest.fixture(autouse=True)
12+
def setup_channel_for_shared_locations(self, channel):
13+
channel.update_partial(
14+
{"config_overrides": {"shared_locations": True}},
15+
)
16+
yield
17+
channel.update_partial(
18+
{"config_overrides": {"shared_locations": False}},
19+
)
20+
21+
def test_get_user_locations(self, client: StreamChat, channel, random_user: Dict):
22+
# Create a message to attach location to
23+
now = datetime.datetime.now(datetime.timezone.utc)
24+
one_hour_later = now + datetime.timedelta(hours=1)
25+
shared_location = {
26+
"created_by_device_id": "test_device_id",
27+
"latitude": 37.7749,
28+
"longitude": -122.4194,
29+
"end_at": one_hour_later.isoformat(),
30+
}
31+
32+
channel.send_message(
33+
{"text": "Message with location", "shared_location": shared_location},
34+
random_user["id"],
35+
)
36+
37+
# Get user locations
38+
response = client.get_user_locations(random_user["id"])
39+
40+
assert "active_live_locations" in response
41+
assert isinstance(response["active_live_locations"], list)
42+
43+
def test_update_user_location(self, client: StreamChat, channel, random_user: Dict):
44+
# Create a message to attach location to
45+
now = datetime.datetime.now(datetime.timezone.utc)
46+
one_hour_later = now + datetime.timedelta(hours=1)
47+
shared_location = {
48+
"created_by_device_id": "test_device_id",
49+
"latitude": 37.7749,
50+
"longitude": -122.4194,
51+
"end_at": one_hour_later.isoformat(),
52+
}
53+
54+
msg = channel.send_message(
55+
{"text": "Message with location", "shared_location": shared_location},
56+
random_user["id"],
57+
)
58+
message_id = msg["message"]["id"]
59+
60+
# Update user location
61+
location_data = {
62+
"created_by_device_id": "test_device_id",
63+
"latitude": 37.7749,
64+
"longitude": -122.4194,
65+
}
66+
response = client.update_user_location(
67+
random_user["id"], message_id, location_data
68+
)
69+
70+
assert response["latitude"] == location_data["latitude"]
71+
assert response["longitude"] == location_data["longitude"]
72+
73+
# Get user locations to verify
74+
locations_response = client.get_user_locations(random_user["id"])
75+
assert "active_live_locations" in locations_response
76+
assert len(locations_response["active_live_locations"]) > 0
77+
location = locations_response["active_live_locations"][0]
78+
assert location["latitude"] == location_data["latitude"]
79+
assert location["longitude"] == location_data["longitude"]

stream_chat/types/shared_locations.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from datetime import datetime
2+
from typing import Optional, TypedDict
3+
4+
5+
class SharedLocationsOptions(TypedDict):
6+
longitude: Optional[int]
7+
latitude: Optional[int]
8+
end_at: Optional[datetime]

0 commit comments

Comments
 (0)