Skip to content
9 changes: 8 additions & 1 deletion src/Clients/IUserClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public interface IUserClient
/// To ban a user, use <see cref="BanAsync"/> method.
/// </summary>
/// <remarks>https://getstream.io/chat/docs/dotnet-csharp/moderation/?language=csharp#ban</remarks>
Task<ApiResponse> UnbanAsync(BanRequest banRequest);
Task<ApiResponse> UnbanAsync(BanRequest banRequest, bool removeFutureChannelsBan = false);

/// <summary>
/// <para>Queries banned users.</para>
Expand All @@ -214,6 +214,13 @@ public interface IUserClient
/// <remarks>https://getstream.io/chat/docs/dotnet-csharp/moderation/?language=csharp#query-banned-users</remarks>
Task<QueryBannedUsersResponse> QueryBannedUsersAsync(QueryBannedUsersRequest request);

/// <summary>
/// <para>Queries future channel bans.</para>
/// Future channel bans are automatically applied when a user creates a new channel
/// or adds a member to an existing channel.
/// </summary>
Task<QueryFutureChannelBansResponse> QueryFutureChannelBansAsync(QueryFutureChannelBansRequest request);

/// <summary>
/// <para>Mutes a user.</para>
/// Any user is allowed to mute another user. Mutes are stored at user level and returned with the
Expand Down
30 changes: 22 additions & 8 deletions src/Clients/UserClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,36 @@ public async Task<ApiResponse> BanAsync(BanRequest banRequest)
HttpStatusCode.Created,
banRequest);

public async Task<ApiResponse> UnbanAsync(BanRequest banRequest)
=> await ExecuteRequestAsync<ApiResponse>("moderation/ban",
public async Task<ApiResponse> UnbanAsync(BanRequest banRequest, bool removeFutureChannelsBan = false)
{
var queryParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("target_user_id", banRequest.TargetUserId),
new KeyValuePair<string, string>("type", banRequest.Type),
new KeyValuePair<string, string>("id", banRequest.Id),
};
if (removeFutureChannelsBan)
{
queryParams.Add(new KeyValuePair<string, string>("remove_future_channels_ban", "true"));
}
return await ExecuteRequestAsync<ApiResponse>("moderation/ban",
HttpMethod.DELETE,
HttpStatusCode.OK,
queryParams: new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("target_user_id", banRequest.TargetUserId),
new KeyValuePair<string, string>("type", banRequest.Type),
new KeyValuePair<string, string>("id", banRequest.Id),
});
queryParams: queryParams);
}

public async Task<QueryBannedUsersResponse> QueryBannedUsersAsync(QueryBannedUsersRequest request)
=> await ExecuteRequestAsync<QueryBannedUsersResponse>("query_banned_users",
HttpMethod.GET,
HttpStatusCode.OK,
queryParams: request.ToQueryParameters());

public async Task<QueryFutureChannelBansResponse> QueryFutureChannelBansAsync(QueryFutureChannelBansRequest request)
=> await ExecuteRequestAsync<QueryFutureChannelBansResponse>("query_future_channel_bans",
HttpMethod.GET,
HttpStatusCode.OK,
queryParams: request.ToQueryParameters());

public async Task<MuteResponse> MuteAsync(string targetId, string id)
=> await ExecuteRequestAsync<MuteResponse>("moderation/mute",
HttpMethod.POST,
Expand Down
66 changes: 66 additions & 0 deletions src/Models/Moderation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class BanRequest

/// <summary>Channel ID to ban user in</summary>
public string Id { get; set; }

/// <summary>When true, the user will be automatically banned from all future channels created by the user who issued the ban</summary>
public bool? BanFromFutureChannels { get; set; }
}

public class ShadowBanRequest : BanRequest
Expand All @@ -53,6 +56,69 @@ public class Ban
public User BannedBy { get; set; }
}

public class FutureChannelBan
{
/// <summary>Gets or sets the banned user (checks multiple possible API response fields).</summary>
[Newtonsoft.Json.JsonProperty("user")]
public User User
{
get => _user ?? Target;
set => _user = value;
}

private User _user;

/// <summary>Gets or sets the banned user (target of the ban).</summary>
[Newtonsoft.Json.JsonProperty("target")]
public User Target { get; set; }

/// <summary>Gets or sets the ID of the banned user.</summary>
[Newtonsoft.Json.JsonProperty("target_id")]
public string TargetId { get; set; }

/// <summary>Gets or sets the ID of the user who created the ban.</summary>
[Newtonsoft.Json.JsonProperty("created_by_id")]
public string CreatedById { get; set; }

/// <summary>Gets or sets the user who created the ban.</summary>
[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<KeyValuePair<string, string>> ToQueryParameters()
{
return new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("payload", StreamJsonConverter.SerializeObject(this)),
};
}
}

public class QueryFutureChannelBansResponse : ApiResponse
{
public List<FutureChannelBan> Bans { get; set; }
}

public class QueryBannedUsersRequest : IQueryParameterConvertible
{
public Dictionary<string, object> FilterConditions { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion tests/MessageClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private async Task EnableUserMessageRemindersAsync()
/// </summary>
private async Task DisableUserMessageRemindersAsync()
{
var request = new PartialUpdateChannelRequest
var request = new PartialUpdateChannelRequest
{
Set = new Dictionary<string, object>
{
Expand Down
91 changes: 87 additions & 4 deletions tests/UserClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ public async Task TestMarkDeliveredAsync()
{
ChannelCID = "channel2",
MessageID = "message2",
}
},
},
UserID = _user1.Id,
};
Expand All @@ -580,7 +580,7 @@ public async Task TestMarkDelivered_WithUserIdAsync()
{
ChannelCID = "channel1",
MessageID = "message1",
}
},
},
UserID = _user1.Id,
};
Expand Down Expand Up @@ -623,13 +623,96 @@ public Task TestMarkDelivered_NoUserOrUserId_ThrowsArgumentExceptionAsync()
{
ChannelCID = "channel1",
MessageID = "message1",
}
}
},
},
};

Func<Task> markDeliveredCall = async () => await _userClient.MarkDeliveredAsync(markDeliveredOptions);

return markDeliveredCall.Should().ThrowAsync<ArgumentException>();
}

[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);
}
}
}
}
Loading