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
{