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
Changes from 1 commit
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
Next Next commit
Implemented guild bulk bans
AnotherZane committed Jul 28, 2024
commit 150014fdc59c8d433e78e8bead4531ad4957985b
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
@@ -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>
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
@@ -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
@@ -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)
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
@@ -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");
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.Entities.Core.Guild;

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 Disqord.Models;
using Disqord.Rest.Entities.Core.Guild;

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)
{ }
}
26 changes: 26 additions & 0 deletions src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Disqord.AuditLogs;
using Disqord.Rest.Entities.Core.Guild;
using Disqord.Rest.Pagination;

namespace Disqord.Rest;
@@ -235,6 +236,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)
@@ -243,6 +245,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 +261,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 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)
{
62 changes: 61 additions & 1 deletion src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Disqord.Http;
using Disqord.Models;
using Disqord.Rest.Api;
using Disqord.Rest.Entities.Core.Guild;
using Disqord.Rest.Pagination;
using Qommon;
using Qommon.Collections;
using Qommon.Collections.ReadOnly;

namespace Disqord.Rest;
@@ -443,13 +446,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)
};

@@ -463,6 +476,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<IReadOnlyList<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)
{
var response = await client.InternalCreateBansAsync(guildId, users, reason, deleteMessageTime, options, cancellationToken).ConfigureAwait(false);
return new[] { response };
}

var enumerable = client.EnumerateBanCreation(guildId, users, reason, deleteMessageTime, options);
return await enumerable.FlattenAsync(cancellationToken);
}

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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Disqord.Rest.Entities.Core.Guild;

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 new []{response};
}
}

Unchanged files with check annotations Beta

/// Attempts to get the members cache from this cache provider.
/// </summary>
/// <param name="lookupOnly"> Whether the intended use of the cache is lookup-only. </param>
public static bool TryGetMembers(this IGatewayCacheProvider cacheProvider, Snowflake guildId,

Check warning on line 40 in src/Disqord.Gateway/Extensions/GatewayCacheProviderExtensions.cs

GitHub Actions / build

Parameter 'cacheProvider' has no matching param tag in the XML comment for 'GatewayCacheProviderExtensions.TryGetMembers(IGatewayCacheProvider, Snowflake, out IThreadSafeDictionary<Snowflake, CachedMember>, bool)' (but other parameters do)

Check warning on line 40 in src/Disqord.Gateway/Extensions/GatewayCacheProviderExtensions.cs

GitHub Actions / build

Parameter 'guildId' has no matching param tag in the XML comment for 'GatewayCacheProviderExtensions.TryGetMembers(IGatewayCacheProvider, Snowflake, out IThreadSafeDictionary<Snowflake, CachedMember>, bool)' (but other parameters do)

Check warning on line 40 in src/Disqord.Gateway/Extensions/GatewayCacheProviderExtensions.cs

GitHub Actions / build

Parameter 'cacheProvider' has no matching param tag in the XML comment for 'GatewayCacheProviderExtensions.TryGetMembers(IGatewayCacheProvider, Snowflake, out IThreadSafeDictionary<Snowflake, CachedMember>, bool)' (but other parameters do)

Check warning on line 40 in src/Disqord.Gateway/Extensions/GatewayCacheProviderExtensions.cs

GitHub Actions / build

Parameter 'guildId' has no matching param tag in the XML comment for 'GatewayCacheProviderExtensions.TryGetMembers(IGatewayCacheProvider, Snowflake, out IThreadSafeDictionary<Snowflake, CachedMember>, bool)' (but other parameters do)
[MaybeNullWhen(false)] out IThreadSafeDictionary<Snowflake, CachedMember> cache, bool lookupOnly = false)

Check warning on line 41 in src/Disqord.Gateway/Extensions/GatewayCacheProviderExtensions.cs

GitHub Actions / build

Parameter 'cache' has no matching param tag in the XML comment for 'GatewayCacheProviderExtensions.TryGetMembers(IGatewayCacheProvider, Snowflake, out IThreadSafeDictionary<Snowflake, CachedMember>, bool)' (but other parameters do)
{
return cacheProvider.TryGetCache(guildId, out cache, lookupOnly);
}
IRestRequestOptions? options = null, CancellationToken cancellationToken = default)
{
var client = member.GetRestClient();
return client.CreateBanAsync(member.GuildId, member.Id, reason, deleteMessageDays, options, cancellationToken);

Check warning on line 53 in src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Member.cs

GitHub Actions / build

'RestClientExtensions.CreateBanAsync(IRestClient, Snowflake, Snowflake, string?, int?, IRestRequestOptions?, CancellationToken)' is obsolete: 'Parameter deleteMessageDays is deprecated, use a TimeSpan instead.'
}
public static Task UnbanAsync(this IMember member,
var selectedValues = entitySelectionInteraction.SelectedValues;
var selectedValueCount = selectedValues.Count;
var selectedEntities = entitySelectionInteraction.ComponentType switch

Check warning on line 56 in src/Disqord.Extensions.Interactivity/Menus/View/Components/EventArgs/SelectionEventArgs.cs

GitHub Actions / build

The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Disqord.SelectionComponentType.String' is not covered.
{
SelectionComponentType.User => new IUser[selectedValueCount],
SelectionComponentType.Role => new IRole[selectedValueCount],
namespace Disqord.Extensions.Interactivity.Menus.Paged;
/// <summary>
/// Represents what essentially is a tuple of <see cref="LocalMessage.Content"/> and <see cref="LocalMessage.Embeds"/> respectively.

Check warning on line 7 in src/Disqord.Extensions.Interactivity/Menus/Paged/Page.cs

GitHub Actions / build

XML comment has cref attribute 'Content' that could not be resolved

Check warning on line 7 in src/Disqord.Extensions.Interactivity/Menus/Paged/Page.cs

GitHub Actions / build

XML comment has cref attribute 'Embeds' that could not be resolved
/// </summary>
public class Page : ILocalConstruct<Page>
{
? GlobalNode
: GuildNodes.GetValueOrDefault(commandGuildId.Value);
var (componentType, customId) = interaction switch

Check warning on line 34 in src/Disqord.Bot/Commands/Implementation/Components/Map/ComponentCommandMap.cs

GitHub Actions / build

The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '_' is not covered.
{
ISelectionComponentInteraction selectionComponentInteraction => (ComponentCommandType.Selection, selectionComponentInteraction.CustomId),
IComponentInteraction componentInteraction => (componentInteraction.ComponentType switch