diff --git a/src/stream-sniper/stream-sniper.sln b/src/stream-sniper/stream-sniper.sln
new file mode 100644
index 0000000..1ed8aa5
--- /dev/null
+++ b/src/stream-sniper/stream-sniper.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30204.135
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stream-sniper", "stream-sniper\stream-sniper.csproj", "{D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {02E9EA2E-1B01-4F93-8D52-142CA749AC0E}
+ EndGlobalSection
+EndGlobal
diff --git a/src/stream-sniper/stream-sniper/App.config b/src/stream-sniper/stream-sniper/App.config
new file mode 100644
index 0000000..716fabe
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/App.config
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/stream-sniper/stream-sniper/App.xaml b/src/stream-sniper/stream-sniper/App.xaml
new file mode 100644
index 0000000..f645256
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/src/stream-sniper/stream-sniper/App.xaml.cs b/src/stream-sniper/stream-sniper/App.xaml.cs
new file mode 100644
index 0000000..fccbe85
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace stream_sniper
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/AliasAttribute.cs
new file mode 100644
index 0000000..16eb3ba
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/AliasAttribute.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Marks the aliases for a command.
+ ///
+ ///
+ /// This attribute allows a command to have one or multiple aliases. In other words, the base command can have
+ /// multiple aliases when triggering the command itself, giving the end-user more freedom of choices when giving
+ /// hot-words to trigger the desired command. See the example for a better illustration.
+ ///
+ ///
+ /// In the following example, the command can be triggered with the base name, "stats", or either "stat" or
+ /// "info".
+ ///
+ /// [Command("stats")]
+ /// [Alias("stat", "info")]
+ /// public async Task GetStatsAsync(IUser user)
+ /// {
+ /// // ...pull stats
+ /// }
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class AliasAttribute : Attribute
+ {
+ ///
+ /// Gets the aliases which have been defined for the command.
+ ///
+ public string[] Aliases { get; }
+
+ ///
+ /// Creates a new with the given aliases.
+ ///
+ public AliasAttribute(params string[] aliases)
+ {
+ Aliases = aliases;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/CommandAttribute.cs
new file mode 100644
index 0000000..d4d9ee3
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/CommandAttribute.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Marks the execution information for a command.
+ ///
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class CommandAttribute : Attribute
+ {
+ ///
+ /// Gets the text that has been set to be recognized as a command.
+ ///
+ public string Text { get; }
+ ///
+ /// Specifies the of the command. This affects how the command is executed.
+ ///
+ public RunMode RunMode { get; set; } = RunMode.Default;
+ public bool? IgnoreExtraArgs { get; }
+
+ ///
+ public CommandAttribute()
+ {
+ Text = null;
+ }
+
+ ///
+ /// Initializes a new attribute with the specified name.
+ ///
+ /// The name of the command.
+ public CommandAttribute(string text)
+ {
+ Text = text;
+ }
+ public CommandAttribute(string text, bool ignoreExtraArgs)
+ {
+ Text = text;
+ IgnoreExtraArgs = ignoreExtraArgs;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs
new file mode 100644
index 0000000..7dbe1a4
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Prevents the marked module from being loaded automatically.
+ ///
+ ///
+ /// This attribute tells to ignore the marked module from being loaded
+ /// automatically (e.g. the method). If a non-public module marked
+ /// with this attribute is attempted to be loaded manually, the loading process will also fail.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public class DontAutoLoadAttribute : Attribute
+ {
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontInjectAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontInjectAttribute.cs
new file mode 100644
index 0000000..72ca92f
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/DontInjectAttribute.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Prevents the marked property from being injected into a module.
+ ///
+ ///
+ /// This attribute prevents the marked member from being injected into its parent module. Useful when you have a
+ /// public property that you do not wish to invoke the library's dependency injection service.
+ ///
+ ///
+ /// In the following example, DatabaseService will not be automatically injected into the module and will
+ /// not throw an error message if the dependency fails to be resolved.
+ ///
+ /// public class MyModule : ModuleBase
+ /// {
+ /// [DontInject]
+ /// public DatabaseService DatabaseService;
+ /// public MyModule()
+ /// {
+ /// DatabaseService = DatabaseFactory.Generate();
+ /// }
+ /// }
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public class DontInjectAttribute : Attribute
+ {
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/GroupAttribute.cs
new file mode 100644
index 0000000..e1e38cf
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/GroupAttribute.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Marks the module as a command group.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public class GroupAttribute : Attribute
+ {
+ ///
+ /// Gets the prefix set for the module.
+ ///
+ public string Prefix { get; }
+
+ ///
+ public GroupAttribute()
+ {
+ Prefix = null;
+ }
+ ///
+ /// Initializes a new with the provided prefix.
+ ///
+ /// The prefix of the module group.
+ public GroupAttribute(string prefix)
+ {
+ Prefix = prefix;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NameAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NameAttribute.cs
new file mode 100644
index 0000000..a6e1f2e
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NameAttribute.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Discord.Commands
+{
+ // Override public name of command/module
+ ///
+ /// Marks the public name of a command, module, or parameter.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+ public class NameAttribute : Attribute
+ {
+ ///
+ /// Gets the name of the command.
+ ///
+ public string Text { get; }
+
+ ///
+ /// Marks the public name of a command, module, or parameter with the provided name.
+ ///
+ /// The public name of the object.
+ public NameAttribute(string text)
+ {
+ Text = text;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs
new file mode 100644
index 0000000..e857172
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/NamedArgumentTypeAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Instructs the command system to treat command parameters of this type
+ /// as a collection of named arguments matching to its properties.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public sealed class NamedArgumentTypeAttribute : Attribute { }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
new file mode 100644
index 0000000..a44dcb6
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Reflection;
+
+namespace Discord.Commands
+{
+ ///
+ /// Marks the to be read by the specified .
+ ///
+ ///
+ /// This attribute will override the to be used when parsing for the
+ /// desired type in the command. This is useful when one wishes to use a particular
+ /// without affecting other commands that are using the same target
+ /// type.
+ ///
+ /// If the given type reader does not inherit from , an
+ /// will be thrown.
+ ///
+ ///
+ ///
+ /// In this example, the will be read by a custom
+ /// , FriendlyTimeSpanTypeReader, instead of the
+ /// shipped by Discord.Net.
+ ///
+ /// [Command("time")]
+ /// public Task GetTimeAsync([OverrideTypeReader(typeof(FriendlyTimeSpanTypeReader))]TimeSpan time)
+ /// => ReplyAsync(time);
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+ public sealed class OverrideTypeReaderAttribute : Attribute
+ {
+ private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo();
+
+ ///
+ /// Gets the specified of the parameter.
+ ///
+ public Type TypeReader { get; }
+
+ ///
+ /// The to be used with the parameter.
+ /// The given does not inherit from .
+ public OverrideTypeReaderAttribute(Type overridenTypeReader)
+ {
+ if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo()))
+ throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}.");
+
+ TypeReader = overridenTypeReader;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs
new file mode 100644
index 0000000..8ee46f9
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// Requires the parameter to pass the specified precondition before execution can begin.
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
+ public abstract class ParameterPreconditionAttribute : Attribute
+ {
+ ///
+ /// Checks whether the condition is met before execution of the command.
+ ///
+ /// The context of the command.
+ /// The parameter of the command being checked against.
+ /// The raw value of the parameter.
+ /// The service collection used for dependency injection.
+ public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services);
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
new file mode 100644
index 0000000..37a08ba
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// Requires the module or class to pass the specified precondition before execution can begin.
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
+ public abstract class PreconditionAttribute : Attribute
+ {
+ ///
+ /// Specifies a group that this precondition belongs to.
+ ///
+ ///
+ /// of the same group require only one of the preconditions to pass in order to
+ /// be successful (A || B). Specifying = null or not at all will
+ /// require *all* preconditions to pass, just like normal (A && B).
+ ///
+ public string Group { get; set; } = null;
+
+ ///
+ /// When overridden in a derived class, uses the supplied string
+ /// as the error message if the precondition doesn't pass.
+ /// Setting this for a class that doesn't override
+ /// this property is a no-op.
+ ///
+ public virtual string ErrorMessage { get { return null; } set { } }
+
+ ///
+ /// Checks if the has the sufficient permission to be executed.
+ ///
+ /// The context of the command.
+ /// The command being executed.
+ /// The service collection used for dependency injection.
+ public abstract Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services);
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
new file mode 100644
index 0000000..5b3b5bd
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// Requires the bot to have a specific permission in the channel a command is invoked in.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class RequireBotPermissionAttribute : PreconditionAttribute
+ {
+ ///
+ /// Gets the specified of the precondition.
+ ///
+ public GuildPermission? GuildPermission { get; }
+ ///
+ /// Gets the specified of the precondition.
+ ///
+ public ChannelPermission? ChannelPermission { get; }
+ ///
+ public override string ErrorMessage { get; set; }
+ ///
+ /// Gets or sets the error message if the precondition
+ /// fails due to being run outside of a Guild channel.
+ ///
+ public string NotAGuildErrorMessage { get; set; }
+
+ ///
+ /// Requires the bot account to have a specific .
+ ///
+ ///
+ /// This precondition will always fail if the command is being invoked in a .
+ ///
+ ///
+ /// The that the bot must have. Multiple permissions can be specified
+ /// by ORing the permissions together.
+ ///
+ public RequireBotPermissionAttribute(GuildPermission permission)
+ {
+ GuildPermission = permission;
+ ChannelPermission = null;
+ }
+ ///
+ /// Requires that the bot account to have a specific .
+ ///
+ ///
+ /// The that the bot must have. Multiple permissions can be
+ /// specified by ORing the permissions together.
+ ///
+ public RequireBotPermissionAttribute(ChannelPermission permission)
+ {
+ ChannelPermission = permission;
+ GuildPermission = null;
+ }
+
+ ///
+ public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
+ {
+ IGuildUser guildUser = null;
+ if (context.Guild != null)
+ guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false);
+
+ if (GuildPermission.HasValue)
+ {
+ if (guildUser == null)
+ return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.");
+ if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
+ return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires guild permission {GuildPermission.Value}.");
+ }
+
+ if (ChannelPermission.HasValue)
+ {
+ ChannelPermissions perms;
+ if (context.Channel is IGuildChannel guildChannel)
+ perms = guildUser.GetPermissions(guildChannel);
+ else
+ perms = ChannelPermissions.All(context.Channel);
+
+ if (!perms.Has(ChannelPermission.Value))
+ return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}.");
+ }
+
+ return PreconditionResult.FromSuccess();
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs
new file mode 100644
index 0000000..a27469c
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// Defines the type of command context (i.e. where the command is being executed).
+ ///
+ [Flags]
+ public enum ContextType
+ {
+ ///
+ /// Specifies the command to be executed within a guild.
+ ///
+ Guild = 0x01,
+ ///
+ /// Specifies the command to be executed within a DM.
+ ///
+ DM = 0x02,
+ ///
+ /// Specifies the command to be executed within a group.
+ ///
+ Group = 0x04
+ }
+
+ ///
+ /// Requires the command to be invoked in a specified context (e.g. in guild, DM).
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class RequireContextAttribute : PreconditionAttribute
+ {
+ ///
+ /// Gets the context required to execute the command.
+ ///
+ public ContextType Contexts { get; }
+ ///
+ public override string ErrorMessage { get; set; }
+
+ /// Requires the command to be invoked in the specified context.
+ /// The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together.
+ ///
+ ///
+ /// [Command("secret")]
+ /// [RequireContext(ContextType.DM | ContextType.Group)]
+ /// public Task PrivateOnlyAsync()
+ /// {
+ /// return ReplyAsync("shh, this command is a secret");
+ /// }
+ ///
+ ///
+ public RequireContextAttribute(ContextType contexts)
+ {
+ Contexts = contexts;
+ }
+
+ ///
+ public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
+ {
+ bool isValid = false;
+
+ if ((Contexts & ContextType.Guild) != 0)
+ isValid = context.Channel is IGuildChannel;
+ if ((Contexts & ContextType.DM) != 0)
+ isValid = isValid || context.Channel is IDMChannel;
+ if ((Contexts & ContextType.Group) != 0)
+ isValid = isValid || context.Channel is IGroupChannel;
+
+ if (isValid)
+ return Task.FromResult(PreconditionResult.FromSuccess());
+ else
+ return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}."));
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs
new file mode 100644
index 0000000..2a9647c
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// Requires the command to be invoked in a channel marked NSFW.
+ ///
+ ///
+ /// The precondition will restrict the access of the command or module to be accessed within a guild channel
+ /// that has been marked as mature or NSFW. If the channel is not of type or the
+ /// channel is not marked as NSFW, the precondition will fail with an erroneous .
+ ///
+ ///
+ /// The following example restricts the command too-cool to an NSFW-enabled channel only.
+ ///
+ /// public class DankModule : ModuleBase
+ /// {
+ /// [Command("cool")]
+ /// public Task CoolAsync()
+ /// => ReplyAsync("I'm cool for everyone.");
+ ///
+ /// [RequireNsfw]
+ /// [Command("too-cool")]
+ /// public Task TooCoolAsync()
+ /// => ReplyAsync("You can only see this if you're cool enough.");
+ /// }
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class RequireNsfwAttribute : PreconditionAttribute
+ {
+ ///
+ public override string ErrorMessage { get; set; }
+
+ ///
+ public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
+ {
+ if (context.Channel is ITextChannel text && text.IsNsfw)
+ return Task.FromResult(PreconditionResult.FromSuccess());
+ else
+ return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? "This command may only be invoked in an NSFW channel."));
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
new file mode 100644
index 0000000..c08e1e9
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// Requires the command to be invoked by the owner of the bot.
+ ///
+ ///
+ /// This precondition will restrict the access of the command or module to the owner of the Discord application.
+ /// If the precondition fails to be met, an erroneous will be returned with the
+ /// message "Command can only be run by the owner of the bot."
+ ///
+ /// This precondition will only work if the account has a of
+ /// ;otherwise, this precondition will always fail.
+ ///
+ ///
+ ///
+ /// The following example restricts the command to a set of sensitive commands that only the owner of the bot
+ /// application should be able to access.
+ ///
+ /// [RequireOwner]
+ /// [Group("admin")]
+ /// public class AdminModule : ModuleBase
+ /// {
+ /// [Command("exit")]
+ /// public async Task ExitAsync()
+ /// {
+ /// Environment.Exit(0);
+ /// }
+ /// }
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class RequireOwnerAttribute : PreconditionAttribute
+ {
+ ///
+ public override string ErrorMessage { get; set; }
+
+ ///
+ public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
+ {
+ switch (context.Client.TokenType)
+ {
+ case TokenType.Bot:
+ var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false);
+ if (context.User.Id != application.Owner.Id)
+ return PreconditionResult.FromError(ErrorMessage ?? "Command can only be run by the owner of the bot.");
+ return PreconditionResult.FromSuccess();
+ default:
+ return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
+ }
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
new file mode 100644
index 0000000..2908a18
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ ///
+ /// Requires the user invoking the command to have a specified permission.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class RequireUserPermissionAttribute : PreconditionAttribute
+ {
+ ///
+ /// Gets the specified of the precondition.
+ ///
+ public GuildPermission? GuildPermission { get; }
+ ///
+ /// Gets the specified of the precondition.
+ ///
+ public ChannelPermission? ChannelPermission { get; }
+ ///
+ public override string ErrorMessage { get; set; }
+ ///
+ /// Gets or sets the error message if the precondition
+ /// fails due to being run outside of a Guild channel.
+ ///
+ public string NotAGuildErrorMessage { get; set; }
+
+ ///
+ /// Requires that the user invoking the command to have a specific .
+ ///
+ ///
+ /// This precondition will always fail if the command is being invoked in a .
+ ///
+ ///
+ /// The that the user must have. Multiple permissions can be
+ /// specified by ORing the permissions together.
+ ///
+ public RequireUserPermissionAttribute(GuildPermission permission)
+ {
+ GuildPermission = permission;
+ ChannelPermission = null;
+ }
+ ///
+ /// Requires that the user invoking the command to have a specific .
+ ///
+ ///
+ /// The that the user must have. Multiple permissions can be
+ /// specified by ORing the permissions together.
+ ///
+ public RequireUserPermissionAttribute(ChannelPermission permission)
+ {
+ ChannelPermission = permission;
+ GuildPermission = null;
+ }
+
+ ///
+ public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
+ {
+ var guildUser = context.User as IGuildUser;
+
+ if (GuildPermission.HasValue)
+ {
+ if (guildUser == null)
+ return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."));
+ if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
+ return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}."));
+ }
+
+ if (ChannelPermission.HasValue)
+ {
+ ChannelPermissions perms;
+ if (context.Channel is IGuildChannel guildChannel)
+ perms = guildUser.GetPermissions(guildChannel);
+ else
+ perms = ChannelPermissions.All(context.Channel);
+
+ if (!perms.Has(ChannelPermission.Value))
+ return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}."));
+ }
+
+ return Task.FromResult(PreconditionResult.FromSuccess());
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PriorityAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PriorityAttribute.cs
new file mode 100644
index 0000000..75ffd25
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/PriorityAttribute.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Sets priority of commands.
+ ///
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class PriorityAttribute : Attribute
+ {
+ ///
+ /// Gets the priority which has been set for the command.
+ ///
+ public int Priority { get; }
+
+ ///
+ /// Initializes a new attribute with the given priority.
+ ///
+ public PriorityAttribute(int priority)
+ {
+ Priority = priority;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemainderAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemainderAttribute.cs
new file mode 100644
index 0000000..33e07f0
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemainderAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// Marks the input to not be parsed by the parser.
+ ///
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+ public class RemainderAttribute : Attribute
+ {
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemarksAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemarksAttribute.cs
new file mode 100644
index 0000000..2fbe2bf
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/RemarksAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Discord.Commands
+{
+ // Extension of the Cosmetic Summary, for Groups, Commands, and Parameters
+ ///
+ /// Attaches remarks to your commands.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public class RemarksAttribute : Attribute
+ {
+ public string Text { get; }
+
+ public RemarksAttribute(string text)
+ {
+ Text = text;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/SummaryAttribute.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/SummaryAttribute.cs
new file mode 100644
index 0000000..57e9b02
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Attributes/SummaryAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Discord.Commands
+{
+ // Cosmetic Summary, for Groups and Commands
+ ///
+ /// Attaches a summary to your command.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+ public class SummaryAttribute : Attribute
+ {
+ public string Text { get; }
+
+ public SummaryAttribute(string text)
+ {
+ Text = text;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/CommandBuilder.cs
new file mode 100644
index 0000000..3f1ca88
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/CommandBuilder.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace Discord.Commands.Builders
+{
+ public class CommandBuilder
+ {
+ private readonly List _preconditions;
+ private readonly List _parameters;
+ private readonly List _attributes;
+ private readonly List _aliases;
+
+ public ModuleBuilder Module { get; }
+ internal Func Callback { get; set; }
+
+ public string Name { get; set; }
+ public string Summary { get; set; }
+ public string Remarks { get; set; }
+ public string PrimaryAlias { get; set; }
+ public RunMode RunMode { get; set; }
+ public int Priority { get; set; }
+ public bool IgnoreExtraArgs { get; set; }
+
+ public IReadOnlyList Preconditions => _preconditions;
+ public IReadOnlyList Parameters => _parameters;
+ public IReadOnlyList Attributes => _attributes;
+ public IReadOnlyList Aliases => _aliases;
+
+ //Automatic
+ internal CommandBuilder(ModuleBuilder module)
+ {
+ Module = module;
+
+ _preconditions = new List();
+ _parameters = new List();
+ _attributes = new List();
+ _aliases = new List();
+ }
+ //User-defined
+ internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback)
+ : this(module)
+ {
+ Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
+ Discord.Preconditions.NotNull(callback, nameof(callback));
+
+ Callback = callback;
+ PrimaryAlias = primaryAlias;
+ _aliases.Add(primaryAlias);
+ }
+
+ public CommandBuilder WithName(string name)
+ {
+ Name = name;
+ return this;
+ }
+ public CommandBuilder WithSummary(string summary)
+ {
+ Summary = summary;
+ return this;
+ }
+ public CommandBuilder WithRemarks(string remarks)
+ {
+ Remarks = remarks;
+ return this;
+ }
+ public CommandBuilder WithRunMode(RunMode runMode)
+ {
+ RunMode = runMode;
+ return this;
+ }
+ public CommandBuilder WithPriority(int priority)
+ {
+ Priority = priority;
+ return this;
+ }
+
+ public CommandBuilder AddAliases(params string[] aliases)
+ {
+ for (int i = 0; i < aliases.Length; i++)
+ {
+ string alias = aliases[i] ?? "";
+ if (!_aliases.Contains(alias))
+ _aliases.Add(alias);
+ }
+ return this;
+ }
+ public CommandBuilder AddAttributes(params Attribute[] attributes)
+ {
+ _attributes.AddRange(attributes);
+ return this;
+ }
+ public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
+ {
+ _preconditions.Add(precondition);
+ return this;
+ }
+ public CommandBuilder AddParameter(string name, Action createFunc)
+ {
+ var param = new ParameterBuilder(this, name, typeof(T));
+ createFunc(param);
+ _parameters.Add(param);
+ return this;
+ }
+ public CommandBuilder AddParameter(string name, Type type, Action createFunc)
+ {
+ var param = new ParameterBuilder(this, name, type);
+ createFunc(param);
+ _parameters.Add(param);
+ return this;
+ }
+ internal CommandBuilder AddParameter(Action createFunc)
+ {
+ var param = new ParameterBuilder(this);
+ createFunc(param);
+ _parameters.Add(param);
+ return this;
+ }
+
+ /// Only the last parameter in a command may have the Remainder or Multiple flag.
+ internal CommandInfo Build(ModuleInfo info, CommandService service)
+ {
+ //Default name to primary alias
+ if (Name == null)
+ Name = PrimaryAlias;
+
+ if (_parameters.Count > 0)
+ {
+ var lastParam = _parameters[_parameters.Count - 1];
+
+ var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple);
+ if ((firstMultipleParam != null) && (firstMultipleParam != lastParam))
+ throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}");
+
+ var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder);
+ if ((firstRemainderParam != null) && (firstRemainderParam != lastParam))
+ throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}");
+ }
+
+ return new CommandInfo(this, info, service);
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleBuilder.cs
new file mode 100644
index 0000000..6dc50db
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleBuilder.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Discord.Commands.Builders
+{
+ public class ModuleBuilder
+ {
+ private readonly List _commands;
+ private readonly List _submodules;
+ private readonly List _preconditions;
+ private readonly List _attributes;
+ private readonly List _aliases;
+
+ public CommandService Service { get; }
+ public ModuleBuilder Parent { get; }
+ public string Name { get; set; }
+ public string Summary { get; set; }
+ public string Remarks { get; set; }
+ public string Group { get; set; }
+
+ public IReadOnlyList Commands => _commands;
+ public IReadOnlyList Modules => _submodules;
+ public IReadOnlyList Preconditions => _preconditions;
+ public IReadOnlyList Attributes => _attributes;
+ public IReadOnlyList Aliases => _aliases;
+
+ internal TypeInfo TypeInfo { get; set; }
+
+ //Automatic
+ internal ModuleBuilder(CommandService service, ModuleBuilder parent)
+ {
+ Service = service;
+ Parent = parent;
+
+ _commands = new List();
+ _submodules = new List();
+ _preconditions = new List();
+ _attributes = new List();
+ _aliases = new List();
+ }
+ //User-defined
+ internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias)
+ : this(service, parent)
+ {
+ Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
+
+ _aliases = new List { primaryAlias };
+ }
+
+ public ModuleBuilder WithName(string name)
+ {
+ Name = name;
+ return this;
+ }
+ public ModuleBuilder WithSummary(string summary)
+ {
+ Summary = summary;
+ return this;
+ }
+ public ModuleBuilder WithRemarks(string remarks)
+ {
+ Remarks = remarks;
+ return this;
+ }
+
+ public ModuleBuilder AddAliases(params string[] aliases)
+ {
+ for (int i = 0; i < aliases.Length; i++)
+ {
+ string alias = aliases[i] ?? "";
+ if (!_aliases.Contains(alias))
+ _aliases.Add(alias);
+ }
+ return this;
+ }
+ public ModuleBuilder AddAttributes(params Attribute[] attributes)
+ {
+ _attributes.AddRange(attributes);
+ return this;
+ }
+ public ModuleBuilder AddPrecondition(PreconditionAttribute precondition)
+ {
+ _preconditions.Add(precondition);
+ return this;
+ }
+ public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc)
+ {
+ var builder = new CommandBuilder(this, primaryAlias, callback);
+ createFunc(builder);
+ _commands.Add(builder);
+ return this;
+ }
+ internal ModuleBuilder AddCommand(Action createFunc)
+ {
+ var builder = new CommandBuilder(this);
+ createFunc(builder);
+ _commands.Add(builder);
+ return this;
+ }
+ public ModuleBuilder AddModule(string primaryAlias, Action createFunc)
+ {
+ var builder = new ModuleBuilder(Service, this, primaryAlias);
+ createFunc(builder);
+ _submodules.Add(builder);
+ return this;
+ }
+ internal ModuleBuilder AddModule(Action createFunc)
+ {
+ var builder = new ModuleBuilder(Service, this);
+ createFunc(builder);
+ _submodules.Add(builder);
+ return this;
+ }
+
+ private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo parent = null)
+ {
+ //Default name to first alias
+ if (Name == null)
+ Name = _aliases[0];
+
+ if (TypeInfo != null && !TypeInfo.IsAbstract)
+ {
+ var moduleInstance = ReflectionUtils.CreateObject(TypeInfo, service, services);
+ moduleInstance.OnModuleBuilding(service, this);
+ }
+
+ return new ModuleInfo(this, service, services, parent);
+ }
+
+ public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services);
+
+ internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent);
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
new file mode 100644
index 0000000..aec8dcb
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
@@ -0,0 +1,315 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+
+using Discord.Commands.Builders;
+
+namespace Discord.Commands
+{
+ internal static class ModuleClassBuilder
+ {
+ private static readonly TypeInfo ModuleTypeInfo = typeof(IModuleBase).GetTypeInfo();
+
+ public static async Task> SearchAsync(Assembly assembly, CommandService service)
+ {
+ bool IsLoadableModule(TypeInfo info)
+ {
+ return info.DeclaredMethods.Any(x => x.GetCustomAttribute() != null) &&
+ info.GetCustomAttribute() == null;
+ }
+
+ var result = new List();
+
+ foreach (var typeInfo in assembly.DefinedTypes)
+ {
+ if (typeInfo.IsPublic || typeInfo.IsNestedPublic)
+ {
+ if (IsValidModuleDefinition(typeInfo) &&
+ !typeInfo.IsDefined(typeof(DontAutoLoadAttribute)))
+ {
+ result.Add(typeInfo);
+ }
+ }
+ else if (IsLoadableModule(typeInfo))
+ {
+ await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.").ConfigureAwait(false);
+ }
+ }
+
+ return result;
+ }
+
+
+ public static Task> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services);
+ public static async Task> BuildAsync(IEnumerable validTypes, CommandService service, IServiceProvider services)
+ {
+ /*if (!validTypes.Any())
+ throw new InvalidOperationException("Could not find any valid modules from the given selection");*/
+
+ var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo()));
+
+ var builtTypes = new List();
+
+ var result = new Dictionary();
+
+ foreach (var typeInfo in topLevelGroups)
+ {
+ // TODO: This shouldn't be the case; may be safe to remove?
+ if (result.ContainsKey(typeInfo.AsType()))
+ continue;
+
+ var module = new ModuleBuilder(service, null);
+
+ BuildModule(module, typeInfo, service, services);
+ BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services);
+ builtTypes.Add(typeInfo);
+
+ result[typeInfo.AsType()] = module.Build(service, services);
+ }
+
+ await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false);
+
+ return result;
+ }
+
+ private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service, IServiceProvider services)
+ {
+ foreach (var typeInfo in subTypes)
+ {
+ if (!IsValidModuleDefinition(typeInfo))
+ continue;
+
+ if (builtTypes.Contains(typeInfo))
+ continue;
+
+ builder.AddModule((module) =>
+ {
+ BuildModule(module, typeInfo, service, services);
+ BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services);
+ });
+
+ builtTypes.Add(typeInfo);
+ }
+ }
+
+ private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service, IServiceProvider services)
+ {
+ var attributes = typeInfo.GetCustomAttributes();
+ builder.TypeInfo = typeInfo;
+
+ foreach (var attribute in attributes)
+ {
+ switch (attribute)
+ {
+ case NameAttribute name:
+ builder.Name = name.Text;
+ break;
+ case SummaryAttribute summary:
+ builder.Summary = summary.Text;
+ break;
+ case RemarksAttribute remarks:
+ builder.Remarks = remarks.Text;
+ break;
+ case AliasAttribute alias:
+ builder.AddAliases(alias.Aliases);
+ break;
+ case GroupAttribute group:
+ builder.Name = builder.Name ?? group.Prefix;
+ builder.Group = group.Prefix;
+ builder.AddAliases(group.Prefix);
+ break;
+ case PreconditionAttribute precondition:
+ builder.AddPrecondition(precondition);
+ break;
+ default:
+ builder.AddAttributes(attribute);
+ break;
+ }
+ }
+
+ //Check for unspecified info
+ if (builder.Aliases.Count == 0)
+ builder.AddAliases("");
+ if (builder.Name == null)
+ builder.Name = typeInfo.Name;
+
+ var validCommands = typeInfo.DeclaredMethods.Where(IsValidCommandDefinition);
+
+ foreach (var method in validCommands)
+ {
+ builder.AddCommand((command) =>
+ {
+ BuildCommand(command, typeInfo, method, service, services);
+ });
+ }
+ }
+
+ private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider)
+ {
+ var attributes = method.GetCustomAttributes();
+
+ foreach (var attribute in attributes)
+ {
+ switch (attribute)
+ {
+ case CommandAttribute command:
+ builder.AddAliases(command.Text);
+ builder.RunMode = command.RunMode;
+ builder.Name = builder.Name ?? command.Text;
+ builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs;
+ break;
+ case NameAttribute name:
+ builder.Name = name.Text;
+ break;
+ case PriorityAttribute priority:
+ builder.Priority = priority.Priority;
+ break;
+ case SummaryAttribute summary:
+ builder.Summary = summary.Text;
+ break;
+ case RemarksAttribute remarks:
+ builder.Remarks = remarks.Text;
+ break;
+ case AliasAttribute alias:
+ builder.AddAliases(alias.Aliases);
+ break;
+ case PreconditionAttribute precondition:
+ builder.AddPrecondition(precondition);
+ break;
+ default:
+ builder.AddAttributes(attribute);
+ break;
+ }
+ }
+
+ if (builder.Name == null)
+ builder.Name = method.Name;
+
+ var parameters = method.GetParameters();
+ int pos = 0, count = parameters.Length;
+ foreach (var paramInfo in parameters)
+ {
+ builder.AddParameter((parameter) =>
+ {
+ BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider);
+ });
+ }
+
+ var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service);
+
+ async Task ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd)
+ {
+ var instance = createInstance(services);
+ instance.SetContext(context);
+
+ try
+ {
+ instance.BeforeExecute(cmd);
+
+ var task = method.Invoke(instance, args) as Task ?? Task.Delay(0);
+ if (task is Task resultTask)
+ {
+ return await resultTask.ConfigureAwait(false);
+ }
+ else
+ {
+ await task.ConfigureAwait(false);
+ return ExecuteResult.FromSuccess();
+ }
+ }
+ finally
+ {
+ instance.AfterExecute(cmd);
+ (instance as IDisposable)?.Dispose();
+ }
+ }
+
+ builder.Callback = ExecuteCallback;
+ }
+
+ private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services)
+ {
+ var attributes = paramInfo.GetCustomAttributes();
+ var paramType = paramInfo.ParameterType;
+
+ builder.Name = paramInfo.Name;
+
+ builder.IsOptional = paramInfo.IsOptional;
+ builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null;
+
+ foreach (var attribute in attributes)
+ {
+ switch (attribute)
+ {
+ case SummaryAttribute summary:
+ builder.Summary = summary.Text;
+ break;
+ case OverrideTypeReaderAttribute typeReader:
+ builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services);
+ break;
+ case ParamArrayAttribute _:
+ builder.IsMultiple = true;
+ paramType = paramType.GetElementType();
+ break;
+ case ParameterPreconditionAttribute precon:
+ builder.AddPrecondition(precon);
+ break;
+ case NameAttribute name:
+ builder.Name = name.Text;
+ break;
+ case RemainderAttribute _:
+ if (position != count - 1)
+ throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}");
+
+ builder.IsRemainder = true;
+ break;
+ default:
+ builder.AddAttributes(attribute);
+ break;
+ }
+ }
+
+ builder.ParameterType = paramType;
+
+ if (builder.TypeReader == null)
+ {
+ builder.TypeReader = service.GetDefaultTypeReader(paramType)
+ ?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value;
+ }
+ }
+
+ internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services)
+ {
+ var readers = service.GetTypeReaders(paramType);
+ TypeReader reader = null;
+ if (readers != null)
+ {
+ if (readers.TryGetValue(typeReaderType, out reader))
+ return reader;
+ }
+
+ //We dont have a cached type reader, create one
+ reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services);
+ service.AddTypeReader(paramType, reader, false);
+
+ return reader;
+ }
+
+ private static bool IsValidModuleDefinition(TypeInfo typeInfo)
+ {
+ return ModuleTypeInfo.IsAssignableFrom(typeInfo) &&
+ !typeInfo.IsAbstract &&
+ !typeInfo.ContainsGenericParameters;
+ }
+
+ private static bool IsValidCommandDefinition(MethodInfo methodInfo)
+ {
+ return methodInfo.IsDefined(typeof(CommandAttribute)) &&
+ (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) &&
+ !methodInfo.IsStatic &&
+ !methodInfo.IsGenericMethod;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ParameterBuilder.cs
new file mode 100644
index 0000000..4ad5bfa
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/Builders/ParameterBuilder.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+using System.Collections.Generic;
+
+namespace Discord.Commands.Builders
+{
+ public class ParameterBuilder
+ {
+ private readonly List _preconditions;
+ private readonly List _attributes;
+
+ public CommandBuilder Command { get; }
+ public string Name { get; internal set; }
+ public Type ParameterType { get; internal set; }
+
+ public TypeReader TypeReader { get; set; }
+ public bool IsOptional { get; set; }
+ public bool IsRemainder { get; set; }
+ public bool IsMultiple { get; set; }
+ public object DefaultValue { get; set; }
+ public string Summary { get; set; }
+
+ public IReadOnlyList Preconditions => _preconditions;
+ public IReadOnlyList Attributes => _attributes;
+
+ //Automatic
+ internal ParameterBuilder(CommandBuilder command)
+ {
+ _preconditions = new List();
+ _attributes = new List();
+
+ Command = command;
+ }
+ //User-defined
+ internal ParameterBuilder(CommandBuilder command, string name, Type type)
+ : this(command)
+ {
+ Discord.Preconditions.NotNull(name, nameof(name));
+
+ Name = name;
+ SetType(type);
+ }
+
+ internal void SetType(Type type)
+ {
+ TypeReader = GetReader(type);
+
+ if (type.GetTypeInfo().IsValueType)
+ DefaultValue = Activator.CreateInstance(type);
+ else if (type.IsArray)
+ type = ParameterType.GetElementType();
+ ParameterType = type;
+ }
+
+ private TypeReader GetReader(Type type)
+ {
+ var commands = Command.Module.Service;
+ if (type.GetTypeInfo().GetCustomAttribute() != null)
+ {
+ IsRemainder = true;
+ var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value;
+ if (reader == null)
+ {
+ Type readerType;
+ try
+ {
+ readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type });
+ }
+ catch (ArgumentException ex)
+ {
+ throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex);
+ }
+
+ reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands });
+ commands.AddTypeReader(type, reader);
+ }
+
+ return reader;
+ }
+
+
+ var readers = commands.GetTypeReaders(type);
+ if (readers != null)
+ return readers.FirstOrDefault().Value;
+ else
+ return commands.GetDefaultTypeReader(type);
+ }
+
+ public ParameterBuilder WithSummary(string summary)
+ {
+ Summary = summary;
+ return this;
+ }
+ public ParameterBuilder WithDefault(object defaultValue)
+ {
+ DefaultValue = defaultValue;
+ return this;
+ }
+ public ParameterBuilder WithIsOptional(bool isOptional)
+ {
+ IsOptional = isOptional;
+ return this;
+ }
+ public ParameterBuilder WithIsRemainder(bool isRemainder)
+ {
+ IsRemainder = isRemainder;
+ return this;
+ }
+ public ParameterBuilder WithIsMultiple(bool isMultiple)
+ {
+ IsMultiple = isMultiple;
+ return this;
+ }
+
+ public ParameterBuilder AddAttributes(params Attribute[] attributes)
+ {
+ _attributes.AddRange(attributes);
+ return this;
+ }
+ public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition)
+ {
+ _preconditions.Add(precondition);
+ return this;
+ }
+
+ internal ParameterInfo Build(CommandInfo info)
+ {
+ if ((TypeReader ?? (TypeReader = GetReader(ParameterType))) == null)
+ throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified");
+
+ return new ParameterInfo(this, info, Command.Module.Service);
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandContext.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandContext.cs
new file mode 100644
index 0000000..393cdf9
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandContext.cs
@@ -0,0 +1,34 @@
+namespace Discord.Commands
+{
+ /// The context of a command which may contain the client, user, guild, channel, and message.
+ public class CommandContext : ICommandContext
+ {
+ ///
+ public IDiscordClient Client { get; }
+ ///
+ public IGuild Guild { get; }
+ ///
+ public IMessageChannel Channel { get; }
+ ///
+ public IUser User { get; }
+ ///
+ public IUserMessage Message { get; }
+
+ /// Indicates whether the channel that the command is executed in is a private channel.
+ public bool IsPrivate => Channel is IPrivateChannel;
+
+ ///
+ /// Initializes a new class with the provided client and message.
+ ///
+ /// The underlying client.
+ /// The underlying message.
+ public CommandContext(IDiscordClient client, IUserMessage msg)
+ {
+ Client = client;
+ Guild = (msg.Channel as IGuildChannel)?.Guild;
+ Channel = msg.Channel;
+ User = msg.Author;
+ Message = msg;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandError.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandError.cs
new file mode 100644
index 0000000..b487d8a
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandError.cs
@@ -0,0 +1,51 @@
+namespace Discord.Commands
+{
+ /// Defines the type of error a command can throw.
+ public enum CommandError
+ {
+ //Search
+ ///
+ /// Thrown when the command is unknown.
+ ///
+ UnknownCommand = 1,
+
+ //Parse
+ ///
+ /// Thrown when the command fails to be parsed.
+ ///
+ ParseFailed,
+ ///
+ /// Thrown when the input text has too few or too many arguments.
+ ///
+ BadArgCount,
+
+ //Parse (Type Reader)
+ //CastFailed,
+ ///
+ /// Thrown when the object cannot be found by the .
+ ///
+ ObjectNotFound,
+ ///
+ /// Thrown when more than one object is matched by .
+ ///
+ MultipleMatches,
+
+ //Preconditions
+ ///
+ /// Thrown when the command fails to meet a 's conditions.
+ ///
+ UnmetPrecondition,
+
+ //Execute
+ ///
+ /// Thrown when an exception occurs mid-command execution.
+ ///
+ Exception,
+
+ //Runtime
+ ///
+ /// Thrown when the command is not successfully executed on runtime.
+ ///
+ Unsuccessful
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandException.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandException.cs
new file mode 100644
index 0000000..6c5ab0a
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandException.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Discord.Commands
+{
+ ///
+ /// The exception that is thrown if another exception occurs during a command execution.
+ ///
+ public class CommandException : Exception
+ {
+ /// Gets the command that caused the exception.
+ public CommandInfo Command { get; }
+ /// Gets the command context of the exception.
+ public ICommandContext Context { get; }
+
+ ///
+ /// Initializes a new instance of the class using a
+ /// information, a context, and the exception that
+ /// interrupted the execution.
+ ///
+ /// The command information.
+ /// The context of the command.
+ /// The exception that interrupted the command execution.
+ public CommandException(CommandInfo command, ICommandContext context, Exception ex)
+ : base($"Error occurred executing {command.GetLogText(context)}.", ex)
+ {
+ Command = command;
+ Context = context;
+ }
+ }
+}
diff --git a/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandMatch.cs b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandMatch.cs
new file mode 100644
index 0000000..c15a332
--- /dev/null
+++ b/src/stream-sniper/stream-sniper/Lib/Discord.NET/Discord.Net.Commands/CommandMatch.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ public struct CommandMatch
+ {
+ /// The command that matches the search result.
+ public CommandInfo Command { get; }
+ /// The alias of the command.
+ public string Alias { get; }
+
+ public CommandMatch(CommandInfo command, string alias)
+ {
+ Command = command;
+ Alias = alias;
+ }
+
+ public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null)
+ => Command.CheckPreconditionsAsync(context, services);
+ public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null)
+ => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services);
+ public Task ExecuteAsync(ICommandContext context, IEnumerable