From f8cd8c7b95b063c49f4d77c26ca7b9ca81e78aff Mon Sep 17 00:00:00 2001 From: Ali Momen Sani Date: Tue, 1 Apr 2025 11:12:47 +0200 Subject: [PATCH] feat: add draft endpoints --- .../io/getstream/chat/java/models/Draft.java | 334 ++++++++++++++++++ .../chat/java/services/DraftService.java | 67 ++++ .../getstream/chat/java/ChannelDraftTest.java | 276 +++++++++++++++ 3 files changed, 677 insertions(+) create mode 100644 src/main/java/io/getstream/chat/java/models/Draft.java create mode 100644 src/main/java/io/getstream/chat/java/services/DraftService.java create mode 100644 src/test/java/io/getstream/chat/java/ChannelDraftTest.java diff --git a/src/main/java/io/getstream/chat/java/models/Draft.java b/src/main/java/io/getstream/chat/java/models/Draft.java new file mode 100644 index 000000000..4d80c117f --- /dev/null +++ b/src/main/java/io/getstream/chat/java/models/Draft.java @@ -0,0 +1,334 @@ +package io.getstream.chat.java.models; + +import com.fasterxml.jackson.annotation.*; +import io.getstream.chat.java.exceptions.StreamException; +import io.getstream.chat.java.models.Message.Attachment; +import io.getstream.chat.java.models.Message.MessageRequestObject; +import io.getstream.chat.java.models.User.UserRequestObject; +import io.getstream.chat.java.models.framework.StreamRequest; +import io.getstream.chat.java.models.framework.StreamResponseObject; +import io.getstream.chat.java.services.DraftService; +import io.getstream.chat.java.services.framework.Client; +import java.util.Date; +import java.util.List; +import java.util.Map; +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import retrofit2.Call; + +/** Represents draft message functionality in Stream Chat. */ +@Data +public class Draft { + /** A draft message. */ + @Data + @NoArgsConstructor + public static class DraftMessage { + @Nullable + @JsonProperty("id") + private String id; + + @Nullable + @JsonProperty("text") + private String text; + + @Nullable + @JsonProperty("html") + private String html; + + @Nullable + @JsonProperty("mml") + private String mml; + + @Nullable + @JsonProperty("parent_id") + private String parentId; + + @Nullable + @JsonProperty("show_in_channel") + private Boolean showInChannel; + + @Nullable + @JsonProperty("attachments") + private List attachments; + + @Nullable + @JsonProperty("mentioned_users") + private List mentionedUsers; + + @Nullable + @JsonProperty("custom") + private Map custom; + + @Nullable + @JsonProperty("quoted_message_id") + private String quotedMessageId; + + @Nullable + @JsonProperty("type") + private String type; + + @Nullable + @JsonProperty("silent") + private Boolean silent; + + @Nullable + @JsonProperty("poll_id") + private String pollId; + } + + /** A draft object containing the message and metadata. */ + @Data + @NoArgsConstructor + public static class DraftObject { + @NotNull + @JsonProperty("channel_cid") + private String channelCid; + + @NotNull + @JsonProperty("created_at") + private Date createdAt; + + @NotNull + @JsonProperty("message") + private DraftMessage message; + + @Nullable + @JsonProperty("channel") + private Channel channel; + + @Nullable + @JsonProperty("parent_id") + private String parentId; + + @Nullable + @JsonProperty("parent_message") + private Message parentMessage; + + @Nullable + @JsonProperty("quoted_message") + private Message quotedMessage; + } + + /** Response for draft creation. */ + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class CreateDraftResponse extends StreamResponseObject { + @NotNull + @JsonProperty("draft") + private DraftObject draft; + } + + /** Response for getting a draft. */ + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class GetDraftResponse extends StreamResponseObject { + @NotNull + @JsonProperty("draft") + private DraftObject draft; + } + + /** Response for querying drafts. */ + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class QueryDraftsResponse extends StreamResponseObject { + @NotNull + @JsonProperty("drafts") + private List drafts; + + @Nullable + @JsonProperty("next") + private String next; + } + + /** Request data for creating a draft. */ + @Builder( + builderClassName = "CreateDraftRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + public static class CreateDraftRequestData { + @NotNull + @JsonProperty("message") + private MessageRequestObject message; + + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private UserRequestObject user; + + public static class CreateDraftRequest extends StreamRequest { + @NotNull private String channelType; + @NotNull private String channelId; + + private CreateDraftRequest(@NotNull String channelType, @NotNull String channelId) { + this.channelType = channelType; + this.channelId = channelId; + } + + @Override + protected Call generateCall(Client client) throws StreamException { + return client + .create(DraftService.class) + .createDraft(this.channelType, this.channelId, this.internalBuild()); + } + } + } + + /** Request for deleting a draft. */ + public static class DeleteDraftRequest extends StreamRequest { + @NotNull private String channelType; + @NotNull private String channelId; + @Nullable private String userId; + @Nullable private String parentId; + + private DeleteDraftRequest(@NotNull String channelType, @NotNull String channelId) { + this.channelType = channelType; + this.channelId = channelId; + } + + @NotNull + public DeleteDraftRequest userId(@NotNull String userId) { + this.userId = userId; + return this; + } + + @NotNull + public DeleteDraftRequest parentId(@Nullable String parentId) { + this.parentId = parentId; + return this; + } + + @Override + protected Call generateCall(Client client) throws StreamException { + return client + .create(DraftService.class) + .deleteDraft(this.channelType, this.channelId, this.userId, this.parentId); + } + } + + /** Request for getting a draft. */ + public static class GetDraftRequest extends StreamRequest { + @NotNull private String channelType; + @NotNull private String channelId; + @Nullable private String userId; + @Nullable private String parentId; + + private GetDraftRequest(@NotNull String channelType, @NotNull String channelId) { + this.channelType = channelType; + this.channelId = channelId; + } + + @NotNull + public GetDraftRequest userId(@NotNull String userId) { + this.userId = userId; + return this; + } + + @NotNull + public GetDraftRequest parentId(@Nullable String parentId) { + this.parentId = parentId; + return this; + } + + @Override + protected Call generateCall(Client client) throws StreamException { + return client + .create(DraftService.class) + .getDraft(this.channelType, this.channelId, this.userId, this.parentId); + } + } + + /** Request data for querying drafts. */ + @Builder( + builderClassName = "QueryDraftsRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + public static class QueryDraftsRequestData { + @Nullable + @JsonProperty("filter") + private Map filter; + + @Singular + @Nullable + @JsonProperty("sort") + private List sorts; + + @NotNull + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("limit") + private Integer limit; + + @Nullable + @JsonProperty("next") + private String next; + + @Nullable + @JsonProperty("prev") + private String prev; + + public static class QueryDraftsRequest extends StreamRequest { + public QueryDraftsRequest() {} + + @Override + protected Call generateCall(Client client) throws StreamException { + return client.create(DraftService.class).queryDrafts(this.internalBuild()); + } + } + } + + /** + * Creates a draft message in a channel + * + * @param type the channel type + * @param id the channel id + * @return the created request + */ + @NotNull + public static CreateDraftRequestData.CreateDraftRequest createDraft( + @NotNull String type, @NotNull String id) { + return new CreateDraftRequestData.CreateDraftRequest(type, id); + } + + /** + * Deletes a draft message from a channel + * + * @param type the channel type + * @param id the channel id + * @return the created request + */ + @NotNull + public static DeleteDraftRequest deleteDraft(@NotNull String type, @NotNull String id) { + return new DeleteDraftRequest(type, id); + } + + /** + * Gets a draft message from a channel + * + * @param type the channel type + * @param id the channel id + * @return the created request + */ + @NotNull + public static GetDraftRequest getDraft(@NotNull String type, @NotNull String id) { + return new GetDraftRequest(type, id); + } + + /** + * Queries all drafts for a user + * + * @return the created request + */ + @NotNull + public static QueryDraftsRequestData.QueryDraftsRequest queryDrafts() { + return new QueryDraftsRequestData.QueryDraftsRequest(); + } +} diff --git a/src/main/java/io/getstream/chat/java/services/DraftService.java b/src/main/java/io/getstream/chat/java/services/DraftService.java new file mode 100644 index 000000000..9597147e4 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/services/DraftService.java @@ -0,0 +1,67 @@ +package io.getstream.chat.java.services; + +import io.getstream.chat.java.models.Draft.CreateDraftRequestData; +import io.getstream.chat.java.models.Draft.CreateDraftResponse; +import io.getstream.chat.java.models.Draft.GetDraftResponse; +import io.getstream.chat.java.models.Draft.QueryDraftsRequestData; +import io.getstream.chat.java.models.Draft.QueryDraftsResponse; +import io.getstream.chat.java.models.framework.StreamResponseObject; +import org.jetbrains.annotations.Nullable; +import retrofit2.Call; +import retrofit2.http.*; + +/** Service for managing draft messages in channels. */ +public interface DraftService { + /** + * Creates a draft message in a channel. + * + * @param type The channel type + * @param id The channel ID + * @param request The draft creation request data + * @return A response with the created draft + */ + @POST("channels/{type}/{id}/draft") + Call createDraft( + @Path("type") String type, @Path("id") String id, @Body CreateDraftRequestData request); + + /** + * Deletes a draft message from a channel. + * + * @param type The channel type + * @param id The channel ID + * @param userId The user ID + * @param parentId Optional parent message ID + * @return A response indicating success + */ + @DELETE("channels/{type}/{id}/draft") + Call deleteDraft( + @Path("type") String type, + @Path("id") String id, + @Query("user_id") String userId, + @Query("parent_id") @Nullable String parentId); + + /** + * Gets a draft message from a channel. + * + * @param type The channel type + * @param id The channel ID + * @param userId The user ID + * @param parentId Optional parent message ID + * @return A response with the draft + */ + @GET("channels/{type}/{id}/draft") + Call getDraft( + @Path("type") String type, + @Path("id") String id, + @Query("user_id") String userId, + @Query("parent_id") @Nullable String parentId); + + /** + * Queries all drafts for a user. + * + * @param request The query parameters + * @return A response with the matching drafts + */ + @POST("drafts/query") + Call queryDrafts(@Body QueryDraftsRequestData request); +} diff --git a/src/test/java/io/getstream/chat/java/ChannelDraftTest.java b/src/test/java/io/getstream/chat/java/ChannelDraftTest.java new file mode 100644 index 000000000..f7461fee4 --- /dev/null +++ b/src/test/java/io/getstream/chat/java/ChannelDraftTest.java @@ -0,0 +1,276 @@ +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.ChannelMemberRequestObject; +import io.getstream.chat.java.models.Channel.ChannelRequestObject; +import io.getstream.chat.java.models.Draft; +import io.getstream.chat.java.models.FilterCondition; +import io.getstream.chat.java.models.Message.MessageRequestObject; +import io.getstream.chat.java.models.Sort; +import io.getstream.chat.java.models.User; +import io.getstream.chat.java.models.User.UserRequestObject; +import java.util.List; +import java.util.UUID; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Tests for the draft message functionality. */ +public class ChannelDraftTest extends BasicTest { + + @DisplayName("Can create/get/delete a draft message in a channel") + @Test + void whenCreatingDraft_thenNoException() throws StreamException { + // Prepare a draft message request + String text = UUID.randomUUID().toString(); + MessageRequestObject messageRequest = + MessageRequestObject.builder().text(text).userId(testUserRequestObject.getId()).build(); + + // Create the draft + Draft.CreateDraftResponse response = + Draft.createDraft(testChannel.getType(), testChannel.getId()) + .message(messageRequest) + .userId(testUserRequestObject.getId()) + .request(); + + // Verify the response + Assertions.assertNotNull(response); + Assertions.assertNotNull(response.getDraft()); + Assertions.assertEquals(text, response.getDraft().getMessage().getText()); + + // Then get the draft - note that userId is passed as a query param + Draft.GetDraftResponse getResponse = + Draft.getDraft(testChannel.getType(), testChannel.getId()) + .userId(testUserRequestObject.getId()) + .request(); + + // Verify the response + Assertions.assertNotNull(getResponse); + Assertions.assertNotNull(getResponse.getDraft()); + Assertions.assertEquals(text, getResponse.getDraft().getMessage().getText()); + + // Then delete the draft - note that userId is passed as a query param + Draft.deleteDraft(testChannel.getType(), testChannel.getId()) + .userId(testUserRequestObject.getId()) + .request(); + + // Verify that the draft is deleted by trying to get it (should throw an exception) + Assertions.assertThrows( + Exception.class, + () -> + Draft.getDraft(testChannel.getType(), testChannel.getId()) + .userId(testUserRequestObject.getId()) + .request()); + } + + @DisplayName("Can create/get/delete a draft message with a parent message") + @Test + void whenCreatingDraftWithParent_thenNoException() throws StreamException { + // Prepare a draft message request with a parent message + String text = UUID.randomUUID().toString(); + MessageRequestObject messageRequest = + MessageRequestObject.builder() + .text(text) + .userId(testUserRequestObject.getId()) + .parentId(testMessage.getId()) + .build(); + + // Create the draft + Draft.CreateDraftResponse response = + Draft.createDraft(testChannel.getType(), testChannel.getId()) + .message(messageRequest) + .userId(testUserRequestObject.getId()) + .request(); + + // Verify the response + Assertions.assertNotNull(response); + Assertions.assertNotNull(response.getDraft()); + Assertions.assertEquals(text, response.getDraft().getMessage().getText()); + Assertions.assertEquals(testMessage.getId(), response.getDraft().getMessage().getParentId()); + + // Then get the draft with parent_id specified as a query param + Draft.GetDraftResponse getResponse = + Draft.getDraft(testChannel.getType(), testChannel.getId()) + .userId(testUserRequestObject.getId()) + .parentId(testMessage.getId()) + .request(); + + // Verify the response + Assertions.assertNotNull(getResponse); + Assertions.assertNotNull(getResponse.getDraft()); + Assertions.assertEquals(text, getResponse.getDraft().getMessage().getText()); + Assertions.assertEquals(testMessage.getId(), getResponse.getDraft().getMessage().getParentId()); + + // Then delete the draft with parent_id specified as a query param + Draft.deleteDraft(testChannel.getType(), testChannel.getId()) + .userId(testUserRequestObject.getId()) + .parentId(testMessage.getId()) + .request(); + + // Verify that the draft is deleted by trying to get it (should throw an exception) + Assertions.assertThrows( + Exception.class, + () -> + Draft.getDraft(testChannel.getType(), testChannel.getId()) + .userId(testUserRequestObject.getId()) + .parentId(testMessage.getId()) + .request()); + } + + @DisplayName("Can query all drafts for a user") + @Test + void whenQueryingDrafts_thenNoException() throws StreamException { + Draft.QueryDraftsResponse queryResponse = + Draft.queryDrafts().userId(testUserRequestObject.getId()).limit(10).request(); + + // Verify the response + Assertions.assertNotNull(queryResponse); + Assertions.assertNotNull(queryResponse.getDrafts()); + + // Create a draft in the first channel + String text1 = "Draft " + UUID.randomUUID().toString(); + MessageRequestObject messageRequest1 = + MessageRequestObject.builder().text(text1).userId(testUserRequestObject.getId()).build(); + + Draft.createDraft(testChannel.getType(), testChannel.getId()) + .message(messageRequest1) + .userId(testUserRequestObject.getId()) + .request(); + + // Create a second channel and add a draft there + Channel secondChannel = createRandomChannel().getChannel(); + + String text2 = "Draft " + UUID.randomUUID().toString(); + MessageRequestObject messageRequest2 = + MessageRequestObject.builder().text(text2).userId(testUserRequestObject.getId()).build(); + + Draft.createDraft(secondChannel.getType(), secondChannel.getId()) + .message(messageRequest2) + .userId(testUserRequestObject.getId()) + .request(); + + // Query all drafts + Draft.QueryDraftsResponse queryResponse2 = + Draft.queryDrafts().userId(testUserRequestObject.getId()).limit(10).request(); + + // Verify the response + Assertions.assertNotNull(queryResponse2); + Assertions.assertNotNull(queryResponse2.getDrafts()); + Assertions.assertEquals(2, queryResponse2.getDrafts().size()); + } + + @DisplayName("Can query drafts with filters and sort") + @Test + void whenQueryingDraftsWithFiltersAndSort_thenNoException() throws StreamException { + // Create a user + UserRequestObject user = + UserRequestObject.builder() + .id("user-" + RandomStringUtils.randomAlphabetic(10)) + .name("User 1") + .build(); + User.upsert().user(user).request(); + // Create a channel with a draft + String channel1Id = UUID.randomUUID().toString(); + Channel.ChannelGetResponse channel1 = + Channel.getOrCreate("messaging", channel1Id) + .data( + ChannelRequestObject.builder() + .createdBy(user) + .members(List.of(ChannelMemberRequestObject.builder().user(user).build())) + .build()) + .request(); + + // Create a draft in the test channel + String text1 = "Draft in channel 1"; + MessageRequestObject messageRequest1 = + MessageRequestObject.builder().text(text1).userId(user.getId()).build(); + + Draft.createDraft(channel1.getChannel().getType(), channel1.getChannel().getId()) + .message(messageRequest1) + .userId(user.getId()) + .request(); + + // Create another channel with a draft + String channel2Id = UUID.randomUUID().toString(); + Channel.ChannelGetResponse channel2 = + Channel.getOrCreate("messaging", channel2Id) + .data( + ChannelRequestObject.builder() + .createdBy(user) + .members(List.of(ChannelMemberRequestObject.builder().user(user).build())) + .build()) + .request(); + + String text2 = "Draft in channel 2"; + MessageRequestObject messageRequest2 = + MessageRequestObject.builder().text(text2).userId(user.getId()).build(); + + Draft.createDraft(channel2.getChannel().getType(), channel2.getChannel().getId()) + .message(messageRequest2) + .userId(user.getId()) + .request(); + + // Query all drafts for the user + Draft.QueryDraftsResponse response = Draft.queryDrafts().userId(user.getId()).request(); + + Assertions.assertNotNull(response); + Assertions.assertNotNull(response.getDrafts()); + Assertions.assertEquals(2, response.getDrafts().size()); + + // Query drafts for a specific channel + Draft.QueryDraftsResponse channelResponse = + Draft.queryDrafts() + .userId(user.getId()) + .filter(FilterCondition.eq("channel_cid", channel2.getChannel().getCId())) + .request(); + + Assertions.assertNotNull(channelResponse); + Assertions.assertNotNull(channelResponse.getDrafts()); + Assertions.assertEquals(1, channelResponse.getDrafts().size()); + Assertions.assertEquals(text2, channelResponse.getDrafts().get(0).getMessage().getText()); + Assertions.assertEquals( + channel2.getChannel().getCId(), channelResponse.getDrafts().get(0).getChannelCid()); + + // Query drafts with sort + Draft.QueryDraftsResponse sortedResponse = + Draft.queryDrafts() + .userId(user.getId()) + .sort(Sort.builder().field("created_at").direction(Sort.Direction.ASC).build()) + .request(); + + Assertions.assertNotNull(sortedResponse); + Assertions.assertNotNull(sortedResponse.getDrafts()); + Assertions.assertEquals(2, sortedResponse.getDrafts().size()); + Assertions.assertEquals( + channel1.getChannel().getCId(), sortedResponse.getDrafts().get(0).getChannelCid()); + Assertions.assertEquals( + channel2.getChannel().getCId(), sortedResponse.getDrafts().get(1).getChannelCid()); + + // Query drafts with pagination + Draft.QueryDraftsResponse paginatedResponse = + Draft.queryDrafts().userId(user.getId()).limit(1).request(); + + Assertions.assertNotNull(paginatedResponse); + Assertions.assertNotNull(paginatedResponse.getDrafts()); + Assertions.assertEquals(1, paginatedResponse.getDrafts().size()); + Assertions.assertEquals( + channel2.getChannel().getCId(), paginatedResponse.getDrafts().get(0).getChannelCid()); + Assertions.assertNotNull(paginatedResponse.getNext()); + + // Query drafts with next page + Draft.QueryDraftsResponse nextPageResponse = + Draft.queryDrafts() + .userId(user.getId()) + .limit(1) + .next(paginatedResponse.getNext()) + .request(); + + Assertions.assertNotNull(nextPageResponse); + Assertions.assertNotNull(nextPageResponse.getDrafts()); + Assertions.assertEquals(1, nextPageResponse.getDrafts().size()); + Assertions.assertEquals( + channel1.getChannel().getCId(), nextPageResponse.getDrafts().get(0).getChannelCid()); + } +}