Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement guild bulk bans #121

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ public static partial class Limits
/// </summary>
public static class Guild
{
/// <summary>
/// The maximum seconds in the past to delete messages for when a user is banned.
/// </summary>
public const int MaxDeleteMessageSeconds = 604800;

/// <summary>
/// The maximum amount of users that can be bulk banned.
/// </summary>
public const int MaxBulkBanUsersAmount = 200;

/// <summary>
/// Represents limits for guild events.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Disqord.Core/Models/GuildBulkBanJsonModel.cs
Original file line number Diff line number Diff line change
@@ -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!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ namespace Disqord.Rest.Api;

public class CreateBanJsonRestRequestContent : JsonModelRestRequestContent
{
[JsonProperty("delete_message_days")]
public Optional<int> DeleteMessageDays;
[JsonProperty("delete_message_seconds")]
public Optional<int> DeleteMessageSeconds;

[JsonProperty("reason")]
public Optional<string> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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<Snowflake> UserIds = null!;

protected override void OnValidate()
{
base.OnValidate();

Guard.IsNotNull(UserIds);
Guard.HasSizeLessThanOrEqualTo(UserIds, Discord.Limits.Guild.MaxBulkBanUsersAmount);
}
}
8 changes: 8 additions & 0 deletions src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ public static Task DeleteBanAsync(this IRestApiClient client,
return client.ExecuteAsync(route, null, options, cancellationToken);
}

public static Task<GuildBulkBanJsonModel> CreateBansAsync(this IRestApiClient client,
Snowflake guildId, CreateBanJsonRestRequestContent content,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
{
var route = Format(Route.Guild.CreateBans, guildId);
return client.ExecuteAsync<GuildBulkBanJsonModel>(route, content, options, cancellationToken);
}

public static Task<RoleJsonModel[]> FetchRolesAsync(this IRestApiClient client,
Snowflake guildId,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
Expand Down
2 changes: 2 additions & 0 deletions src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
10 changes: 10 additions & 0 deletions src/Disqord.Rest/Entities/Core/Guild/IBulkBanResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Collections.Generic;

namespace Disqord.Rest;

public interface IBulkBanResponse
{
IReadOnlyList<Snowflake> BannedUserIds { get; }

IReadOnlyList<Snowflake> FailedUserIds { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;

namespace Disqord.Rest;

public class CombinedBulkBanResponse : IBulkBanResponse
{
public IReadOnlyList<Snowflake> BannedUserIds { get; }
public IReadOnlyList<Snowflake> FailedUserIds { get; }

public CombinedBulkBanResponse(IEnumerable<IBulkBanResponse> responses)
{
BannedUserIds = responses.SelectMany(response => response.BannedUserIds).ToArray();
FailedUserIds = responses.SelectMany(response => response.FailedUserIds).ToArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using Disqord.Models;

namespace Disqord.Rest;

public class TransientBulkBanResponse : TransientEntity<GuildBulkBanJsonModel>, IBulkBanResponse
{
public IReadOnlyList<Snowflake> BannedUserIds => Model.BannedUsers;

public IReadOnlyList<Snowflake> FailedUserIds => Model.FailedUsers;

public TransientBulkBanResponse(GuildBulkBanJsonModel model)
: base(model)
{ }
}
25 changes: 25 additions & 0 deletions src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ public static Task<IReadOnlyList<IBan>> 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)
Expand All @@ -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)
Expand All @@ -251,6 +260,22 @@ public static Task DeleteBanAsync(this IGuild guild,
return client.DeleteBanAsync(guild.Id, userId, options, cancellationToken);
}

public static IPagedEnumerable<IBulkBanResponse> EnumerateBanCreation(this IGuild guild,
IEnumerable<Snowflake> 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<IBulkBanResponse> CreateBansAsync(this IGuild guild,
IEnumerable<Snowflake> 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<IReadOnlyList<IRole>> FetchRolesAsync(this IGuild guild,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
{
Expand Down
59 changes: 58 additions & 1 deletion src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,13 +443,23 @@ internal static async Task<IReadOnlyList<IBan>> 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)
};

Expand All @@ -463,6 +473,53 @@ public static Task DeleteBanAsync(this IRestClient client,
return client.ApiClient.DeleteBanAsync(guildId, userId, options, cancellationToken);
}

public static IPagedEnumerable<IBulkBanResponse> EnumerateBanCreation(this IRestClient client,
Snowflake guildId, IEnumerable<Snowflake> 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<IBulkBanResponse> CreateBansAsync(this IRestClient client,
Snowflake guildId, IEnumerable<Snowflake> 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<IBulkBanResponse> InternalCreateBansAsync(this IRestClient client,
Snowflake guildId, ArraySegment<Snowflake> 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<IReadOnlyList<IRole>> FetchRolesAsync(this IRestClient client,
Snowflake guildId,
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IBulkBanResponse>
{
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<IReadOnlyList<IBulkBanResponse>> NextPageAsync(IReadOnlyList<IBulkBanResponse>? previousPage, IRestRequestOptions? options = null,
CancellationToken cancellationToken = default)
{
var amount = NextPageSize;
var segment = new ArraySegment<Snowflake>(_userIds, _offset, amount);
_offset += amount;
var response = await Client.InternalCreateBansAsync(_guildId, segment, _reason, _deleteMessagesTime, options,
cancellationToken);
return Enumerable.Repeat(response, PageSize).ToReadOnlyList();
}
}
21 changes: 11 additions & 10 deletions src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,22 @@ public async ValueTask<bool> 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The paged enumerator would need to be reworked to handle cases like this better.

// 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;
}

/// <inheritdoc/>
public virtual ValueTask DisposeAsync()
=> default;
}
}
Loading