@@ -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}."); | |||
} | |||
} | |||
} |