diff --git a/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs b/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs
index f981f192e..1a642a08b 100644
--- a/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs
+++ b/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs
@@ -9,6 +9,16 @@ public static partial class Limits
///
public static class Guild
{
+ ///
+ /// The maximum seconds in the past to delete messages for when a user is banned.
+ ///
+ public const int MaxDeleteMessageSeconds = 604800;
+
+ ///
+ /// The maximum amount of users that can be bulk banned.
+ ///
+ public const int MaxBulkBanUsersAmount = 200;
+
///
/// Represents limits for guild events.
///
diff --git a/src/Disqord.Core/Models/GuildBulkBanJsonModel.cs b/src/Disqord.Core/Models/GuildBulkBanJsonModel.cs
new file mode 100644
index 000000000..943be157c
--- /dev/null
+++ b/src/Disqord.Core/Models/GuildBulkBanJsonModel.cs
@@ -0,0 +1,12 @@
+using Disqord.Serialization.Json;
+
+namespace Disqord.Models;
+
+public class GuildBulkBanJsonModel : JsonModel
+{
+ [JsonProperty("banned_users")]
+ public Snowflake[] BannedUsers = null!;
+
+ [JsonProperty("failed_users")]
+ public Snowflake[] FailedUsers = null!;
+}
diff --git a/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs b/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs
index 8b57ed0f2..b2e4cdcc6 100644
--- a/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs
+++ b/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs
@@ -5,14 +5,15 @@ namespace Disqord.Rest.Api;
public class CreateBanJsonRestRequestContent : JsonModelRestRequestContent
{
- [JsonProperty("delete_message_days")]
- public Optional DeleteMessageDays;
+ [JsonProperty("delete_message_seconds")]
+ public Optional DeleteMessageSeconds;
[JsonProperty("reason")]
public Optional Reason;
protected override void OnValidate()
{
+ OptionalGuard.CheckValue(DeleteMessageSeconds, seconds => Guard.IsBetweenOrEqualTo(seconds, 0, Discord.Limits.Guild.MaxDeleteMessageSeconds));
OptionalGuard.CheckValue(Reason, static reason => Guard.HasSizeLessThanOrEqualTo(reason, Discord.Limits.Rest.MaxAuditLogReasonLength));
}
}
diff --git a/src/Disqord.Rest.Api/Content/Json/CreateBansJsonRestRequestContent.cs b/src/Disqord.Rest.Api/Content/Json/CreateBansJsonRestRequestContent.cs
new file mode 100644
index 000000000..04195cead
--- /dev/null
+++ b/src/Disqord.Rest.Api/Content/Json/CreateBansJsonRestRequestContent.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using Disqord.Serialization.Json;
+using Qommon;
+
+namespace Disqord.Rest.Api;
+
+public class CreateBansJsonRestRequestContent : CreateBanJsonRestRequestContent
+{
+ [JsonProperty("user_ids")]
+ public IList UserIds = null!;
+
+ protected override void OnValidate()
+ {
+ base.OnValidate();
+
+ Guard.IsNotNull(UserIds);
+ Guard.HasSizeLessThanOrEqualTo(UserIds, Discord.Limits.Guild.MaxBulkBanUsersAmount);
+ }
+}
diff --git a/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs b/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs
index c555988f5..ee67c63b2 100644
--- a/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs
+++ b/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs
@@ -245,6 +245,14 @@ public static Task DeleteBanAsync(this IRestApiClient client,
return client.ExecuteAsync(route, null, options, cancellationToken);
}
+ public static Task CreateBansAsync(this IRestApiClient client,
+ Snowflake guildId, CreateBanJsonRestRequestContent content,
+ IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var route = Format(Route.Guild.CreateBans, guildId);
+ return client.ExecuteAsync(route, content, options, cancellationToken);
+ }
+
public static Task FetchRolesAsync(this IRestApiClient client,
Snowflake guildId,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
diff --git a/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs b/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs
index 81ba04d41..f55f1e53f 100644
--- a/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs
+++ b/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs
@@ -188,6 +188,8 @@ public static class Guild
public static readonly Route DeleteBan = Delete("guilds/{0:guild_id}/bans/{1:user_id}");
+ public static readonly Route CreateBans = Post("guilds/{0:guild_id}/bulk-ban");
+
public static readonly Route GetRoles = Get("guilds/{0:guild_id}/roles");
public static readonly Route CreateRole = Post("guilds/{0:guild_id}/roles");
diff --git a/src/Disqord.Rest/Entities/Core/Guild/IBulkBanResponse.cs b/src/Disqord.Rest/Entities/Core/Guild/IBulkBanResponse.cs
new file mode 100644
index 000000000..092a4410e
--- /dev/null
+++ b/src/Disqord.Rest/Entities/Core/Guild/IBulkBanResponse.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Disqord.Rest;
+
+public interface IBulkBanResponse
+{
+ IReadOnlyList BannedUserIds { get; }
+
+ IReadOnlyList FailedUserIds { get; }
+}
diff --git a/src/Disqord.Rest/Entities/Transient/Guild/CombinedBulkBanResponse.cs b/src/Disqord.Rest/Entities/Transient/Guild/CombinedBulkBanResponse.cs
new file mode 100644
index 000000000..b61729b04
--- /dev/null
+++ b/src/Disqord.Rest/Entities/Transient/Guild/CombinedBulkBanResponse.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Disqord.Rest;
+
+public class CombinedBulkBanResponse : IBulkBanResponse
+{
+ public IReadOnlyList BannedUserIds { get; }
+ public IReadOnlyList FailedUserIds { get; }
+
+ public CombinedBulkBanResponse(IEnumerable responses)
+ {
+ BannedUserIds = responses.SelectMany(response => response.BannedUserIds).ToArray();
+ FailedUserIds = responses.SelectMany(response => response.FailedUserIds).ToArray();
+ }
+}
diff --git a/src/Disqord.Rest/Entities/Transient/Guild/TransientBulkBanResponse.cs b/src/Disqord.Rest/Entities/Transient/Guild/TransientBulkBanResponse.cs
new file mode 100644
index 000000000..ba344cb59
--- /dev/null
+++ b/src/Disqord.Rest/Entities/Transient/Guild/TransientBulkBanResponse.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Disqord.Models;
+
+namespace Disqord.Rest;
+
+public class TransientBulkBanResponse : TransientEntity, IBulkBanResponse
+{
+ public IReadOnlyList BannedUserIds => Model.BannedUsers;
+
+ public IReadOnlyList FailedUserIds => Model.FailedUsers;
+
+ public TransientBulkBanResponse(GuildBulkBanJsonModel model)
+ : base(model)
+ { }
+}
diff --git a/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs b/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs
index 436749780..475b6ba83 100644
--- a/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs
+++ b/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs
@@ -235,6 +235,7 @@ public static Task> FetchBansAsync(this IGuild guild,
return client.FetchBanAsync(guild.Id, userId, options, cancellationToken);
}
+ [Obsolete("Parameter deleteMessageDays is deprecated, use a TimeSpan instead.")]
public static Task CreateBanAsync(this IGuild guild,
Snowflake userId, string? reason = null, int? deleteMessageDays = null,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
@@ -243,6 +244,14 @@ public static Task CreateBanAsync(this IGuild guild,
return client.CreateBanAsync(guild.Id, userId, reason, deleteMessageDays, options, cancellationToken);
}
+ public static Task CreateBanAsync(this IGuild guild,
+ Snowflake userId, string? reason = null, TimeSpan? deleteMessageTime = null,
+ IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var client = guild.GetRestClient();
+ return client.CreateBanAsync(guild.Id, userId, reason, deleteMessageTime, options, cancellationToken);
+ }
+
public static Task DeleteBanAsync(this IGuild guild,
Snowflake userId,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
@@ -251,6 +260,22 @@ public static Task DeleteBanAsync(this IGuild guild,
return client.DeleteBanAsync(guild.Id, userId, options, cancellationToken);
}
+ public static IPagedEnumerable EnumerateBanCreation(this IGuild guild,
+ IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null,
+ IRestRequestOptions? options = null)
+ {
+ var client = guild.GetRestClient();
+ return client.EnumerateBanCreation(guild.Id, userIds, reason, deleteMessageTime, options);
+ }
+
+ public static Task CreateBansAsync(this IGuild guild,
+ IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null,
+ IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var client = guild.GetRestClient();
+ return client.CreateBansAsync(guild.Id, userIds, reason, deleteMessageTime, options, cancellationToken);
+ }
+
public static Task> FetchRolesAsync(this IGuild guild,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
{
diff --git a/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs b/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs
index 3f3617503..3506353d0 100644
--- a/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs
+++ b/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs
@@ -443,13 +443,23 @@ internal static async Task> InternalFetchBansAsync(this IRes
}
}
+ [Obsolete("Parameter deleteMessageDays is deprecated, use a TimeSpan instead.")]
public static Task CreateBanAsync(this IRestClient client,
Snowflake guildId, Snowflake userId, string? reason = null, int? deleteMessageDays = null,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ return client.CreateBanAsync(guildId, userId, reason,
+ deleteMessageDays.HasValue ? TimeSpan.FromDays(deleteMessageDays.Value) : null,
+ options, cancellationToken);
+ }
+
+ public static Task CreateBanAsync(this IRestClient client,
+ Snowflake guildId, Snowflake userId, string? reason = null, TimeSpan? deleteMessageTime = null,
+ IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
{
var content = new CreateBanJsonRestRequestContent
{
- DeleteMessageDays = Optional.FromNullable(deleteMessageDays),
+ DeleteMessageSeconds = Optional.Convert(Optional.FromNullable(deleteMessageTime), time => (int) time.TotalSeconds),
Reason = Optional.FromNullable(reason)
};
@@ -463,6 +473,53 @@ public static Task DeleteBanAsync(this IRestClient client,
return client.ApiClient.DeleteBanAsync(guildId, userId, options, cancellationToken);
}
+ public static IPagedEnumerable EnumerateBanCreation(this IRestClient client,
+ Snowflake guildId, IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null,
+ IRestRequestOptions? options = null)
+ {
+ Guard.IsNotNull(userIds);
+
+ return PagedEnumerable.Create((state, cancellationToken) =>
+ {
+ var (client, guildId, userIds, reason, deleteMessageTime, options) = state;
+ return new CreateBansPagedEnumerator(client, guildId, userIds, reason, deleteMessageTime, options, cancellationToken);
+ }, (client, guildId, userIds.ToArray(), reason, deleteMessageTime, options));
+ }
+
+ public static async Task CreateBansAsync(this IRestClient client,
+ Snowflake guildId, IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null,
+ IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ Guard.IsNotNull(userIds);
+
+ var users = userIds.ToArray();
+ Guard.IsNotEmpty(users);
+
+ if (users.Length <= Discord.Limits.Guild.MaxBulkBanUsersAmount)
+ {
+ return await client.InternalCreateBansAsync(guildId, users, reason, deleteMessageTime, options, cancellationToken).ConfigureAwait(false);
+ }
+
+ var enumerable = client.EnumerateBanCreation(guildId, users, reason, deleteMessageTime, options);
+ var flattened = await enumerable.FlattenAsync(cancellationToken);
+ return new CombinedBulkBanResponse(flattened);
+ }
+
+ internal static async Task InternalCreateBansAsync(this IRestClient client,
+ Snowflake guildId, ArraySegment userIds, string? reason = null, TimeSpan? deleteMessageTime = null,
+ IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var content = new CreateBansJsonRestRequestContent
+ {
+ UserIds = userIds,
+ DeleteMessageSeconds = Optional.Convert(Optional.FromNullable(deleteMessageTime), time => (int) time.TotalSeconds),
+ Reason = Optional.FromNullable(reason)
+ };
+
+ var model = await client.ApiClient.CreateBansAsync(guildId, content, options, cancellationToken);
+ return new TransientBulkBanResponse(model);;
+ }
+
public static async Task> FetchRolesAsync(this IRestClient client,
Snowflake guildId,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
diff --git a/src/Disqord.Rest/Requests/Pagination/Implementation/CreateBansPagedEnumerator.cs b/src/Disqord.Rest/Requests/Pagination/Implementation/CreateBansPagedEnumerator.cs
new file mode 100644
index 000000000..ee33c8b47
--- /dev/null
+++ b/src/Disqord.Rest/Requests/Pagination/Implementation/CreateBansPagedEnumerator.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Qommon.Collections.ReadOnly;
+
+namespace Disqord.Rest;
+
+public class CreateBansPagedEnumerator : PagedEnumerator
+{
+ public override int PageSize => Discord.Limits.Guild.MaxBulkBanUsersAmount;
+
+ private readonly Snowflake _guildId;
+ private readonly Snowflake[] _userIds;
+ private readonly string? _reason;
+ private readonly TimeSpan? _deleteMessagesTime = null;
+
+ private int _offset;
+
+ public CreateBansPagedEnumerator(
+ IRestClient client,
+ Snowflake guildId, Snowflake[] userIds, string? reason = null, TimeSpan? deleteMessagesTime = null,
+ IRestRequestOptions? options = null,
+ CancellationToken cancellationToken = default)
+ : base(client, userIds.Length, options, cancellationToken)
+ {
+ _guildId = guildId;
+ _userIds = userIds;
+ _reason = reason;
+ _deleteMessagesTime = deleteMessagesTime;
+ }
+
+ protected override async Task> NextPageAsync(IReadOnlyList? previousPage, IRestRequestOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ var amount = NextPageSize;
+ var segment = new ArraySegment(_userIds, _offset, amount);
+ _offset += amount;
+ var response = await Client.InternalCreateBansAsync(_guildId, segment, _reason, _deleteMessagesTime, options,
+ cancellationToken);
+ return Enumerable.Repeat(response, PageSize).ToReadOnlyList();
+ }
+}
diff --git a/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs b/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs
index 397d44dcd..f59ceffc4 100644
--- a/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs
+++ b/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs
@@ -82,16 +82,17 @@ public async ValueTask MoveNextAsync(IRestRequestOptions? options = null)
return false;
}
- if (current.Count < PageSize)
- {
- // If Discord returns less entities than the page size,
- // it means there are no more entities beyond the ones we just received.
- RemainingCount = 0;
- }
- else
- {
+ // TODO: Rework paged enumerator
+ // if (current.Count < PageSize)
+ // {
+ // // If Discord returns less entities than the page size,
+ // // it means there are no more entities beyond the ones we just received.
+ // RemainingCount = 0;
+ // }
+ // else
+ // {
RemainingCount -= current.Count;
- }
+ // }
return true;
}
@@ -99,4 +100,4 @@ public async ValueTask MoveNextAsync(IRestRequestOptions? options = null)
///
public virtual ValueTask DisposeAsync()
=> default;
-}
\ No newline at end of file
+}