diff --git a/build.gradle b/build.gradle index 1bc209043..a63acc598 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,7 @@ dependencies { testImplementation 'org.apache.commons:commons-lang3:3.12.0' compileOnly 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32' + compileOnly 'org.jetbrains:annotations:24.1.0' testCompileOnly 'org.projectlombok:lombok:1.18.32' testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' diff --git a/src/main/java/io/getstream/chat/java/models/Channel.java b/src/main/java/io/getstream/chat/java/models/Channel.java index 2d99aee3d..4a796a2e5 100644 --- a/src/main/java/io/getstream/chat/java/models/Channel.java +++ b/src/main/java/io/getstream/chat/java/models/Channel.java @@ -423,6 +423,10 @@ public static class ConfigOverridesRequestObject { @Nullable @JsonProperty("user_message_reminders") private Boolean userMessageReminders; + + @Nullable + @JsonProperty("shared_locations") + private Boolean sharedLocations; } @Builder @@ -1234,6 +1238,10 @@ public static class ChannelGetResponse extends StreamResponseObject { @Nullable @JsonProperty("hide_messages_before") private Date hideMessagesBefore; + + @Nullable + @JsonProperty("active_live_locations") + private List activeLiveLocations; } @Data diff --git a/src/main/java/io/getstream/chat/java/models/Message.java b/src/main/java/io/getstream/chat/java/models/Message.java index f037c063a..7b4f19ac3 100644 --- a/src/main/java/io/getstream/chat/java/models/Message.java +++ b/src/main/java/io/getstream/chat/java/models/Message.java @@ -168,6 +168,10 @@ public class Message { @JsonProperty("pinned_at") private Date pinnedAt; + @Nullable + @JsonProperty("shared_location") + private SharedLocation sharedLocation; + @NotNull @JsonIgnore private Map additionalFields = new HashMap<>(); @JsonAnyGetter @@ -491,6 +495,16 @@ public static class MessageRequestObject { @JsonProperty("pinned_at") private Date pinnedAt; + @Nullable + @JsonProperty("shared_location") + private SharedLocation sharedLocation; + + @NotNull + public MessageRequestObject setSharedLocation(@Nullable SharedLocation sharedLocation) { + this.sharedLocation = sharedLocation; + return this; + } + @Singular @Nullable @JsonIgnore private Map additionalFields; @JsonAnyGetter diff --git a/src/main/java/io/getstream/chat/java/models/SharedLocation.java b/src/main/java/io/getstream/chat/java/models/SharedLocation.java new file mode 100644 index 000000000..32ce19938 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/models/SharedLocation.java @@ -0,0 +1,168 @@ +package io.getstream.chat.java.models; + +import com.fasterxml.jackson.annotation.*; +import io.getstream.chat.java.models.framework.StreamRequest; +import io.getstream.chat.java.models.framework.StreamResponseObject; +import io.getstream.chat.java.services.SharedLocationService; +import io.getstream.chat.java.services.framework.Client; +import java.util.Date; +import java.util.List; +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import retrofit2.Call; + +@Data +@NoArgsConstructor +public class SharedLocation { + @JsonProperty("channel_cid") + private String channelCid; + + @JsonProperty("created_at") + private Date createdAt; + + @JsonProperty("created_by_device_id") + private String createdByDeviceId; + + @JsonProperty("end_at") + private Date endAt; + + private Double latitude; + private Double longitude; + + @JsonProperty("message_id") + private String messageId; + + @JsonProperty("updated_at") + private Date updatedAt; + + @JsonProperty("user_id") + private String userId; + + @Data + @NoArgsConstructor + public static class SharedLocationRequest { + @JsonProperty("message_id") + private String messageId; + + @JsonProperty("created_by_device_id") + private String createdByDeviceId; + + @Nullable + @JsonProperty("end_at") + private String endAt; + + @Nullable private Double latitude; + + @Nullable private Double longitude; + + @JsonProperty("user_id") + private String userId; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class SharedLocationResponse extends StreamResponseObject { + @JsonProperty("created_by_device_id") + private String createdByDeviceId; + + @JsonProperty("end_at") + private String endAt; + + private Double latitude; + private Double longitude; + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class ActiveLiveLocationsResponse extends StreamResponseObject { + @JsonProperty("active_live_locations") + private List activeLiveLocations; + } + + public static class UpdateLocationRequestData { + @NotNull + @JsonProperty("request") + private SharedLocationRequest request; + + public UpdateLocationRequestData() {} + + public UpdateLocationRequestData(SharedLocationRequest request) { + this.request = request; + } + + public SharedLocationRequest getRequest() { + return request; + } + + public void setRequest(SharedLocationRequest request) { + this.request = request; + } + + public static class UpdateLocationRequest extends StreamRequest { + private SharedLocationRequest request; + private String userId; + + public UpdateLocationRequest() {} + + public UpdateLocationRequest(SharedLocationRequest request) { + this.request = request; + } + + public UpdateLocationRequest request(SharedLocationRequest request) { + this.request = request; + return this; + } + + public UpdateLocationRequest userId(String userId) { + this.userId = userId; + return this; + } + + @Override + protected Call generateCall(Client client) { + return client.create(SharedLocationService.class).updateLiveLocation(userId, request); + } + } + } + + public static class GetLocationsRequestData { + public static class GetLocationsRequest extends StreamRequest { + private String userId; + + public GetLocationsRequest() {} + + public GetLocationsRequest userId(String userId) { + this.userId = userId; + return this; + } + + @Override + protected Call generateCall(Client client) { + return client.create(SharedLocationService.class).getLiveLocations(userId); + } + } + } + + /** + * Creates an update location request + * + * @return the created request + */ + @NotNull + public static UpdateLocationRequestData.UpdateLocationRequest updateLocation() { + return new UpdateLocationRequestData.UpdateLocationRequest(); + } + + /** + * Creates a get locations request + * + * @return the created request + */ + @NotNull + public static GetLocationsRequestData.GetLocationsRequest getLocations() { + return new GetLocationsRequestData.GetLocationsRequest(); + } +} diff --git a/src/main/java/io/getstream/chat/java/services/SharedLocationService.java b/src/main/java/io/getstream/chat/java/services/SharedLocationService.java new file mode 100644 index 000000000..580cb1ca8 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/services/SharedLocationService.java @@ -0,0 +1,17 @@ +package io.getstream.chat.java.services; + +import io.getstream.chat.java.models.SharedLocation; +import org.jetbrains.annotations.NotNull; +import retrofit2.Call; +import retrofit2.http.*; + +public interface SharedLocationService { + @GET("users/live_locations") + Call getLiveLocations( + @NotNull @Query("user_id") String userId); + + @PUT("users/live_locations") + Call updateLiveLocation( + @NotNull @Query("user_id") String userId, + @NotNull @Body SharedLocation.SharedLocationRequest sharedLocationRequest); +} diff --git a/src/test/java/io/getstream/chat/java/SharedLocationTest.java b/src/test/java/io/getstream/chat/java/SharedLocationTest.java new file mode 100644 index 000000000..ee21a7135 --- /dev/null +++ b/src/test/java/io/getstream/chat/java/SharedLocationTest.java @@ -0,0 +1,243 @@ +package io.getstream.chat.java; + +import io.getstream.chat.java.exceptions.StreamException; +import io.getstream.chat.java.models.Channel; +import io.getstream.chat.java.models.Channel.ChannelGetResponse; +import io.getstream.chat.java.models.Channel.ChannelRequestObject; +import io.getstream.chat.java.models.Message; +import io.getstream.chat.java.models.Message.MessageRequestObject; +import io.getstream.chat.java.models.Message.MessageType; +import io.getstream.chat.java.models.SharedLocation; +import io.getstream.chat.java.models.SharedLocation.ActiveLiveLocationsResponse; +import io.getstream.chat.java.models.SharedLocation.SharedLocationRequest; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class SharedLocationTest extends BasicTest { + + @BeforeAll + static void setupSharedLocations() { + Map configOverrides = new HashMap<>(); + configOverrides.put("shared_locations", true); + + Assertions.assertDoesNotThrow( + () -> + Channel.partialUpdate(testChannel.getType(), testChannel.getId()) + .setValue("config_overrides", configOverrides) + .request()); + } + + @DisplayName("Can send message with shared location and verify") + @Test + void whenSendingMessageWithSharedLocation_thenCanGetThroughUsersLocations() + throws StreamException, ParseException { + // Create a unique device ID for this test + String deviceId = "device-" + UUID.randomUUID().toString(); + + // Create shared location request + SharedLocationRequest locationRequest = new SharedLocation.SharedLocationRequest(); + locationRequest.setCreatedByDeviceId(deviceId); + locationRequest.setLatitude(40.7128); + locationRequest.setLongitude(-74.0060); + locationRequest.setEndAt("2025-12-31T23:59:59Z"); + locationRequest.setUserId(testUserRequestObject.getId()); + + // Convert request to SharedLocation + SharedLocation sharedLocation = new SharedLocation(); + sharedLocation.setCreatedByDeviceId(locationRequest.getCreatedByDeviceId()); + sharedLocation.setLatitude(locationRequest.getLatitude()); + sharedLocation.setLongitude(locationRequest.getLongitude()); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + sharedLocation.setEndAt(dateFormat.parse(locationRequest.getEndAt())); + + // Send message with shared location + MessageRequestObject messageRequest = + MessageRequestObject.builder() + .text("I'm sharing my live location") + .userId(testUserRequestObject.getId()) + .type(MessageType.REGULAR) + .sharedLocation(sharedLocation) + .build(); + + Message message = + Message.send(testChannel.getType(), testChannel.getId()) + .message(messageRequest) + .request() + .getMessage(); + + // Verify message was sent with correct shared location + Assertions.assertNotNull(message); + Assertions.assertNotNull(message.getSharedLocation()); + Assertions.assertEquals(deviceId, message.getSharedLocation().getCreatedByDeviceId()); + Assertions.assertEquals(40.7128, message.getSharedLocation().getLatitude()); + Assertions.assertEquals(-74.0060, message.getSharedLocation().getLongitude()); + + // Parse and verify the endAt date + Date expectedEndAt = dateFormat.parse("2025-12-31T23:59:59Z"); + Assertions.assertEquals(expectedEndAt, message.getSharedLocation().getEndAt()); + } + + @DisplayName("Can create live location, update it and verify the update") + @Test + void whenUpdatingLiveLocation_thenCanGetUpdatedLocation() throws StreamException, ParseException { + // Create a unique device ID for this test + String deviceId = "device-" + UUID.randomUUID().toString(); + + // Create initial shared location request + SharedLocationRequest initialLocationRequest = new SharedLocation.SharedLocationRequest(); + initialLocationRequest.setCreatedByDeviceId(deviceId); + initialLocationRequest.setLatitude(40.7128); + initialLocationRequest.setLongitude(-74.0060); + initialLocationRequest.setEndAt("2025-12-31T23:59:59Z"); + initialLocationRequest.setUserId(testUserRequestObject.getId()); + + // Convert request to SharedLocation + SharedLocation initialSharedLocation = new SharedLocation(); + initialSharedLocation.setCreatedByDeviceId(initialLocationRequest.getCreatedByDeviceId()); + initialSharedLocation.setLatitude(initialLocationRequest.getLatitude()); + initialSharedLocation.setLongitude(initialLocationRequest.getLongitude()); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + initialSharedLocation.setEndAt(dateFormat.parse(initialLocationRequest.getEndAt())); + + // Send initial message with shared location + MessageRequestObject initialMessageRequest = + MessageRequestObject.builder() + .text("I'm sharing my live location") + .userId(testUserRequestObject.getId()) + .type(MessageType.REGULAR) + .sharedLocation(initialSharedLocation) + .build(); + + Message initialMessage = + Message.send(testChannel.getType(), testChannel.getId()) + .message(initialMessageRequest) + .request() + .getMessage(); + + // Create updated location request + SharedLocationRequest updatedLocationRequest = new SharedLocation.SharedLocationRequest(); + updatedLocationRequest.setMessageId(initialMessage.getId()); + updatedLocationRequest.setCreatedByDeviceId(deviceId); + updatedLocationRequest.setLatitude(40.7589); // Updated latitude + updatedLocationRequest.setLongitude(-73.9851); // Updated longitude + updatedLocationRequest.setEndAt("2025-12-31T23:59:59Z"); + updatedLocationRequest.setUserId(testUserRequestObject.getId()); + + // Update the location + SharedLocation.SharedLocationResponse updateResponse = + SharedLocation.updateLocation() + .userId(testUserRequestObject.getId()) + .request(updatedLocationRequest) + .request(); + + // Get active live locations + ActiveLiveLocationsResponse activeLocations = + SharedLocation.getLocations().userId(testUserRequestObject.getId()).request(); + + // Verify the updated location + Assertions.assertNotNull(activeLocations); + Assertions.assertNotNull(activeLocations.getActiveLiveLocations()); + Assertions.assertFalse(activeLocations.getActiveLiveLocations().isEmpty()); + + // Find our location in the response + SharedLocation updatedLocation = + activeLocations.getActiveLiveLocations().stream() + .filter(loc -> deviceId.equals(loc.getCreatedByDeviceId())) + .findFirst() + .orElse(null); + + Assertions.assertNotNull(updatedLocation); + Assertions.assertEquals(deviceId, updatedLocation.getCreatedByDeviceId()); + Assertions.assertEquals(40.7589, updatedLocation.getLatitude()); + Assertions.assertEquals(-73.9851, updatedLocation.getLongitude()); + + // Verify the endAt date + Date expectedEndAt = dateFormat.parse("2025-12-31T23:59:59Z"); + Assertions.assertEquals(expectedEndAt, updatedLocation.getEndAt()); + } + + @DisplayName("Can verify live location in channel") + @Test + void whenQueryingChannel_thenShouldHaveLiveLocation() throws StreamException, ParseException { + // Create a unique device ID for this test + String deviceId = "device-" + UUID.randomUUID().toString(); + + // Create shared location request + SharedLocationRequest locationRequest = new SharedLocation.SharedLocationRequest(); + locationRequest.setCreatedByDeviceId(deviceId); + locationRequest.setLatitude(40.7128); + locationRequest.setLongitude(-74.0060); + locationRequest.setEndAt("2025-12-31T23:59:59Z"); + locationRequest.setUserId(testUserRequestObject.getId()); + + // Convert request to SharedLocation + SharedLocation sharedLocation = new SharedLocation(); + sharedLocation.setCreatedByDeviceId(locationRequest.getCreatedByDeviceId()); + sharedLocation.setLatitude(locationRequest.getLatitude()); + sharedLocation.setLongitude(locationRequest.getLongitude()); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + sharedLocation.setEndAt(dateFormat.parse(locationRequest.getEndAt())); + + // Send message with shared location + MessageRequestObject messageRequest = + MessageRequestObject.builder() + .text("I'm sharing my live location") + .userId(testUserRequestObject.getId()) + .type(MessageType.REGULAR) + .sharedLocation(sharedLocation) + .build(); + + Message message = + Message.send(testChannel.getType(), testChannel.getId()) + .message(messageRequest) + .request() + .getMessage(); + + // Verify message was sent with correct shared location + Assertions.assertNotNull(message); + Assertions.assertNotNull(message.getSharedLocation()); + Assertions.assertEquals(deviceId, message.getSharedLocation().getCreatedByDeviceId()); + Assertions.assertEquals(40.7128, message.getSharedLocation().getLatitude()); + Assertions.assertEquals(-74.0060, message.getSharedLocation().getLongitude()); + + // Parse and verify the endAt date + Date expectedEndAt = dateFormat.parse("2025-12-31T23:59:59Z"); + Assertions.assertEquals(expectedEndAt, message.getSharedLocation().getEndAt()); + + // Query the channel to verify it has the live location + ChannelGetResponse response = + Channel.getOrCreate(testChannel.getType(), testChannel.getId()) + .data(ChannelRequestObject.builder().createdBy(testUserRequestObject).build()) + .request(); + + // Verify the channel has active live locations + Assertions.assertNotNull(response.getActiveLiveLocations()); + Assertions.assertFalse(response.getActiveLiveLocations().isEmpty()); + + // Find our location in the active live locations + SharedLocation channelLocation = + response.getActiveLiveLocations().stream() + .filter(loc -> deviceId.equals(loc.getCreatedByDeviceId())) + .findFirst() + .orElse(null); + + // Verify the location details + Assertions.assertNotNull(channelLocation); + Assertions.assertEquals(deviceId, channelLocation.getCreatedByDeviceId()); + Assertions.assertEquals(40.7128, channelLocation.getLatitude()); + Assertions.assertEquals(-74.0060, channelLocation.getLongitude()); + Assertions.assertEquals(expectedEndAt, channelLocation.getEndAt()); + } +}