diff --git a/src/Clients/ChannelBatchUpdater.cs b/src/Clients/ChannelBatchUpdater.cs new file mode 100644 index 0000000..ab62e1e --- /dev/null +++ b/src/Clients/ChannelBatchUpdater.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using StreamChat.Models; + +namespace StreamChat.Clients +{ + /// + /// Provides convenience methods for batch channel operations. + /// + public class ChannelBatchUpdater + { + private readonly IChannelClient _client; + + public ChannelBatchUpdater(IChannelClient client) + { + _client = client; + } + + /// + /// Adds members to channels matching the filter. + /// + public async Task AddMembersAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddMembers, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Removes members from channels matching the filter. + /// + public async Task RemoveMembersAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.RemoveMembers, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Invites members to channels matching the filter. + /// + public async Task InviteMembersAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.InviteMembers, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Assigns roles to members in channels matching the filter. + /// + public async Task AssignRolesAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AssignRoles, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Adds moderators to channels matching the filter. + /// + public async Task AddModeratorsAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddModerators, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Removes moderator role from members in channels matching the filter. + /// + public async Task DemoteModeratorsAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.DemoteModerators, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Hides channels matching the filter for the specified members. + /// + public async Task HideAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Hide, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Shows channels matching the filter for the specified members. + /// + public async Task ShowAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Show, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Archives channels matching the filter for the specified members. + /// + public async Task ArchiveAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Archive, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Unarchives channels matching the filter for the specified members. + /// + public async Task UnarchiveAsync(ChannelsBatchFilters filter, IEnumerable members) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.Unarchive, + Filter = filter, + Members = members?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Updates data on channels matching the filter. + /// + public async Task UpdateDataAsync(ChannelsBatchFilters filter, ChannelDataUpdate data) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.UpdateData, + Filter = filter, + Data = data, + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Adds filter tags to channels matching the filter. + /// + public async Task AddFilterTagsAsync(ChannelsBatchFilters filter, IEnumerable tags) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddFilterTags, + Filter = filter, + FilterTagsUpdate = tags?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + + /// + /// Removes filter tags from channels matching the filter. + /// + public async Task RemoveFilterTagsAsync(ChannelsBatchFilters filter, IEnumerable tags) + { + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.RemoveFilterTags, + Filter = filter, + FilterTagsUpdate = tags?.ToList(), + }; + return await _client.UpdateChannelsBatchAsync(options); + } + } +} diff --git a/src/Clients/ChannelClient.cs b/src/Clients/ChannelClient.cs index f20a9d3..f7ec886 100644 --- a/src/Clients/ChannelClient.cs +++ b/src/Clients/ChannelClient.cs @@ -38,6 +38,14 @@ public async Task DeleteChannelsAsync(IEnumerable UpdateChannelsBatchAsync(ChannelsBatchOptions options) + => await ExecuteRequestAsync("channels/batch", + HttpMethod.PUT, + HttpStatusCode.Created, + options); + + public ChannelBatchUpdater BatchUpdater() => new ChannelBatchUpdater(this); + public async Task HideAsync(string channelType, string channelId, string userId, bool clearHistory = false) => await ExecuteRequestAsync($"channels/{channelType}/{channelId}/hide", HttpMethod.POST, diff --git a/src/Clients/IChannelClient.cs b/src/Clients/IChannelClient.cs index 53e5763..f3c4e77 100644 --- a/src/Clients/IChannelClient.cs +++ b/src/Clients/IChannelClient.cs @@ -50,6 +50,18 @@ Task AssignRolesAsync(string channelType, string channelI /// https://getstream.io/chat/docs/dotnet-csharp/channel_delete/?language=csharp Task DeleteChannelsAsync(IEnumerable cids, bool hardDelete = false); + /// + /// Updates channels in batch based on the provided options. + /// This is an asynchronous operation and the returned value is a task Id. + /// You can use to check the status of the task. + /// + Task UpdateChannelsBatchAsync(ChannelsBatchOptions options); + + /// + /// Returns a instance for convenient batch channel operations. + /// + ChannelBatchUpdater BatchUpdater(); + /// /// Takes away moderators status from given user ids. /// diff --git a/src/Models/Channel.cs b/src/Models/Channel.cs index bfc34ab..2e2278d 100644 --- a/src/Models/Channel.cs +++ b/src/Models/Channel.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Runtime.Serialization; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace StreamChat.Models { @@ -155,4 +157,110 @@ public class ChannelUnmuteRequest : ChannelMuteRequest public class ChannelUnmuteResponse : ChannelMuteResponse { } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ChannelBatchOperation + { + [EnumMember(Value = "addMembers")] + AddMembers, + + [EnumMember(Value = "removeMembers")] + RemoveMembers, + + [EnumMember(Value = "inviteMembers")] + InviteMembers, + + [EnumMember(Value = "assignRoles")] + AssignRoles, + + [EnumMember(Value = "addModerators")] + AddModerators, + + [EnumMember(Value = "demoteModerators")] + DemoteModerators, + + [EnumMember(Value = "hide")] + Hide, + + [EnumMember(Value = "show")] + Show, + + [EnumMember(Value = "archive")] + Archive, + + [EnumMember(Value = "unarchive")] + Unarchive, + + [EnumMember(Value = "updateData")] + UpdateData, + + [EnumMember(Value = "addFilterTags")] + AddFilterTags, + + [EnumMember(Value = "removeFilterTags")] + RemoveFilterTags, + } + + public class ChannelBatchMemberRequest + { + [JsonProperty("user_id")] + public string UserId { get; set; } + + [JsonProperty("channel_role")] + public string ChannelRole { get; set; } + } + + public class ChannelDataUpdate + { + [JsonProperty("frozen")] + public bool? Frozen { get; set; } + + [JsonProperty("disabled")] + public bool? Disabled { get; set; } + + [JsonProperty("custom")] + public Dictionary Custom { get; set; } + + [JsonProperty("team")] + public string Team { get; set; } + + [JsonProperty("config_overrides")] + public Dictionary ConfigOverrides { get; set; } + + [JsonProperty("auto_translation_enabled")] + public bool? AutoTranslationEnabled { get; set; } + + [JsonProperty("auto_translation_language")] + public string AutoTranslationLanguage { get; set; } + } + + public class ChannelsBatchFilters + { + [JsonProperty("cids")] + public object Cids { get; set; } + + [JsonProperty("types")] + public object Types { get; set; } + + [JsonProperty("filter_tags")] + public object FilterTags { get; set; } + } + + public class ChannelsBatchOptions + { + [JsonProperty("operation", DefaultValueHandling = DefaultValueHandling.Include)] + public ChannelBatchOperation Operation { get; set; } + + [JsonProperty("filter")] + public ChannelsBatchFilters Filter { get; set; } + + [JsonProperty("members")] + public IEnumerable Members { get; set; } + + [JsonProperty("data")] + public ChannelDataUpdate Data { get; set; } + + [JsonProperty("filter_tags_update")] + public IEnumerable FilterTagsUpdate { get; set; } + } } diff --git a/tests/ChannelBatchUpdaterTests.cs b/tests/ChannelBatchUpdaterTests.cs new file mode 100644 index 0000000..c00e0e0 --- /dev/null +++ b/tests/ChannelBatchUpdaterTests.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; +using StreamChat.Clients; +using StreamChat.Models; + +namespace StreamChatTests +{ + /// Tests for + /// + /// The tests follow arrange-act-assert pattern divided by empty lines. + /// Please make sure to follow the pattern to keep the consistency. + /// + [TestFixture] + public class ChannelBatchUpdaterTests : TestBase + { + [Test] + public async Task TestUpdateChannelsBatchWithValidOptionsAsync() + { + var ch1 = await CreateChannelAsync(createdByUserId: (await UpsertNewUserAsync()).Id); + var ch2 = await CreateChannelAsync(createdByUserId: (await UpsertNewUserAsync()).Id); + var userToAdd = await UpsertNewUserAsync(); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var options = new ChannelsBatchOptions + { + Operation = ChannelBatchOperation.AddMembers, + Filter = filter, + Members = new[] { new ChannelBatchMemberRequest { UserId = userToAdd.Id } }, + }; + + var response = await _channelClient.UpdateChannelsBatchAsync(options); + + response.TaskId.Should().NotBeNullOrEmpty(); + } + + [Test] + public async Task TestChannelBatchUpdaterAddMembersAsync() + { + var user1 = await UpsertNewUserAsync(); + var ch1 = await CreateChannelAsync(createdByUserId: user1.Id); + var ch2 = await CreateChannelAsync(createdByUserId: user1.Id); + + var user2 = await UpsertNewUserAsync(); + var user3 = await UpsertNewUserAsync(); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var members = new[] + { + new ChannelBatchMemberRequest { UserId = user2.Id }, + new ChannelBatchMemberRequest { UserId = user3.Id }, + }; + + var updater = _channelClient.BatchUpdater(); + var response = await updater.AddMembersAsync(filter, members); + + response.TaskId.Should().NotBeNullOrEmpty(); + await WaitUntilTaskSucceedsAsync(response.TaskId); + + var ch1Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch1.Type, + Id = ch1.Id, + FilterConditions = new Dictionary(), + }); + + var ch2Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch2.Type, + Id = ch2.Id, + FilterConditions = new Dictionary(), + }); + + ch1Members.Members.Should().Contain(m => m.UserId == user2.Id); + ch1Members.Members.Should().Contain(m => m.UserId == user3.Id); + ch2Members.Members.Should().Contain(m => m.UserId == user2.Id); + ch2Members.Members.Should().Contain(m => m.UserId == user3.Id); + } + + [Test] + public async Task TestChannelBatchUpdaterRemoveMembersAsync() + { + var user1 = await UpsertNewUserAsync(); + var user2 = await UpsertNewUserAsync(); + var user3 = await UpsertNewUserAsync(); + + var ch1 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id, user3.Id }); + var ch2 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id, user3.Id }); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var members = new[] + { + new ChannelBatchMemberRequest { UserId = user2.Id }, + }; + + var updater = _channelClient.BatchUpdater(); + var response = await updater.RemoveMembersAsync(filter, members); + + response.TaskId.Should().NotBeNullOrEmpty(); + await WaitUntilTaskSucceedsAsync(response.TaskId); + + await WaitForAsync(async () => + { + var ch1Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch1.Type, + Id = ch1.Id, + FilterConditions = new Dictionary(), + }); + + var ch2Members = await _channelClient.QueryMembersAsync(new QueryMembersRequest + { + Type = ch2.Type, + Id = ch2.Id, + FilterConditions = new Dictionary(), + }); + + return ch1Members.Members.All(m => m.UserId != user2.Id) + && ch2Members.Members.All(m => m.UserId != user2.Id); + }, timeout: 20000, delay: 1000); + } + + [Test] + public async Task TestChannelBatchUpdaterArchiveAsync() + { + var user1 = await UpsertNewUserAsync(); + var user2 = await UpsertNewUserAsync(); + + var ch1 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id }); + var ch2 = await CreateChannelAsync(createdByUserId: user1.Id, members: new[] { user1.Id, user2.Id }); + + var filter = new ChannelsBatchFilters + { + Cids = new Dictionary + { + { "$in", new[] { ch1.Cid, ch2.Cid } }, + }, + }; + + var members = new[] + { + new ChannelBatchMemberRequest { UserId = user1.Id }, + }; + + var updater = _channelClient.BatchUpdater(); + var response = await updater.ArchiveAsync(filter, members); + + response.TaskId.Should().NotBeNullOrEmpty(); + await WaitUntilTaskSucceedsAsync(response.TaskId); + + var ch1State = await _channelClient.GetOrCreateAsync(ch1.Type, ch1.Id, new ChannelGetRequest()); + var ch1Member = ch1State.Members.FirstOrDefault(m => m.UserId == user1.Id); + + ch1Member.Should().NotBeNull(); + ch1Member.ArchivedAt.Should().NotBeNull(); + } + } +} diff --git a/tests/TestBase.cs b/tests/TestBase.cs index 3e96d44..fb8f52f 100644 --- a/tests/TestBase.cs +++ b/tests/TestBase.cs @@ -212,7 +212,7 @@ protected async Task TryMultipleAsync(Func testBody, } } - private async Task WaitUntilTaskSucceedsAsync(string taskId) + protected async Task WaitUntilTaskSucceedsAsync(string taskId) { try {