diff --git a/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs new file mode 100644 index 000000000..92a9e064b --- /dev/null +++ b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DisCatSharp.Configuration.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace DisCatSharp.Configuration.Tests +{ + public class ConfigurationExtensionTests + { + #region Test Classes + class SampleClass + { + public int Amount { get; set; } + public string? Email { get; set; } + } + + class ClassWithArray + { + public int[] Values { get; set; } = { 1, 2, 3, 4, 5 }; + public string[] Strings { get; set; } = { "1", "2", "3", "4", "5" }; + } + + class ClassWithEnumerable + { + public IEnumerable Values { get; set; } = new[] { 1, 2, 3, 4, 5 }; + public IEnumerable Strings { get; set; } = new[] { "1", "2", "3", "4", "5" }; + } + + class ClassWithList + { + public List Strings { get; set; } = new List + { + "1", + "2", + "3", + "4", + "5" + }; + + public List Values { get; set; } = new List + { + 1, + 2, + 3, + 4, + 5 + }; + } + + class SampleClass2 + { + public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(7); + public string Name { get; set; } = "Sample"; + public string ConstructorValue { get; } + public SampleClass2(string value) + { + this.ConstructorValue = value; + } + } + #endregion + + private IConfiguration EnumerableTestConfiguration() => + new ConfigurationBuilder() + .AddJsonFile("enumerable-test.json") + .Build(); + + private IConfiguration HasSectionWithSuffixConfiguration() => + new ConfigurationBuilder() + .AddJsonFile("section-with-suffix.json") + .Build(); + + private IConfiguration HasSectionNoSuffixConfiguration() => + new ConfigurationBuilder() + .AddJsonFile("section-no-suffix.json") + .Build(); + + private IConfiguration BasicDiscordConfiguration() => new ConfigurationBuilder() + .AddJsonFile("default-discord.json") + .Build(); + + private IConfiguration DiscordIntentsConfig() => new ConfigurationBuilder() + .AddJsonFile("intents-discord.json") + .Build(); + + private IConfiguration DiscordHaphazardConfig() => new ConfigurationBuilder() + .AddJsonFile("haphazard-discord.json") + .Build(); + + private IConfiguration SampleConfig() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "Sample:Amount", "200" }, + { "Sample:Email", "test@gmail.com" } + }) + .Build(); + + private IConfiguration SampleClass2Configuration_Default() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"Random:Stuff", "Meow"}, + {"SampleClass2:Name", "Purfection"} + }) + .Build(); + + private IConfiguration SampleClass2Configuration_Change() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "SampleClass:Timeout", "01:30:00" }, { "SampleClass:NotValid", "Something" } + }) + .Build(); + + private IConfiguration SampleClass2EnumerableTest() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "SampleClass:EnumerableTest", "[\"10\",\"20\",\"30\"]" } + }) + .Build(); + + private IConfiguration SampleClass2ArrayTest() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "SampleClass:ArrayTest", "[\"10\",\"20\",\"30\"]" } + }) + .Build(); + + private IConfiguration SampleClass2ListTest() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "SampleClass:ListTest", "[\"10\",\"20\",\"30\"]" } + }) + .Build(); + + [Fact] + public void TestExtractDiscordConfig_Intents() + { + var source = this.DiscordIntentsConfig(); + + DiscordConfiguration config = source.ExtractConfig("Discord"); + + var expected = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers | + DiscordIntents.GuildInvites | DiscordIntents.GuildMessageReactions; + + Assert.Equal(expected, config.Intents); + } + + [Fact] + public void TestExtractDiscordConfig_Haphzard() + { + var source = this.DiscordHaphazardConfig(); + + DiscordConfiguration config = source.ExtractConfig("Discord"); + var expectedIntents = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers | + DiscordIntents.Guilds; + + Assert.Equal(expectedIntents, config.Intents); + Assert.True(config.MobileStatus); + Assert.Equal(1000, config.LargeThreshold); + Assert.Equal(TimeSpan.FromHours(10), config.HttpTimeout); + } + + [Fact] + public void TestExtractDiscordConfig_Default() + { + var source = this.BasicDiscordConfiguration(); + DiscordConfiguration config = source.ExtractConfig("Discord"); + + Assert.Equal("1234567890", config.Token); + Assert.Equal(TokenType.Bot, config.TokenType); + Assert.Equal(LogLevel.Information, config.MinimumLogLevel); + Assert.True(config.UseRelativeRatelimit); + Assert.Equal("yyyy-MM-dd HH:mm:ss zzz", config.LogTimestampFormat); + Assert.Equal(250, config.LargeThreshold); + Assert.True(config.AutoReconnect); + Assert.Equal(123123, config.ShardId); + Assert.Equal(GatewayCompressionLevel.Stream, config.GatewayCompressionLevel); + Assert.Equal(1024, config.MessageCacheSize); + Assert.Equal(TimeSpan.FromSeconds(20), config.HttpTimeout); + Assert.False(config.ReconnectIndefinitely); + Assert.True(config.AlwaysCacheMembers); + Assert.Equal(DiscordIntents.AllUnprivileged, config.Intents); + Assert.False(config.MobileStatus); + Assert.False(config.UseCanary); + Assert.False(config.AutoRefreshChannelCache); + } + + [Fact] + public void TestSection() + { + var source = this.SampleConfig(); + SampleClass config = source.ExtractConfig("Sample", null); + + Assert.Equal(200, config.Amount); + Assert.Equal("test@gmail.com", config.Email); + } + + [Fact] + public void TestExtractConfig_V2_Default() + { + var source = this.SampleClass2Configuration_Default(); + var config = (SampleClass2) source.ExtractConfig("SampleClass", () => new SampleClass2("Test"), null); + Assert.Equal(TimeSpan.FromMinutes(7), config.Timeout); + Assert.Equal("Test", config.ConstructorValue); + Assert.Equal("Sample", config.Name); + } + + [Fact] + public void TestExtractConfig_V2_Change() + { + var source = this.SampleClass2Configuration_Change(); + var config = (SampleClass2) source.ExtractConfig("SampleClass", () => new SampleClass2("Test123"), null); + var span = new TimeSpan(0, 1, 30, 0); + Assert.Equal(span, config.Timeout); + Assert.Equal("Test123", config.ConstructorValue); + Assert.Equal("Sample", config.Name); + } + + [Fact] + public void TestExtractConfig_V3_Default() + { + var source = this.SampleClass2Configuration_Default(); + var config = + (SampleClass2)new ConfigSection(ref source, "SampleClass", null).ExtractConfig(() => + new SampleClass2("Meow")); + + Assert.Equal("Meow", config.ConstructorValue); + Assert.Equal(TimeSpan.FromMinutes(7), config.Timeout); + Assert.Equal("Sample", config.Name); + } + + [Fact] + public void TestExtractConfig_V3_Change() + { + var source = this.SampleClass2Configuration_Change(); + var config = + (SampleClass2)new ConfigSection(ref source, "SampleClass", null).ExtractConfig(() => + new SampleClass2("Meow")); + + Assert.Equal("Meow", config.ConstructorValue); + var span = new TimeSpan(0, 1, 30, 0); + Assert.Equal(span, config.Timeout); + Assert.Equal("Sample", config.Name); + } + + [Fact] + public void TestExtractConfig_Enumerable() + { + var source = this.EnumerableTestConfiguration(); + var config = + (ClassWithEnumerable)new ConfigSection(ref source, "ClassWithEnumerable", null).ExtractConfig(() => + new ClassWithEnumerable()); + + Assert.NotNull(config.Values); + Assert.Equal(3, config.Values.Count()); + Assert.NotNull(config.Strings); + Assert.Equal(3, config.Values.Count()); + } + + [Fact] + public void TestExtractConfig_Array() + { + var source = this.EnumerableTestConfiguration(); + var config = + (ClassWithArray)new ConfigSection(ref source, "ClassWithArray", null).ExtractConfig(() => + new ClassWithArray()); + Assert.NotNull(config.Values); + Assert.Equal(3, config.Values.Length); + Assert.NotNull(config.Strings); + Assert.Equal(3, config.Values.Length); + } + + [Fact] + public void TestExtractConfig_List() + { + var source = this.EnumerableTestConfiguration(); + var config = + (ClassWithList)new ConfigSection(ref source, "ClassWithList", null).ExtractConfig(() => + new ClassWithList()); + Assert.NotNull(config.Values); + Assert.Equal(3, config.Values.Count); + Assert.NotNull(config.Strings); + Assert.Equal(3, config.Values.Count); + } + + [Fact] + public void TestHasSectionWithSuffix() + { + var source = this.HasSectionWithSuffixConfiguration(); + + Assert.True(source.HasSection("DiscordConfiguration")); + Assert.False(source.HasSection("Discord")); +#pragma warning disable 8625 + Assert.False(source.HasSection("DiscordConfiguration", null)); +#pragma warning restore 8625 + } + + [Fact] + public void TestHasSectionNoSuffix() + { + var source = this.HasSectionNoSuffixConfiguration(); + + Assert.True(source.HasSection("Discord")); + Assert.False(source.HasSection("DiscordConfiguration")); +#pragma warning disable 8625 + Assert.False(source.HasSection("Discord", null)); +#pragma warning restore 8625 + } + } +} + diff --git a/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj b/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj new file mode 100644 index 000000000..d09b883e9 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/DisCatSharp.Configuration.Tests.csproj @@ -0,0 +1,51 @@ + + + + net5.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/DisCatSharp.Configuration.Tests/default-discord.json b/DisCatSharp.Configuration.Tests/default-discord.json new file mode 100644 index 000000000..127aa83e5 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/default-discord.json @@ -0,0 +1,23 @@ +{ + "DisCatSharp": { + "Discord": { + "Token": "1234567890", + "TokenType": "Bot", + "MinimumLogLevel": "Information", + "UseRelativeRateLimit": true, + "LogTimestampFormat": "yyyy-MM-dd HH:mm:ss zzz", + "LargeThreshold": 250, + "AutoReconnect": true, + "ShardId": 123123, + "GatewayCompressionLevel": "Stream", + "MessageCacheSize": 1024, + "HttpTimeout": "00:00:20", + "ReconnectIndefinitely": false, + "AlwaysCacheMembers": true, + "MobileStatus": false, + "UseCanary": false, + "AutoRefreshChannelCache": false, + "Intents": "AllUnprivileged" + } + } +} diff --git a/DisCatSharp.Configuration.Tests/enumerable-test.json b/DisCatSharp.Configuration.Tests/enumerable-test.json new file mode 100644 index 000000000..459fc9cd5 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/enumerable-test.json @@ -0,0 +1,37 @@ +{ + "ClassWithArray": { + "Values": [ + 10,11,12 + ], + + "Strings": [ + "One", + "Two", + "Three" + ] + }, + + "ClassWithEnumerable": { + "Values": [ + 10,11,12 + ], + + "Strings": [ + "One", + "Two", + "Three" + ] + }, + + "ClassWithList": { + "Values": [ + 10,11,12 + ], + + "Strings": [ + "One", + "Two", + "Three" + ] + } +} diff --git a/DisCatSharp.Configuration.Tests/haphazard-discord.json b/DisCatSharp.Configuration.Tests/haphazard-discord.json new file mode 100644 index 000000000..428af39e7 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/haphazard-discord.json @@ -0,0 +1,10 @@ +{ + "DisCatSharp": { + "Discord": { + "Intents": "GuildEmojisAndStickers,GuildMembers,Guilds", + "MobileStatus": true, + "LargeThreshold": 1000, + "HttpTimeout": "10:00:00" + } + } +} diff --git a/DisCatSharp.Configuration.Tests/intents-discord.json b/DisCatSharp.Configuration.Tests/intents-discord.json new file mode 100644 index 000000000..520d54744 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/intents-discord.json @@ -0,0 +1,7 @@ +{ + "DisCatSharp": { + "Discord": { + "Intents": "GuildEmojisAndStickers,GuildMembers,GuildInvites,GuildMessageReactions" + } + } +} diff --git a/DisCatSharp.Configuration.Tests/section-no-suffix.json b/DisCatSharp.Configuration.Tests/section-no-suffix.json new file mode 100644 index 000000000..1d5ad0682 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/section-no-suffix.json @@ -0,0 +1,5 @@ +{ + "Discord": { + "Values": [1,2,3,4,5] + } +} diff --git a/DisCatSharp.Configuration.Tests/section-with-suffix.json b/DisCatSharp.Configuration.Tests/section-with-suffix.json new file mode 100644 index 000000000..1489e8295 --- /dev/null +++ b/DisCatSharp.Configuration.Tests/section-with-suffix.json @@ -0,0 +1,5 @@ +{ + "DiscordConfiguration": { + "Values": [1,2,3,4,5] + } +} diff --git a/DisCatSharp.Configuration/AssemblyProperties.cs b/DisCatSharp.Configuration/AssemblyProperties.cs new file mode 100644 index 000000000..0a6d35e6c --- /dev/null +++ b/DisCatSharp.Configuration/AssemblyProperties.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DisCatSharp.Configuration.Tests")] +[assembly: InternalsVisibleTo("DisCatSharp.Hosting")] diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs new file mode 100644 index 000000000..931e424e8 --- /dev/null +++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs @@ -0,0 +1,270 @@ +// 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.Collections; +using System.Linq; +using System.Reflection; +using DisCatSharp.Configuration.Models; +using Microsoft.Extensions.Configuration; + +namespace DisCatSharp.Configuration +{ + internal static class ConfigurationExtensions + { + private const string FactoryErrorMessage = "Require a function which provides a default entity to work with"; + public const string DefaultRootLib = "DisCatSharp"; + private const string ConfigSuffix = "Configuration"; + + + /// + /// Easily piece together paths that will work within + /// + /// (not used - only for adding context based functionality) + /// The strings to piece together + /// Strings joined together via ':' + public static string ConfigPath(this IConfiguration config, params string[] values) => string.Join(":", values); + + /// + /// Skims over the configuration section and only overrides values that are explicitly defined within the config + /// + /// Instance of config + /// Section which contains values for + private static void HydrateInstance(ref object config, ConfigSection section) + { + PropertyInfo[] props = config.GetType().GetProperties(); + + foreach (var prop in props) + { + // Must have a set method for this to work, otherwise continue on + if (prop.SetMethod == null) + continue; + + string entry = section.GetValue(prop.Name); + object? value = null; + + if (typeof(string) == prop.PropertyType) + { + // We do NOT want to override value if nothing was provided + if(!string.IsNullOrEmpty(entry)) + prop.SetValue(config, entry); + + continue; + } + + // We need to address collections a bit differently + // They can come in the form of "root:section:name" with a string representation OR + // "root:section:name:0" <--- this is not detectable when checking the above path + if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)) + { + if (string.IsNullOrEmpty(section.GetValue(prop.Name))) + value = section.Config + .GetSection(section.GetPath(prop.Name)).Get(prop.PropertyType); + else + value = Newtonsoft.Json.JsonConvert.DeserializeObject(entry, prop.PropertyType); + + if (value == null) + continue; + + prop.SetValue(config, value); + } + + // From this point onward we require the 'entry' value to have something useful + if (string.IsNullOrEmpty(entry)) + continue; + + try + { + // Primitive types are simple to convert + if (prop.PropertyType.IsPrimitive) + value = Convert.ChangeType(entry, prop.PropertyType); + else + { + // The following types require a different approach + if (prop.PropertyType.IsEnum) + value = Enum.Parse(prop.PropertyType, entry); + else if (typeof(TimeSpan) == prop.PropertyType) + value = TimeSpan.Parse(entry); + else if (typeof(DateTime) == prop.PropertyType) + value = DateTime.Parse(entry); + else if (typeof(DateTimeOffset) == prop.PropertyType) + value = DateTimeOffset.Parse(entry); + } + + // Update value within our config instance + prop.SetValue(config, value); + } + catch (Exception ex) + { + Console.Error.WriteLine( + $"Unable to convert value of '{entry}' to type '{prop.PropertyType.Name}' for prop '{prop.Name}' in config '{config.GetType().Name}'\n\t\t{ex.Message}"); + } + } + } + + /// + /// Instantiate an entity using then walk through the specified + /// and translate user-defined config values to the instantiated instance from + /// + /// Section containing values for targeted config + /// Function which generates a default entity + /// Hydrated instance of an entity which contains user-defined values (if any) + /// When is null + public static object ExtractConfig(this ConfigSection section, Func factory) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory),FactoryErrorMessage); + + // Create default instance + object config = factory(); + + HydrateInstance(ref config, section); + + return config; + } + + /// + /// Instantiate an entity using then walk through the specified + /// in . Translate user-defined config values to the instantiated instance from + /// + /// Loaded App Configuration + /// Name of section to load + /// Function which creates a default entity to work with + /// (Optional) Used when section is nested within another. Default value is + /// Hydrated instance of an entity which contains user-defined values (if any) + /// When is null + public static object ExtractConfig(this IConfiguration config, string sectionName, Func factory, + string? rootSectionName = DefaultRootLib) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory), FactoryErrorMessage); + + // create default instance + object instance = factory(); + + HydrateInstance(ref instance, new ConfigSection(ref config, sectionName, rootSectionName)); + + return instance; + } + + /// + /// Instantiate a new instance of , then walk through the specified + /// in . Translate user-defined config values to the instance. + /// + /// Loaded App Configuration + /// Name of section to load + /// (Optional) Used when section is nested with another. Default value is + /// Type of instance that represents + /// Hydrated instance of which contains the user-defined values (if any). + public static TConfig ExtractConfig(this IConfiguration config, string sectionName, string? rootSectionName = DefaultRootLib) + where TConfig : new() + { + // Default values should hopefully be provided from the constructor + object configInstance = new TConfig(); + + HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName)); + + return (TConfig) configInstance; + } + + /// + /// Determines if contains a particular section/object (not value) + /// + /// + /// + /// { + /// "Discord": { // this is a section/object + /// + /// }, + /// "Value": "something" // this is not a section/object + /// } + /// + /// + /// + /// + /// True if section exists, otherwise false + public static bool HasSection(this IConfiguration config, params string[] values) + { + if (!values.Any()) + return false; + + if (values.Length == 1) + return config.GetChildren().Any(x => x.Key == values[0]); + + if (config.GetChildren().All(x => x.Key != values[0])) + return false; + + IConfigurationSection current = config.GetSection(values[0]); + + for (int i = 1; i < values.Length - 1; i++) + { + if (current.GetChildren().All(x => x.Key != values[i])) + return false; + + current = current.GetSection(values[i]); + } + + return current.GetChildren().Any(x=>x.Key == values[^1]); + } + + /// + /// Instantiates an instance of , then consumes any custom + /// configuration from user/developer from .
+ /// View remarks for more info + ///
+ /// + /// This is an example of how your JSON structure should look if you wish + /// to override one or more of the default values from + /// + /// { + /// "DisCatSharp": { + /// "Discord": { } + /// } + /// } + /// + ///
+ /// Alternatively, you can use the type name itself + /// + /// { + /// "DisCatSharp": { + /// "DiscordConfiguration": { } + /// } + /// } + /// + ///
+ /// + /// Instance of + public static DiscordClient BuildClient(this IConfiguration config) + { + var section = config.HasSection(DefaultRootLib, "Discord") + ? "Discord" + : config.HasSection(DefaultRootLib, $"Discord{ConfigSuffix}") + ? $"Discord:{ConfigSuffix}" + : null; + + if (string.IsNullOrEmpty(section)) + return new DiscordClient(new()); + + return new DiscordClient(config.ExtractConfig(section)); + } + } +} diff --git a/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj new file mode 100644 index 000000000..8b14dac70 --- /dev/null +++ b/DisCatSharp.Configuration/DisCatSharp.Configuration.csproj @@ -0,0 +1,42 @@ + + + + + + + + + + net5.0 + enable + DisCatSharp.Configuration + DisCatSharp.Configuration + True + + + + DisCatSharp.Configuration + Configuration for DisCatSharp. + discord, discord-api, bots, discord-bots, net-sdk, dcs, discatsharp, csharp, dotnet, vb-net, fsharp + + LICENSE.md + + + + + + + + + + + True + + + + + + + + + diff --git a/DisCatSharp.Configuration/Models/ConfigSection.cs b/DisCatSharp.Configuration/Models/ConfigSection.cs new file mode 100644 index 000000000..162981435 --- /dev/null +++ b/DisCatSharp.Configuration/Models/ConfigSection.cs @@ -0,0 +1,87 @@ +// 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 Microsoft.Extensions.Configuration; + +namespace DisCatSharp.Configuration.Models +{ + /// + /// Represents an object in + /// + internal readonly struct ConfigSection + { + /// + /// Key within which represents an object containing multiple values + /// + public string SectionName { get;} + + /// + /// Optional used to indicate this section is nested within another + /// + public string? Root { get; } + + /// + /// Reference to used within application + /// + public IConfiguration Config { get; } + + /// Reference to config + /// Section of interest + /// (Optional) Indicates is nested within this name. Default value is DisCatSharp + public ConfigSection(ref IConfiguration config, string sectionName, string? rootName = "DisCatSharp") + { + this.Config = config; + this.SectionName = sectionName; + this.Root = rootName; + } + + /// + /// Checks if key exists in + /// + /// Property / Key to search for in section + /// True if key exists, otherwise false. Outputs path to config regardless + public bool ContainsKey(string name) + { + string path = string.IsNullOrEmpty(this.Root) + ? this.Config.ConfigPath(this.SectionName, name) + : this.Config.ConfigPath(this.Root, this.SectionName, name); + + return !string.IsNullOrEmpty(this.Config[path]); + } + + /// + /// Attempts to get value associated to the config path.
Should be used in unison with + ///
+ /// Config path to value + /// Value found at + public string GetValue(string propName) + => this.Config[this.GetPath(propName)]; + + public string GetPath(string value) + { + if (string.IsNullOrEmpty(this.Root)) + return this.Config.ConfigPath(this.SectionName, value); + + return this.Config.ConfigPath(this.Root, this.SectionName, value); + } + } +} diff --git a/DisCatSharp.Docs/articles/basics/web_app.md b/DisCatSharp.Docs/articles/basics/web_app.md new file mode 100644 index 000000000..cdd6a3647 --- /dev/null +++ b/DisCatSharp.Docs/articles/basics/web_app.md @@ -0,0 +1,140 @@ +--- +uid: basics_web_app +title: Bot as Hosted Service +--- + +# Prerequisites +Install the following packages: + - DisCatSharp + - DisCatSharp.Hosting + +# Bot.cs +Create a new class called `Bot` which inherits from `DiscordHostedService`. + +```cs +public class Bot : DiscordHostedService +{ + public Bot(IConfiguration config, ILogger logger, IServiceProvider) : base(config,logger,provider) + { + } +} +``` + +# Startup.cs +By using the `DisCatSharp.Hosting.DependencyInjection` module, this 1 line is enough to get +your basic bot running... + +```cs +public void ConfigureServices(IServiceCollection services) +{ + services.AddDiscordHostedService(); +} +``` + +If you prefer another DI approach / the manual route -- the following two +lines are all you need! + +```cs +public void ConfigureServices(IServiceCollection services) +{ + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); +} +``` + +Singleton - we only want 1 instance of Bot to ever run during runtime.
+Then we take the registered singleton to run as a `HostedService`. + +# How to reference +Within a DI environment, when you want to reference your `Bot` all you have to do is add `IDiscordHostedService` +as a parameter in the constructor. + +# How to Configure +You must provide a token in order for the bot to work. + +Add the following to `appsettings.json` +```json +{ + "DisCatSharp": { + "Discord": { + "Token": "YOUR TOKEN HERE" + } + } +} +``` + +## Extensions +If you wish to add additional modules/extensions you can do so one of two ways. +1. Use the full namespace name +2. Namespace without the `DisCatSharp` prefix - because we assume the extension starts with DisCatSharp. + +To add the extensions `Interactivity` and `CommandsNext`: +```json +{ + "DisCatSharp": { + "Using": [ + "DisCatSharp.Interactivity", + "CommandsNext" + ], + + "Discord": { + "Token": "YOUR TOKEN HERE" + }, + + "Interactivity": { + "PollBehaviour": "KeepEmojis" + }, + + "CommandsNext": { + "StringPrefixes": [ "!" ] + } + } +} +``` + +Note: to configure an extension, you simply add a section for it under `DisCatSharp` in `appsettings.json`. You only have +to include values you **WISH TO OVERRIDE**. There is no need to include all config options if you only need to change 1 value. + +For more info on which values are available checkout the following classes: + - `ApplicationCommandsConfiguration` + - `CommandsNextConfiguration` + - `DiscordConfiguration` + - `InteractivityConfiguration` + - `LavalinkConfiguration` + - `VoiceNextConfiguration` + +____ +## Values +It's worth mentioning the required formats for certain value types + +### Enum +- Single Flag/Value + - "`Value`" +- Multiple Flags + - "`Flag1|Flag2|Flag3`" + +#### Example +```json +{ + "DisCatSharp": { + "Discord": { + "Intents": "GuildMembers|GuildsBans" + } + } +} +``` + +### TimeSpan +Hours:Minutes:Seconds "`HH:mm:ss`" + +#### Example +HttpTimeout of 5 minutes +```json +{ + "DisCatSharp": { + "Discord": { + "HttpTimeout": "00:05:00" + } + } +} +``` diff --git a/DisCatSharp.Docs/articles/toc.yml b/DisCatSharp.Docs/articles/toc.yml index d59c77dde..6699a12ff 100644 --- a/DisCatSharp.Docs/articles/toc.yml +++ b/DisCatSharp.Docs/articles/toc.yml @@ -1,91 +1,93 @@ - name: Preamble href: preamble.md - name: Important Changes items: - name: Version 9.8.3 href: important_changes/9_8_3.md - name: Version 9.8.2 href: important_changes/9_8_2.md - name: The Basics items: - name: Creating a Bot Account href: basics/bot_account.md - name: Writing Your First Bot href: basics/first_bot.md + - name: Bot as Hosted Service + href: basics/web_app.md - name: Beyond Basics items: - name: Events href: beyond_basics/events.md - name: Logging href: beyond_basics/logging/default.md items: - name: The Default Logger href: beyond_basics/logging/default.md - name: Third Party Loggers href: beyond_basics/logging/third_party.md - name: Intents href: beyond_basics/intents.md - name: Sharding href: beyond_basics/sharding.md - name: Message Builder href: beyond_basics/messagebuilder.md - name: Components items: - name: Buttons href: beyond_basics/components/buttons.md - name: Select Menu href: beyond_basics/components/select_menus.md - name: Workarounds href: beyond_basics/workarounds.md - name: Application Commands items: - name: Introduction href: application_commands/intro.md - name: Options href: application_commands/options.md - name: Events href: application_commands/events.md - name: Commands items: - name: Introduction href: commands/intro.md - name: Command Attributes href: commands/command_attributes.md - name: Dependency Injection href: commands/dependency_injection.md - name: Customization items: - name: Help Formatter href: commands/help_formatter.md - name: Argument Converters href: commands/argument_converters.md - name: Command Handler href: commands/command_handler.md - name: Audio items: - name: Lavalink items: - name: Setup href: audio/lavalink/setup.md - name: Configuration href: audio/lavalink/configuration.md - name: Music Commands href: audio/lavalink/music_commands.md - name: VoiceNext items: - name: Prerequisites href: audio/voicenext/prerequisites.md - name: Transmitting href: audio/voicenext/transmit.md - name: Receiving href: audio/voicenext/receive.md - name: Interactivity href: interactivity.md - name: Hosting href: hosting.md - name: Miscellaneous items: - name: Nightly Builds href: misc/nightly_builds.md - name: Reporting Issues href: misc/reporting_issues.md diff --git a/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj new file mode 100644 index 000000000..2627031e5 --- /dev/null +++ b/DisCatSharp.Hosting.DependencyInjection/DisCatSharp.Hosting.DependencyInjection.csproj @@ -0,0 +1,20 @@ + + + + + + + net5.0 + enable + True + + + + + + + + + + + diff --git a/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..092098806 --- /dev/null +++ b/DisCatSharp.Hosting.DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace DisCatSharp.Hosting.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + /// + /// Add as a background service + /// + /// + /// is scoped to ServiceLifetime.Singleton.
+ /// Maps to Implementation of + ///
+ /// + /// + /// + public static IServiceCollection AddDiscordHostedService(this IServiceCollection services) + where TService : class, IDiscordHostedService + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + return services; + } + } +} diff --git a/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj new file mode 100644 index 000000000..bd43e41d4 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj @@ -0,0 +1,38 @@ + + + + net5.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + Always + + + + diff --git a/DisCatSharp.Hosting.Tests/ExtensionTests.cs b/DisCatSharp.Hosting.Tests/ExtensionTests.cs new file mode 100644 index 000000000..3e4efa479 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/ExtensionTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Xunit; +using DisCatSharp.Interactivity; +using DisCatSharp.Lavalink; + +namespace DisCatSharp.Hosting.Tests +{ + public class HostExtensionTests + { + #region Reference to external assemblies - required to ensure they're loaded + #pragma warning disable 414 + + private InteractivityConfiguration? _interactivityConfig = null; + private LavalinkConfiguration? _lavalinkConfig = null; + private DiscordConfiguration? _discordConfig = null; + + #pragma warning restore 414 + #endregion + + private Dictionary DefaultDiscord() => + new() + { + { "DisCatSharp:Discord:Token", "1234567890" }, + { "DisCatSharp:Discord:TokenType", "Bot" }, + { "DisCatSharp:Discord:MinimumLogLevel", "Information" }, + { "DisCatSharp:Discord:UseRelativeRateLimit", "true" }, + { "DisCatSharp:Discord:LogTimestampFormat", "yyyy-MM-dd HH:mm:ss zzz" }, + { "DisCatSharp:Discord:LargeThreshold", "250" }, + { "DisCatSharp:Discord:AutoReconnect", "true" }, + { "DisCatSharp:Discord:ShardId", "123123" }, + { "DisCatSharp:Discord:GatewayCompressionLevel", "Stream" }, + { "DisCatSharp:Discord:MessageCacheSize", "1024" }, + { "DisCatSharp:Discord:HttpTimeout", "00:00:20" }, + { "DisCatSharp:Discord:ReconnectIndefinitely", "false" }, + { "DisCatSharp:Discord:AlwaysCacheMembers", "true" }, + { "DisCatSharp:Discord:DiscordIntents", "AllUnprivileged" }, + { "DisCatSharp:Discord:MobileStatus", "false" }, + { "DisCatSharp:Discord:UseCanary", "false" }, + { "DisCatSharp:Discord:AutoRefreshChannelCache", "false" }, + { "DisCatSharp:Discord:Intents", "AllUnprivileged" } + }; + + public IConfiguration DiscordInteractivityConfiguration() => new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary(this.DefaultDiscord()) + { + {"DisCatSharp:Using", "[\"DisCatSharp.Interactivity\"]"} // this should be enough to automatically add the extension + }) + .Build(); + + public IConfiguration DiscordOnlyConfiguration() => new ConfigurationBuilder() + .AddInMemoryCollection(this.DefaultDiscord()) + .Build(); + + public IConfiguration DiscordInteractivityAndLavaLinkConfiguration() => new ConfigurationBuilder() + .AddJsonFile("interactivity-lavalink.json") + .Build(); + + [Fact] + public void DiscoverExtensions_Interactivity() + { + var source = this.DiscordInteractivityConfiguration(); + var discovered = source.FindImplementedExtensions(); + + // Remember that DiscordConfiguration does not have an implementation type which is assignable to BaseExtension + Assert.Single(discovered); + var item = discovered.First(); + + Assert.Equal(typeof(InteractivityConfiguration), item.Value.ConfigType); + Assert.Equal(typeof(InteractivityExtension), item.Value.ImplementationType); + Assert.Equal("InteractivityExtension", item.Key); + } + + [Fact] + public void DiscoverExtensions_InteractivityAndLavaLink() + { + var source = this.DiscordInteractivityAndLavaLinkConfiguration(); + var discovered = source.FindImplementedExtensions(); + + Assert.Equal(2, discovered.Count); + var first = discovered.First(); + var last = discovered.Last(); + + Assert.Equal(typeof(InteractivityConfiguration), first.Value.ConfigType); + Assert.Equal(typeof(InteractivityExtension), first.Value.ImplementationType); + Assert.True("InteractivityExtension".Equals(first.Key, StringComparison.OrdinalIgnoreCase)); + + Assert.Equal(typeof(LavalinkConfiguration), last.Value.ConfigType); + Assert.Equal(typeof(LavalinkExtension), last.Value.ImplementationType); + Assert.True("LavalinkExtension".Equals(last.Key, StringComparison.OrdinalIgnoreCase)); + } + } +} + + diff --git a/DisCatSharp.Hosting.Tests/HostTests.cs b/DisCatSharp.Hosting.Tests/HostTests.cs new file mode 100644 index 000000000..ec7e88fc8 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/HostTests.cs @@ -0,0 +1,149 @@ +// 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.Collections.Generic; +using DisCatSharp.Interactivity; +using DisCatSharp.Lavalink; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace DisCatSharp.Hosting.Tests +{ + public class Bot : DiscordHostedService + { + public Bot(IConfiguration config, ILogger logger, IServiceProvider provider) : base(config, logger, provider) + { + } + } + + public class HostTests + { + private Dictionary DefaultDiscord() => + new() + { + { "DisCatSharp:Discord:Token", "1234567890" }, + { "DisCatSharp:Discord:TokenType", "Bot" }, + { "DisCatSharp:Discord:MinimumLogLevel", "Information" }, + { "DisCatSharp:Discord:UseRelativeRateLimit", "true" }, + { "DisCatSharp:Discord:LogTimestampFormat", "yyyy-MM-dd HH:mm:ss zzz" }, + { "DisCatSharp:Discord:LargeThreshold", "250" }, + { "DisCatSharp:Discord:AutoReconnect", "true" }, + { "DisCatSharp:Discord:ShardId", "123123" }, + { "DisCatSharp:Discord:GatewayCompressionLevel", "Stream" }, + { "DisCatSharp:Discord:MessageCacheSize", "1024" }, + { "DisCatSharp:Discord:HttpTimeout", "00:00:20" }, + { "DisCatSharp:Discord:ReconnectIndefinitely", "false" }, + { "DisCatSharp:Discord:AlwaysCacheMembers", "true" }, + { "DisCatSharp:Discord:DiscordIntents", "AllUnprivileged" }, + { "DisCatSharp:Discord:MobileStatus", "false" }, + { "DisCatSharp:Discord:UseCanary", "false" }, + { "DisCatSharp:Discord:AutoRefreshChannelCache", "false" }, + { "DisCatSharp:Discord:Intents", "AllUnprivileged" } + }; + + public Dictionary DiscordInteractivity() => new (this.DefaultDiscord()) + { + {"DisCatSharp:Using","[\"DisCatSharp.Interactivity\"]"}, + }; + + public Dictionary DiscordInteractivityAndLavalink() => new (this.DefaultDiscord()) + { + {"DisCatSharp:Using","[\"DisCatSharp.Interactivity\", \"DisCatSharp.Lavalink\"]"}, + }; + + IHostBuilder Create(Dictionary configValues) => + Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + }) + .ConfigureHostConfiguration(builder => + { + builder.AddInMemoryCollection(configValues); + }); + + [Fact] + public void TestNoExtensions() + { + IHost? host = null; + + try + { + host = this.Create(this.DefaultDiscord()).Build(); + + var service = host.Services.GetRequiredService(); + Assert.NotNull(service); + Assert.NotNull(service.Client); + } + finally + { + host?.Dispose(); + } + } + + [Fact] + public void TestInteractivityExtension() + { + IHost? host = null; + + try + { + host = this.Create(this.DiscordInteractivity()).Build(); + + var service = host.Services.GetRequiredService(); + Assert.NotNull(service); + Assert.NotNull(service.Client); + Assert.NotNull(service.Client.GetExtension()); + } + finally + { + host?.Dispose(); + } + } + + [Fact] + public void TestInteractivityLavalinkExtensions() + { + IHost? host = null; + + try + { + host = this.Create(this.DiscordInteractivityAndLavalink()).Build(); + + var service = host.Services.GetRequiredService(); + + Assert.NotNull(service); + Assert.NotNull(service.Client); + Assert.NotNull(service.Client.GetExtension()); + Assert.NotNull(service.Client.GetExtension()); + } + finally + { + host?.Dispose(); + } + } + } +} diff --git a/DisCatSharp.Hosting.Tests/interactivity-lavalink.json b/DisCatSharp.Hosting.Tests/interactivity-lavalink.json new file mode 100644 index 000000000..1121fb529 --- /dev/null +++ b/DisCatSharp.Hosting.Tests/interactivity-lavalink.json @@ -0,0 +1,8 @@ +{ + "DisCatSharp": { + "Using": [ + "Interactivity", + "Lavalink" + ] + } +} diff --git a/DisCatSharp.Hosting/AssemblyProperties.cs b/DisCatSharp.Hosting/AssemblyProperties.cs new file mode 100644 index 000000000..71ebab38f --- /dev/null +++ b/DisCatSharp.Hosting/AssemblyProperties.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DisCatSharp.Hosting")] +[assembly: InternalsVisibleTo("DisCatSharp.Hosting.Tests")] diff --git a/DisCatSharp.Hosting/ConfigurationExtensions.cs b/DisCatSharp.Hosting/ConfigurationExtensions.cs new file mode 100644 index 000000000..947d7688c --- /dev/null +++ b/DisCatSharp.Hosting/ConfigurationExtensions.cs @@ -0,0 +1,191 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Reflection; +using DisCatSharp.Configuration; +using DisCatSharp.Configuration.Models; +using Microsoft.Extensions.Configuration; + +namespace DisCatSharp.Hosting +{ + internal struct ExtensionConfigResult + { + public ConfigSection? Section { get; set; } + public Type ConfigType { get; set; } + public Type ImplementationType { get; set; } + } + + internal static class ConfigurationExtensions + { + /// + /// Find assemblies that match the names provided via . + /// + /// + /// In some cases the assembly the user is after could be used in the application but + /// not appear within the .
+ /// The workaround for this is to check the assemblies in the , as well as referenced + /// assemblies. If the targeted assembly is a reference, we need to load it into our workspace to get more info. + ///
+ /// Names of assemblies to look for + /// Assemblies which meet the given names. No duplicates + public static Assembly[] FindAssemblies(string[]? names) + { + + /* + There is a possibility that an assembly can be referenced in multiple assemblies. + To alleviate duplicates we need to shrink our queue as we find things + */ + List results = new(); + + if (names is null) + return results.ToArray(); + + List queue = new(names); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!queue.Any()) + break; + + var loadedAssemblyName = assembly.GetName().Name; + + // Kinda need the name to do our thing + if (loadedAssemblyName == null) + continue; + + // Is this something we're looking for? + if (queue.Contains(loadedAssemblyName)) + { + results.Add(assembly); + + // Shrink queue so we don't accidentally add the same assembly > 1 times + queue.Remove(loadedAssemblyName); + } + + // Time to check if one of the referenced assemblies is something we're looking for + foreach(var referencedAssembly in assembly.GetReferencedAssemblies() + .Where(x => x.Name != null && queue.Contains(x.Name))) + try + { + // Must load the assembly into our workspace so we can do stuff with it later + results.Add(Assembly.Load(referencedAssembly)); + + #pragma warning disable 8604 + queue.Remove(referencedAssembly.Name); + #pragma warning restore 8604 + } + catch (Exception ex) + { + Console.Error.WriteLine($"Unable to load referenced assembly: '{referencedAssembly.Name}' \n\t{ex.Message}"); + } + } + + return results.ToArray(); + } + + /// + /// Easily identify which configuration types have been added to the
+ /// This way we can dynamically load extensions without explicitly doing so + ///
+ /// + /// + /// Dictionary where Key -> Name of implemented type
Value ->
+ public static Dictionary FindImplementedExtensions(this IConfiguration configuration, + string rootName = Configuration.ConfigurationExtensions.DefaultRootLib) + { + if (string.IsNullOrEmpty(rootName)) + throw new ArgumentNullException(nameof(rootName), "Root name must be provided"); + + Dictionary results = new(); + string[]? assemblyNames; + + // Has the user defined a using section within the root name? + if (!configuration.HasSection(rootName, "Using")) + return results; + + /* + There are 2 ways a user could list which assemblies are used + "Using": "[\"Assembly.Name\"]" + "Using": ["Assembly.Name"] + + JSON or as Text. + */ + if (string.IsNullOrEmpty(configuration[configuration.ConfigPath(rootName, "Using")])) + assemblyNames = configuration.GetSection(configuration.ConfigPath(rootName, "Using")).Get(); + else + assemblyNames = + Newtonsoft.Json.JsonConvert.DeserializeObject( + configuration[configuration.ConfigPath(rootName, "Using")]); + + #pragma warning disable 8604 + foreach (var assembly in FindAssemblies(assemblyNames.Select(x=> x.StartsWith(Constants.LibName) ? x : $"{Constants.LibName}.{x}").ToArray())) + { + ExtensionConfigResult result = new(); + + foreach (var type in assembly.ExportedTypes + .Where(x => x.Name.EndsWith(Constants.ConfigSuffix) && !x.IsAbstract && !x.IsInterface)) + { + string sectionName = type.Name; + string prefix = type.Name.Replace(Constants.ConfigSuffix, ""); + + result.ConfigType = type; + + // Does a section exist with the classname? (DiscordConfiguration - for instance) + if(configuration.HasSection( rootName, sectionName)) + result.Section = new ConfigSection(ref configuration, type.Name, rootName); + + // Does a section exist with the classname minus Configuration? (Discord - for Instance) + else if (configuration.HasSection(rootName, prefix)) + result.Section = new ConfigSection(ref configuration, prefix, rootName); + + // IF THE SECTION IS NOT PROVIDED --> WE WILL USE DEFAULT CONFIG IMPLEMENTATION + + /* + Now we need to find the type which should consume our config + + In the event a user has some "fluff" between prefix and suffix we'll + just check for beginning and ending values. + + Type should not be an interface or abstract, should also be assignable to BaseExtension + */ + + var implementationType = assembly.ExportedTypes.FirstOrDefault(x => + !x.IsAbstract && !x.IsInterface && x.Name.StartsWith(prefix) && + x.Name.EndsWith(Constants.ExtensionSuffix) && x.IsAssignableTo(typeof(BaseExtension))); + + // If the implementation type was found we can add it to our result set + if (implementationType != null) + { + result.ImplementationType = implementationType; + results.Add(implementationType.Name, result); + } + } + } + #pragma warning restore 8604 + + return results; + } + + } +} diff --git a/DisCatSharp/Properties/AssemblyProperties.cs b/DisCatSharp.Hosting/Constants.cs similarity index 65% copy from DisCatSharp/Properties/AssemblyProperties.cs copy to DisCatSharp.Hosting/Constants.cs index 5bdc09503..e7e9cbafc 100644 --- a/DisCatSharp/Properties/AssemblyProperties.cs +++ b/DisCatSharp.Hosting/Constants.cs @@ -1,32 +1,31 @@ -// This file is part of the DisCatSharp project. +// 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.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("DisCatSharp.Common")] -[assembly: InternalsVisibleTo("DisCatSharp.ApplicationCommands")] -[assembly: InternalsVisibleTo("DisCatSharp.CommandsNext")] -[assembly: InternalsVisibleTo("DisCatSharp.Interactivity")] -[assembly: InternalsVisibleTo("DisCatSharp.Lavalink")] -[assembly: InternalsVisibleTo("DisCatSharp.VoiceNext")] -[assembly: InternalsVisibleTo("DisCatSharp.VoiceNext.Natives")] -[assembly: InternalsVisibleTo("Nyaw")] // Ignore pls, DisCatSharp Dev Debug +namespace DisCatSharp.Hosting +{ + internal static class Constants + { + public static string LibName => Configuration.ConfigurationExtensions.DefaultRootLib; + public static string ConfigSuffix => "Configuration"; + public static string ExtensionSuffix => "Extension"; + } +} diff --git a/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj new file mode 100644 index 000000000..5851d19c8 --- /dev/null +++ b/DisCatSharp.Hosting/DisCatSharp.Hosting.csproj @@ -0,0 +1,22 @@ + + + + + + + net5.0 + enable + True + + + + + + + + + + + + + diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs new file mode 100644 index 000000000..a3f3ba128 --- /dev/null +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -0,0 +1,149 @@ +// 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.Collections.Generic; +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; } + + private readonly ILogger _logger; + + #pragma warning disable 8618 + protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider) + { + this._logger = logger; + this.Initialize(config, provider); + } + + #pragma warning restore 8618 + + /// + /// Automatically search for and configure + /// + /// + /// + private void Initialize(IConfiguration config, IServiceProvider provider) + { + var typeMap = config.FindImplementedExtensions(); + + this._logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}"); + + this.Client = config.BuildClient(); + + 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 + */ + + object configInstance = typePair.Value.Section.HasValue + ? typePair.Value.Section.Value.ExtractConfig(() => + ActivatorUtilities.CreateInstance(provider, typePair.Value.ConfigType)) + : ActivatorUtilities.CreateInstance(provider, typePair.Value.ConfigType); + + /* + Explanation for bindings + + Internal Constructors --> NonPublic + Public Constructors --> Public + Constructors --> Instance + */ + + BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; + var ctors = typePair.Value.ImplementationType.GetConstructors(flags); + + object? instance; + + /* + 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 (ctors.Any(x => x.GetParameters().Length == 1 && x.GetParameters().First().ParameterType == typePair.Value.ConfigType)) + instance = Activator.CreateInstance(typePair.Value.ImplementationType, flags, null, + new[] { configInstance }, null); + else + instance = Activator.CreateInstance(typePair.Value.ImplementationType, true); + + 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}"); + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (this.Client == null) + throw new NullReferenceException("Discord Client cannot be null"); + + await this.PreConnect(); + await this.Client.ConnectAsync(); + await this.PostConnect(); + + // 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/Properties/AssemblyProperties.cs b/DisCatSharp.Hosting/IDiscordHostedService.cs similarity index 65% copy from DisCatSharp/Properties/AssemblyProperties.cs copy to DisCatSharp.Hosting/IDiscordHostedService.cs index 5bdc09503..353a4a99e 100644 --- a/DisCatSharp/Properties/AssemblyProperties.cs +++ b/DisCatSharp.Hosting/IDiscordHostedService.cs @@ -1,32 +1,38 @@ -// This file is part of the DisCatSharp project. +// 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.Runtime.CompilerServices; +using System.Collections.Generic; -[assembly: InternalsVisibleTo("DisCatSharp.Common")] -[assembly: InternalsVisibleTo("DisCatSharp.ApplicationCommands")] -[assembly: InternalsVisibleTo("DisCatSharp.CommandsNext")] -[assembly: InternalsVisibleTo("DisCatSharp.Interactivity")] -[assembly: InternalsVisibleTo("DisCatSharp.Lavalink")] -[assembly: InternalsVisibleTo("DisCatSharp.VoiceNext")] -[assembly: InternalsVisibleTo("DisCatSharp.VoiceNext.Natives")] -[assembly: InternalsVisibleTo("Nyaw")] // Ignore pls, DisCatSharp Dev Debug +namespace DisCatSharp.Hosting +{ + + /// + /// Contract required for to work in a web hosting environment + /// + public interface IDiscordHostedService : Microsoft.Extensions.Hosting.IHostedService + { + /// + /// Reference to connected client + /// + DiscordClient Client { get; } + } +} diff --git a/DisCatSharp.sln b/DisCatSharp.sln index 26d6c715d..2a0af2afc 100644 --- a/DisCatSharp.sln +++ b/DisCatSharp.sln @@ -1,111 +1,145 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp", "DisCatSharp\DisCatSharp.csproj", "{EB3D8310-DFAD-4295-97F9-82E253647583}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext", "DisCatSharp.VoiceNext\DisCatSharp.VoiceNext.csproj", "{FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4255B64D-92EC-46B3-BC3B-ED2C3A8073EE}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitattributes = .gitattributes .gitignore = .gitignore BUILDING.md = BUILDING.md CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md CONTRIBUTING.md = CONTRIBUTING.md LICENSE.md = LICENSE.md README.md = README.md SECURITY.md = SECURITY.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.CommandsNext", "DisCatSharp.CommandsNext\DisCatSharp.CommandsNext.csproj", "{C8ED55FB-E028-468D-955F-1534C20274EF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Interactivity", "DisCatSharp.Interactivity\DisCatSharp.Interactivity.csproj", "{DD32BEC3-0189-479F-86DC-CCF95E5634A9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F953F5D0-F0C9-41E6-ADBF-60A76D295899}" ProjectSection(SolutionItems) = preProject .nuget\NuGet.config = .nuget\NuGet.config EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Items", "Build Items", "{84464D70-687B-40A8-836D-C4F737698969}" ProjectSection(SolutionItems) = preProject appveyor.yml = appveyor.yml .github\dependabot.yml = .github\dependabot.yml DisCatSharp.targets = DisCatSharp.targets docs-oneclick-rebuild.ps1 = docs-oneclick-rebuild.ps1 NuGet.targets = NuGet.targets oneclick-rebuild.ps1 = oneclick-rebuild.ps1 Package.targets = Package.targets rebuild-all.ps1 = rebuild-all.ps1 rebuild-docs.ps1 = rebuild-docs.ps1 rebuild-lib.ps1 = rebuild-lib.ps1 Version.targets = Version.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{430C28D8-5F85-4D6E-AA68-211549435245}" ProjectSection(SolutionItems) = preProject .github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md .github\FUNDING.yml = .github\FUNDING.yml .github\pull_request_template.md = .github\pull_request_template.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Lavalink", "DisCatSharp.Lavalink\DisCatSharp.Lavalink.csproj", "{A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.VoiceNext.Natives", "DisCatSharp.VoiceNext.Natives\DisCatSharp.VoiceNext.Natives.csproj", "{BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.Common", "DisCatSharp.Common\DisCatSharp.Common.csproj", "{CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DisCatSharp.ApplicationCommands", "DisCatSharp.ApplicationCommands\DisCatSharp.ApplicationCommands.csproj", "{AD530FD0-523C-4DE7-9AF6-B9A3785492C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Configuration", "DisCatSharp.Configuration\DisCatSharp.Configuration.csproj", "{603287D3-1EF2-47F1-A611-C7F25869DE14}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Configuration.Tests", "DisCatSharp.Configuration.Tests\DisCatSharp.Configuration.Tests.csproj", "{E15E88B4-63AD-42DE-B685-D31697C62194}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Hosting", "DisCatSharp.Hosting\DisCatSharp.Hosting.csproj", "{72CCE5D5-926B-432A-876A-065FA2BC9B7B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Hosting.Tests", "DisCatSharp.Hosting.Tests\DisCatSharp.Hosting.Tests.csproj", "{D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisCatSharp.Hosting.DependencyInjection", "DisCatSharp.Hosting.DependencyInjection\DisCatSharp.Hosting.DependencyInjection.csproj", "{2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B15E40E0-03FD-4852-B19B-2C50BCC67704}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB3D8310-DFAD-4295-97F9-82E253647583}.Release|Any CPU.Build.0 = Release|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB6B9EE9-65FB-4DFB-8D51-06F0BE6C1BA5}.Release|Any CPU.Build.0 = Release|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8ED55FB-E028-468D-955F-1534C20274EF}.Release|Any CPU.Build.0 = Release|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD32BEC3-0189-479F-86DC-CCF95E5634A9}.Release|Any CPU.Build.0 = Release|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8B8FB09-C6AF-4F28-89B8-B53EE0DCE6E5}.Release|Any CPU.Build.0 = Release|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BEC47B41-71E4-41D1-A4F9-BB7C56A1B82B}.Release|Any CPU.Build.0 = Release|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD84A5C7-C7FF-48CA-B23D-FA726CF80E09}.Release|Any CPU.Build.0 = Release|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD530FD0-523C-4DE7-9AF6-B9A3785492C2}.Release|Any CPU.Build.0 = Release|Any CPU + {603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {603287D3-1EF2-47F1-A611-C7F25869DE14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {603287D3-1EF2-47F1-A611-C7F25869DE14}.Release|Any CPU.Build.0 = Release|Any CPU + {E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E15E88B4-63AD-42DE-B685-D31697C62194}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E15E88B4-63AD-42DE-B685-D31697C62194}.Release|Any CPU.Build.0 = Release|Any CPU + {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72CCE5D5-926B-432A-876A-065FA2BC9B7B}.Release|Any CPU.Build.0 = Release|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94}.Release|Any CPU.Build.0 = Release|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D67D1DD-E5B2-40C7-80E2-54D63730E7F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {430C28D8-5F85-4D6E-AA68-211549435245} = {4255B64D-92EC-46B3-BC3B-ED2C3A8073EE} + {D02B598A-F0C9-4A8C-B8DE-7C0BAC8C9B94} = {B15E40E0-03FD-4852-B19B-2C50BCC67704} + {E15E88B4-63AD-42DE-B685-D31697C62194} = {B15E40E0-03FD-4852-B19B-2C50BCC67704} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {23F3A981-51B8-4285-A38C-3267F1D25FE7} EndGlobalSection EndGlobal diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index feab27ed8..678a72ec2 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,249 +1,249 @@ // 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.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 10 seconds. + /// Defaults to 20 seconds. /// - public TimeSpan HttpTimeout { internal get; set; } = TimeSpan.FromSeconds(100); + 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; /// /// Creates a new configuration with default values. /// public DiscordConfiguration() { } /// /// 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; } } } diff --git a/DisCatSharp/Properties/AssemblyProperties.cs b/DisCatSharp/Properties/AssemblyProperties.cs index 5bdc09503..4261a925d 100644 --- a/DisCatSharp/Properties/AssemblyProperties.cs +++ b/DisCatSharp/Properties/AssemblyProperties.cs @@ -1,32 +1,38 @@ // 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.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("DisCatSharp.Common")] [assembly: InternalsVisibleTo("DisCatSharp.ApplicationCommands")] [assembly: InternalsVisibleTo("DisCatSharp.CommandsNext")] +[assembly: InternalsVisibleTo("DisCatSharp.Common")] +[assembly: InternalsVisibleTo("DisCatSharp.Configuration")] +[assembly: InternalsVisibleTo("DisCatSharp.Configuration.Tests")] +[assembly: InternalsVisibleTo("DisCatSharp.Hosting")] +[assembly: InternalsVisibleTo("DisCatSharp.Test")] [assembly: InternalsVisibleTo("DisCatSharp.Interactivity")] [assembly: InternalsVisibleTo("DisCatSharp.Lavalink")] +[assembly: InternalsVisibleTo("DisCatSharp.Phabricator")] +[assembly: InternalsVisibleTo("DisCatSharp.Support")] [assembly: InternalsVisibleTo("DisCatSharp.VoiceNext")] [assembly: InternalsVisibleTo("DisCatSharp.VoiceNext.Natives")] -[assembly: InternalsVisibleTo("Nyaw")] // Ignore pls, DisCatSharp Dev Debug +[assembly: InternalsVisibleTo("Nyaw")]