diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs
index 3c836ea2f..c7ece00b6 100644
--- a/DisCatSharp.Configuration/ConfigurationExtensions.cs
+++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs
@@ -1,279 +1,287 @@
// 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 DisCatSharp.Configuration.Models;
using Microsoft.Extensions.Configuration;
namespace DisCatSharp.Configuration
{
///
/// The configuration extensions.
///
internal static class ConfigurationExtensions
{
///
/// The factory error message.
///
private const string FactoryErrorMessage = "Require a function which provides a default entity to work with";
///
/// The default root lib.
///
public const string DefaultRootLib = "DisCatSharp";
///
/// The config suffix.
///
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)
{
var 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;
var 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))
{
value = string.IsNullOrEmpty(section.GetValue(prop.Name))
? section.Config
.GetSection(section.GetPath(prop.Name)).Get(prop.PropertyType)
: 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
var 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
var 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;
var current = config.GetSection(values[0]);
for (var 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": { }
/// }
/// }
///
+ ///
+ /// {
+ /// "botSectionName": {
+ /// "DiscordConfiguration": { }
+ /// }
+ /// }
+ ///
///
///
+ ///
/// Instance of
- public static DiscordClient BuildClient(this IConfiguration config)
+ public static DiscordClient BuildClient(this IConfiguration config, string botSectionName = DefaultRootLib)
{
- var section = config.HasSection(DefaultRootLib, "Discord")
+ var section = config.HasSection(botSectionName, "Discord")
? "Discord"
- : config.HasSection(DefaultRootLib, $"Discord{ConfigSuffix}")
+ : config.HasSection(botSectionName, $"Discord{ConfigSuffix}")
? $"Discord:{ConfigSuffix}"
: null;
return string.IsNullOrEmpty(section)
? new DiscordClient(new())
- : new DiscordClient(config.ExtractConfig(section));
+ : new DiscordClient(config.ExtractConfig(section, botSectionName));
}
}
}
diff --git a/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj
index 0b717fab1..e529ad0bd 100644
--- a/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj
+++ b/DisCatSharp.Hosting.Tests/DisCatSharp.Hosting.Tests.csproj
@@ -1,39 +1,45 @@
net5.0
enable
false
false
runtime; build; native; contentfiles; analyzers; buildtransitive
all
runtime; build; native; contentfiles; analyzers; buildtransitive
all
Always
+
+ Always
+
+
+ Always
+
diff --git a/DisCatSharp.Hosting.Tests/HostTests.cs b/DisCatSharp.Hosting.Tests/HostTests.cs
index df546cb3f..b565873c7 100644
--- a/DisCatSharp.Hosting.Tests/HostTests.cs
+++ b/DisCatSharp.Hosting.Tests/HostTests.cs
@@ -1,143 +1,207 @@
// 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 Bot(IConfiguration config, ILogger logger, IServiceProvider provider) : base(config, logger, provider)
+ {
+ }
+ }
+
+ public class MyCustomBot : DiscordHostedService
+ {
+ public MyCustomBot(IConfiguration config, ILogger logger, IServiceProvider provider) : base(config, logger, provider, "MyCustomBot")
{
}
}
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));
+ IHostBuilder Create(string filename) =>
+ Host.CreateDefaultBuilder()
+ .ConfigureServices(services => services.AddSingleton())
+ .ConfigureHostConfiguration(builder => builder.AddJsonFile(filename));
+
+ [Fact]
+ public void TestDifferentSection_InteractivityOnly()
+ {
+ IHost? host = null;
+
+ try
+ {
+ host = this.Create("interactivity-different-section.json").Build();
+ var service = host.Services.GetRequiredService();
+
+ Assert.NotNull(service);
+ Assert.NotNull(service.Client);
+ Assert.Null(service.Client.GetExtension());
+
+ var intents = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers |
+ DiscordIntents.Guilds;
+ Assert.Equal(intents, service.Client.Intents);
+
+
+ var interactivity = service.Client.GetExtension();
+ Assert.NotNull(interactivity);
+ }
+ finally
+ {
+ host?.Dispose();
+ }
+ }
+
+ [Fact]
+ public void TestDifferentSection_LavalinkOnly()
+ {
+ IHost? host = null;
+
+ try
+ {
+ host = this.Create("lavalink-different-section.json").Build();
+ var service = host.Services.GetRequiredService();
+
+ Assert.NotNull(service);
+ Assert.NotNull(service.Client);
+ Assert.NotNull(service.Client.GetExtension());
+ Assert.Null(service.Client.GetExtension());
+
+ var intents = DiscordIntents.Guilds;
+ Assert.Equal(intents, service.Client.Intents);
+ }
+ finally
+ {
+ host?.Dispose();
+ }
+ }
+
[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-different-section.json b/DisCatSharp.Hosting.Tests/interactivity-different-section.json
new file mode 100644
index 000000000..1fdcef8fe
--- /dev/null
+++ b/DisCatSharp.Hosting.Tests/interactivity-different-section.json
@@ -0,0 +1,12 @@
+{
+ "MyCustomBot": {
+ "Using": [
+ "Interactivity"
+ ],
+
+ "Discord": {
+ "Token": "1234567890",
+ "Intents": "GuildEmojisAndStickers,GuildMembers,Guilds"
+ }
+ }
+}
diff --git a/DisCatSharp.Hosting.Tests/lavalink-different-section.json b/DisCatSharp.Hosting.Tests/lavalink-different-section.json
new file mode 100644
index 000000000..91d48b92f
--- /dev/null
+++ b/DisCatSharp.Hosting.Tests/lavalink-different-section.json
@@ -0,0 +1,12 @@
+{
+ "MyCustomBot": {
+ "Using": [
+ "Lavalink"
+ ],
+
+ "Discord": {
+ "Token": "1234567890",
+ "Intents": "Guilds"
+ }
+ }
+}
diff --git a/DisCatSharp.Hosting/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs
index 39099f0c6..5cecc59c9 100644
--- a/DisCatSharp.Hosting/DiscordHostedService.cs
+++ b/DisCatSharp.Hosting/DiscordHostedService.cs
@@ -1,157 +1,159 @@
// 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;
#pragma warning disable 8618
///
/// Initializes a new instance of the class.
///
/// The config.
/// The logger.
/// The provider.
- protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider)
+ /// Name within the configuration which contains the config info for our bot. Default is DisCatSharp
+ protected DiscordHostedService(IConfiguration config, ILogger logger, IServiceProvider provider, string configBotSection = Configuration.ConfigurationExtensions.DefaultRootLib)
{
this.Logger = logger;
- this.Initialize(config, provider);
+ this.Initialize(config, provider, configBotSection);
}
#pragma warning restore 8618
///
/// Automatically search for and configure
///
///
///
- private void Initialize(IConfiguration config, IServiceProvider provider)
+ /// Name within the configuration which contains the config info for our bot
+ private void Initialize(IConfiguration config, IServiceProvider provider, string configBotSection)
{
- var typeMap = config.FindImplementedExtensions();
+ var typeMap = config.FindImplementedExtensions(configBotSection);
this.Logger.LogDebug($"Found the following config types: {string.Join("\n\t", typeMap.Keys)}");
- this.Client = config.BuildClient();
+ this.Client = config.BuildClient(configBotSection);
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(provider, typePair.Value.ConfigType))
: ActivatorUtilities.CreateInstance(provider, 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}");
}
}
///
/// Executes the bot.
///
/// The stopping token.
/// A Task.
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.Hosting/IDiscordHostedService.cs b/DisCatSharp.Hosting/IDiscordHostedService.cs
index 353a4a99e..22c5ee59f 100644
--- a/DisCatSharp.Hosting/IDiscordHostedService.cs
+++ b/DisCatSharp.Hosting/IDiscordHostedService.cs
@@ -1,38 +1,36 @@
// 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.Collections.Generic;
-
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; }
}
}