From c1f74faea6cb180df2869d9a956edbdb2715eafd Mon Sep 17 00:00:00 2001 From: Lennart Kuijs Date: Sat, 17 Jan 2026 14:08:29 +0100 Subject: [PATCH 1/3] feat: [CHA-1699] add Future Channel Bans support - Add BanFromFutureChannels property to BanRequest - Add removeFutureChannelsBan parameter to UnbanAsync - Add FutureChannelBan class - Add QueryFutureChannelBansRequest and QueryFutureChannelBansResponse - Add QueryFutureChannelBansAsync method to IUserClient and UserClient --- src/Clients/IUserClient.cs | 9 ++++++++- src/Clients/UserClient.cs | 30 ++++++++++++++++++++++-------- src/Models/Moderation.cs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/Clients/IUserClient.cs b/src/Clients/IUserClient.cs index f58cba5..eb2aa37 100644 --- a/src/Clients/IUserClient.cs +++ b/src/Clients/IUserClient.cs @@ -201,7 +201,7 @@ public interface IUserClient /// To ban a user, use method. /// /// https://getstream.io/chat/docs/dotnet-csharp/moderation/?language=csharp#ban - Task UnbanAsync(BanRequest banRequest); + Task UnbanAsync(BanRequest banRequest, bool removeFutureChannelsBan = false); /// /// Queries banned users. @@ -214,6 +214,13 @@ public interface IUserClient /// https://getstream.io/chat/docs/dotnet-csharp/moderation/?language=csharp#query-banned-users Task QueryBannedUsersAsync(QueryBannedUsersRequest request); + /// + /// Queries future channel bans. + /// Future channel bans are automatically applied when a user creates a new channel + /// or adds a member to an existing channel. + /// + Task QueryFutureChannelBansAsync(QueryFutureChannelBansRequest request); + /// /// Mutes a user. /// Any user is allowed to mute another user. Mutes are stored at user level and returned with the diff --git a/src/Clients/UserClient.cs b/src/Clients/UserClient.cs index cae8b08..5106562 100644 --- a/src/Clients/UserClient.cs +++ b/src/Clients/UserClient.cs @@ -130,22 +130,36 @@ public async Task BanAsync(BanRequest banRequest) HttpStatusCode.Created, banRequest); - public async Task UnbanAsync(BanRequest banRequest) - => await ExecuteRequestAsync("moderation/ban", + public async Task UnbanAsync(BanRequest banRequest, bool removeFutureChannelsBan = false) + { + var queryParams = new List> + { + new KeyValuePair("target_user_id", banRequest.TargetUserId), + new KeyValuePair("type", banRequest.Type), + new KeyValuePair("id", banRequest.Id), + }; + if (removeFutureChannelsBan) + { + queryParams.Add(new KeyValuePair("remove_future_channels_ban", "true")); + } + return await ExecuteRequestAsync("moderation/ban", HttpMethod.DELETE, HttpStatusCode.OK, - queryParams: new List> - { - new KeyValuePair("target_user_id", banRequest.TargetUserId), - new KeyValuePair("type", banRequest.Type), - new KeyValuePair("id", banRequest.Id), - }); + queryParams: queryParams); + } + public async Task QueryBannedUsersAsync(QueryBannedUsersRequest request) => await ExecuteRequestAsync("query_banned_users", HttpMethod.GET, HttpStatusCode.OK, queryParams: request.ToQueryParameters()); + public async Task QueryFutureChannelBansAsync(QueryFutureChannelBansRequest request) + => await ExecuteRequestAsync("query_future_channel_bans", + HttpMethod.GET, + HttpStatusCode.OK, + queryParams: request.ToQueryParameters()); + public async Task MuteAsync(string targetId, string id) => await ExecuteRequestAsync("moderation/mute", HttpMethod.POST, diff --git a/src/Models/Moderation.cs b/src/Models/Moderation.cs index 23912a9..6591922 100644 --- a/src/Models/Moderation.cs +++ b/src/Models/Moderation.cs @@ -32,6 +32,9 @@ public class BanRequest /// Channel ID to ban user in public string Id { get; set; } + + /// When true, the user will be automatically banned from all future channels created by the user who issued the ban + public bool? BanFromFutureChannels { get; set; } } public class ShadowBanRequest : BanRequest @@ -53,6 +56,36 @@ public class Ban public User BannedBy { get; set; } } + public class FutureChannelBan + { + public User User { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset? Expires { get; set; } + public string Reason { get; set; } + public bool Shadow { get; set; } + } + + public class QueryFutureChannelBansRequest : IQueryParameterConvertible + { + public string UserId { get; set; } + public bool? ExcludeExpiredBans { get; set; } + public int? Limit { get; set; } + public int? Offset { get; set; } + + public List> ToQueryParameters() + { + return new List> + { + new KeyValuePair("payload", StreamJsonConverter.SerializeObject(this)), + }; + } + } + + public class QueryFutureChannelBansResponse : ApiResponse + { + public List Bans { get; set; } + } + public class QueryBannedUsersRequest : IQueryParameterConvertible { public Dictionary FilterConditions { get; set; } From 6a633d6924be303f97d693e4fa06e843caea67e9 Mon Sep 17 00:00:00 2001 From: Lennart Kuijs Date: Tue, 20 Jan 2026 11:21:31 +0100 Subject: [PATCH 2/3] feat: add TargetUserId to QueryFutureChannelBansRequest Add target_user_id parameter to allow filtering future channel bans by target user, especially for client-side requests. Co-Authored-By: Claude Opus 4.5 --- src/Models/Moderation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Models/Moderation.cs b/src/Models/Moderation.cs index 6591922..3db3404 100644 --- a/src/Models/Moderation.cs +++ b/src/Models/Moderation.cs @@ -68,6 +68,7 @@ public class FutureChannelBan public class QueryFutureChannelBansRequest : IQueryParameterConvertible { public string UserId { get; set; } + public string TargetUserId { get; set; } public bool? ExcludeExpiredBans { get; set; } public int? Limit { get; set; } public int? Offset { get; set; } From 29c127606c9080b10637f5675f857e595db3e3d7 Mon Sep 17 00:00:00 2001 From: Lennart Kuijs Date: Tue, 20 Jan 2026 11:30:27 +0100 Subject: [PATCH 3/3] test: add QueryFutureChannelBans test with TargetUserId filter Test the new TargetUserId parameter for filtering future channel bans. Co-Authored-By: Claude Opus 4.5 --- tests/UserClientTests.cs | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/UserClientTests.cs b/tests/UserClientTests.cs index ba5b1ad..9cb4b6b 100644 --- a/tests/UserClientTests.cs +++ b/tests/UserClientTests.cs @@ -631,5 +631,76 @@ public Task TestMarkDelivered_NoUserOrUserId_ThrowsArgumentExceptionAsync() return markDeliveredCall.Should().ThrowAsync(); } + + [Test] + public async Task TestQueryFutureChannelBansWithTargetUserIdAsync() + { + var creator = await UpsertNewUserAsync(); + var target1 = await UpsertNewUserAsync(); + var target2 = await UpsertNewUserAsync(); + + try + { + // Ban both targets from future channels created by creator + await _userClient.BanAsync(new BanRequest + { + TargetUserId = target1.Id, + UserId = creator.Id, + BanFromFutureChannels = true, + Reason = "test ban 1", + }); + + await _userClient.BanAsync(new BanRequest + { + TargetUserId = target2.Id, + UserId = creator.Id, + BanFromFutureChannels = true, + Reason = "test ban 2", + }); + + // Query with TargetUserId filter - should only return the specific target + var resp = await _userClient.QueryFutureChannelBansAsync(new QueryFutureChannelBansRequest + { + UserId = creator.Id, + TargetUserId = target1.Id, + }); + + resp.Bans.Should().HaveCount(1); + resp.Bans[0].User.Id.Should().Be(target1.Id); + + // Query for the other target + resp = await _userClient.QueryFutureChannelBansAsync(new QueryFutureChannelBansRequest + { + UserId = creator.Id, + TargetUserId = target2.Id, + }); + + resp.Bans.Should().HaveCount(1); + resp.Bans[0].User.Id.Should().Be(target2.Id); + + // Query all future channel bans by creator (without target filter) + resp = await _userClient.QueryFutureChannelBansAsync(new QueryFutureChannelBansRequest + { + UserId = creator.Id, + }); + + resp.Bans.Should().HaveCountGreaterOrEqualTo(2); + } + finally + { + // Cleanup - unban both users + await _userClient.UnbanAsync(new BanRequest + { + TargetUserId = target1.Id, + UserId = creator.Id, + }); + await _userClient.UnbanAsync(new BanRequest + { + TargetUserId = target2.Id, + UserId = creator.Id, + }); + await TryDeleteUsersAsync(creator.Id, target1.Id, target2.Id); + } + } } } \ No newline at end of file