How to create a structure like Discord.NET

I want to write an extension to a library ive started some time ago and was wondering how i could build up a similar system to Discord.NET here is an example of what i mean You have a public class that inherits InteractionModuleBase and from that moment on you can create things like
[SlashCommand("some-command", "command description")]
public async Task myCommand(string arg1, string arg2) {}
[SlashCommand("some-command", "command description")]
public async Task myCommand(string arg1, string arg2) {}
And within that function you can access a Context object which contains some data about the Interaction context i was wondering how i could create / achieve such a thing and what the steps for that are
22 Replies
Angius
Angius2mo ago
Attributes are either used with reflections or source generators What Discord.NET probably does, is it looks up any classes that inherit from InteractionModuleBase, then looks up all methods with [SlashCommand] attribute, and registers them... somewhere
Somgör from Human Resources
yes it does that you have to call something like RegisterModulesAsync and then pass in the entry assembly
Pobiega
Pobiega2mo ago
Context is likely a property on InteractionModuleBase and thats how you get access to it
Somgör from Human Resources
like Assembly.GetEntryAssembly or whatever it was and then you need to create an event handler that executes the interaction with the corresponding function / attribute yes Context comes from InteractionModuleBase i love the concept and id love to know more about it cause it seems so convinient when applied properly
Angius
Angius2mo ago
In pseudocode, it would be something like
var bot = DiscordBot();

var classes = GetAllClassesInheritingFrom(InteractionModuleBase);
foreach (var c in classes)
{
var methods = c.GetAllMethodsWithAttribute(SlashCommand);
foreach (var m in methods)
{
bot.AddCommand(m.Attribute.CommandName, m.Attribute.Description, m);
}
}
var bot = DiscordBot();

var classes = GetAllClassesInheritingFrom(InteractionModuleBase);
foreach (var c in classes)
{
var methods = c.GetAllMethodsWithAttribute(SlashCommand);
foreach (var m in methods)
{
bot.AddCommand(m.Attribute.CommandName, m.Attribute.Description, m);
}
}
Somgör from Human Resources
and when i have a list of functions with attributes i could check for example if the given command is "some-command" and then call the function with the given parameters?
Angius
Angius2mo ago
Yep
Somgör from Human Resources
and i assume if the class or function is anything but public it wont get picked up as a candidate for that list?
Angius
Angius2mo ago
With reflections/sourcegen it's not a limitation You can exclude non-public classes and methods, sure But you don't have to
Somgör from Human Resources
Discord.NET uses reflection i assume since the parameter to pass in the entry assemlbly
Angius
Angius2mo ago
Probably reflections, yes
Somgör from Human Resources
so my general workflow should be to get all classes that inherit my target class and then get all functions from that class that uses a specific attribute and such?
Angius
Angius2mo ago
It's easier, albeit less performant than source generators Yep
Somgör from Human Resources
interesting ill get back to it later if i have followup questions thanks!
Angius
Angius2mo ago
Anytime :Ok:
Somgör from Human Resources
performance isnt really a priority for that project tbh where would i keep the "database" of my functions and how would i structure them do i keep them inside of the BaseClient or the InteractionBase or perhaps a static Utils class with Lists of Objects descripting the functions and Queue
Angius
Angius2mo ago
You mean storing the methods you got via reflections?
Somgör from Human Resources
yes so i can later call them
Angius
Angius2mo ago
Anywhere and anyhow that suits you, really
Somgör from Human Resources
oki so its totally fine if i just put them into an object with additional info like parameters and the attributes they carry and then just store them in a static List or dictionary
Angius
Angius2mo ago
Sure
Somgör from Human Resources
I've decided to store both queue and modules inside of the base client as it will be a thing that should stay existent over the entire runtime duration This should also allow for a multi client structure if someone really wants to Ive gotten pretty far into the process This is a structure ive come up with
public class Commands : InteractionBase
{

[Command("ping", "Simple Ping Command")]
[EnabledInDms(false)]
[PermissionGroup(PermissionGroup.PermissionGroups.Agent)]
public async Task HandleCommand(string arg1, string[] arg2 = null)
{
await Console.Out.WriteLineAsync("Pluh");
}
}
public class Commands : InteractionBase
{

[Command("ping", "Simple Ping Command")]
[EnabledInDms(false)]
[PermissionGroup(PermissionGroup.PermissionGroups.Agent)]
public async Task HandleCommand(string arg1, string[] arg2 = null)
{
await Console.Out.WriteLineAsync("Pluh");
}
}
And its working pretty well, i can now register my modules via Assembly or direct class reference in a <T> type, this is how my InteractionModule object looks like
public class InteractionModule
{
public string ModuleCommandName { get; set; }
public string ModuleCommandDescription { get; set; }
public int? ModuleCommandCommunity { get; set; }

public bool ModuleCommandEnabledInDms { get; set; } = true;
public PermissionGroup.PermissionGroups ModulePermissionGroup { get; set; } = PermissionGroup.PermissionGroups.All;

public List<(string, bool)> ModuleCommandParameters { get; set; } = new List<(string, bool)>();

public delegate Task InteractionMethodDelegate(params object[] args);

public InteractionMethodDelegate ModuleInteractionMethod { get; set; }

}
public class InteractionModule
{
public string ModuleCommandName { get; set; }
public string ModuleCommandDescription { get; set; }
public int? ModuleCommandCommunity { get; set; }

public bool ModuleCommandEnabledInDms { get; set; } = true;
public PermissionGroup.PermissionGroups ModulePermissionGroup { get; set; } = PermissionGroup.PermissionGroups.All;

public List<(string, bool)> ModuleCommandParameters { get; set; } = new List<(string, bool)>();

public delegate Task InteractionMethodDelegate(params object[] args);

public InteractionMethodDelegate ModuleInteractionMethod { get; set; }

}
but now im wondering how i can achieve the ability to call my ModuleInteractionMethod using both optional parameters and only required ones for example my function will call if i do
module.ModuleInteractionMethod.Invoke("", new[] {""});
module.ModuleInteractionMethod.Invoke("", new[] {""});
but will not be called if i do
module.ModuleInteractionMethod.Invoke(""); // Since arg2 is optional this should be no issue
module.ModuleInteractionMethod.Invoke(""); // Since arg2 is optional this should be no issue
This is how my function is getting set
module.ModuleInteractionMethod = async (args) => await Task.FromResult(method.Invoke(Activator.CreateInstance(moduleType), args));
// moduleType is T typeof(InteractionBase) OR the current class its going over
module.ModuleInteractionMethod = async (args) => await Task.FromResult(method.Invoke(Activator.CreateInstance(moduleType), args));
// moduleType is T typeof(InteractionBase) OR the current class its going over
for reference on how im registering my modules
myInteractionsClient.RegisterModule<Commands>();
// OR
myInteractionsClient.RegisterModules(Assembly.GetEntryAssemlbly());
myInteractionsClient.RegisterModule<Commands>();
// OR
myInteractionsClient.RegisterModules(Assembly.GetEntryAssemlbly());
ive fixed that issue, however, now i have an issue with my Context / InteractionBase class How can i make sure it sets the proper values on every call it makes, my approach would be to also save that class / instance inside of my module object so i can modify the Context object to my needs but now my InteractionBase is always null this is how i retrieve it: module.ModuleInteractionBase = (InteractionBase)Activator.CreateInstance(moduleType); update: the InteractionBase values seem to get set as indicated by this JSON:
{"Context":{"InteractionChatId":null,"InteractionName":"test","BaseModule":null,"Message":null,"InteractionTimestamp":0,"InteractionId":null}}
{"Context":{"InteractionChatId":null,"InteractionName":"test","BaseModule":null,"Message":null,"InteractionTimestamp":0,"InteractionId":null}}
Which makes me believe that the InteractionBase my class inherits is a different one from the one im setting