Skip to content

Commit 1f36385

Browse files
Add sharding support to DiscordClient (#1971)
* preliminary sharding design --------- Co-authored-by: Velvet Toroyashi <42438262+VelvetToroyashi@users.noreply.github.com> * we do need to disconnect too :P * implement a rudimentary transport service * reduce lifetime allocations of a DSharpPlus bot by up to one * support decompression in TransportService * commit to ask for feedback * slightly more state handling effort + options * alert GatewayClient of errors * able to start a single shard (in theory) * theoretically multi-shard orchestration works too now * delete DiscordClient.WebSocket.cs * make sharding clients possible to create * delete the legacy websocketclient * add support for getting latency and connection state * DiscordClient may receive events, as a treat * expose zombied to the user * remove the sharded client * implement sending payloads and reconnecting * remove more legacy ShardedClient code * make the shard count default to whatever Discord tells us * move reconnecting logic into GatewayClient * build * convenience methods * docs * xmldocs for IGatewayClient * works for single shards * don't send opcode 11 to dispatch * improve logging between shards * implement the 5s concurrency limit * accursed * don't explode on reconnecting * retry resuming if the connection cuts out * remove unused using * check whether all shards have connected before firing GDC --------- Co-authored-by: Velvet Toroyashi <42438262+VelvetToroyashi@users.noreply.github.com>
1 parent 4f01c88 commit 1f36385

62 files changed

Lines changed: 2248 additions & 3377 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DSharpPlus.Commands/ExtensionMethods.cs

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,6 @@ public static CommandsExtension UseCommands(this DiscordClient client, CommandsC
3333
return extension;
3434
}
3535

36-
/// <summary>
37-
/// Registers the extension with all the shards on the <see cref="DiscordShardedClient"/>.
38-
/// </summary>
39-
/// <param name="shardedClient">The client to register the extension with.</param>
40-
/// <param name="configuration">The configuration to use for the extension.</param>
41-
public static async Task<IReadOnlyDictionary<int, CommandsExtension>> UseCommandsAsync(this DiscordShardedClient shardedClient, CommandsConfiguration? configuration = null)
42-
{
43-
ArgumentNullException.ThrowIfNull(shardedClient);
44-
45-
await shardedClient.InitializeShardsAsync();
46-
configuration ??= new();
47-
48-
Dictionary<int, CommandsExtension> extensions = [];
49-
foreach (DiscordClient shard in shardedClient.ShardClients.Values)
50-
{
51-
extensions[shard.ShardId] = shard.GetExtension<CommandsExtension>() ?? shard.UseCommands(configuration);
52-
}
53-
54-
return extensions.AsReadOnly();
55-
}
56-
5736
/// <summary>
5837
/// Retrieves the <see cref="CommandsExtension"/> from the <see cref="DiscordClient"/>.
5938
/// </summary>
@@ -62,27 +41,6 @@ public static async Task<IReadOnlyDictionary<int, CommandsExtension>> UseCommand
6241
? throw new ArgumentNullException(nameof(client))
6342
: client.GetExtension<CommandsExtension>();
6443

65-
/// <summary>
66-
/// Retrieves the <see cref="CommandsExtension"/> from all of the shards on <see cref="DiscordShardedClient"/>.
67-
/// </summary>
68-
/// <param name="shardedClient">The client to retrieve the extension from.</param>
69-
public static IReadOnlyDictionary<int, CommandsExtension> GetCommandsExtensions(this DiscordShardedClient shardedClient)
70-
{
71-
ArgumentNullException.ThrowIfNull(shardedClient);
72-
73-
Dictionary<int, CommandsExtension> extensions = [];
74-
foreach (DiscordClient shard in shardedClient.ShardClients.Values)
75-
{
76-
CommandsExtension? extension = shard.GetExtension<CommandsExtension>();
77-
if (extension is not null)
78-
{
79-
extensions[shard.ShardId] = extension;
80-
}
81-
}
82-
83-
return extensions.AsReadOnly();
84-
}
85-
8644
/// <inheritdoc cref="Array.IndexOf{T}(T[], T)"/>
8745
internal static int IndexOf<T>(this IEnumerable<T> array, T? value) where T : IEquatable<T>
8846
{

DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,7 @@ public override async ValueTask ConfigureAsync(CommandsExtension extension)
6767
{
6868
this.configured = true;
6969
extension.Client.InteractionCreated += ExecuteInteractionAsync;
70-
extension.Client.GuildDownloadCompleted += async (client, eventArgs) =>
71-
{
72-
if (client.ShardId == 0)
73-
{
74-
await RegisterSlashCommandsAsync(extension);
75-
}
76-
};
70+
extension.Client.GuildDownloadCompleted += async (client, eventArgs) => await RegisterSlashCommandsAsync(extension);
7771
}
7872
}
7973

DSharpPlus.CommandsNext/ExtensionMethods.cs

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ public static CommandsNextExtension UseCommandsNext(this DiscordClient client, C
2828
throw new InvalidOperationException("CommandsNext is already enabled for that client.");
2929
}
3030

31-
if (!Utilities.HasMessageIntents(client.Configuration.Intents))
31+
if (!Utilities.HasMessageIntents(client.Intents))
3232
{
3333
client.Logger.LogCritical(CommandsNextEvents.Intents, "The CommandsNext extension is registered but there are no message intents enabled. It is highly recommended to enable them.");
3434
}
3535

36-
if (!client.Configuration.Intents.HasIntent(DiscordIntents.Guilds))
36+
if (!client.Intents.HasIntent(DiscordIntents.Guilds))
3737
{
3838
client.Logger.LogCritical(CommandsNextEvents.Intents, "The CommandsNext extension is registered but the guilds intent is not enabled. It is highly recommended to enable it.");
3939
}
@@ -43,26 +43,6 @@ public static CommandsNextExtension UseCommandsNext(this DiscordClient client, C
4343
return cnext;
4444
}
4545

46-
/// <summary>
47-
/// Enables CommandsNext module on all shards in this <see cref="DiscordShardedClient"/>.
48-
/// </summary>
49-
/// <param name="client">Client to enable CommandsNext for.</param>
50-
/// <param name="cfg">CommandsNext configuration to use.</param>
51-
/// <returns>A dictionary of created <see cref="CommandsNextExtension"/>, indexed by shard id.</returns>
52-
public static async Task<IReadOnlyDictionary<int, CommandsNextExtension>> UseCommandsNextAsync(this DiscordShardedClient client, CommandsNextConfiguration cfg)
53-
{
54-
Dictionary<int, CommandsNextExtension> modules = [];
55-
await client.InitializeShardsAsync();
56-
57-
foreach (DiscordClient? shard in client.ShardClients.Select(xkvp => xkvp.Value))
58-
{
59-
CommandsNextExtension? cnext = shard.GetExtension<CommandsNextExtension>() ?? shard.UseCommandsNext(cfg);
60-
modules[shard.ShardId] = cnext;
61-
}
62-
63-
return new ReadOnlyDictionary<int, CommandsNextExtension>(modules);
64-
}
65-
6646
/// <summary>
6747
/// Gets the active CommandsNext module for this client.
6848
/// </summary>
@@ -71,24 +51,6 @@ public static async Task<IReadOnlyDictionary<int, CommandsNextExtension>> UseCom
7151
public static CommandsNextExtension GetCommandsNext(this DiscordClient client)
7252
=> client.GetExtension<CommandsNextExtension>();
7353

74-
/// <summary>
75-
/// Gets the active CommandsNext modules for all shards in this client.
76-
/// </summary>
77-
/// <param name="client">Client to get CommandsNext instances from.</param>
78-
/// <returns>A dictionary of the modules, indexed by shard id.</returns>
79-
public static async Task<IReadOnlyDictionary<int, CommandsNextExtension>> GetCommandsNextAsync(this DiscordShardedClient client)
80-
{
81-
await client.InitializeShardsAsync();
82-
Dictionary<int, CommandsNextExtension> extensions = [];
83-
84-
foreach (DiscordClient? shard in client.ShardClients.Select(xkvp => xkvp.Value))
85-
{
86-
extensions.Add(shard.ShardId, shard.GetExtension<CommandsNextExtension>());
87-
}
88-
89-
return new ReadOnlyDictionary<int, CommandsNextExtension>(extensions);
90-
}
91-
9254
/// <summary>
9355
/// Registers all commands from a given assembly. The command classes need to be public to be considered for registration.
9456
/// </summary>
Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Collections.ObjectModel;
4-
using System.Linq;
5-
using System.Threading.Tasks;
62

73
namespace DSharpPlus.Interactivity.Extensions;
84

95
/// <summary>
10-
/// Interactivity extension methods for <see cref="DiscordClient"/> and <see cref="DiscordShardedClient"/>.
6+
/// Interactivity extension methods for <see cref="DiscordClient"/>.
117
/// </summary>
128
public static class ClientExtensions
139
{
@@ -32,49 +28,11 @@ public static InteractivityExtension UseInteractivity(this DiscordClient client,
3228
return extension;
3329
}
3430

35-
/// <summary>
36-
/// Enables interactivity for each shard.
37-
/// </summary>
38-
/// <param name="client">The shard client to enable interactivity for.</param>
39-
/// <param name="configuration">Configuration to use for all shards. If one isn't provided, default configuration values will be used.</param>
40-
/// <returns>A dictionary containing new <see cref="InteractivityExtension"/> instances for each shard.</returns>
41-
public static async Task<IReadOnlyDictionary<int, InteractivityExtension>> UseInteractivityAsync(this DiscordShardedClient client, InteractivityConfiguration configuration = null)
42-
{
43-
Dictionary<int, InteractivityExtension> extensions = [];
44-
await client.InitializeShardsAsync();
45-
46-
foreach (DiscordClient? shard in client.ShardClients.Select(xkvp => xkvp.Value))
47-
{
48-
InteractivityExtension extension = shard.GetExtension<InteractivityExtension>() ?? shard.UseInteractivity(configuration);
49-
extensions.Add(shard.ShardId, extension);
50-
}
51-
52-
return new ReadOnlyDictionary<int, InteractivityExtension>(extensions);
53-
}
54-
5531
/// <summary>
5632
/// Retrieves the registered <see cref="InteractivityExtension"/> instance for this client.
5733
/// </summary>
5834
/// <param name="client">The client to retrieve an <see cref="InteractivityExtension"/> instance from.</param>
5935
/// <returns>An existing <see cref="InteractivityExtension"/> instance, or <see langword="null"/> if interactivity is not enabled for the <see cref="DiscordClient"/> instance.</returns>
6036
public static InteractivityExtension GetInteractivity(this DiscordClient client)
6137
=> client.GetExtension<InteractivityExtension>();
62-
63-
/// <summary>
64-
/// Retrieves a <see cref="InteractivityExtension"/> instance for each shard.
65-
/// </summary>
66-
/// <param name="client">The shard client to retrieve interactivity instances from.</param>
67-
/// <returns>A dictionary containing <see cref="InteractivityExtension"/> instances for each shard.</returns>
68-
public static async Task<ReadOnlyDictionary<int, InteractivityExtension>> GetInteractivityAsync(this DiscordShardedClient client)
69-
{
70-
await client.InitializeShardsAsync();
71-
Dictionary<int, InteractivityExtension> extensions = [];
72-
73-
foreach (DiscordClient? shard in client.ShardClients.Select(xkvp => xkvp.Value))
74-
{
75-
extensions.Add(shard.ShardId, shard.GetExtension<InteractivityExtension>());
76-
}
77-
78-
return new ReadOnlyDictionary<int, InteractivityExtension>(extensions);
79-
}
8038
}

DSharpPlus.Interactivity/InteractivityExtension.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected internal override void Setup(DiscordClient client)
7575
/// <returns></returns>
7676
public async Task<ReadOnlyCollection<PollEmoji>> DoPollAsync(DiscordMessage m, IEnumerable<DiscordEmoji> emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null)
7777
{
78-
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
78+
if (!Utilities.HasReactionIntents(this.Client.Intents))
7979
{
8080
throw new InvalidOperationException("No reaction intents are enabled.");
8181
}
@@ -559,7 +559,7 @@ public async Task<InteractivityResult<ComponentInteractionCreatedEventArgs>> Wai
559559
public async Task<InteractivityResult<DiscordMessage>> WaitForMessageAsync(Func<DiscordMessage, bool> predicate,
560560
TimeSpan? timeoutoverride = null)
561561
{
562-
if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents))
562+
if (!Utilities.HasMessageIntents(this.Client.Intents))
563563
{
564564
throw new InvalidOperationException("No message intents are enabled.");
565565
}
@@ -579,7 +579,7 @@ public async Task<InteractivityResult<DiscordMessage>> WaitForMessageAsync(Func<
579579
public async Task<InteractivityResult<MessageReactionAddedEventArgs>> WaitForReactionAsync(Func<MessageReactionAddedEventArgs, bool> predicate,
580580
TimeSpan? timeoutoverride = null)
581581
{
582-
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
582+
if (!Utilities.HasReactionIntents(this.Client.Intents))
583583
{
584584
throw new InvalidOperationException("No reaction intents are enabled.");
585585
}
@@ -592,7 +592,7 @@ public async Task<InteractivityResult<MessageReactionAddedEventArgs>> WaitForRea
592592

593593
/// <summary>
594594
/// Wait for a specific reaction.
595-
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <seealso cref="DiscordConfiguration.Intents"/>
595+
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <see cref="BaseDiscordClient.Intents"/>
596596
/// </summary>
597597
/// <param name="message">Message reaction was added to.</param>
598598
/// <param name="user">User that made the reaction.</param>
@@ -604,7 +604,7 @@ public async Task<InteractivityResult<MessageReactionAddedEventArgs>> WaitForRea
604604

605605
/// <summary>
606606
/// Waits for a specific reaction.
607-
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <seealso cref="DiscordConfiguration.Intents"/>
607+
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <see cref="BaseDiscordClient.Intents"/>
608608
/// </summary>
609609
/// <param name="predicate">Predicate to match.</param>
610610
/// <param name="message">Message reaction was added to.</param>
@@ -617,7 +617,7 @@ public async Task<InteractivityResult<MessageReactionAddedEventArgs>> WaitForRea
617617

618618
/// <summary>
619619
/// Waits for a specific reaction.
620-
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <seealso cref="DiscordConfiguration.Intents"/>
620+
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <see cref="BaseDiscordClient.Intents"/>
621621
/// </summary>
622622
/// <param name="predicate">predicate to match.</param>
623623
/// <param name="user">User that made the reaction.</param>
@@ -637,7 +637,7 @@ public async Task<InteractivityResult<MessageReactionAddedEventArgs>> WaitForRea
637637
public async Task<InteractivityResult<TypingStartedEventArgs>> WaitForUserTypingAsync(DiscordUser user,
638638
DiscordChannel channel, TimeSpan? timeoutoverride = null)
639639
{
640-
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
640+
if (!Utilities.HasTypingIntents(this.Client.Intents))
641641
{
642642
throw new InvalidOperationException("No typing intents are enabled.");
643643
}
@@ -658,7 +658,7 @@ public async Task<InteractivityResult<TypingStartedEventArgs>> WaitForUserTyping
658658
/// <returns></returns>
659659
public async Task<InteractivityResult<TypingStartedEventArgs>> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null)
660660
{
661-
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
661+
if (!Utilities.HasTypingIntents(this.Client.Intents))
662662
{
663663
throw new InvalidOperationException("No typing intents are enabled.");
664664
}
@@ -679,7 +679,7 @@ public async Task<InteractivityResult<TypingStartedEventArgs>> WaitForUserTyping
679679
/// <returns></returns>
680680
public async Task<InteractivityResult<TypingStartedEventArgs>> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null)
681681
{
682-
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
682+
if (!Utilities.HasTypingIntents(this.Client.Intents))
683683
{
684684
throw new InvalidOperationException("No typing intents are enabled.");
685685
}
@@ -700,7 +700,7 @@ public async Task<InteractivityResult<TypingStartedEventArgs>> WaitForTypingAsyn
700700
/// <returns></returns>
701701
public async Task<ReadOnlyCollection<Reaction>> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null)
702702
{
703-
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
703+
if (!Utilities.HasReactionIntents(this.Client.Intents))
704704
{
705705
throw new InvalidOperationException("No reaction intents are enabled.");
706706
}
@@ -822,7 +822,7 @@ public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user,
822822

823823
/// <summary>
824824
/// Sends a paginated message.
825-
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <seealso cref="DiscordConfiguration.Intents"/>
825+
/// For this Event you need the <see cref="DiscordIntents.GuildMessageReactions"/> intent specified in <see cref="BaseDiscordClient.Intents"/>
826826
/// </summary>
827827
/// <param name="channel">Channel to send paginated message in.</param>
828828
/// <param name="user">User to give control.</param>

DSharpPlus.Rest/DiscordRestClient.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,22 @@ public override IReadOnlyDictionary<ulong, DiscordGuild> Guilds
2424
internal Dictionary<ulong, DiscordGuild> guilds = [];
2525
private bool disposedValue;
2626

27-
public DiscordRestClient(DiscordConfiguration config) : base()
27+
public string Token { get; }
28+
29+
public TokenType TokenType { get; }
30+
31+
public DiscordRestClient(DiscordConfiguration config, string token, TokenType tokenType) : base()
2832
{
2933
this.ApiClient = new(new
3034
(
3135
new(),
3236
TimeSpan.FromSeconds(10),
33-
NullLogger.Instance,
34-
config.MaximumRatelimitRetries,
35-
config.RatelimitRetryDelayFallback,
36-
config.TimeoutForInitialApiRequest,
37-
config.MaximumRestRequestsPerSecond
37+
NullLogger.Instance
3838
));
3939

4040
this.ApiClient.SetClient(this);
41+
this.Token = token;
42+
this.TokenType = tokenType;
4143
}
4244

4345
/// <summary>
@@ -1015,7 +1017,7 @@ public async Task UnpinMessageAsync(ulong channelId, ulong messageId)
10151017
/// <param name="nickname">DM nickname</param>
10161018
/// <returns></returns>
10171019
public async Task JoinGroupDmAsync(ulong channelId, string nickname)
1018-
=> await this.ApiClient.AddGroupDmRecipientAsync(channelId, this.CurrentUser.Id, this.Configuration.Token, nickname);
1020+
=> await this.ApiClient.AddGroupDmRecipientAsync(channelId, this.CurrentUser.Id, this.Token, nickname);
10191021

10201022
/// <summary>
10211023
/// Adds a member to a group DM
@@ -1063,7 +1065,7 @@ public async Task<DiscordDmChannel> CreateGroupDmAsync(IEnumerable<string> acces
10631065
public async Task<DiscordDmChannel> CreateGroupDmWithCurrentUserAsync(IEnumerable<string> accessTokens, IDictionary<ulong, string> nicks)
10641066
{
10651067
List<string> a = accessTokens.ToList();
1066-
a.Add(this.Configuration.Token);
1068+
a.Add(this.Token);
10671069
return await this.ApiClient.CreateGroupDmAsync(a, nicks);
10681070
}
10691071

0 commit comments

Comments
 (0)