| @@ -0,0 +1,72 @@ | |||
| using Discord; | |||
| using Discord.Commands; | |||
| using Discord.SlashCommands; | |||
| using Discord.WebSocket; | |||
| using Microsoft.Extensions.DependencyInjection; | |||
| using System; | |||
| using System.Reflection; | |||
| using System.Threading.Tasks; | |||
| namespace SlashCommandsExample | |||
| { | |||
| class DiscordClient | |||
| { | |||
| public static DiscordSocketClient socketClient { get; set; } = new DiscordSocketClient(); | |||
| public static SlashCommandService _commands { get; set; } | |||
| public static IServiceProvider _services { get; set; } | |||
| private string botToken = "<YOUR TOKEN HERE>"; | |||
| public DiscordClient() | |||
| { | |||
| _commands = new SlashCommandService(); | |||
| _services = new ServiceCollection() | |||
| .AddSingleton(socketClient) | |||
| .AddSingleton(_commands) | |||
| .BuildServiceProvider(); | |||
| socketClient.Log += SocketClient_Log; | |||
| _commands.Log += SocketClient_Log; | |||
| socketClient.InteractionCreated += InteractionHandler; | |||
| // This is for dev purposes. | |||
| // To avoid the situation in which you accidentally push your bot token to upstream, you can use | |||
| // EnviromentVariables to store your key. | |||
| botToken = Environment.GetEnvironmentVariable("DiscordSlashCommandsBotToken", EnvironmentVariableTarget.User); | |||
| // Uncomment the next line of code to set the environment variable. | |||
| // ------------------------------------------------------------------ | |||
| // | WARNING! | | |||
| // | | | |||
| // | MAKE SURE TO DELETE YOUR TOKEN AFTER YOU HAVE SET THE VARIABLE | | |||
| // | | | |||
| // ------------------------------------------------------------------ | |||
| //Environment.SetEnvironmentVariable("DiscordSlashCommandsBotToken", | |||
| // "[YOUR TOKEN GOES HERE DELETE & COMMENT AFTER USE]", | |||
| // EnvironmentVariableTarget.User); | |||
| } | |||
| public async Task RunAsync() | |||
| { | |||
| await socketClient.LoginAsync(TokenType.Bot, botToken); | |||
| await socketClient.StartAsync(); | |||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | |||
| await Task.Delay(-1); | |||
| } | |||
| private async Task InteractionHandler(SocketInteraction arg) | |||
| { | |||
| if(arg.Type == InteractionType.ApplicationCommand) | |||
| { | |||
| await _commands.ExecuteAsync(arg); | |||
| } | |||
| } | |||
| private Task SocketClient_Log(LogMessage arg) | |||
| { | |||
| Console.WriteLine("[Discord] " + arg.ToString()); | |||
| return Task.CompletedTask; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| using Discord.SlashCommands; | |||
| using Discord.WebSocket; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Text; | |||
| namespace SlashCommandsExample.Modules | |||
| { | |||
| // Doesn't inherit from SlashCommandModule | |||
| public class InvalidDefinition : Object | |||
| { | |||
| // commands | |||
| } | |||
| // Isn't public | |||
| class PrivateDefinition : SlashCommandModule<SocketInteraction> | |||
| { | |||
| // commands | |||
| } | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| using Discord.Commands; | |||
| using Discord.Commands.SlashCommands.Types; | |||
| using Discord.SlashCommands; | |||
| using Discord.WebSocket; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace SlashCommandsExample.Modules | |||
| { | |||
| public class PingCommand : SlashCommandModule<SocketInteraction> | |||
| { | |||
| [SlashCommand("johnny-test", "Ping the bot to see if it is alive!")] | |||
| public async Task PingAsync() | |||
| { | |||
| await Interaction.FollowupAsync(":white_check_mark: **Bot Online**"); | |||
| } | |||
| } | |||
| } | |||
| /* | |||
| The base way of defining a command using the regular command service: | |||
| public class PingModule : ModuleBase<SocketCommandContext> | |||
| { | |||
| [Command("ping")] | |||
| [Summary("Pong! Check if the bot is alive.")] | |||
| public async Task PingAsync() | |||
| { | |||
| await ReplyAsync(":white_check_mark: **Bot Online**"); | |||
| } | |||
| } | |||
| */ | |||
| @@ -4,14 +4,23 @@ | |||
| * this project should be re-made into one that could be used as an example usage of the new Slash Command Service. | |||
| */ | |||
| using System; | |||
| using System.Threading.Tasks; | |||
| using Discord; | |||
| using Discord.Commands; | |||
| using Discord.SlashCommands; | |||
| using Discord.WebSocket; | |||
| namespace SlashCommandsExample | |||
| { | |||
| class Program | |||
| { | |||
| static void Main(string[] args) | |||
| static void Main(string[] args) | |||
| { | |||
| Console.WriteLine("Hello World!"); | |||
| } | |||
| } | |||
| DiscordClient discordClient = new DiscordClient(); | |||
| // This could instead be handled in another thread, if for whatever reason you want to continue execution in the main Thread. | |||
| discordClient.RunAsync().GetAwaiter().GetResult(); | |||
| } | |||
| } | |||
| } | |||
| @@ -5,4 +5,18 @@ | |||
| <TargetFramework>netcoreapp3.1</TargetFramework> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> | |||
| </ItemGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" /> | |||
| <ProjectReference Include="..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" /> | |||
| <ProjectReference Include="..\src\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
| <ProjectReference Include="..\src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" /> | |||
| <ProjectReference Include="..\src\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||
| <ProjectReference Include="..\src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" /> | |||
| <ProjectReference Include="..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -10,6 +10,7 @@ | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
| <ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -15,15 +15,21 @@ namespace Discord.SlashCommands | |||
| /// <summary> | |||
| /// The name of this slash command. | |||
| /// </summary> | |||
| public string CommandName; | |||
| public string commandName; | |||
| /// <summary> | |||
| /// The description of this slash command. | |||
| /// </summary> | |||
| public string description; | |||
| /// <summary> | |||
| /// Tells the <see cref="SlashCommandService"/> that this class/function is a slash command. | |||
| /// </summary> | |||
| /// <param name="CommandName">The name of this slash command.</param> | |||
| public SlashCommand(string CommandName) | |||
| /// <param name="commandName">The name of this slash command.</param> | |||
| public SlashCommand(string commandName, string description = "No description.") | |||
| { | |||
| this.CommandName = CommandName; | |||
| this.commandName = commandName; | |||
| this.description = description; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,64 @@ | |||
| using Discord.Commands; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.SlashCommands | |||
| { | |||
| public class SlashCommandInfo | |||
| { | |||
| /// <summary> | |||
| /// Gets the module that the command belongs in. | |||
| /// </summary> | |||
| public SlashModuleInfo Module { get; } | |||
| /// <summary> | |||
| /// Gets the name of the command. | |||
| /// </summary> | |||
| public string Name { get; } | |||
| /// <summary> | |||
| /// Gets the name of the command. | |||
| /// </summary> | |||
| public string Description { get; } | |||
| /// <summary> | |||
| /// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters | |||
| /// </summary> | |||
| public Delegate userMethod; | |||
| /// <summary> | |||
| /// The callback that we call to start the delegate. | |||
| /// </summary> | |||
| public Func<object[], Task<IResult>> callback; | |||
| public SlashCommandInfo(SlashModuleInfo module, string name, string description, Delegate userMethod) | |||
| { | |||
| Module = module; | |||
| Name = name; | |||
| Description = description; | |||
| this.userMethod = userMethod; | |||
| this.callback = new Func<object[], Task<IResult>>(async (args) => | |||
| { | |||
| // Try-catch it and see what we get - error or success | |||
| try | |||
| { | |||
| await Task.Run(() => | |||
| { | |||
| userMethod.DynamicInvoke(args); | |||
| }).ConfigureAwait(false); | |||
| } | |||
| catch(Exception e) | |||
| { | |||
| return ExecuteResult.FromError(e); | |||
| } | |||
| return ExecuteResult.FromSuccess(); | |||
| }); | |||
| } | |||
| public async Task<IResult> ExecuteAsync(object[] args) | |||
| { | |||
| return await callback.Invoke(args).ConfigureAwait(false); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.SlashCommands | |||
| { | |||
| public class SlashModuleInfo | |||
| { | |||
| public SlashModuleInfo(SlashCommandService service) | |||
| { | |||
| Service = service; | |||
| } | |||
| /// <summary> | |||
| /// Gets the command service associated with this module. | |||
| /// </summary> | |||
| public SlashCommandService Service { get; } | |||
| /// <summary> | |||
| /// Gets a read-only list of commands associated with this module. | |||
| /// </summary> | |||
| public List<SlashCommandInfo> Commands { get; private set; } | |||
| /// <summary> | |||
| /// The user command module defined as the interface ISlashCommandModule | |||
| /// Used to set context. | |||
| /// </summary> | |||
| public ISlashCommandModule userCommandModule; | |||
| public void SetCommands(List<SlashCommandInfo> commands) | |||
| { | |||
| if (this.Commands == null) | |||
| { | |||
| this.Commands = commands; | |||
| } | |||
| } | |||
| public void SetCommandModule(object userCommandModule) | |||
| { | |||
| if (this.userCommandModule == null) | |||
| { | |||
| this.userCommandModule = userCommandModule as ISlashCommandModule; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,19 +1,37 @@ | |||
| using Discord.Commands; | |||
| using Discord.Logging; | |||
| using Discord.WebSocket; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Reflection; | |||
| using System.Threading; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.SlashCommands | |||
| { | |||
| public class SlashCommandService | |||
| { | |||
| private List<SlashCommandModule> _modules; | |||
| // This semaphore is used to prevent race conditions. | |||
| private readonly SemaphoreSlim _moduleLock; | |||
| // This contains a dictionary of all definde SlashCommands, based on it's name | |||
| public Dictionary<string, SlashCommandInfo> commandDefs; | |||
| // This contains a list of all slash command modules defined by their user in their assembly. | |||
| public Dictionary<Type, SlashModuleInfo> moduleDefs; | |||
| // This is such a complicated method to log stuff... | |||
| public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||
| internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||
| internal Logger _logger; | |||
| internal LogManager _logManager; | |||
| public SlashCommandService() // TODO: possible config? | |||
| { | |||
| // max one thread | |||
| _moduleLock = new SemaphoreSlim(1, 1); | |||
| _logManager = new LogManager(LogSeverity.Info); | |||
| _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||
| _logger = new Logger(_logManager, "SlshCommand"); | |||
| } | |||
| public void AddAssembly() | |||
| @@ -21,10 +39,50 @@ namespace Discord.SlashCommands | |||
| } | |||
| public async Task<IResult> ExecuteAsync() | |||
| /// <summary> | |||
| /// Execute a slash command. | |||
| /// </summary> | |||
| /// <param name="interaction">Interaction data recieved from discord.</param> | |||
| /// <returns></returns> | |||
| public async Task<IResult> ExecuteAsync(SocketInteraction interaction) | |||
| { | |||
| // First, get the info about this command, if it exists | |||
| SlashCommandInfo commandInfo; | |||
| if (commandDefs.TryGetValue(interaction.Data.Name, out commandInfo)) | |||
| { | |||
| // TODO: implement everything that has to do with parameters :) | |||
| // Then, set the context in which the command will be executed | |||
| commandInfo.Module.userCommandModule.SetContext(interaction); | |||
| // Then run the command (with no parameters) | |||
| return await commandInfo.ExecuteAsync(new object[] { }).ConfigureAwait(false); | |||
| } | |||
| else | |||
| { | |||
| return SearchResult.FromError(CommandError.UnknownCommand, $"There is no registered slash command with the name {interaction.Data.Name}"); | |||
| } | |||
| } | |||
| public async Task AddModulesAsync(Assembly assembly, IServiceProvider services) | |||
| { | |||
| // TODO: handle execution | |||
| return null; | |||
| // First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | |||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
| try | |||
| { | |||
| // Get all of the modules that were properly defined by the user. | |||
| IReadOnlyList<TypeInfo> types = await SlashCommandServiceHelper.GetValidModuleClasses(assembly, this).ConfigureAwait(false); | |||
| // Then, based on that, make an instance out of each of them, and get the resulting SlashModuleInfo s | |||
| moduleDefs = await SlashCommandServiceHelper.InstantiateModules(types, this).ConfigureAwait(false); | |||
| // After that, internally register all of the commands into SlashCommandInfo | |||
| commandDefs = await SlashCommandServiceHelper.PrepareAsync(types,moduleDefs,this).ConfigureAwait(false); | |||
| // TODO: And finally, register the commands with discord. | |||
| await SlashCommandServiceHelper.RegisterCommands(commandDefs, this, services).ConfigureAwait(false); | |||
| } | |||
| finally | |||
| { | |||
| _moduleLock.Release(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,151 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Linq.Expressions; | |||
| using System.Reflection; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.SlashCommands | |||
| { | |||
| internal static class SlashCommandServiceHelper | |||
| { | |||
| /// <summary> | |||
| /// Get all of the valid user-defined slash command modules | |||
| /// </summary> | |||
| public static async Task<IReadOnlyList<TypeInfo>> GetValidModuleClasses(Assembly assembly, SlashCommandService service) | |||
| { | |||
| var result = new List<TypeInfo>(); | |||
| foreach (TypeInfo typeInfo in assembly.DefinedTypes) | |||
| { | |||
| if (IsValidModuleDefinition(typeInfo)) | |||
| { | |||
| // To simplify our lives, we need the modules to be public. | |||
| if (typeInfo.IsPublic || typeInfo.IsNestedPublic) | |||
| { | |||
| result.Add(typeInfo); | |||
| } | |||
| else | |||
| { | |||
| await service._logger.WarningAsync($"Found class {typeInfo.FullName} as a valid SlashCommand Module, but it's not public!"); | |||
| } | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| private static bool IsValidModuleDefinition(TypeInfo typeInfo) | |||
| { | |||
| // See if the base type (SlashCommandInfo<T>) implements interface ISlashCommandModule | |||
| return typeInfo.BaseType.GetInterfaces() | |||
| .Any(n => n == typeof(ISlashCommandModule)); | |||
| } | |||
| /// <summary> | |||
| /// Create an instance of each user-defined module | |||
| /// </summary> | |||
| public static async Task<Dictionary<Type, SlashModuleInfo>> InstantiateModules(IReadOnlyList<TypeInfo> types, SlashCommandService slashCommandService) | |||
| { | |||
| var result = new Dictionary<Type, SlashModuleInfo>(); | |||
| foreach (Type userModuleType in types) | |||
| { | |||
| SlashModuleInfo moduleInfo = new SlashModuleInfo(slashCommandService); | |||
| // If they want a constructor with different parameters, this is the place to add them. | |||
| object instance = userModuleType.GetConstructor(Type.EmptyTypes).Invoke(null); | |||
| moduleInfo.SetCommandModule(instance); | |||
| result.Add(userModuleType, moduleInfo); | |||
| } | |||
| return result; | |||
| } | |||
| /// <summary> | |||
| /// Prepare all of the commands and register them internally. | |||
| /// </summary> | |||
| public static async Task<Dictionary<string, SlashCommandInfo>> PrepareAsync(IReadOnlyList<TypeInfo> types, Dictionary<Type, SlashModuleInfo> moduleDefs, SlashCommandService slashCommandService) | |||
| { | |||
| var result = new Dictionary<string, SlashCommandInfo>(); | |||
| // fore each user-defined module | |||
| foreach (var userModule in types) | |||
| { | |||
| // Get its associated information | |||
| SlashModuleInfo moduleInfo; | |||
| if (moduleDefs.TryGetValue(userModule, out moduleInfo)) | |||
| { | |||
| // and get all of its method, and check if they are valid, and if so create a new SlashCommandInfo for them. | |||
| var commandMethods = userModule.GetMethods(); | |||
| List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | |||
| foreach (var commandMethod in commandMethods) | |||
| { | |||
| SlashCommand slashCommand; | |||
| if (IsValidSlashCommand(commandMethod, out slashCommand)) | |||
| { | |||
| Delegate delegateMethod = CreateDelegate(commandMethod, moduleInfo.userCommandModule); | |||
| SlashCommandInfo commandInfo = new SlashCommandInfo( | |||
| module: moduleInfo, | |||
| name: slashCommand.commandName, | |||
| description: slashCommand.description, | |||
| userMethod: delegateMethod | |||
| ); | |||
| result.Add(slashCommand.commandName, commandInfo); | |||
| commandInfos.Add(commandInfo); | |||
| } | |||
| } | |||
| moduleInfo.SetCommands(commandInfos); | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| private static bool IsValidSlashCommand(MethodInfo method, out SlashCommand slashCommand) | |||
| { | |||
| // Verify that we only have one [SlashCommand(...)] attribute | |||
| IEnumerable<Attribute> slashCommandAttributes = method.GetCustomAttributes(typeof(SlashCommand)); | |||
| if (slashCommandAttributes.Count() > 1) | |||
| { | |||
| throw new Exception("Too many SlashCommand attributes on a single method. It can only contain one!"); | |||
| } | |||
| // And at least one | |||
| if (slashCommandAttributes.Count() == 0) | |||
| { | |||
| slashCommand = null; | |||
| return false; | |||
| } | |||
| // And return the first (and only) attribute | |||
| slashCommand = slashCommandAttributes.First() as SlashCommand; | |||
| return true; | |||
| } | |||
| /// <summary> | |||
| /// Creae a delegate from methodInfo. Taken from | |||
| /// https://stackoverflow.com/a/40579063/8455128 | |||
| /// </summary> | |||
| public static Delegate CreateDelegate(MethodInfo methodInfo, object target) | |||
| { | |||
| Func<Type[], Type> getType; | |||
| var isAction = methodInfo.ReturnType.Equals((typeof(void))); | |||
| var types = methodInfo.GetParameters().Select(p => p.ParameterType); | |||
| if (isAction) | |||
| { | |||
| getType = Expression.GetActionType; | |||
| } | |||
| else | |||
| { | |||
| getType = Expression.GetFuncType; | |||
| types = types.Concat(new[] { methodInfo.ReturnType }); | |||
| } | |||
| if (methodInfo.IsStatic) | |||
| { | |||
| return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo); | |||
| } | |||
| return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); | |||
| } | |||
| public static async Task RegisterCommands(Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, IServiceProvider services) | |||
| { | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| using Discord.Commands.Builders; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.SlashCommands | |||
| { | |||
| public interface ISlashCommandModule | |||
| { | |||
| void SetContext(IDiscordInteraction interaction); | |||
| //void BeforeExecute(CommandInfo command); | |||
| //void AfterExecute(CommandInfo command); | |||
| //void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); | |||
| } | |||
| } | |||
| @@ -1,14 +1,21 @@ | |||
| using Discord.Commands.SlashCommands.Types; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Reflection; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Discord.SlashCommands | |||
| { | |||
| internal class SlashCommandModule | |||
| public class SlashCommandModule<T> : ISlashCommandModule where T : class, IDiscordInteraction | |||
| { | |||
| /// <summary> | |||
| /// The underlying interaction of the command. | |||
| /// </summary> | |||
| /// <seealso cref="T:Discord.IDiscordInteraction"/> | |||
| /// <seealso cref="T:Discord.WebSocket.SocketInteraction" /> | |||
| public T Interaction { get; private set; } | |||
| void ISlashCommandModule.SetContext(IDiscordInteraction interaction) | |||
| { | |||
| var newValue = interaction as T; | |||
| Interaction = newValue ?? throw new InvalidOperationException($"Invalid interaction type. Expected {typeof(T).Name}, got {interaction.GetType().Name}."); | |||
| } | |||
| } | |||
| } | |||