diff --git a/src/Clients/IUserClient.cs b/src/Clients/IUserClient.cs index f58cba57..eb2aa37b 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 cae8b088..51065625 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 23912a90..89738d5d 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,69 @@ public class Ban public User BannedBy { get; set; } } + public class FutureChannelBan + { + /// Gets or sets the banned user (checks multiple possible API response fields). + [Newtonsoft.Json.JsonProperty("user")] + public User User + { + get => _user ?? Target; + set => _user = value; + } + + private User _user; + + /// Gets or sets the banned user (target of the ban). + [Newtonsoft.Json.JsonProperty("target")] + public User Target { get; set; } + + /// Gets or sets the ID of the banned user. + [Newtonsoft.Json.JsonProperty("target_id")] + public string TargetId { get; set; } + + /// Gets or sets the ID of the user who created the ban. + [Newtonsoft.Json.JsonProperty("created_by_id")] + public string CreatedById { get; set; } + + /// Gets or sets the user who created the ban. + [Newtonsoft.Json.JsonProperty("created_by")] + public User CreatedBy { get; set; } + + [Newtonsoft.Json.JsonProperty("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [Newtonsoft.Json.JsonProperty("expires")] + public DateTimeOffset? Expires { get; set; } + + [Newtonsoft.Json.JsonProperty("reason")] + public string Reason { get; set; } + + [Newtonsoft.Json.JsonProperty("shadow")] + public bool Shadow { get; set; } + } + + 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; } + + 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; } diff --git a/tests/MessageClientTests.cs b/tests/MessageClientTests.cs index b9357183..97b94d06 100644 --- a/tests/MessageClientTests.cs +++ b/tests/MessageClientTests.cs @@ -65,7 +65,7 @@ private async Task EnableUserMessageRemindersAsync() /// private async Task DisableUserMessageRemindersAsync() { - var request = new PartialUpdateChannelRequest + var request = new PartialUpdateChannelRequest { Set = new Dictionary { diff --git a/tests/UserClientTests.cs b/tests/UserClientTests.cs index ba5b1ade..4f23f700 100644 --- a/tests/UserClientTests.cs +++ b/tests/UserClientTests.cs @@ -559,7 +559,7 @@ public async Task TestMarkDeliveredAsync() { ChannelCID = "channel2", MessageID = "message2", - } + }, }, UserID = _user1.Id, }; @@ -580,7 +580,7 @@ public async Task TestMarkDelivered_WithUserIdAsync() { ChannelCID = "channel1", MessageID = "message1", - } + }, }, UserID = _user1.Id, }; @@ -623,13 +623,96 @@ public Task TestMarkDelivered_NoUserOrUserId_ThrowsArgumentExceptionAsync() { ChannelCID = "channel1", MessageID = "message1", - } - } + }, + }, }; Func markDeliveredCall = async () => await _userClient.MarkDeliveredAsync(markDeliveredOptions); return markDeliveredCall.Should().ThrowAsync(); } + + [Test] + public async Task TestQueryFutureChannelBansWithTargetUserIdAsync() + { + var creator = await UpsertNewUserAsync(); + var target1 = await UpsertNewUserAsync(); + var target2 = await UpsertNewUserAsync(); + var channel = await CreateChannelAsync(createdByUserId: creator.Id); + + try + { + // Ban both targets from future channels created by creator + // Note: ban_from_future_channels requires a channel_cid to be set + await _userClient.BanAsync(new BanRequest + { + TargetUserId = target1.Id, + UserId = creator.Id, + Type = channel.Type, + Id = channel.Id, + BanFromFutureChannels = true, + Reason = "test ban 1", + }); + + await _userClient.BanAsync(new BanRequest + { + TargetUserId = target2.Id, + UserId = creator.Id, + Type = channel.Type, + Id = channel.Id, + BanFromFutureChannels = true, + Reason = "test ban 2", + }); + + // Query all future channel bans by creator + var resp = await _userClient.QueryFutureChannelBansAsync(new QueryFutureChannelBansRequest + { + UserId = creator.Id, + }); + + // Should have at least the 2 bans we just created + resp.Bans.Should().HaveCountGreaterOrEqualTo(2); + + // Verify we can find our bans by checking the reasons we set + var reasons = resp.Bans.Select(b => b.Reason).ToList(); + reasons.Should().Contain("test ban 1"); + reasons.Should().Contain("test ban 2"); + + // Query with TargetUserId filter - should only return the specific target + resp = await _userClient.QueryFutureChannelBansAsync(new QueryFutureChannelBansRequest + { + UserId = creator.Id, + TargetUserId = target1.Id, + }); + + resp.Bans.Should().HaveCount(1); + resp.Bans[0].Reason.Should().Be("test ban 1"); + + // 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].Reason.Should().Be("test ban 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