diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs index dc3de879c..a2216f0e2 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,209 +1,208 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 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.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Configuration; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace DisCatSharp.Hosting { /// /// Simple implementation for to work as a /// public abstract class DiscordHostedService : BackgroundService, IDiscordHostedService { /// public DiscordClient Client { get; private set; } protected readonly ILogger Logger; protected readonly IHostApplicationLifetime ApplicationLifetime; protected readonly IConfiguration Configuration; protected readonly IServiceProvider ServiceProvider; private readonly string _botSection; #pragma warning disable 8618 /// /// Initializes a new instance of the class. /// /// The config. /// The logger. /// The provider. /// Current hosting environment. This will be used for shutting down the application on error /// Name within the configuration which contains the config info for our bot. Default is DisCatSharp protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider, IHostApplicationLifetime applicationLifetime, string configBotSection = DisCatSharp.Configuration.ConfigurationExtensions.DefaultRootLib) { this.Logger = logger; this.ApplicationLifetime = applicationLifetime; this.Configuration = config; this._botSection = configBotSection; this.ServiceProvider = provider; this.Initialize(); } #pragma warning restore 8618 /// /// When the bot fails to start, this method will be invoked. (Default behavior is to shutdown) /// /// The exception/reason the bot couldn't start protected virtual void OnInitializationError(Exception ex) { this.ApplicationLifetime.StopApplication(); } /// /// Dynamically loads extensions by using , and /// /// protected virtual void InitializeExtensions() { var typeMap = this.Configuration.FindImplementedExtensions(this._botSection); this.Logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}"); foreach (var typePair in typeMap) try { /* If section is null --> utilize the default constructor This means the extension was explicitly added in the 'Using' array, but user did not wish to override any value(s) in the extension's config */ var configInstance = typePair.Value.Section.HasValue ? typePair.Value.Section.Value.ExtractConfig(() => ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType)) : ActivatorUtilities.CreateInstance(this.ServiceProvider, typePair.Value.ConfigType); /* Explanation for bindings Internal Constructors --> NonPublic Public Constructors --> Public Constructors --> Instance */ var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; var ctors = typePair.Value.ImplementationType.GetConstructors(flags); var instance = ctors.Any(x => x.GetParameters().Length == 1 && x.GetParameters().First().ParameterType == typePair.Value.ConfigType) ? Activator.CreateInstance(typePair.Value.ImplementationType, flags, null, new[] { configInstance }, null) : Activator.CreateInstance(typePair.Value.ImplementationType, true); /* Certain extensions do not require a configuration argument Those who do -- pass config instance in, Those who don't -- simply instantiate ActivatorUtilities requires a public constructor, anything with internal breaks */ if (instance == null) { this.Logger.LogError($"Unable to instantiate '{typePair.Value.ImplementationType.Name}'"); continue; } // Add an easy reference to our extensions for later use this.Client.AddExtension((BaseExtension)instance); } catch (Exception ex) { this.Logger.LogError($"Unable to register '{typePair.Value.ImplementationType.Name}': \n\t{ex.Message}"); this.OnInitializationError(ex); } } /// /// Automatically search for and configure /// /// /// /// Name within the configuration which contains the config info for our bot private void Initialize() { try { this.Client = this.Configuration.BuildClient(this._botSection); - this.Client.Services = this.ServiceProvider; } catch (Exception ex) { this.Logger.LogError($"Was unable to build {nameof(DiscordClient)} for {this.GetType().Name}"); this.OnInitializationError(ex); } } /// /// Executes the bot. /// /// The stopping token. /// A Task. protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { if (this.Client == null) throw new NullReferenceException("Discord Client cannot be null"); await this.PreConnect(); await this.Client.ConnectAsync(); this.InitializeExtensions(); await this.PostConnect(); } catch (Exception ex) { /* * Anything before DOTNET 6 will * fail silently despite throwing an exception in this method * So to overcome this obstacle we need to log what happened and manually exit */ this.Logger.LogError($"Was unable to start {this.GetType().Name} Bot as a hosted service."); this.OnInitializationError(ex); } // Wait indefinitely -- but use stopping token so we can properly cancel if needed await Task.Delay(-1, stoppingToken); } /// /// Runs just prior to the bot connecting /// protected virtual Task PreConnect() => Task.CompletedTask; /// /// Runs immediately after the bot connects /// protected virtual Task PostConnect() => Task.CompletedTask; } } diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs index 934f16984..57395a843 100644 --- a/DisCatSharp/Clients/BaseDiscordClient.cs +++ b/DisCatSharp/Clients/BaseDiscordClient.cs @@ -1,301 +1,301 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; 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 configuration. /// internal protected DiscordConfiguration Configuration { get; } /// /// Gets the instance of the logger for this client. /// public ILogger Logger { get; } /// /// Gets the string representing the version of bot lib. /// public string VersionString { get; } /// /// Gets the bot library name. /// public string BotLibrary { get; } /// /// Gets the library team. /// public DisCatSharpTeam LibraryDeveloperTeam => this.ApiClient.GetDisCatSharpTeamAsync().Result; /// /// Gets the current user. /// public DiscordUser CurrentUser { get; internal set; } /// /// Gets the current application. /// public DiscordApplication CurrentApplication { get; internal set; } /// /// Gets the cached guilds for this client. /// public abstract IReadOnlyDictionary Guilds { get; } /// /// Gets the cached users for this client. /// protected internal ConcurrentDictionary UserCache { get; } /// /// Gets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// internal IServiceProvider Services { get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// public IReadOnlyDictionary VoiceRegions => this._voice_regions_lazy.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> _voice_regions_lazy; /// /// Initializes this Discord API client. /// /// Configuration for this client. protected BaseDiscordClient(DiscordConfiguration config) { this.Configuration = new DiscordConfiguration(config); if (this.Configuration.LoggerFactory == null) { this.Configuration.LoggerFactory = new DefaultLoggerFactory(); this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this)); } this.Logger = this.Configuration.LoggerFactory.CreateLogger(); this.ApiClient = new DiscordApiClient(this); this.UserCache = new ConcurrentDictionary(); this.InternalVoiceRegions = new ConcurrentDictionary(); this._voice_regions_lazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions)); 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}"; } this.BotLibrary = "DisCatSharp"; - this.Services = config.Services; + this.Services = config.ServiceProvider; } /// /// Gets the current API application. /// /// Current API application. public async Task GetCurrentApplicationAsync() { var tapp = await this.ApiClient.GetCurrentApplicationInfoAsync().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, RequiresCodeGrant = tapp.BotRequiresCodeGrant, IsPublic = tapp.IsPublicBot, PrivacyPolicyUrl = tapp.PrivacyPolicyUrl, TermsOfServiceUrl = tapp.TermsOfServiceUrl }; // do team and owners // tbh fuck doing this properly if (tapp.Team == null) { // singular owner app.Owners = new ReadOnlyCollection(new[] { new DiscordUser(tapp.Owner) }); app.Team = null; app.TeamName = null; } else { // team owner app.Team = new DiscordTeam(tapp.Team); var members = tapp.Team.Members .Select(x => new DiscordTeamMember(x) { Team = app.Team, User = new DiscordUser(x.User) }) .ToArray(); var owners = members .Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted) .Select(x => x.User) .ToArray(); app.Owners = new ReadOnlyCollection(owners); app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId); app.Team.Members = new ReadOnlyCollection(members); app.TeamName = app.Team.Name; } app.GuildId = tapp.GuildId.HasValue ? tapp.GuildId.Value : null; app.Slug = tapp.Slug.HasValue ? tapp.Slug.Value : null; app.PrimarySkuId = tapp.PrimarySkuId.HasValue ? tapp.PrimarySkuId.Value : null; app.VerifyKey = tapp.VerifyKey.HasValue ? tapp.VerifyKey.Value : null; app.CoverImageHash = tapp.CoverImageHash.HasValue ? tapp.CoverImageHash.Value : null; return app; } /// /// Gets a list of 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.Count == 0) { var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false); foreach (var xvr in vrs) this.InternalVoiceRegions.TryAdd(xvr.Id, xvr); } } /// /// 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 a cached user. /// /// The user_id. internal DiscordUser GetCachedOrEmptyUserInternal(ulong user_id) { this.TryGetCachedUserInternal(user_id, out var user); return user; } /// /// Tries the get a cached user. /// /// The user_id. /// The user. internal bool TryGetCachedUserInternal(ulong user_id, out DiscordUser user) { if (this.UserCache.TryGetValue(user_id, out user)) return true; user = new DiscordUser { Id = user_id, Discord = this }; return false; } /// /// Disposes this client. /// public abstract void Dispose(); } } diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index 5ce72cae4..c47950077 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,258 +1,268 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Net; 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 9. /// public string ApiVersion { internal get; set; } = "9"; /// /// 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 true. /// 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 true. /// 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 null. /// 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 false. /// 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 true. /// public bool AlwaysCacheMembers { internal get; set; } = true; /// /// 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 false. /// public bool MobileStatus { internal get; set; } = false; /// /// Use canary. /// Defaults to false. /// public bool UseCanary { internal get; set; } = false; /// /// Refresh full guild channel cache. /// Defaults to false. /// public bool AutoRefreshChannelCache { internal get; set; } = false; /// /// Sets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// - public IServiceProvider Services { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); + public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// 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.AutoRefreshChannelCache = other.AutoRefreshChannelCache; this.ApiVersion = other.ApiVersion; - this.Services = other.Services; + this.ServiceProvider = other.ServiceProvider; } } }