diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs
index ac2ec34fd..b4966ff31 100644
--- a/DisCatSharp/Clients/BaseDiscordClient.cs
+++ b/DisCatSharp/Clients/BaseDiscordClient.cs
@@ -1,507 +1,508 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.Attributes;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;
using Sentry;
namespace DisCatSharp;
///
/// Represents a common base for various Discord Client implementations.
///
public abstract class BaseDiscordClient : IDisposable
{
///
/// Gets the api client.
///
internal protected DiscordApiClient ApiClient { get; }
///
/// Gets the sentry client.
///
internal SentryClient Sentry { get; set; }
///
/// Gets the sentry dsn.
///
internal static string SentryDsn { get; set; } = "https://1da216e26a2741b99e8ccfccea1b7ac8@o1113828.ingest.sentry.io/4504901362515968";
///
/// Gets the configuration.
///
internal protected DiscordConfiguration Configuration { get; }
///
/// Gets the instance of the logger for this client.
///
public ILogger Logger { get; internal set; }
///
/// Gets the string representing the version of bot lib.
///
public string VersionString { get; }
///
/// Gets the bot library name.
///
public string BotLibrary
=> "DisCatSharp";
///
/// Gets the current user.
///
public DiscordUser CurrentUser { get; internal set; }
///
/// Gets the current application.
///
public DiscordApplication CurrentApplication { get; internal set; }
///
/// Exposes a Http Client for custom operations.
///
public HttpClient RestClient { get; internal set; }
///
/// Gets the cached guilds for this client.
///
public abstract IReadOnlyDictionary Guilds { get; }
///
/// Gets the cached users for this client.
///
public ConcurrentDictionary UserCache { get; internal set; }
///
/// Gets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to null.
///
internal IServiceProvider ServiceProvider { get; set; }
///
/// Gets the list of available voice regions. Note that this property will not contain VIP voice regions.
///
public IReadOnlyDictionary VoiceRegions
=> this.VoiceRegionsLazy.Value;
///
/// Gets the list of available voice regions. This property is meant as a way to modify .
///
protected internal ConcurrentDictionary InternalVoiceRegions { get; set; }
internal Lazy> VoiceRegionsLazy;
///
/// Initializes this Discord API client.
///
/// Configuration for this client.
protected BaseDiscordClient(DiscordConfiguration config)
{
this.Configuration = new DiscordConfiguration(config);
this.ServiceProvider = config.ServiceProvider;
if (this.Configuration.CustomSentryDsn != null)
SentryDsn = this.Configuration.CustomSentryDsn;
if (this.ServiceProvider != null)
{
this.Configuration.LoggerFactory ??= config.ServiceProvider.GetService()!;
this.Logger = config.ServiceProvider.GetService>()!;
}
if (this.Configuration.LoggerFactory == null && !this.Configuration.EnableSentry)
{
this.Configuration.LoggerFactory = new DefaultLoggerFactory();
this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this));
}
else if (this.Configuration.LoggerFactory == null && this.Configuration.EnableSentry)
{
var configureNamedOptions = new ConfigureNamedOptions(string.Empty, x =>
{
x.Format = ConsoleLoggerFormat.Default;
x.TimestampFormat = this.Configuration.LogTimestampFormat;
x.LogToStandardErrorThreshold = this.Configuration.MinimumLogLevel;
});
var optionsFactory = new OptionsFactory(new[] { configureNamedOptions }, Enumerable.Empty>());
var optionsMonitor = new OptionsMonitor(optionsFactory, Enumerable.Empty>(), new OptionsCache());
var l = new ConsoleLoggerProvider(optionsMonitor);
this.Configuration.LoggerFactory = new LoggerFactory();
this.Configuration.LoggerFactory.AddProvider(l);
}
var ass = typeof(DiscordClient).GetTypeInfo().Assembly;
var vrs = "";
var ivr = ass.GetCustomAttribute();
if (ivr != null)
vrs = ivr.InformationalVersion;
else
{
var v = ass.GetName().Version;
vrs = v?.ToString(3);
}
if (!this.Configuration.HasShardLogger)
if (this.Configuration.LoggerFactory != null && this.Configuration.EnableSentry)
{
this.Configuration.LoggerFactory.AddSentry(o =>
{
o.InitializeSdk = true;
o.Dsn = SentryDsn;
o.DetectStartupTime = StartupTimeDetectionMode.Fast;
o.DiagnosticLevel = SentryLevel.Debug;
o.Environment = "dev";
o.IsGlobalModeEnabled = false;
o.TracesSampleRate = 1.0;
o.ReportAssembliesMode = ReportAssembliesMode.InformationalVersion;
o.AddInAppInclude("DisCatSharp");
o.AttachStacktrace = true;
o.StackTraceMode = StackTraceMode.Enhanced;
o.Release = $"{this.BotLibrary}@{vrs}";
o.SendClientReports = true;
o.IsEnvironmentUser = false;
o.UseAsyncFileIO = true;
o.EnableScopeSync = true;
if (!this.Configuration.DisableExceptionFilter)
o.AddExceptionFilter(new DisCatSharpExceptionFilter(this.Configuration));
o.Debug = this.Configuration.SentryDebug;
o.BeforeSend = e =>
{
if (!this.Configuration.DisableExceptionFilter)
{
if (e.Exception != null)
{
if (!this.Configuration.TrackExceptions.Contains(e.Exception.GetType()))
return null;
}
else if (e.Extra.Count == 0 || !e.Extra.ContainsKey("Found Fields"))
return null;
}
if (!e.HasUser())
if (this.Configuration.AttachUserInfo && this.CurrentUser! != null!)
e.User = new()
{
Id = this.CurrentUser.Id.ToString(),
Username = this.CurrentUser.UsernameWithDiscriminator,
Other = new Dictionary()
{
{ "developer", this.Configuration.DeveloperUserId?.ToString() ?? "not_given" },
{ "email", this.Configuration.FeedbackEmail ?? "not_given" }
}
};
return e;
};
});
}
if (this.Configuration.EnableSentry)
this.Sentry = new SentryClient(new SentryOptions()
{
DetectStartupTime = StartupTimeDetectionMode.Fast,
DiagnosticLevel = SentryLevel.Debug,
Environment = "dev",
IsGlobalModeEnabled = false,
TracesSampleRate = 1.0,
ReportAssembliesMode = ReportAssembliesMode.InformationalVersion,
Dsn = SentryDsn,
AttachStacktrace = true,
StackTraceMode = StackTraceMode.Enhanced,
SendClientReports = true,
Release = $"{this.BotLibrary}@{vrs}",
IsEnvironmentUser = false,
UseAsyncFileIO = true,
EnableScopeSync = true,
Debug = this.Configuration.SentryDebug,
BeforeSend = e =>
{
if (!this.Configuration.DisableExceptionFilter)
{
if (e.Exception != null)
{
if (!this.Configuration.TrackExceptions.Contains(e.Exception.GetType()))
return null;
}
else if (e.Extra.Count == 0 || !e.Extra.ContainsKey("Found Fields"))
return null;
}
if (!e.HasUser())
if (this.Configuration.AttachUserInfo && this.CurrentUser! != null!)
e.User = new()
{
Id = this.CurrentUser.Id.ToString(),
Username = this.CurrentUser.UsernameWithDiscriminator,
Other = new Dictionary()
{
{ "developer", this.Configuration.DeveloperUserId?.ToString() ?? "not_given" },
{ "email", this.Configuration.FeedbackEmail ?? "not_given" }
}
};
return e;
}
});
this.Logger ??= this.Configuration.LoggerFactory!.CreateLogger();
this.ApiClient = new DiscordApiClient(this);
this.UserCache = new ConcurrentDictionary();
this.InternalVoiceRegions = new ConcurrentDictionary();
this.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions));
this.RestClient = new();
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-locale", this.Configuration.Locale);
- this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", this.Configuration.Timezone);
+ if (!string.IsNullOrWhiteSpace(this.Configuration.Timezone))
+ this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", this.Configuration.Timezone);
if (this.Configuration.Override != null)
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this.Configuration.Override);
var a = typeof(DiscordClient).GetTypeInfo().Assembly;
var iv = a.GetCustomAttribute();
if (iv != null)
{
this.VersionString = iv.InformationalVersion;
}
else
{
var v = a.GetName().Version;
var vs = v.ToString(3);
if (v.Revision > 0)
this.VersionString = $"{vs}, CI build {v.Revision}";
}
}
///
/// Gets the current API application.
///
public async Task GetCurrentApplicationAsync()
{
var tapp = await this.ApiClient.GetCurrentApplicationOauth2InfoAsync().ConfigureAwait(false);
var app = new DiscordApplication
{
Discord = this,
Id = tapp.Id,
Name = tapp.Name,
Description = tapp.Description,
Summary = tapp.Summary,
IconHash = tapp.IconHash,
RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null,
Flags = tapp.Flags,
IsHook = tapp.IsHook,
Type = tapp.Type,
PrivacyPolicyUrl = tapp.PrivacyPolicyUrl,
TermsOfServiceUrl = tapp.TermsOfServiceUrl,
CustomInstallUrl = tapp.CustomInstallUrl,
InstallParams = tapp.InstallParams,
RoleConnectionsVerificationUrl = tapp.RoleConnectionsVerificationUrl,
Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray()
};
if (tapp.Team == null)
{
app.Owners = new List(new[] { new DiscordUser(tapp.Owner) });
app.Team = null;
app.TeamName = null;
}
else
{
app.Team = new DiscordTeam(tapp.Team);
var members = tapp.Team.Members
.Select(x => new DiscordTeamMember(x) { TeamId = app.Team.Id, TeamName = app.Team.Name, User = new DiscordUser(x.User) })
.ToArray();
var owners = members
.Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted)
.Select(x => x.User)
.ToArray();
app.Owners = new List(owners);
app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId);
app.Team.Members = new List(members);
app.TeamName = app.Team.Name;
}
app.GuildId = tapp.GuildId.ValueOrDefault();
app.Slug = tapp.Slug.ValueOrDefault();
app.PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault();
app.VerifyKey = tapp.VerifyKey.ValueOrDefault();
app.CoverImageHash = tapp.CoverImageHash.ValueOrDefault();
app.Guild = tapp.Guild.ValueOrDefault();
app.ApproximateGuildCount = tapp.ApproximateGuildCount.ValueOrDefault();
app.RequiresCodeGrant = tapp.BotRequiresCodeGrant.ValueOrDefault();
app.IsPublic = tapp.IsPublicBot.ValueOrDefault();
app.RedirectUris = tapp.RedirectUris.ValueOrDefault();
app.InteractionsEndpointUrl = tapp.InteractionsEndpointUrl.ValueOrDefault();
return app;
}
///
/// Updates the current API application.
///
/// The new description.
/// The new interactions endpoint url.
/// The new role connections verification url.
/// The new tags.
/// The new application icon.
/// The updated application.
public async Task UpdateCurrentApplicationInfoAsync(Optional description, Optional interactionsEndpointUrl, Optional roleConnectionsVerificationUrl, Optional?> tags, Optional icon)
{
var iconb64 = ImageTool.Base64FromStream(icon);
if (tags != null && tags.HasValue && tags.Value != null)
if (tags.Value.Any(x => x.Length > 20))
throw new InvalidOperationException("Tags can not exceed 20 chars.");
_ = await this.ApiClient.ModifyCurrentApplicationInfoAsync(description, interactionsEndpointUrl, roleConnectionsVerificationUrl, tags, iconb64);
// We use GetCurrentApplicationAsync because modify returns internal data not meant for developers.
var app = await this.GetCurrentApplicationAsync();
this.CurrentApplication = app;
return app;
}
///
/// Gets a list of voice regions.
///
/// Thrown when Discord is unable to process the request.
public Task> ListVoiceRegionsAsync()
=> this.ApiClient.ListVoiceRegionsAsync();
///
/// Initializes this client. This method fetches information about current user, application, and voice regions.
///
public virtual async Task InitializeAsync()
{
if (this.CurrentUser == null)
{
this.CurrentUser = await this.ApiClient.GetCurrentUserAsync().ConfigureAwait(false);
this.UserCache.AddOrUpdate(this.CurrentUser.Id, this.CurrentUser, (id, xu) => this.CurrentUser);
}
if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null)
this.CurrentApplication = await this.GetCurrentApplicationAsync().ConfigureAwait(false);
if (this.Configuration.TokenType != TokenType.Bearer && this.InternalVoiceRegions.IsEmpty)
{
var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false);
foreach (var xvr in vrs)
this.InternalVoiceRegions.TryAdd(xvr.Id, xvr);
}
if (this.Configuration.EnableSentry && this.Configuration.AttachUserInfo)
SentrySdk.ConfigureScope(x => x.User = new User()
{
Id = this.CurrentUser.Id.ToString(),
Username = this.CurrentUser.UsernameWithDiscriminator,
Other = new Dictionary()
{
{ "developer", this.Configuration.DeveloperUserId?.ToString() ?? "not_given" },
{ "email", this.Configuration.FeedbackEmail ?? "not_given" }
}
});
}
///
/// Gets the current gateway info for the provided token.
/// If no value is provided, the configuration value will be used instead.
///
/// A gateway info object.
public async Task GetGatewayInfoAsync(string token = null)
{
if (this.Configuration.TokenType != TokenType.Bot)
throw new InvalidOperationException("Only bot tokens can access this info.");
if (string.IsNullOrEmpty(this.Configuration.Token))
{
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Could not locate a valid token.");
this.Configuration.Token = token;
var res = await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
this.Configuration.Token = null;
return res;
}
return await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
}
///
/// Gets some information about the development team behind DisCatSharp.
/// Can be used for crediting etc.
/// Note: This call contacts servers managed by the DCS team, no information is collected.
/// The team, or null with errors being logged on failure.
///
[Deprecated("Don't use this right now, inactive")]
public async Task GetLibraryDevelopmentTeamAsync()
=> await DisCatSharpTeam.Get(this.RestClient, this.Logger, this.ApiClient).ConfigureAwait(false);
///
/// Gets a cached user.
///
/// The user id.
internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId)
{
this.TryGetCachedUserInternal(userId, out var user);
return user;
}
///
/// Tries the get a cached user.
///
/// The user id.
/// The user.
internal bool TryGetCachedUserInternal(ulong userId, out DiscordUser user)
{
if (this.UserCache.TryGetValue(userId, out user))
return true;
user = new DiscordUser { Id = userId, Discord = this };
return false;
}
///
/// Disposes this client.
///
public abstract void Dispose();
}
diff --git a/DisCatSharp/Clients/DiscordShardedClient.cs b/DisCatSharp/Clients/DiscordShardedClient.cs
index 018539b32..a75ed211c 100644
--- a/DisCatSharp/Clients/DiscordShardedClient.cs
+++ b/DisCatSharp/Clients/DiscordShardedClient.cs
@@ -1,887 +1,888 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using Sentry;
namespace DisCatSharp;
///
/// A Discord client that shards automatically.
///
public sealed partial class DiscordShardedClient
{
#region Public Properties
///
/// Gets the logger for this client.
///
public ILogger Logger { get; }
///
/// Gets all client shards.
///
public IReadOnlyDictionary ShardClients { get; }
///
/// Gets the gateway info for the client's session.
///
public GatewayInfo GatewayInfo { get; private set; }
///
/// Gets the current user.
///
public DiscordUser CurrentUser { get; private set; }
///
/// Gets the bot library name.
///
public string BotLibrary
=> "DisCatSharp";
///
/// Gets the current application.
///
public DiscordApplication CurrentApplication { get; private set; }
///
/// Gets the list of available voice regions. Note that this property will not contain VIP voice regions.
///
public IReadOnlyDictionary VoiceRegions
=> this._voiceRegionsLazy?.Value;
#endregion
#region Private Properties/Fields
///
/// Gets the configuration.
///
private readonly DiscordConfiguration _configuration;
///
/// Gets the list of available voice regions. This property is meant as a way to modify .
///
private ConcurrentDictionary _internalVoiceRegions;
///
/// Gets a list of shards.
///
private readonly ConcurrentDictionary _shards = new();
///
/// Gets a lazy list of voice regions.
///
private Lazy> _voiceRegionsLazy;
///
/// Whether the shard client is started.
///
private bool _isStarted;
///
/// Whether manual sharding is enabled.
///
private readonly bool _manuallySharding;
#endregion
#region Constructor
///
/// Initializes a new auto-sharding Discord client.
///
/// The configuration to use.
public DiscordShardedClient(DiscordConfiguration config)
{
this.InternalSetup();
if (config.ShardCount > 1)
this._manuallySharding = true;
this._configuration = config;
this.ShardClients = new ReadOnlyConcurrentDictionary(this._shards);
if (this._configuration.CustomSentryDsn != null)
BaseDiscordClient.SentryDsn = this._configuration.CustomSentryDsn;
if (this._configuration.LoggerFactory == null && !this._configuration.EnableSentry)
{
this._configuration.LoggerFactory = new DefaultLoggerFactory();
this._configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this._configuration.MinimumLogLevel, this._configuration.LogTimestampFormat));
}
else if (this._configuration.LoggerFactory == null && this._configuration.EnableSentry)
{
var configureNamedOptions = new ConfigureNamedOptions(string.Empty, x =>
{
x.Format = ConsoleLoggerFormat.Default;
x.TimestampFormat = this._configuration.LogTimestampFormat;
x.LogToStandardErrorThreshold = this._configuration.MinimumLogLevel;
}); var optionsFactory = new OptionsFactory(new[] { configureNamedOptions }, Enumerable.Empty>());
var optionsMonitor = new OptionsMonitor(optionsFactory, Enumerable.Empty>(), new OptionsCache());
var l = new ConsoleLoggerProvider(optionsMonitor);
this._configuration.LoggerFactory = new LoggerFactory();
this._configuration.LoggerFactory.AddProvider(l);
}
if (this._configuration.LoggerFactory != null && this._configuration.EnableSentry)
this._configuration.LoggerFactory.AddSentry(o =>
{
var a = typeof(DiscordClient).GetTypeInfo().Assembly;
var vs = "";
var iv = a.GetCustomAttribute();
if (iv != null)
vs = iv.InformationalVersion;
else
{
var v = a.GetName().Version;
vs = v?.ToString(3);
}
o.InitializeSdk = true;
o.Dsn = BaseDiscordClient.SentryDsn;
o.DetectStartupTime = StartupTimeDetectionMode.Fast;
o.DiagnosticLevel = SentryLevel.Debug;
o.Environment = "dev";
o.IsGlobalModeEnabled = false;
o.TracesSampleRate = 1.0;
o.ReportAssembliesMode = ReportAssembliesMode.InformationalVersion;
o.AddInAppInclude("DisCatSharp");
o.AttachStacktrace = true;
o.StackTraceMode = StackTraceMode.Enhanced;
o.Release = $"{this.BotLibrary}@{vs}";
o.SendClientReports = true;
if (!this._configuration.DisableExceptionFilter)
o.AddExceptionFilter(new DisCatSharpExceptionFilter(this._configuration));
o.IsEnvironmentUser = false;
o.UseAsyncFileIO = true;
o.Debug = this._configuration.SentryDebug;
o.EnableScopeSync = true;
o.BeforeSend = e =>
{
if (!this._configuration.DisableExceptionFilter)
{
if (e.Exception != null)
{
if (!this._configuration.TrackExceptions.Contains(e.Exception.GetType()))
return null;
}
else if (e.Extra.Count == 0 || !e.Extra.ContainsKey("Found Fields"))
return null;
}
if (!e.HasUser())
if (this._configuration.AttachUserInfo && this.CurrentUser! != null!)
e.User = new()
{
Id = this.CurrentUser.Id.ToString(),
Username = this.CurrentUser.UsernameWithDiscriminator,
Other = new Dictionary()
{
{ "developer", this._configuration.DeveloperUserId?.ToString() ?? "not_given" },
{ "email", this._configuration.FeedbackEmail ?? "not_given" }
}
};
return e;
};
});
this._configuration.HasShardLogger = true;
this.Logger ??= this._configuration.LoggerFactory!.CreateLogger();
}
#endregion
#region Public Methods
///
/// Initializes and connects all shards.
///
///
///
public async Task StartAsync()
{
if (this._isStarted)
throw new InvalidOperationException("This client has already been started.");
this._isStarted = true;
try
{
if (this._configuration.TokenType != TokenType.Bot)
this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful.");
this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this._botLibrary, this._versionString.Value);
var shardc = await this.InitializeShardsAsync().ConfigureAwait(false);
var connectTasks = new List();
this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booting {0} shards.", shardc);
for (var i = 0; i < shardc; i++)
{
//This should never happen, but in case it does...
if (this.GatewayInfo.SessionBucket.MaxConcurrency < 1)
this.GatewayInfo.SessionBucket.MaxConcurrency = 1;
if (this.GatewayInfo.SessionBucket.MaxConcurrency == 1)
await this.ConnectShardAsync(i).ConfigureAwait(false);
else
{
//Concurrent login.
connectTasks.Add(this.ConnectShardAsync(i));
if (connectTasks.Count == this.GatewayInfo.SessionBucket.MaxConcurrency)
{
await Task.WhenAll(connectTasks).ConfigureAwait(false);
connectTasks.Clear();
}
}
}
}
catch (Exception ex)
{
await this.InternalStopAsync(false).ConfigureAwait(false);
var message = $"Shard initialization failed, check inner exceptions for details: ";
this.Logger.LogCritical(LoggerEvents.ShardClientError, $"{message}\n{ex}");
throw new AggregateException(message, ex);
}
}
///
/// Disconnects and disposes all shards.
///
///
public Task StopAsync()
=> this.InternalStopAsync();
///
/// Gets a shard from a guild id.
///
/// If automatically sharding, this will use the method.
/// Otherwise if manually sharding, it will instead iterate through each shard's guild caches.
///
///
/// The guild ID for the shard.
/// The found shard. Otherwise null if the shard was not found for the guild id.
public DiscordClient GetShard(ulong guildId)
{
var index = this._manuallySharding ? this.GetShardIdFromGuilds(guildId) : Utilities.GetShardId(guildId, this.ShardClients.Count);
return index != -1 ? this._shards[index] : null;
}
///
/// Gets a shard from a guild.
///
/// If automatically sharding, this will use the method.
/// Otherwise if manually sharding, it will instead iterate through each shard's guild caches.
///
///
/// The guild for the shard.
/// The found shard. Otherwise null if the shard was not found for the guild.
public DiscordClient GetShard(DiscordGuild guild)
=> this.GetShard(guild.Id);
///
/// Updates the status on all shards.
///
/// The activity to set. Defaults to null.
/// The optional status to set. Defaults to null.
/// Since when is the client performing the specified activity. Defaults to null.
/// Asynchronous operation.
public async Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null)
{
var tasks = new List();
foreach (var client in this._shards.Values)
tasks.Add(client.UpdateStatusAsync(activity, userStatus, idleSince));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
///
///
///
[Obsolete("Don't use this right now, inactive")]
public async Task GetLibraryDevelopmentTeamAsync()
=> await this.GetShard(0).GetLibraryDevelopmentTeamAsync().ConfigureAwait(false);
#endregion
#region Internal Methods
///
/// Initializes the shards.
///
/// The count of initialized shards.
internal async Task InitializeShardsAsync()
{
if (!this._shards.IsEmpty)
return this._shards.Count;
this.GatewayInfo = await this.GetGatewayInfoAsync().ConfigureAwait(false);
var shardCount = this._configuration.ShardCount == 1 ? this.GatewayInfo.ShardCount : this._configuration.ShardCount;
var lf = new ShardedLoggerFactory(this.Logger);
for (var i = 0; i < shardCount; i++)
{
var cfg = new DiscordConfiguration(this._configuration)
{
ShardId = i,
ShardCount = shardCount,
LoggerFactory = lf
};
var client = new DiscordClient(cfg);
if (!this._shards.TryAdd(i, client))
throw new InvalidOperationException("Could not initialize shards.");
}
return shardCount;
}
#endregion
#region Private Methods & Version Property
///
/// Gets the gateway info.
///
private async Task GetGatewayInfoAsync()
{
var url = $"{Utilities.GetApiBaseUri(this._configuration)}{Endpoints.GATEWAY}{Endpoints.BOT}";
var http = new HttpClient();
http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(this._configuration));
http.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-locale", this._configuration.Locale);
- http.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", this._configuration.Timezone);
+ if (!string.IsNullOrWhiteSpace(this._configuration.Timezone))
+ http.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", this._configuration.Timezone);
if (this._configuration.Override != null)
http.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this._configuration.Override);
this.Logger.LogDebug(LoggerEvents.ShardRest, $"Obtaining gateway information from GET {Endpoints.GATEWAY}{Endpoints.BOT}...");
var resp = await http.GetAsync(url).ConfigureAwait(false);
http.Dispose();
if (!resp.IsSuccessStatusCode)
{
var ratelimited = await HandleHttpError(url, resp).ConfigureAwait(false);
if (ratelimited)
return await this.GetGatewayInfoAsync().ConfigureAwait(false);
}
var timer = new Stopwatch();
timer.Start();
var jo = JObject.Parse(await resp.Content.ReadAsStringAsync().ConfigureAwait(false));
var info = jo.ToObject();
//There is a delay from parsing here.
timer.Stop();
info.SessionBucket.ResetAfterInternal -= (int)timer.ElapsedMilliseconds;
info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal);
return info;
async Task HandleHttpError(string reqUrl, HttpResponseMessage msg)
{
var code = (int)msg.StatusCode;
if (code == 401 || code == 403)
{
throw new Exception($"Authentication failed, check your token and try again: {code} {msg.ReasonPhrase}");
}
else if (code == 429)
{
this.Logger.LogError(LoggerEvents.ShardClientError, $"Ratelimit hit, requeuing request to {reqUrl}");
var hs = msg.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase);
var waitInterval = 0;
if (hs.TryGetValue("Retry-After", out var retryAfterRaw))
waitInterval = int.Parse(retryAfterRaw, CultureInfo.InvariantCulture);
await Task.Delay(waitInterval).ConfigureAwait(false);
return true;
}
else if (code >= 500)
{
throw new Exception($"Internal Server Error: {code} {msg.ReasonPhrase}");
}
else
{
throw new Exception($"An unsuccessful HTTP status code was encountered: {code} {msg.ReasonPhrase}");
}
}
}
///
/// Gets the version string.
///
private readonly Lazy _versionString = new(() =>
{
var a = typeof(DiscordShardedClient).GetTypeInfo().Assembly;
var iv = a.GetCustomAttribute();
if (iv != null)
return iv.InformationalVersion;
var v = a.GetName().Version;
var vs = v.ToString(3);
if (v.Revision > 0)
vs = $"{vs}, CI build {v.Revision}";
return vs;
});
///
/// Gets the name of the used bot library.
///
private readonly string _botLibrary = "DisCatSharp";
#endregion
#region Private Connection Methods
///
/// Connects a shard.
///
/// The shard id.
private async Task ConnectShardAsync(int i)
{
if (!this._shards.TryGetValue(i, out var client))
throw new Exception($"Could not initialize shard {i}.");
client.IsShard = true;
if (this.GatewayInfo != null)
{
client.GatewayInfo = this.GatewayInfo;
client.GatewayUri = new Uri(client.GatewayInfo.Url);
}
if (this.CurrentUser != null)
client.CurrentUser = this.CurrentUser;
if (this.CurrentApplication != null)
client.CurrentApplication = this.CurrentApplication;
if (this._internalVoiceRegions != null)
{
client.InternalVoiceRegions = this._internalVoiceRegions;
client.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(client.InternalVoiceRegions));
}
this.HookEventHandlers(client);
await client.ConnectAsync();
this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booted shard {0}.", i);
this.GatewayInfo ??= client.GatewayInfo;
if (this.CurrentUser == null)
this.CurrentUser = client.CurrentUser;
if (this.CurrentApplication == null)
this.CurrentApplication = client.CurrentApplication;
if (this._internalVoiceRegions == null)
{
this._internalVoiceRegions = client.InternalVoiceRegions;
this._voiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this._internalVoiceRegions));
}
}
///
/// Stops all shards.
///
/// Whether to enable the logger.
private Task InternalStopAsync(bool enableLogger = true)
{
if (!this._isStarted)
throw new InvalidOperationException("This client has not been started.");
if (enableLogger)
this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disposing {0} shards.", this._shards.Count);
this._isStarted = false;
this._voiceRegionsLazy = null;
this.GatewayInfo = null;
this.CurrentUser = null;
this.CurrentApplication = null;
for (var i = 0; i < this._shards.Count; i++)
{
if (this._shards.TryGetValue(i, out var client))
{
this.UnhookEventHandlers(client);
client.Dispose();
if (enableLogger)
this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disconnected shard {0}.", i);
}
}
this._shards.Clear();
return Task.CompletedTask;
}
#endregion
#region Event Handler Initialization/Registering
///
/// Sets the shard client up internally..
///
private void InternalSetup()
{
this._clientErrored = new AsyncEvent("CLIENT_ERRORED", DiscordClient.EventExecutionLimit, this.Goof);
this._socketErrored = new AsyncEvent("SOCKET_ERRORED", DiscordClient.EventExecutionLimit, this.Goof);
this._socketOpened = new AsyncEvent("SOCKET_OPENED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._socketClosed = new AsyncEvent("SOCKET_CLOSED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._ready = new AsyncEvent("READY", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._resumed = new AsyncEvent("RESUMED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._channelCreated = new AsyncEvent("CHANNEL_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildCreated = new AsyncEvent("GUILD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildUpdated = new AsyncEvent("GUILD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildDeleted = new AsyncEvent("GUILD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildDownloadCompleted = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._inviteCreated = new AsyncEvent("INVITE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._inviteDeleted = new AsyncEvent("INVITE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageCreated = new AsyncEvent("MESSAGE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._typingStarted = new AsyncEvent("TYPING_STARTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._userUpdated = new AsyncEvent("USER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildMembersChunk = new AsyncEvent("GUILD_MEMBERS_CHUNKED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._heartbeated = new AsyncEvent("HEARTBEATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._threadCreated = new AsyncEvent("THREAD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._threadUpdated = new AsyncEvent("THREAD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._threadDeleted = new AsyncEvent("THREAD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._zombied = new AsyncEvent("ZOMBIED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildMemberTimeoutAdded = new AsyncEvent("GUILD_MEMBER_TIMEOUT_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildMemberTimeoutChanged = new AsyncEvent("GUILD_MEMBER_TIMEOUT_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildMemberTimeoutRemoved = new AsyncEvent("GUILD_MEMBER_TIMEOUT_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._automodRuleCreated = new AsyncEvent("AUTO_MODERATION_RULE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._automodRuleUpdated = new AsyncEvent("AUTO_MODERATION_RULE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._automodRuleDeleted = new AsyncEvent("AUTO_MODERATION_RULE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._automodActionExecuted = new AsyncEvent("AUTO_MODERATION_ACTION_EXECUTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
this._guildAuditLogEntryCreated = new AsyncEvent("GUILD_AUDIT_LOG_ENTRY_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler);
}
///
/// Hooks the event handlers.
///
/// The client.
private void HookEventHandlers(DiscordClient client)
{
client.ClientErrored += this.Client_ClientError;
client.SocketErrored += this.Client_SocketError;
client.SocketOpened += this.Client_SocketOpened;
client.SocketClosed += this.Client_SocketClosed;
client.Ready += this.Client_Ready;
client.Resumed += this.Client_Resumed;
client.ChannelCreated += this.Client_ChannelCreated;
client.ChannelUpdated += this.Client_ChannelUpdated;
client.ChannelDeleted += this.Client_ChannelDeleted;
client.DmChannelDeleted += this.Client_DMChannelDeleted;
client.ChannelPinsUpdated += this.Client_ChannelPinsUpdated;
client.GuildCreated += this.Client_GuildCreated;
client.GuildAvailable += this.Client_GuildAvailable;
client.GuildUpdated += this.Client_GuildUpdated;
client.GuildDeleted += this.Client_GuildDeleted;
client.GuildUnavailable += this.Client_GuildUnavailable;
client.GuildDownloadCompleted += this.Client_GuildDownloadCompleted;
client.InviteCreated += this.Client_InviteCreated;
client.InviteDeleted += this.Client_InviteDeleted;
client.MessageCreated += this.Client_MessageCreated;
client.PresenceUpdated += this.Client_PresenceUpdate;
client.GuildBanAdded += this.Client_GuildBanAdd;
client.GuildBanRemoved += this.Client_GuildBanRemove;
client.GuildEmojisUpdated += this.Client_GuildEmojisUpdate;
client.GuildStickersUpdated += this.Client_GuildStickersUpdate;
client.GuildIntegrationsUpdated += this.Client_GuildIntegrationsUpdate;
client.GuildMemberAdded += this.Client_GuildMemberAdd;
client.GuildMemberRemoved += this.Client_GuildMemberRemove;
client.GuildMemberUpdated += this.Client_GuildMemberUpdate;
client.GuildRoleCreated += this.Client_GuildRoleCreate;
client.GuildRoleUpdated += this.Client_GuildRoleUpdate;
client.GuildRoleDeleted += this.Client_GuildRoleDelete;
client.MessageUpdated += this.Client_MessageUpdate;
client.MessageDeleted += this.Client_MessageDelete;
client.MessagesBulkDeleted += this.Client_MessageBulkDelete;
client.InteractionCreated += this.Client_InteractionCreate;
client.ComponentInteractionCreated += this.Client_ComponentInteractionCreate;
client.ContextMenuInteractionCreated += this.Client_ContextMenuInteractionCreate;
client.TypingStarted += this.Client_TypingStart;
client.UserSettingsUpdated += this.Client_UserSettingsUpdate;
client.UserUpdated += this.Client_UserUpdate;
client.VoiceStateUpdated += this.Client_VoiceStateUpdate;
client.VoiceServerUpdated += this.Client_VoiceServerUpdate;
client.GuildMembersChunked += this.Client_GuildMembersChunk;
client.UnknownEvent += this.Client_UnknownEvent;
client.MessageReactionAdded += this.Client_MessageReactionAdd;
client.MessageReactionRemoved += this.Client_MessageReactionRemove;
client.MessageReactionsCleared += this.Client_MessageReactionRemoveAll;
client.MessageReactionRemovedEmoji += this.Client_MessageReactionRemovedEmoji;
client.WebhooksUpdated += this.Client_WebhooksUpdate;
client.Heartbeated += this.Client_HeartBeated;
client.ApplicationCommandCreated += this.Client_ApplicationCommandCreated;
client.ApplicationCommandUpdated += this.Client_ApplicationCommandUpdated;
client.ApplicationCommandDeleted += this.Client_ApplicationCommandDeleted;
client.GuildApplicationCommandCountUpdated += this.Client_GuildApplicationCommandCountUpdated;
client.ApplicationCommandPermissionsUpdated += this.Client_ApplicationCommandPermissionsUpdated;
client.GuildIntegrationCreated += this.Client_GuildIntegrationCreated;
client.GuildIntegrationUpdated += this.Client_GuildIntegrationUpdated;
client.GuildIntegrationDeleted += this.Client_GuildIntegrationDeleted;
client.StageInstanceCreated += this.Client_StageInstanceCreated;
client.StageInstanceUpdated += this.Client_StageInstanceUpdated;
client.StageInstanceDeleted += this.Client_StageInstanceDeleted;
client.ThreadCreated += this.Client_ThreadCreated;
client.ThreadUpdated += this.Client_ThreadUpdated;
client.ThreadDeleted += this.Client_ThreadDeleted;
client.ThreadListSynced += this.Client_ThreadListSynced;
client.ThreadMemberUpdated += this.Client_ThreadMemberUpdated;
client.ThreadMembersUpdated += this.Client_ThreadMembersUpdated;
client.Zombied += this.Client_Zombied;
client.PayloadReceived += this.Client_PayloadReceived;
client.GuildScheduledEventCreated += this.Client_GuildScheduledEventCreated;
client.GuildScheduledEventUpdated += this.Client_GuildScheduledEventUpdated;
client.GuildScheduledEventDeleted += this.Client_GuildScheduledEventDeleted;
client.GuildScheduledEventUserAdded += this.Client_GuildScheduledEventUserAdded; ;
client.GuildScheduledEventUserRemoved += this.Client_GuildScheduledEventUserRemoved;
client.EmbeddedActivityUpdated += this.Client_EmbeddedActivityUpdated;
client.GuildMemberTimeoutAdded += this.Client_GuildMemberTimeoutAdded;
client.GuildMemberTimeoutChanged += this.Client_GuildMemberTimeoutChanged;
client.GuildMemberTimeoutRemoved += this.Client_GuildMemberTimeoutRemoved;
client.AutomodRuleCreated += this.Client_AutomodRuleCreated;
client.AutomodRuleUpdated += this.Client_AutomodRuleUpdated;
client.AutomodRuleDeleted += this.Client_AutomodRuleDeleted;
client.AutomodActionExecuted += this.Client_AutomodActionExecuted;
client.GuildAuditLogEntryCreated += this.Client_GuildAuditLogEntryCreated;
}
///
/// Unhooks the event handlers.
///
/// The client.
private void UnhookEventHandlers(DiscordClient client)
{
client.ClientErrored -= this.Client_ClientError;
client.SocketErrored -= this.Client_SocketError;
client.SocketOpened -= this.Client_SocketOpened;
client.SocketClosed -= this.Client_SocketClosed;
client.Ready -= this.Client_Ready;
client.Resumed -= this.Client_Resumed;
client.ChannelCreated -= this.Client_ChannelCreated;
client.ChannelUpdated -= this.Client_ChannelUpdated;
client.ChannelDeleted -= this.Client_ChannelDeleted;
client.DmChannelDeleted -= this.Client_DMChannelDeleted;
client.ChannelPinsUpdated -= this.Client_ChannelPinsUpdated;
client.GuildCreated -= this.Client_GuildCreated;
client.GuildAvailable -= this.Client_GuildAvailable;
client.GuildUpdated -= this.Client_GuildUpdated;
client.GuildDeleted -= this.Client_GuildDeleted;
client.GuildUnavailable -= this.Client_GuildUnavailable;
client.GuildDownloadCompleted -= this.Client_GuildDownloadCompleted;
client.InviteCreated -= this.Client_InviteCreated;
client.InviteDeleted -= this.Client_InviteDeleted;
client.MessageCreated -= this.Client_MessageCreated;
client.PresenceUpdated -= this.Client_PresenceUpdate;
client.GuildBanAdded -= this.Client_GuildBanAdd;
client.GuildBanRemoved -= this.Client_GuildBanRemove;
client.GuildEmojisUpdated -= this.Client_GuildEmojisUpdate;
client.GuildStickersUpdated -= this.Client_GuildStickersUpdate;
client.GuildIntegrationsUpdated -= this.Client_GuildIntegrationsUpdate;
client.GuildMemberAdded -= this.Client_GuildMemberAdd;
client.GuildMemberRemoved -= this.Client_GuildMemberRemove;
client.GuildMemberUpdated -= this.Client_GuildMemberUpdate;
client.GuildRoleCreated -= this.Client_GuildRoleCreate;
client.GuildRoleUpdated -= this.Client_GuildRoleUpdate;
client.GuildRoleDeleted -= this.Client_GuildRoleDelete;
client.MessageUpdated -= this.Client_MessageUpdate;
client.MessageDeleted -= this.Client_MessageDelete;
client.MessagesBulkDeleted -= this.Client_MessageBulkDelete;
client.InteractionCreated -= this.Client_InteractionCreate;
client.ComponentInteractionCreated -= this.Client_ComponentInteractionCreate;
client.ContextMenuInteractionCreated -= this.Client_ContextMenuInteractionCreate;
client.TypingStarted -= this.Client_TypingStart;
client.UserSettingsUpdated -= this.Client_UserSettingsUpdate;
client.UserUpdated -= this.Client_UserUpdate;
client.VoiceStateUpdated -= this.Client_VoiceStateUpdate;
client.VoiceServerUpdated -= this.Client_VoiceServerUpdate;
client.GuildMembersChunked -= this.Client_GuildMembersChunk;
client.UnknownEvent -= this.Client_UnknownEvent;
client.MessageReactionAdded -= this.Client_MessageReactionAdd;
client.MessageReactionRemoved -= this.Client_MessageReactionRemove;
client.MessageReactionsCleared -= this.Client_MessageReactionRemoveAll;
client.MessageReactionRemovedEmoji -= this.Client_MessageReactionRemovedEmoji;
client.WebhooksUpdated -= this.Client_WebhooksUpdate;
client.Heartbeated -= this.Client_HeartBeated;
client.ApplicationCommandCreated -= this.Client_ApplicationCommandCreated;
client.ApplicationCommandUpdated -= this.Client_ApplicationCommandUpdated;
client.ApplicationCommandDeleted -= this.Client_ApplicationCommandDeleted;
client.GuildApplicationCommandCountUpdated -= this.Client_GuildApplicationCommandCountUpdated;
client.ApplicationCommandPermissionsUpdated -= this.Client_ApplicationCommandPermissionsUpdated;
client.GuildIntegrationCreated -= this.Client_GuildIntegrationCreated;
client.GuildIntegrationUpdated -= this.Client_GuildIntegrationUpdated;
client.GuildIntegrationDeleted -= this.Client_GuildIntegrationDeleted;
client.StageInstanceCreated -= this.Client_StageInstanceCreated;
client.StageInstanceUpdated -= this.Client_StageInstanceUpdated;
client.StageInstanceDeleted -= this.Client_StageInstanceDeleted;
client.ThreadCreated -= this.Client_ThreadCreated;
client.ThreadUpdated -= this.Client_ThreadUpdated;
client.ThreadDeleted -= this.Client_ThreadDeleted;
client.ThreadListSynced -= this.Client_ThreadListSynced;
client.ThreadMemberUpdated -= this.Client_ThreadMemberUpdated;
client.ThreadMembersUpdated -= this.Client_ThreadMembersUpdated;
client.Zombied -= this.Client_Zombied;
client.PayloadReceived -= this.Client_PayloadReceived;
client.GuildScheduledEventCreated -= this.Client_GuildScheduledEventCreated;
client.GuildScheduledEventUpdated -= this.Client_GuildScheduledEventUpdated;
client.GuildScheduledEventDeleted -= this.Client_GuildScheduledEventDeleted;
client.GuildScheduledEventUserAdded -= this.Client_GuildScheduledEventUserAdded; ;
client.GuildScheduledEventUserRemoved -= this.Client_GuildScheduledEventUserRemoved;
client.EmbeddedActivityUpdated -= this.Client_EmbeddedActivityUpdated;
client.GuildMemberTimeoutAdded -= this.Client_GuildMemberTimeoutAdded;
client.GuildMemberTimeoutChanged -= this.Client_GuildMemberTimeoutChanged;
client.GuildMemberTimeoutRemoved -= this.Client_GuildMemberTimeoutRemoved;
client.AutomodRuleCreated -= this.Client_AutomodRuleCreated;
client.AutomodRuleUpdated -= this.Client_AutomodRuleUpdated;
client.AutomodRuleDeleted -= this.Client_AutomodRuleDeleted;
client.AutomodActionExecuted -= this.Client_AutomodActionExecuted;
client.GuildAuditLogEntryCreated -= this.Client_GuildAuditLogEntryCreated;
}
///
/// Gets the shard id from guilds.
///
/// The id.
/// An int.
private int GetShardIdFromGuilds(ulong id)
{
foreach (var s in this._shards.Values)
{
if (s.GuildsInternal.TryGetValue(id, out _))
{
return s.ShardId;
}
}
return -1;
}
#endregion
#region Destructor
~DiscordShardedClient()
{
this.InternalStopAsync(false).GetAwaiter().GetResult();
}
#endregion
}
diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs
index 46aeabd79..8a39d1a30 100644
--- a/DisCatSharp/DiscordConfiguration.cs
+++ b/DisCatSharp/DiscordConfiguration.cs
@@ -1,426 +1,426 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using DisCatSharp.Attributes;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Udp;
using DisCatSharp.Net.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace DisCatSharp;
///
/// Represents configuration for and .
///
public sealed class DiscordConfiguration
{
///
/// Sets the token used to identify the client.
///
public string Token
{
internal get => this._token;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value), "Token cannot be null, empty, or all whitespace.");
this._token = value.Trim();
}
}
private string _token = "";
///
/// Sets the type of the token used to identify the client.
/// Defaults to .
///
public TokenType TokenType { internal get; set; } = TokenType.Bot;
///
/// Sets the minimum logging level for messages.
/// Typically, the default value of is ok for most uses.
///
public LogLevel MinimumLogLevel { internal get; set; } = LogLevel.Information;
///
/// Overwrites the api version.
/// Defaults to 10.
///
public string ApiVersion { internal get; set; } = "10";
///
/// Sets whether to rely on Discord for NTP (Network Time Protocol) synchronization with the "X-Ratelimit-Reset-After" header.
/// If the system clock is unsynced, setting this to true will ensure ratelimits are synced with Discord and reduce the risk of hitting one.
/// This should only be set to false if the system clock is synced with NTP.
/// Defaults to .
///
public bool UseRelativeRatelimit { internal get; set; } = true;
///
/// Allows you to overwrite the time format used by the internal debug logger.
/// Only applicable when is set left at default value. Defaults to ISO 8601-like format.
///
public string LogTimestampFormat { internal get; set; } = "yyyy-MM-dd HH:mm:ss zzz";
///
/// Sets the member count threshold at which guilds are considered large.
/// Defaults to 250.
///
public int LargeThreshold { internal get; set; } = 250;
///
/// Sets whether to automatically reconnect in case a connection is lost.
/// Defaults to .
///
public bool AutoReconnect { internal get; set; } = true;
///
/// Sets the ID of the shard to connect to.
/// If not sharding, or sharding automatically, this value should be left with the default value of 0.
///
public int ShardId { internal get; set; } = 0;
///
/// Sets the total number of shards the bot is on. If not sharding, this value should be left with a default value of 1.
/// If sharding automatically, this value will indicate how many shards to boot. If left default for automatic sharding, the client will determine the shard count automatically.
///
public int ShardCount { internal get; set; } = 1;
///
/// Sets the level of compression for WebSocket traffic.
/// Disabling this option will increase the amount of traffic sent via WebSocket. Setting will enable compression for READY and GUILD_CREATE payloads. Setting will enable compression for the entire WebSocket stream, drastically reducing amount of traffic.
/// Defaults to .
///
public GatewayCompressionLevel GatewayCompressionLevel { internal get; set; } = GatewayCompressionLevel.Stream;
///
/// Sets the size of the global message cache.
/// Setting this to 0 will disable message caching entirely.
/// Defaults to 1024.
///
public int MessageCacheSize { internal get; set; } = 1024;
///
/// Sets the proxy to use for HTTP and WebSocket connections to Discord.
/// Defaults to .
///
public IWebProxy Proxy { internal get; set; } = null!;
///
/// Sets the timeout for HTTP requests.
/// Set to to disable timeouts.
/// Defaults to 20 seconds.
///
public TimeSpan HttpTimeout { internal get; set; } = TimeSpan.FromSeconds(20);
///
/// Defines that the client should attempt to reconnect indefinitely.
/// This is typically a very bad idea to set to true, as it will swallow all connection errors.
/// Defaults to .
///
public bool ReconnectIndefinitely { internal get; set; } = false;
///
/// Sets whether the client should attempt to cache members if exclusively using unprivileged intents.
///
/// This will only take effect if there are no or
/// intents specified. Otherwise, this will always be overwritten to true.
///
/// Defaults to .
///
public bool AlwaysCacheMembers { internal get; set; } = true;
///
/// Sets whether a shard logger is attached.
///
internal bool HasShardLogger { get; set; } = false;
///
/// Sets the gateway intents for this client.
/// If set, the client will only receive events that they specify with intents.
/// Defaults to .
///
public DiscordIntents Intents { internal get; set; } = DiscordIntents.AllUnprivileged;
///
/// Sets the factory method used to create instances of WebSocket clients.
/// Use and equivalents on other implementations to switch out client implementations.
/// Defaults to .
///
public WebSocketClientFactoryDelegate WebSocketClientFactory
{
internal get => this._webSocketClientFactory;
set
{
if (value == null)
throw new InvalidOperationException("You need to supply a valid WebSocket client factory method.");
this._webSocketClientFactory = value;
}
}
private WebSocketClientFactoryDelegate _webSocketClientFactory = WebSocketClient.CreateNew;
///
/// Sets the factory method used to create instances of UDP clients.
/// Use and equivalents on other implementations to switch out client implementations.
/// Defaults to .
///
public UdpClientFactoryDelegate UdpClientFactory
{
internal get => this._udpClientFactory;
set => this._udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method.");
}
private UdpClientFactoryDelegate _udpClientFactory = DcsUdpClient.CreateNew;
///
/// Sets the logger implementation to use.
/// To create your own logger, implement the instance.
/// Defaults to built-in implementation.
///
public ILoggerFactory LoggerFactory { internal get; set; } = null!;
///
/// Sets if the bot's status should show the mobile icon.
/// Defaults to .
///
public bool MobileStatus { internal get; set; } = false;
///
/// Whether to use canary. has to be false.
/// Defaults to .
///
[Deprecated("Use ApiChannel instead.")]
public bool UseCanary
{
internal get => this.ApiChannel == ApiChannel.Canary;
set
{
if (value)
this.ApiChannel = ApiChannel.Canary;
}
}
///
/// Whether to use ptb. has to be false.
/// Defaults to .
///
[Deprecated("Use ApiChannel instead.")]
public bool UsePtb
{
internal get => this.ApiChannel == ApiChannel.PTB;
set
{
if (value)
this.ApiChannel = ApiChannel.PTB;
}
}
///
/// Which api channel to use.
/// Defaults to .
///
public ApiChannel ApiChannel { internal get; set; } = ApiChannel.Stable;
///
/// Refresh full guild channel cache.
/// Defaults to .
///
public bool AutoRefreshChannelCache { internal get; set; } = false;
///
/// Do not use, this is meant for DisCatSharp Devs.
/// Defaults to .
///
public string Override { internal get; set; } = null!;
///
/// Sets your preferred API language. See for valid locales.
///
public string Locale { internal get; set; } = DiscordLocales.AMERICAN_ENGLISH;
///
/// Sets your timezone.
///
- public string Timezone { internal get; set; } = "Europe/Berlin";
+ public string? Timezone { internal get; set; } = null;
///
/// Whether to report missing fields for discord object.
/// Useful for library development.
/// Defaults to .
///
public bool ReportMissingFields { internal get; set; } = false;
///
/// Sets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to an empty service provider.
///
public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true);
///
/// Whether to report missing fields for discord object.
/// This helps us to track missing data and library bugs better.
/// Defaults to .
///
public bool EnableSentry { internal get; set; } = false;
///
/// Whether to attach the bots username and id to sentry reports.
/// This helps us to pinpoint problems.
/// Defaults to .
///
public bool AttachUserInfo { internal get; set; } = false;
///
/// Your email address we can reach out when your bot encounters library bugs.
/// Will only be transmitted if is .
/// Defaults to .
///
public string? FeedbackEmail { internal get; set; } = null;
///
/// Your discord user id we can reach out when your bot encounters library bugs.
/// Will only be transmitted if is .
/// Defaults to .
///
public ulong? DeveloperUserId { internal get; set; } = null;
///
/// Sets which exceptions to track with sentry.
/// Do not touch this unless you're developing the library.
///
/// Thrown when the base type of all exceptions is not .
internal List TrackExceptions
{
get => this._exceptions;
set {
if (!this.EnableLibraryDeveloperMode)
throw new AccessViolationException("Cannot set this as non-library-dev");
else if (value == null)
this._exceptions.Clear();
else this._exceptions = value.All(val => val.BaseType == typeof(DisCatSharpException))
? value
: throw new InvalidOperationException("Can only track exceptions who inherit from " + nameof(DisCatSharpException) + " and must be constructed with typeof(Type)");
}
}
///
/// The exception we track with sentry.
///
private List _exceptions = new()
{
typeof(ServerErrorException),
typeof(BadRequestException)
};
///
/// Whether to enable the library developer mode.
/// Defaults .
///
internal bool EnableLibraryDeveloperMode { get; set; } = false;
///
/// Whether to turn sentry's debug mode on.
///
internal bool SentryDebug { get; set; } = false;
///
/// Whether to disable the exception filter.
///
internal bool DisableExceptionFilter { get; set; } = false;
///
/// Custom Sentry Dsn.
///
internal string? CustomSentryDsn { get; set; } = null;
///
/// Creates a new configuration with default values.
///
public DiscordConfiguration()
{ }
///
/// Utilized via Dependency Injection Pipeline
///
///
[ActivatorUtilitiesConstructor]
public DiscordConfiguration(IServiceProvider provider)
{
this.ServiceProvider = provider;
}
///
/// Creates a clone of another discord configuration.
///
/// Client configuration to clone.
public DiscordConfiguration(DiscordConfiguration other)
{
this.Token = other.Token;
this.TokenType = other.TokenType;
this.MinimumLogLevel = other.MinimumLogLevel;
this.UseRelativeRatelimit = other.UseRelativeRatelimit;
this.LogTimestampFormat = other.LogTimestampFormat;
this.LargeThreshold = other.LargeThreshold;
this.AutoReconnect = other.AutoReconnect;
this.ShardId = other.ShardId;
this.ShardCount = other.ShardCount;
this.GatewayCompressionLevel = other.GatewayCompressionLevel;
this.MessageCacheSize = other.MessageCacheSize;
this.WebSocketClientFactory = other.WebSocketClientFactory;
this.UdpClientFactory = other.UdpClientFactory;
this.Proxy = other.Proxy;
this.HttpTimeout = other.HttpTimeout;
this.ReconnectIndefinitely = other.ReconnectIndefinitely;
this.Intents = other.Intents;
this.LoggerFactory = other.LoggerFactory;
this.MobileStatus = other.MobileStatus;
this.UseCanary = other.UseCanary;
this.UsePtb = other.UsePtb;
this.AutoRefreshChannelCache = other.AutoRefreshChannelCache;
this.ApiVersion = other.ApiVersion;
this.ServiceProvider = other.ServiceProvider;
this.Override = other.Override;
this.Locale = other.Locale;
this.Timezone = other.Timezone;
this.ReportMissingFields = other.ReportMissingFields;
this.EnableSentry = other.EnableSentry;
this.AttachUserInfo = other.AttachUserInfo;
this.FeedbackEmail = other.FeedbackEmail;
this.DeveloperUserId = other.DeveloperUserId;
this.HasShardLogger = other.HasShardLogger;
this._exceptions = other._exceptions;
this.EnableLibraryDeveloperMode = other.EnableLibraryDeveloperMode;
this.SentryDebug = other.SentryDebug;
this.DisableExceptionFilter = other.DisableExceptionFilter;
this.CustomSentryDsn = other.CustomSentryDsn;
}
}
diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs
index b40cd3af4..9229a5d64 100644
--- a/DisCatSharp/Net/Rest/RestClient.cs
+++ b/DisCatSharp/Net/Rest/RestClient.cs
@@ -1,893 +1,895 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Exceptions;
using Microsoft.Extensions.Logging;
using Sentry;
namespace DisCatSharp.Net;
///
/// Represents a client used to make REST requests.
///
internal sealed class RestClient : IDisposable
{
///
/// Gets the route argument regex.
///
private static Regex s_routeArgumentRegex { get; } = new(@":([a-z_]+)");
///
/// Gets the http client.
///
internal HttpClient HttpClient { get; }
///
/// Gets the discord client.
///
private readonly BaseDiscordClient _discord;
///
/// Gets a value indicating whether debug is enabled.
///
internal bool Debug { get; set; }
///
/// Gets the logger.
///
private readonly ILogger _logger;
///
/// Gets the routes to hashes.
///
private readonly ConcurrentDictionary _routesToHashes;
///
/// Gets the hashes to buckets.
///
private readonly ConcurrentDictionary _hashesToBuckets;
///
/// Gets the request queue.
///
private readonly ConcurrentDictionary _requestQueue;
///
/// Gets the global rate limit event.
///
private readonly AsyncManualResetEvent _globalRateLimitEvent;
///
/// Gets a value indicating whether use reset after.
///
private readonly bool _useResetAfter;
private CancellationTokenSource _bucketCleanerTokenSource;
private readonly TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60);
private volatile bool _cleanerRunning;
private Task _cleanerTask;
private volatile bool _disposed;
///
/// Initializes a new instance of the class.
///
/// The client.
internal RestClient(BaseDiscordClient client)
: this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger)
{
this._discord = client;
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client));
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-locale", client.Configuration.Locale);
- this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", client.Configuration.Timezone);
+ if (!string.IsNullOrWhiteSpace(client.Configuration.Timezone))
+ this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", client.Configuration.Timezone);
if (client.Configuration.Override != null)
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", client.Configuration.Override);
}
///
/// Initializes a new instance of the class.
/// This is for meta-clients, such as the webhook client.
///
/// The proxy.
/// The timeout.
/// Whether to use relative ratelimit.
/// The logger.
internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit,
ILogger logger)
{
this._logger = logger;
var httphandler = new HttpClientHandler
{
UseCookies = false,
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
UseProxy = proxy != null,
Proxy = proxy
};
this.HttpClient = new HttpClient(httphandler)
{
BaseAddress = new Uri(Utilities.GetApiBaseUri(this._discord?.Configuration)),
Timeout = timeout
};
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
if (this._discord != null && this._discord.Configuration != null) {
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-locale", this._discord.Configuration.Locale);
- this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", this._discord.Configuration.Timezone);
+ if (!string.IsNullOrWhiteSpace(this._discord.Configuration.Timezone))
+ this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-discord-timezone", this._discord.Configuration.Timezone);
if (this._discord.Configuration.Override != null)
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this._discord.Configuration.Override);
}
this._routesToHashes = new ConcurrentDictionary();
this._hashesToBuckets = new ConcurrentDictionary();
this._requestQueue = new ConcurrentDictionary();
this._globalRateLimitEvent = new AsyncManualResetEvent(true);
this._useResetAfter = useRelativeRatelimit;
}
///
/// Gets a ratelimit bucket.
///
/// The method.
/// The route.
/// The route parameters.
/// The url.
/// A ratelimit bucket.
public RateLimitBucket GetBucket(RestRequestMethod method, string route, object routeParams, out string url)
{
var rparamsProps = routeParams.GetType()
.GetTypeInfo()
.DeclaredProperties;
var rparams = new Dictionary();
foreach (var xp in rparamsProps)
{
var val = xp.GetValue(routeParams);
rparams[xp.Name] = val is string xs
? xs
: val is DateTime dt
? dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture)
: val is DateTimeOffset dto
? dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture)
: val is IFormattable xf ? xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString();
}
var guildId = rparams.TryGetValue("guild_id", out var rparam) ? rparam : "";
var channelId = rparams.TryGetValue("channel_id", out var rparam1) ? rparam1 : "";
var webhookId = rparams.TryGetValue("webhook_id", out var rparam2) ? rparam2 : "";
// Create a generic route (minus major params) key
// ex: POST:/channels/channel_id/messages
var hashKey = RateLimitBucket.GenerateHashKey(method, route);
// We check if the hash is present, using our generic route (without major params)
// ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33
// If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant
// and assign this to the route to hash cache
// ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited
var hash = this._routesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route));
// Next we use the hash to generate the key to obtain the bucket.
// ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id
// or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id
var bucketId = RateLimitBucket.GenerateBucketId(hash, guildId, channelId, webhookId);
// If it's not in cache, create a new bucket and index it by its bucket id.
var bucket = this._hashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guildId, channelId, webhookId));
bucket.LastAttemptAt = DateTimeOffset.UtcNow;
// Cache the routes for each bucket so it can be used for GC later.
if (!bucket.RouteHashes.Contains(bucketId))
bucket.RouteHashes.Add(bucketId);
// Add the current route to the request queue, which indexes the amount
// of requests occurring to the bucket id.
_ = this._requestQueue.TryGetValue(bucketId, out var count);
// Increment by one atomically due to concurrency
this._requestQueue[bucketId] = Interlocked.Increment(ref count);
// Start bucket cleaner if not already running.
if (!this._cleanerRunning)
{
this._cleanerRunning = true;
this._bucketCleanerTokenSource = new CancellationTokenSource();
this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token);
this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started.");
}
url = s_routeArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]);
return bucket;
}
///
/// Executes the request.
///
/// The request to be executed.
public Task ExecuteRequestAsync(BaseRestRequest request)
=> request == null ? throw new ArgumentNullException(nameof(request)) : this.ExecuteRequestAsync(request, null, null);
///
/// Executes the request.
/// This is to allow proper rescheduling of the first request from a bucket.
///
/// The request to be executed.
/// The bucket.
/// The ratelimit task completion source.
private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource ratelimitTcs)
{
if (this._disposed)
return;
HttpResponseMessage res = default;
try
{
await this._globalRateLimitEvent.WaitAsync().ConfigureAwait(false);
bucket ??= request.RateLimitBucket;
ratelimitTcs ??= await this.WaitForInitialRateLimit(bucket).ConfigureAwait(false);
if (ratelimitTcs == null) // check rate limit only if we are not the probe request
{
var now = DateTimeOffset.UtcNow;
await bucket.TryResetLimitAsync(now).ConfigureAwait(false);
// Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket
if (Interlocked.Decrement(ref bucket.RemainingInternal) < 0)
{
this._logger.LogWarning(LoggerEvents.RatelimitDiag, "Request for {bucket} is blocked. Url: {url}", bucket.ToString(), request.Url.AbsoluteUri);
var delay = bucket.Reset - now;
var resetDate = bucket.Reset;
if (this._useResetAfter)
{
delay = bucket.ResetAfter.Value;
resetDate = bucket.ResetAfterOffset;
}
if (delay < new TimeSpan(-TimeSpan.TicksPerMinute))
{
this._logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket");
bucket.RemainingInternal = 1;
}
if (delay < TimeSpan.Zero)
delay = TimeSpan.FromMilliseconds(100);
this._logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Preemptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay);
Task.Delay(delay)
.ContinueWith(_ => this.ExecuteRequestAsync(request, null, null))
.LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
return;
}
this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {bucket} is allowed. Url: {url}", bucket.ToString(), request.Url.AbsoluteUri);
}
else
this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {bucket} is allowed. Url: {url}", bucket.ToString(), request.Url.AbsoluteUri);
var req = this.BuildRequest(request);
if (this.Debug)
this._logger.LogTrace(LoggerEvents.Misc, await req.Content.ReadAsStringAsync());
var response = new RestResponse();
try
{
if (this._disposed)
return;
res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false);
var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
var txt = Utilities.UTF8.GetString(bts, 0, bts.Length);
this._logger.LogTrace(LoggerEvents.RestRx, txt);
response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase);
response.Response = txt;
response.ResponseCode = (int)res.StatusCode;
}
catch (HttpRequestException httpex)
{
this._logger.LogError(LoggerEvents.RestError, httpex, "Request to {0} triggered an HttpException", request.Url.AbsoluteUri);
request.SetFaulted(httpex);
this.FailInitialRateLimitTest(request, ratelimitTcs);
return;
}
this.UpdateBucket(request, response, ratelimitTcs);
Exception ex = null;
Exception senex = null;
switch (response.ResponseCode)
{
case 400:
case 405:
ex = new BadRequestException(request, response);
senex = new Exception(ex.Message + "\nJson Response: " + (ex as BadRequestException).JsonMessage ?? "null", ex);
break;
case 401:
case 403:
ex = new UnauthorizedException(request, response);
break;
case 404:
ex = new NotFoundException(request, response);
break;
case 413:
ex = new RequestSizeException(request, response);
break;
case 429:
ex = new RateLimitException(request, response);
// check the limit info and requeue
this.Handle429(response, out var wait, out var global);
if (wait != null)
{
if (global)
{
bucket.IsGlobal = true;
this._logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down for {uri}", request.Url.AbsoluteUri);
try
{
this._globalRateLimitEvent.Reset();
await wait.ConfigureAwait(false);
}
finally
{
// we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here
_ = this._globalRateLimitEvent.SetAsync();
}
this.ExecuteRequestAsync(request, bucket, ratelimitTcs)
.LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request");
}
else
{
if (this._discord is DiscordClient)
{
await (this._discord as DiscordClient)._rateLimitHit.InvokeAsync(this._discord as DiscordClient, new EventArgs.RateLimitExceptionEventArgs(this._discord.ServiceProvider)
{
Exception = ex as RateLimitException,
ApiEndpoint = request.Url.AbsoluteUri
});
}
this._logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeuing request to {url}", request.Url.AbsoluteUri);
await wait.ConfigureAwait(false);
this.ExecuteRequestAsync(request, bucket, ratelimitTcs)
.LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request");
}
return;
}
break;
case 500:
case 502:
case 503:
case 504:
ex = new ServerErrorException(request, response);
senex = new Exception(ex.Message + "\nJson Response: " + (ex as ServerErrorException).JsonMessage ?? "null", ex);
break;
}
if (ex != null)
{
if (this._discord.Configuration.EnableSentry)
{
if (senex != null)
{
Dictionary debugInfo = new()
{
{ "route", request.Route },
{ "time", DateTimeOffset.UtcNow }
};
senex.AddSentryContext("Request", debugInfo);
this._discord.Sentry.CaptureException(senex);
}
}
request.SetFaulted(ex);
}
else
request.SetCompleted(response);
}
catch (Exception ex)
{
this._logger.LogError(LoggerEvents.RestError, ex, "Request to {0} triggered an exception", request.Url.AbsoluteUri);
// if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run
if (bucket != null && ratelimitTcs != null && bucket.LimitTesting != 0)
this.FailInitialRateLimitTest(request, ratelimitTcs);
if (!request.TrySetFaulted(ex))
throw;
}
finally
{
res?.Dispose();
// Get and decrement active requests in this bucket by 1.
_ = this._requestQueue.TryGetValue(bucket.BucketId, out var count);
this._requestQueue[bucket.BucketId] = Interlocked.Decrement(ref count);
// If it's 0 or less, we can remove the bucket from the active request queue,
// along with any of its past routes.
if (count <= 0)
{
foreach (var r in bucket.RouteHashes)
{
if (this._requestQueue.ContainsKey(r))
{
_ = this._requestQueue.TryRemove(r, out _);
}
}
}
}
}
///
/// Fails the initial rate limit test.
///
/// The request.
/// The ratelimit task completion source.
/// Whether to reset to initial values.
private void FailInitialRateLimitTest(BaseRestRequest request, TaskCompletionSource ratelimitTcs, bool resetToInitial = false)
{
if (ratelimitTcs == null && !resetToInitial)
return;
var bucket = request.RateLimitBucket;
bucket.LimitValid = false;
bucket.LimitTestFinished = null;
bucket.LimitTesting = 0;
//Reset to initial values.
if (resetToInitial)
{
this.UpdateHashCaches(request, bucket);
bucket.Maximum = 0;
bucket.RemainingInternal = 0;
return;
}
// no need to wait on all the potentially waiting tasks
_ = Task.Run(() => ratelimitTcs.TrySetResult(false));
}
///
/// Waits for the initial rate limit.
///
/// The bucket.
private async Task> WaitForInitialRateLimit(RateLimitBucket bucket)
{
while (!bucket.LimitValid)
{
if (bucket.LimitTesting == 0)
{
if (Interlocked.CompareExchange(ref bucket.LimitTesting, 1, 0) == 0)
{
// if we got here when the first request was just finishing, we must not create the waiter task as it would signal ExecuteRequestAsync to bypass rate limiting
if (bucket.LimitValid)
return null;
// allow exactly one request to go through without having rate limits available
var ratelimitsTcs = new TaskCompletionSource();
bucket.LimitTestFinished = ratelimitsTcs.Task;
return ratelimitsTcs;
}
}
// it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits
Task waitTask = null;
while (bucket.LimitTesting != 0 && (waitTask = bucket.LimitTestFinished) == null)
await Task.Yield();
if (waitTask != null)
await waitTask.ConfigureAwait(false);
// if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here
}
return null;
}
///
/// Builds the request.
///
/// The request.
/// A http request message.
private HttpRequestMessage BuildRequest(BaseRestRequest request)
{
var req = new HttpRequestMessage(new HttpMethod(request.Method.ToString()), request.Url);
if (request.Headers != null && request.Headers.Any())
foreach (var kvp in request.Headers)
req.Headers.Add(kvp.Key, kvp.Value);
if (request is RestRequest nmprequest && !string.IsNullOrWhiteSpace(nmprequest.Payload))
{
this._logger.LogTrace(LoggerEvents.RestTx, nmprequest.Payload);
req.Content = new StringContent(nmprequest.Payload);
req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
if (request is MultipartWebRequest mprequest)
{
this._logger.LogTrace(LoggerEvents.RestTx, "");
var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
req.Headers.Add("Connection", "keep-alive");
req.Headers.Add("Keep-Alive", "600");
var content = new MultipartFormDataContent(boundary);
if (mprequest.Values != null && mprequest.Values.Any())
foreach (var kvp in mprequest.Values)
content.Add(new StringContent(kvp.Value), kvp.Key);
var fileId = mprequest.OverwriteFileIdStart ?? 0;
if (mprequest.Files != null && mprequest.Files.Any())
{
foreach (var f in mprequest.Files)
{
var name = $"files[{fileId.ToString(CultureInfo.InvariantCulture)}]";
content.Add(new StreamContent(f.Value), name, f.Key);
fileId++;
}
}
req.Content = content;
}
if (request is MultipartStickerWebRequest mpsrequest)
{
this._logger.LogTrace(LoggerEvents.RestTx, "");
var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
req.Headers.Add("Connection", "keep-alive");
req.Headers.Add("Keep-Alive", "600");
var sc = new StreamContent(mpsrequest.File.Stream);
if (mpsrequest.File.ContentType != null)
sc.Headers.ContentType = new MediaTypeHeaderValue(mpsrequest.File.ContentType);
var fileName = mpsrequest.File.FileName;
if (mpsrequest.File.FileType != null)
fileName += '.' + mpsrequest.File.FileType;
var content = new MultipartFormDataContent(boundary)
{
{ new StringContent(mpsrequest.Name), "name" },
{ new StringContent(mpsrequest.Tags), "tags" },
{ new StringContent(mpsrequest.Description), "description" },
{ sc, "file", fileName }
};
req.Content = content;
}
return req;
}
///
/// Handles the HTTP 429 status.
///
/// The response.
/// The wait task.
/// If true, global.
private void Handle429(RestResponse response, out Task waitTask, out bool global)
{
waitTask = null;
global = false;
if (response.Headers == null)
return;
var hs = response.Headers;
// handle the wait
if (hs.TryGetValue("Retry-After", out var retryAfterRaw))
{
var retryAfter = TimeSpan.FromSeconds(int.Parse(retryAfterRaw, CultureInfo.InvariantCulture));
waitTask = Task.Delay(retryAfter);
}
// check if global b1nzy
if (hs.TryGetValue("X-RateLimit-Global", out var isGlobal) && isGlobal.ToLowerInvariant() == "true")
{
// global
global = true;
}
}
///
/// Updates the bucket.
///
/// The request.
/// The response.
/// The ratelimit task completion source.
private void UpdateBucket(BaseRestRequest request, RestResponse response, TaskCompletionSource ratelimitTcs)
{
var bucket = request.RateLimitBucket;
if (response.Headers == null)
{
if (response.ResponseCode != 429) // do not fail when ratelimit was or the next request will be scheduled hitting the rate limit again
this.FailInitialRateLimitTest(request, ratelimitTcs);
return;
}
var hs = response.Headers;
if (hs.TryGetValue("X-RateLimit-Scope", out var scope))
{
bucket.Scope = scope;
}
if (hs.TryGetValue("X-RateLimit-Global", out var isGlobal) && isGlobal.ToLowerInvariant() == "true")
{
if (response.ResponseCode != 429)
{
bucket.IsGlobal = true;
this.FailInitialRateLimitTest(request, ratelimitTcs);
}
return;
}
var r1 = hs.TryGetValue("X-RateLimit-Limit", out var usesMax);
var r2 = hs.TryGetValue("X-RateLimit-Remaining", out var usesLeft);
var r3 = hs.TryGetValue("X-RateLimit-Reset", out var reset);
var r4 = hs.TryGetValue("X-Ratelimit-Reset-After", out var resetAfter);
var r5 = hs.TryGetValue("X-Ratelimit-Bucket", out var hash);
if (!r1 || !r2 || !r3 || !r4)
{
//If the limits were determined before this request, make the bucket initial again.
if (response.ResponseCode != 429)
this.FailInitialRateLimitTest(request, ratelimitTcs, ratelimitTcs == null);
return;
}
var clientTime = DateTimeOffset.UtcNow;
var resetTime = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(double.Parse(reset, CultureInfo.InvariantCulture));
var serverTime = clientTime;
if (hs.TryGetValue("Date", out var rawDate))
serverTime = DateTimeOffset.Parse(rawDate, CultureInfo.InvariantCulture).ToUniversalTime();
var resetDelta = resetTime - serverTime;
//var difference = clientTime - serverTime;
//if (Math.Abs(difference.TotalSeconds) >= 1)
//// this.Logger.LogMessage(LogLevel.DebugBaseDiscordClient.RestEventId, $"Difference between machine and server time: {difference.TotalMilliseconds.ToString("#,##0.00", CultureInfo.InvariantCulture)}ms", DateTime.Now);
//else
// difference = TimeSpan.Zero;
if (request.RateLimitWaitOverride.HasValue)
resetDelta = TimeSpan.FromSeconds(request.RateLimitWaitOverride.Value);
var newReset = clientTime + resetDelta;
if (this._useResetAfter)
{
bucket.ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter, CultureInfo.InvariantCulture));
newReset = clientTime + bucket.ResetAfter.Value + (request.RateLimitWaitOverride.HasValue
? resetDelta
: TimeSpan.Zero);
bucket.ResetAfterOffset = newReset;
}
else
bucket.Reset = newReset;
var maximum = int.Parse(usesMax, CultureInfo.InvariantCulture);
var remaining = int.Parse(usesLeft, CultureInfo.InvariantCulture);
if (ratelimitTcs != null)
{
// initial population of the ratelimit data
bucket.SetInitialValues(maximum, remaining, newReset);
_ = Task.Run(() => ratelimitTcs.TrySetResult(true));
}
else
{
// only update the bucket values if this request was for a newer interval than the one
// currently in the bucket, to avoid issues with concurrent requests in one bucket
// remaining is reset by TryResetLimit and not the response, just allow that to happen when it is time
if (bucket.NextReset == 0)
bucket.NextReset = newReset.UtcTicks;
}
this.UpdateHashCaches(request, bucket, hash);
}
///
/// Updates the hash caches.
///
/// The request.
/// The bucket.
/// The new hash.
private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null)
{
var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route);
if (!this._routesToHashes.TryGetValue(hashKey, out var oldHash))
return;
// This is an unlimited bucket, which we don't need to keep track of.
if (newHash == null)
{
_ = this._routesToHashes.TryRemove(hashKey, out _);
_ = this._hashesToBuckets.TryRemove(bucket.BucketId, out _);
return;
}
// Only update the hash once, due to a bug on Discord's end.
// This will cause issues if the bucket hashes are dynamically changed from the API while running,
// in which case, Dispose will need to be called to clear the caches.
if (bucket.IsUnlimited && newHash != oldHash)
{
this._logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {0}: \"{1}\" -> \"{2}\"", hashKey, oldHash, newHash);
var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId);
_ = this._routesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) =>
{
bucket.Hash = newHash;
var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId);
// Remove the old unlimited bucket.
_ = this._hashesToBuckets.TryRemove(oldBucketId, out _);
_ = this._hashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket);
return newHash;
});
}
return;
}
///
/// Cleans the buckets.
///
private async Task CleanupBucketsAsync()
{
while (!this._bucketCleanerTokenSource.IsCancellationRequested)
{
try
{
await Task.Delay(this._bucketCleanupDelay, this._bucketCleanerTokenSource.Token).ConfigureAwait(false);
}
catch { }
if (this._disposed)
return;
//Check and clean request queue first in case it wasn't removed properly during requests.
foreach (var key in this._requestQueue.Keys)
{
var bucket = this._hashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key));
if (bucket == null || (bucket != null && bucket.LastAttemptAt.AddSeconds(5) < DateTimeOffset.UtcNow))
_ = this._requestQueue.TryRemove(key, out _);
}
var removedBuckets = 0;
StringBuilder bucketIdStrBuilder = default;
foreach (var kvp in this._hashesToBuckets)
{
bucketIdStrBuilder ??= new StringBuilder();
var key = kvp.Key;
var value = kvp.Value;
// Don't remove the bucket if it's currently being handled by the rest client, unless it's an unlimited bucket.
if (this._requestQueue.ContainsKey(value.BucketId) && !value.IsUnlimited)
continue;
var resetOffset = this._useResetAfter ? value.ResetAfterOffset : value.Reset;
// Don't remove the bucket if it's reset date is less than now + the additional wait time, unless it's an unlimited bucket.
if (!value.IsUnlimited && (resetOffset > DateTimeOffset.UtcNow || DateTimeOffset.UtcNow - resetOffset < this._bucketCleanupDelay))
continue;
_ = this._hashesToBuckets.TryRemove(key, out _);
removedBuckets++;
bucketIdStrBuilder.Append(value.BucketId + ", ");
}
if (removedBuckets > 0)
this._logger.LogDebug(LoggerEvents.RestCleaner, "Removed {0} unused bucket{1}: [{2}]", removedBuckets, removedBuckets > 1 ? "s" : string.Empty, bucketIdStrBuilder.ToString().TrimEnd(',', ' '));
if (this._hashesToBuckets.IsEmpty)
break;
}
if (!this._bucketCleanerTokenSource.IsCancellationRequested)
this._bucketCleanerTokenSource.Cancel();
this._cleanerRunning = false;
this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped.");
}
~RestClient()
{
this.Dispose();
}
///
/// Disposes the rest client.
///
public void Dispose()
{
if (this._disposed)
return;
this._disposed = true;
this._globalRateLimitEvent.Reset();
if (this._bucketCleanerTokenSource?.IsCancellationRequested == false)
{
this._bucketCleanerTokenSource?.Cancel();
this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped.");
}
try
{
this._cleanerTask?.Dispose();
this._bucketCleanerTokenSource?.Dispose();
this.HttpClient?.Dispose();
}
catch { }
this._routesToHashes.Clear();
this._hashesToBuckets.Clear();
this._requestQueue.Clear();
GC.SuppressFinalize(this);
}
}