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 ffe6d9e7..10f75f6d 100644 --- a/src/main/java/io/getstream/chat/java/models/Channel.java +++ b/src/main/java/io/getstream/chat/java/models/Channel.java @@ -1875,4 +1875,166 @@ public static ChannelMemberPartialUpdateRequest unarchive( public static MarkDeliveredRequest markDelivered() { return new MarkDeliveredRequest(); } + + /** Channel batch operation types */ + public enum ChannelBatchOperation { + @JsonProperty("addMembers") + ADD_MEMBERS, + @JsonProperty("removeMembers") + REMOVE_MEMBERS, + @JsonProperty("inviteMembers") + INVITE_MEMBERS, + @JsonProperty("assignRoles") + ASSIGN_ROLES, + @JsonProperty("addModerators") + ADD_MODERATORS, + @JsonProperty("demoteModerators") + DEMOTE_MODERATORS, + @JsonProperty("hide") + HIDE, + @JsonProperty("show") + SHOW, + @JsonProperty("archive") + ARCHIVE, + @JsonProperty("unarchive") + UNARCHIVE, + @JsonProperty("updateData") + UPDATE_DATA, + @JsonProperty("addFilterTags") + ADD_FILTER_TAGS, + @JsonProperty("removeFilterTags") + REMOVE_FILTER_TAGS + } + + /** Represents a member in batch operations */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ChannelBatchMemberRequest { + @NotNull + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("channel_role") + private String channelRole; + } + + /** Represents data that can be updated on channels in batch */ + @Data + @NoArgsConstructor + public static class ChannelDataUpdate { + @Nullable + @JsonProperty("frozen") + private Boolean frozen; + + @Nullable + @JsonProperty("disabled") + private Boolean disabled; + + @Nullable + @JsonProperty("custom") + private Map custom; + + @Nullable + @JsonProperty("team") + private String team; + + @Nullable + @JsonProperty("config_overrides") + private Map configOverrides; + + @Nullable + @JsonProperty("auto_translation_enabled") + private Boolean autoTranslationEnabled; + + @Nullable + @JsonProperty("auto_translation_language") + private String autoTranslationLanguage; + } + + /** Represents filters for batch channel updates */ + @Data + @NoArgsConstructor + public static class ChannelsBatchFilters { + @Nullable + @JsonProperty("cids") + private Object cids; + + @Nullable + @JsonProperty("types") + private Object types; + + @Nullable + @JsonProperty("filter_tags") + private Object filterTags; + } + + /** Represents options for batch channel updates */ + @Data + @NoArgsConstructor + public static class ChannelsBatchOptions { + @NotNull + @JsonProperty("operation") + private ChannelBatchOperation operation; + + @NotNull + @JsonProperty("filter") + private ChannelsBatchFilters filter; + + @Nullable + @JsonProperty("members") + private List members; + + @Nullable + @JsonProperty("data") + private ChannelDataUpdate data; + + @Nullable + @JsonProperty("filter_tags_update") + private List filterTagsUpdate; + } + + @Getter + @EqualsAndHashCode + @RequiredArgsConstructor + public static class ChannelsBatchUpdateRequest + extends StreamRequest { + @NotNull private ChannelsBatchOptions options; + + @Override + protected Call generateCall(Client client) throws StreamException { + return client.create(ChannelService.class).updateBatch(this.options); + } + } + + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class ChannelsBatchUpdateResponse extends StreamResponseObject { + @NotNull + @JsonProperty("task_id") + private String taskId; + } + + /** + * Creates a batch update request + * + * @param options the batch update options + * @return the created request + */ + @NotNull + public static ChannelsBatchUpdateRequest updateBatch(@NotNull ChannelsBatchOptions options) { + return new ChannelsBatchUpdateRequest(options); + } + + /** + * Returns a ChannelBatchUpdater instance for batch channel operations. + * + * @return ChannelBatchUpdater instance + */ + @NotNull + public static ChannelBatchUpdater channelBatchUpdater() { + return new ChannelBatchUpdater(); + } } diff --git a/src/main/java/io/getstream/chat/java/models/ChannelBatchUpdater.java b/src/main/java/io/getstream/chat/java/models/ChannelBatchUpdater.java new file mode 100644 index 00000000..19114d50 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/models/ChannelBatchUpdater.java @@ -0,0 +1,230 @@ +package io.getstream.chat.java.models; + +import io.getstream.chat.java.models.Channel.*; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** Provides convenience methods for batch channel operations. */ +public class ChannelBatchUpdater { + + /** + * Adds members to channels matching the filter. + * + * @param filter the filter to match channels + * @param members list of members to add + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest addMembers( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.ADD_MEMBERS); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Removes members from channels matching the filter. + * + * @param filter the filter to match channels + * @param members list of members to remove + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest removeMembers( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.REMOVE_MEMBERS); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Invites members to channels matching the filter. + * + * @param filter the filter to match channels + * @param members list of members to invite + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest inviteMembers( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.INVITE_MEMBERS); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Adds moderators to channels matching the filter. + * + * @param filter the filter to match channels + * @param members list of members to add as moderators + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest addModerators( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.ADD_MODERATORS); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Removes moderator role from members in channels matching the filter. + * + * @param filter the filter to match channels + * @param members list of members to demote from moderators + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest demoteModerators( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.DEMOTE_MODERATORS); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Assigns roles to members in channels matching the filter. + * + * @param filter the filter to match channels + * @param members list of members with roles to assign + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest assignRoles( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.ASSIGN_ROLES); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Hides channels matching the filter for the specified members. + * + * @param filter the filter to match channels + * @param members list of members for whom to hide channels + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest hide( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.HIDE); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Shows channels matching the filter for the specified members. + * + * @param filter the filter to match channels + * @param members list of members for whom to show channels + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest show( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.SHOW); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Archives channels matching the filter for the specified members. + * + * @param filter the filter to match channels + * @param members list of members for whom to archive channels + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest archive( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.ARCHIVE); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Unarchives channels matching the filter for the specified members. + * + * @param filter the filter to match channels + * @param members list of members for whom to unarchive channels + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest unarchive( + @NotNull ChannelsBatchFilters filter, @NotNull List members) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.UNARCHIVE); + options.setFilter(filter); + options.setMembers(members); + return Channel.updateBatch(options); + } + + /** + * Updates data on channels matching the filter. + * + * @param filter the filter to match channels + * @param data channel data to update + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest updateData( + @NotNull ChannelsBatchFilters filter, @NotNull ChannelDataUpdate data) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.UPDATE_DATA); + options.setFilter(filter); + options.setData(data); + return Channel.updateBatch(options); + } + + /** + * Adds filter tags to channels matching the filter. + * + * @param filter the filter to match channels + * @param tags list of filter tags to add + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest addFilterTags( + @NotNull ChannelsBatchFilters filter, @NotNull List tags) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.ADD_FILTER_TAGS); + options.setFilter(filter); + options.setFilterTagsUpdate(tags); + return Channel.updateBatch(options); + } + + /** + * Removes filter tags from channels matching the filter. + * + * @param filter the filter to match channels + * @param tags list of filter tags to remove + * @return the batch update request + */ + @NotNull + public ChannelsBatchUpdateRequest removeFilterTags( + @NotNull ChannelsBatchFilters filter, @NotNull List tags) { + ChannelsBatchOptions options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.REMOVE_FILTER_TAGS); + options.setFilter(filter); + options.setFilterTagsUpdate(tags); + return Channel.updateBatch(options); + } +} diff --git a/src/main/java/io/getstream/chat/java/services/ChannelService.java b/src/main/java/io/getstream/chat/java/services/ChannelService.java index 55ca0ea7..82a2b8e2 100644 --- a/src/main/java/io/getstream/chat/java/services/ChannelService.java +++ b/src/main/java/io/getstream/chat/java/services/ChannelService.java @@ -108,4 +108,8 @@ Call updateMemberPartial( Call markDelivered( @NotNull @Body MarkDeliveredRequestData markDeliveredOptions, @Query("user_id") String userId); + + @PUT("channels/batch") + Call updateBatch( + @NotNull @Body Channel.ChannelsBatchOptions options); } diff --git a/src/test/java/io/getstream/chat/java/ChannelBatchUpdaterTest.java b/src/test/java/io/getstream/chat/java/ChannelBatchUpdaterTest.java new file mode 100644 index 00000000..b109e7fe --- /dev/null +++ b/src/test/java/io/getstream/chat/java/ChannelBatchUpdaterTest.java @@ -0,0 +1,355 @@ +package io.getstream.chat.java; + +import io.getstream.chat.java.models.Channel; +import io.getstream.chat.java.models.Channel.*; +import io.getstream.chat.java.models.TaskStatus; +import io.getstream.chat.java.models.TaskStatus.TaskStatusGetResponse; +import io.getstream.chat.java.models.User; +import io.getstream.chat.java.models.User.UserRequestObject; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ChannelBatchUpdaterTest extends BasicTest { + + @Test + @DisplayName("Can update channels batch with valid options") + void whenUpdatingChannelsBatchWithValidOptions_thenNoException() { + Assertions.assertDoesNotThrow( + () -> { + var ch1 = Assertions.assertDoesNotThrow(BasicTest::createRandomChannel).getChannel(); + Assertions.assertNotNull(ch1); + var ch2 = Assertions.assertDoesNotThrow(BasicTest::createRandomChannel).getChannel(); + Assertions.assertNotNull(ch2); + + var userToAdd = + UserRequestObject.builder() + .id(RandomStringUtils.randomAlphabetic(10)) + .name("Test User") + .build(); + User.upsert().user(userToAdd).request(); + + var filter = new ChannelsBatchFilters(); + Map cidsFilter = new HashMap<>(); + cidsFilter.put("$in", List.of(ch1.getCId(), ch2.getCId())); + filter.setCids(cidsFilter); + + var options = new ChannelsBatchOptions(); + options.setOperation(ChannelBatchOperation.ADD_MEMBERS); + options.setFilter(filter); + options.setMembers(List.of(new ChannelBatchMemberRequest(userToAdd.getId(), null))); + + var response = Channel.updateBatch(options).request(); + Assertions.assertNotNull(response.getTaskId()); + Assertions.assertFalse(response.getTaskId().isEmpty()); + }); + } + + @Test + @DisplayName("ChannelBatchUpdater can add members") + void whenAddingMembers_thenNoException() { + Assertions.assertDoesNotThrow( + () -> { + var ch1 = Assertions.assertDoesNotThrow(BasicTest::createRandomChannel).getChannel(); + Assertions.assertNotNull(ch1); + var ch2 = Assertions.assertDoesNotThrow(BasicTest::createRandomChannel).getChannel(); + Assertions.assertNotNull(ch2); + + var user1 = + UserRequestObject.builder() + .id(RandomStringUtils.randomAlphabetic(10)) + .name("Test User 1") + .build(); + var user2 = + UserRequestObject.builder() + .id(RandomStringUtils.randomAlphabetic(10)) + .name("Test User 2") + .build(); + User.upsert().user(user1).user(user2).request(); + var usersToAdd = List.of(user1, user2); + + var filter = new ChannelsBatchFilters(); + Map cidsFilter = new HashMap<>(); + cidsFilter.put("$in", List.of(ch1.getCId(), ch2.getCId())); + filter.setCids(cidsFilter); + + var updater = Channel.channelBatchUpdater(); + var members = + usersToAdd.stream() + .map(user -> new ChannelBatchMemberRequest(user.getId(), null)) + .collect(Collectors.toList()); + + var response = updater.addMembers(filter, members).request(); + Assertions.assertNotNull(response.getTaskId()); + var taskId = response.getTaskId(); + + waitFor( + () -> { + TaskStatusGetResponse taskStatusResponse = + Assertions.assertDoesNotThrow(() -> TaskStatus.get(taskId).request()); + return "completed".equals(taskStatusResponse.getStatus()); + }); + + // Verify members were added to both channels + waitFor( + () -> { + var ch1State = + Assertions.assertDoesNotThrow( + () -> + Channel.getOrCreate(ch1.getType(), ch1.getId()).request().getChannel()); + var ch2State = + Assertions.assertDoesNotThrow( + () -> + Channel.getOrCreate(ch2.getType(), ch2.getId()).request().getChannel()); + if (ch1State.getMembers() == null || ch2State.getMembers() == null) { + return false; + } + var ch1MemberIds = + ch1State.getMembers().stream() + .map(ChannelMember::getUserId) + .collect(Collectors.toList()); + var ch2MemberIds = + ch2State.getMembers().stream() + .map(ChannelMember::getUserId) + .collect(Collectors.toList()); + var userIdsToAdd = + usersToAdd.stream().map(user -> user.getId()).collect(Collectors.toList()); + return ch1MemberIds.containsAll(userIdsToAdd) + && ch2MemberIds.containsAll(userIdsToAdd); + }); + }); + } + + @Test + @DisplayName("ChannelBatchUpdater can remove members") + void whenRemovingMembers_thenNoException() { + Assertions.assertDoesNotThrow( + () -> { + var user1 = + UserRequestObject.builder() + .id(RandomStringUtils.randomAlphabetic(10)) + .name("Test User 1") + .build(); + var user2 = + UserRequestObject.builder() + .id(RandomStringUtils.randomAlphabetic(10)) + .name("Test User 2") + .build(); + User.upsert().user(user1).user(user2).request(); + var membersId = List.of(user1.getId(), user2.getId()); + + var ch1 = + Channel.getOrCreate("messaging", RandomStringUtils.randomAlphabetic(12)) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + membersId.stream() + .map( + id -> + ChannelMemberRequestObject.builder() + .user(UserRequestObject.builder().id(id).build()) + .build()) + .collect(Collectors.toList())) + .build()) + .request() + .getChannel(); + + var ch2 = + Channel.getOrCreate("messaging", RandomStringUtils.randomAlphabetic(12)) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + membersId.stream() + .map( + id -> + ChannelMemberRequestObject.builder() + .user(UserRequestObject.builder().id(id).build()) + .build()) + .collect(Collectors.toList())) + .build()) + .request() + .getChannel(); + + // Verify members are present + var ch1State = Channel.getOrCreate(ch1.getType(), ch1.getId()).request().getChannel(); + var ch2State = Channel.getOrCreate(ch2.getType(), ch2.getId()).request().getChannel(); + Assertions.assertNotNull(ch1State.getMembers()); + Assertions.assertNotNull(ch2State.getMembers()); + Assertions.assertTrue(ch1State.getMembers().size() >= 2); + Assertions.assertTrue(ch2State.getMembers().size() >= 2); + + var ch1MemberIds = + ch1State.getMembers().stream() + .map(ChannelMember::getUserId) + .collect(Collectors.toList()); + var ch2MemberIds = + ch2State.getMembers().stream() + .map(ChannelMember::getUserId) + .collect(Collectors.toList()); + Assertions.assertTrue(ch1MemberIds.containsAll(membersId)); + Assertions.assertTrue(ch2MemberIds.containsAll(membersId)); + + // Remove a member + var updater = Channel.channelBatchUpdater(); + var memberToRemove = membersId.get(0); + + var filter = new ChannelsBatchFilters(); + Map cidsFilter = new HashMap<>(); + cidsFilter.put("$in", List.of(ch1.getCId(), ch2.getCId())); + filter.setCids(cidsFilter); + + var response = + updater + .removeMembers( + filter, List.of(new ChannelBatchMemberRequest(memberToRemove, null))) + .request(); + Assertions.assertNotNull(response.getTaskId()); + var taskId = response.getTaskId(); + + waitFor( + () -> { + TaskStatusGetResponse taskStatusResponse = + Assertions.assertDoesNotThrow(() -> TaskStatus.get(taskId).request()); + return "completed".equals(taskStatusResponse.getStatus()); + }); + + // Verify member was removed from both channels + waitFor( + () -> { + var ch1StateAfter = + Assertions.assertDoesNotThrow( + () -> + Channel.getOrCreate(ch1.getType(), ch1.getId()).request().getChannel()); + var ch2StateAfter = + Assertions.assertDoesNotThrow( + () -> + Channel.getOrCreate(ch2.getType(), ch2.getId()).request().getChannel()); + if (ch1StateAfter.getMembers() == null || ch2StateAfter.getMembers() == null) { + return false; + } + var ch1MemberIdsAfter = + ch1StateAfter.getMembers().stream() + .map(ChannelMember::getUserId) + .collect(Collectors.toList()); + var ch2MemberIdsAfter = + ch2StateAfter.getMembers().stream() + .map(ChannelMember::getUserId) + .collect(Collectors.toList()); + return !ch1MemberIdsAfter.contains(memberToRemove) + && !ch2MemberIdsAfter.contains(memberToRemove); + }); + }); + } + + @Test + @DisplayName("ChannelBatchUpdater can archive channels") + void whenArchivingChannels_thenNoException() { + Assertions.assertDoesNotThrow( + () -> { + var user1 = + UserRequestObject.builder() + .id(RandomStringUtils.randomAlphabetic(10)) + .name("Test User 1") + .build(); + var user2 = + UserRequestObject.builder() + .id(RandomStringUtils.randomAlphabetic(10)) + .name("Test User 2") + .build(); + User.upsert().user(user1).user(user2).request(); + var membersId = List.of(user1.getId(), user2.getId()); + + var ch1 = + Channel.getOrCreate("messaging", RandomStringUtils.randomAlphabetic(12)) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + membersId.stream() + .map( + id -> + ChannelMemberRequestObject.builder() + .user(UserRequestObject.builder().id(id).build()) + .build()) + .collect(Collectors.toList())) + .build()) + .request() + .getChannel(); + + var ch2 = + Channel.getOrCreate("messaging", RandomStringUtils.randomAlphabetic(12)) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + membersId.stream() + .map( + id -> + ChannelMemberRequestObject.builder() + .user(UserRequestObject.builder().id(id).build()) + .build()) + .collect(Collectors.toList())) + .build()) + .request() + .getChannel(); + + var updater = Channel.channelBatchUpdater(); + + var filter = new ChannelsBatchFilters(); + Map cidsFilter = new HashMap<>(); + cidsFilter.put("$in", List.of(ch1.getCId(), ch2.getCId())); + filter.setCids(cidsFilter); + + var response = + updater + .archive(filter, List.of(new ChannelBatchMemberRequest(membersId.get(0), null))) + .request(); + Assertions.assertNotNull(response.getTaskId()); + var taskId = response.getTaskId(); + + waitFor( + () -> { + TaskStatusGetResponse taskStatusResponse = + Assertions.assertDoesNotThrow(() -> TaskStatus.get(taskId).request()); + return "completed".equals(taskStatusResponse.getStatus()); + }); + + // Verify channel was archived for the specified member in both channels + waitFor( + () -> { + var ch1State = + Assertions.assertDoesNotThrow( + () -> + Channel.getOrCreate(ch1.getType(), ch1.getId()).request().getChannel()); + var ch2State = + Assertions.assertDoesNotThrow( + () -> + Channel.getOrCreate(ch2.getType(), ch2.getId()).request().getChannel()); + if (ch1State.getMembers() == null || ch2State.getMembers() == null) { + return false; + } + var ch1Member = + ch1State.getMembers().stream() + .filter(m -> m.getUserId().equals(membersId.get(0))) + .findFirst() + .orElse(null); + var ch2Member = + ch2State.getMembers().stream() + .filter(m -> m.getUserId().equals(membersId.get(0))) + .findFirst() + .orElse(null); + return ch1Member != null + && ch1Member.getArchivedAt() != null + && ch2Member != null + && ch2Member.getArchivedAt() != null; + }); + }); + } +}