diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs index 28ed752e8..9d1721702 100644 --- a/DisCatSharp/Clients/BaseDiscordClient.cs +++ b/DisCatSharp/Clients/BaseDiscordClient.cs @@ -1,483 +1,498 @@ // 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 => "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.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 = true; + 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; + o.AddExceptionFilter(new DisCatSharpExceptionFilter(this.Configuration)); o.BeforeSend = e => { - if (e.Exception?.Message?.ToLower()?.Contains("connection") ?? false) + 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 = true, + 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, BeforeSend = e => { + 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 (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 b146d98f5..621ca8b6e 100644 --- a/DisCatSharp/Clients/DiscordShardedClient.cs +++ b/DisCatSharp/Clients/DiscordShardedClient.cs @@ -1,871 +1,880 @@ // 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.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 = true; + 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; + o.AddExceptionFilter(new DisCatSharpExceptionFilter(this._configuration)); o.IsEnvironmentUser = false; o.UseAsyncFileIO = true; + o.Debug = true; o.EnableScopeSync = true; o.BeforeSend = e => { - if (e.Exception?.Message?.ToLower()?.Contains("connection") ?? false) + 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 (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 b8a00d92c..a14918680 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,365 +1,397 @@ // 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; } /// /// 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"; /// /// 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; - internal bool HasShardLogger { get; set; } = false; + + /// + /// Sets which exceptions to track with sentry. + /// + /// Thrown when the base type of all exceptions is not . + public List TrackExceptions + { + internal get => this._exceptions; + set { + 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) + }; /// /// 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; } } diff --git a/DisCatSharp/Exceptions/BadRequestException.cs b/DisCatSharp/Exceptions/BadRequestException.cs index d5463c340..51b197475 100644 --- a/DisCatSharp/Exceptions/BadRequestException.cs +++ b/DisCatSharp/Exceptions/BadRequestException.cs @@ -1,86 +1,87 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when a malformed request is sent. /// -public class BadRequestException : Exception +public class BadRequestException : DisCatSharpException { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the error code for this exception. /// public int Code { get; internal set; } /// /// Gets the JSON message received. /// public string JsonMessage { get; internal set; } /// /// Gets the form error responses in JSON format. /// public string Errors { get; internal set; } /// /// Initializes a new instance of the class. /// /// The request. /// The response. - internal BadRequestException(BaseRestRequest request, RestResponse response) : base("Bad request: " + response.ResponseCode) + internal BadRequestException(BaseRestRequest request, RestResponse response) + : base("Bad request: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["code"] != null) this.Code = (int)j["code"]; if (j["message"] != null) this.JsonMessage = j["message"].ToString(); if (j["errors"] != null) this.Errors = j["errors"].ToString(); } catch { } } } diff --git a/DisCatSharp/Exceptions/RateLimitException.cs b/DisCatSharp/Exceptions/DisCatSharpException.cs similarity index 52% copy from DisCatSharp/Exceptions/RateLimitException.cs copy to DisCatSharp/Exceptions/DisCatSharpException.cs index 6d9c8b590..ce66caadd 100644 --- a/DisCatSharp/Exceptions/RateLimitException.cs +++ b/DisCatSharp/Exceptions/DisCatSharpException.cs @@ -1,70 +1,32 @@ // 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 DisCatSharp.Net; - -using Newtonsoft.Json.Linq; - namespace DisCatSharp.Exceptions; -/// -/// Represents an exception thrown when too many requests are sent. -/// -public class RateLimitException : Exception +public class DisCatSharpException : Exception { - /// - /// Gets the request that caused the exception. - /// - public BaseRestRequest WebRequest { get; internal set; } - - /// - /// Gets the response to the request. - /// - public RestResponse WebResponse { get; internal set; } - - /// - /// Gets the JSON received. - /// - public string JsonMessage { get; internal set; } - - /// - /// Initializes a new instance of the class. - /// - /// The request. - /// The response. - internal RateLimitException(BaseRestRequest request, RestResponse response) : base("Rate limited: " + response.ResponseCode) - { - this.WebRequest = request; - this.WebResponse = response; - - try - { - var j = JObject.Parse(response.Response); - - if (j["message"] != null) - this.JsonMessage = j["message"].ToString(); - } - catch (Exception) { } - } + internal DisCatSharpException(string message) + : base(message) + { } } diff --git a/DisCatSharp/Exceptions/RateLimitException.cs b/DisCatSharp/Exceptions/ExceptionFilter.cs similarity index 52% copy from DisCatSharp/Exceptions/RateLimitException.cs copy to DisCatSharp/Exceptions/ExceptionFilter.cs index 6d9c8b590..5ffbb03e1 100644 --- a/DisCatSharp/Exceptions/RateLimitException.cs +++ b/DisCatSharp/Exceptions/ExceptionFilter.cs @@ -1,70 +1,40 @@ // 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 DisCatSharp.Net; - -using Newtonsoft.Json.Linq; +using Sentry.Extensibility; namespace DisCatSharp.Exceptions; -/// -/// Represents an exception thrown when too many requests are sent. -/// -public class RateLimitException : Exception +public class DisCatSharpExceptionFilter : IExceptionFilter { - /// - /// Gets the request that caused the exception. - /// - public BaseRestRequest WebRequest { get; internal set; } - - /// - /// Gets the response to the request. - /// - public RestResponse WebResponse { get; internal set; } + internal DiscordConfiguration config { get; set; } - /// - /// Gets the JSON received. - /// - public string JsonMessage { get; internal set; } + public bool Filter(Exception ex) + => !this.config.TrackExceptions.Contains(ex.GetType()); - /// - /// Initializes a new instance of the class. - /// - /// The request. - /// The response. - internal RateLimitException(BaseRestRequest request, RestResponse response) : base("Rate limited: " + response.ResponseCode) + internal DisCatSharpExceptionFilter(DiscordConfiguration configuration) { - this.WebRequest = request; - this.WebResponse = response; - - try - { - var j = JObject.Parse(response.Response); - - if (j["message"] != null) - this.JsonMessage = j["message"].ToString(); - } - catch (Exception) { } + this.config = configuration; } } diff --git a/DisCatSharp/Exceptions/NotFoundException.cs b/DisCatSharp/Exceptions/NotFoundException.cs index 1b48f0d30..1b6a97ed3 100644 --- a/DisCatSharp/Exceptions/NotFoundException.cs +++ b/DisCatSharp/Exceptions/NotFoundException.cs @@ -1,70 +1,71 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when a requested resource is not found. /// -public class NotFoundException : Exception +public class NotFoundException : DisCatSharpException { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// /// Initializes a new instance of the class. /// /// The request. /// The response. - internal NotFoundException(BaseRestRequest request, RestResponse response) : base("Not found: " + response.ResponseCode) + internal NotFoundException(BaseRestRequest request, RestResponse response) + : base("Not found: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } diff --git a/DisCatSharp/Exceptions/RateLimitException.cs b/DisCatSharp/Exceptions/RateLimitException.cs index 6d9c8b590..10577ac22 100644 --- a/DisCatSharp/Exceptions/RateLimitException.cs +++ b/DisCatSharp/Exceptions/RateLimitException.cs @@ -1,70 +1,71 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when too many requests are sent. /// -public class RateLimitException : Exception +public class RateLimitException : DisCatSharpException { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// /// Initializes a new instance of the class. /// /// The request. /// The response. - internal RateLimitException(BaseRestRequest request, RestResponse response) : base("Rate limited: " + response.ResponseCode) + internal RateLimitException(BaseRestRequest request, RestResponse response) + : base("Rate limited: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } diff --git a/DisCatSharp/Exceptions/RequestSizeException.cs b/DisCatSharp/Exceptions/RequestSizeException.cs index 71205e0a9..f9f3c3c42 100644 --- a/DisCatSharp/Exceptions/RequestSizeException.cs +++ b/DisCatSharp/Exceptions/RequestSizeException.cs @@ -1,70 +1,71 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when the request sent to Discord is too large. /// -public class RequestSizeException : Exception +public class RequestSizeException : DisCatSharpException { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// /// Initializes a new instance of the class. /// /// The request. /// The response. - internal RequestSizeException(BaseRestRequest request, RestResponse response) : base($"Request entity too large: {response.ResponseCode}. Make sure the data sent is within Discord's upload limit.") + internal RequestSizeException(BaseRestRequest request, RestResponse response) + : base($"Request entity too large: {response.ResponseCode}. Make sure the data sent is within Discord's upload limit.") { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } diff --git a/DisCatSharp/Exceptions/ServerErrorException.cs b/DisCatSharp/Exceptions/ServerErrorException.cs index 863f1caed..782e539ea 100644 --- a/DisCatSharp/Exceptions/ServerErrorException.cs +++ b/DisCatSharp/Exceptions/ServerErrorException.cs @@ -1,70 +1,71 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when Discord returns an Internal Server Error. /// -public class ServerErrorException : Exception +public class ServerErrorException : DisCatSharpException { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// /// Initializes a new instance of the class. /// /// The request. /// The response. - internal ServerErrorException(BaseRestRequest request, RestResponse response) : base("Internal Server Error: " + response.ResponseCode) + internal ServerErrorException(BaseRestRequest request, RestResponse response) + : base("Internal Server Error: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } diff --git a/DisCatSharp/Exceptions/UnauthorizedException.cs b/DisCatSharp/Exceptions/UnauthorizedException.cs index f25938ea5..741da47af 100644 --- a/DisCatSharp/Exceptions/UnauthorizedException.cs +++ b/DisCatSharp/Exceptions/UnauthorizedException.cs @@ -1,70 +1,71 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when requester doesn't have necessary permissions to complete the request. /// -public class UnauthorizedException : Exception +public class UnauthorizedException : DisCatSharpException { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// /// Initializes a new instance of the class. /// /// The request. /// The response. - internal UnauthorizedException(BaseRestRequest request, RestResponse response) : base("Unauthorized: " + response.ResponseCode) + internal UnauthorizedException(BaseRestRequest request, RestResponse response) + : base("Unauthorized: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } }