1 /** 2 Utilities for building user-controlled commands with the dcord bot interface 3 */ 4 5 module dcord.bot.command; 6 7 import std.regex, 8 std.array, 9 std.algorithm, 10 std..string; 11 12 import dcord.types, 13 dcord.gateway; 14 15 /// Commands (currently not working) are members of Plugin class or classes which inherit from it. See: examples/src/basic.d 16 /// Example usage: 17 /// 18 /// @Command("hello", "hello2") //The command may respond to multiple triggers. 19 /// @Enabled(true) //false turns the command off. It may be re-enabled elsewhere in code at run-time. 20 /// @Description("A command that says 'hello' in channel.") //A description of what the command does. 21 /// @Group("Leet") //The group this command can respond to. 22 /// @RegEx(true) //Defaults to true, but can disable RegEx handling for command triggers. 23 /// @CommandLevel(1) //The power level that the command requires ( normal = 1, mod = 50, admin = 100,) 24 /// void onHello(CommandEvent event) { 25 /// event.msg.reply("Hello, world!"); 26 /// } 27 //Easy aliases to use for UDAs 28 //A UDA that can be used to flag a function as a command handler in a Plugin. 29 alias Command = CommandConfig!TypeTriggers; 30 alias Enabled = CommandConfig!TypeEnabled; 31 alias Description = CommandConfig!TypeDescription; 32 alias Group = CommandConfig!TypeGroup; 33 alias RegEx = CommandConfig!TypeRegEx; 34 alias CommandLevel = CommandConfig!TypeLevel; 35 36 //Aliases for deprecated UDAs 37 alias CommandDescription = CommandConfig!TypeDescription; 38 alias CommandGroup = CommandConfig!TypeGroup; 39 alias CommandRegex = CommandConfig!TypeRegEx; 40 41 //Custom types to select which CommandConfig details we get. 42 //Note: The names *must* correspond to property names in CommandObject. 43 enum TypeEnabled = "enabled"; 44 enum TypeTriggers = "triggers"; 45 enum TypeDescription = "description"; 46 enum TypeRegEx = "useRegex"; 47 enum TypeGroup = "group"; 48 enum TypeLevel = "level"; 49 50 //Template overloads for implementations of each type of CommandConfig info 51 template CommandConfig(alias T) if(T==TypeTriggers){ 52 static struct CommandConfig { 53 this(string[] args...){ 54 triggers = args.dup; 55 } 56 string[] triggers; 57 } 58 } 59 template CommandConfig(alias T) if(T == TypeEnabled){ 60 static struct CommandConfig { bool enabled; } 61 } 62 template CommandConfig(alias T) if(T == TypeDescription){ 63 static struct CommandConfig { string description; } 64 } 65 template CommandConfig(alias T) if(T == TypeGroup){ 66 static struct CommandConfig { string group; } 67 } 68 template CommandConfig(alias T) if(T == TypeRegEx){ 69 static struct CommandConfig { bool regex; } 70 } 71 template CommandConfig(alias T) if(T == TypeLevel){ 72 static struct CommandConfig { int level; } 73 } 74 75 /** 76 Base set of levels plugins can use. 77 */ 78 enum Level : int { 79 NORMAL = 1, 80 MOD = 50, 81 ADMIN = 100, 82 } 83 84 /** 85 A delegate type which can be used in UDAs to adjust a CommandObject's settings 86 or behavior. 87 */ 88 alias CommandObjectUpdate = void delegate(CommandObject); 89 90 /// Sets a guild permission requirement. 91 CommandObjectUpdate CommandGuildPermission(Permission p) { 92 return (c) { 93 c.pre ~= (ce) { 94 return (ce.msg.guild && ce.msg.guild.can(ce.msg.author, p)); 95 }; 96 }; 97 } 98 99 /// Sets a channel permission requirement. 100 CommandObjectUpdate CommandChannelPermission(Permission p) { 101 return (c) { 102 c.pre ~= (ce) { 103 return (ce.msg.channel.can(ce.msg.author, p)); 104 }; 105 }; 106 } 107 108 /// A delegate type which represents a function used for handling commands. 109 alias CommandHandler = void delegate(CommandEvent); 110 111 /// A delegate type which represents a function used for filtering commands. 112 alias CommandHandlerWrapper = bool delegate(CommandEvent); 113 114 /** 115 A CommandObject represents the configuration/state for a single command. 116 */ 117 class CommandObject { 118 /// The description / help text for the command 119 string description; 120 121 /// The permissions level required for the command 122 int level; 123 124 /// Whether this command is enabled 125 bool enabled = true; 126 127 /// Function to run before main command handler. 128 CommandHandlerWrapper[] pre; 129 130 /// The function handler for this command 131 CommandHandler func; 132 133 private { 134 /// Triggers for this command 135 string[] triggers; 136 137 // Compiled matching regex 138 Regex!char rgx; 139 140 string group; 141 bool useRegex; 142 } 143 144 /// Takes an aliasSeq of arguments from getUDAs, which automatically expands for as many UDAs as are given 145 this(T...)(CommandHandler func, T t) { 146 this.func = func; //Assign the event handler. 147 148 foreach(arg; t){ 149 // This will be our run-time variable 150 //auto argType = split(typeid(arg).toString, '"')[1]; 151 152 //Use the string passed by the enum (e.g. TypeTriggers) to assign parameters at run-time 153 mixin("this." ~ split(typeof(arg).stringof, '"')[1] ~ " = " ~ "(arg.tupleof)[0];"); 154 } 155 156 this.rebuild(); 157 } 158 159 /// Sets this command's triggers 160 void setTriggers(string[] triggers) { 161 this.triggers = triggers; 162 this.rebuild(); 163 } 164 165 /// Adds a trigger for this command 166 void addTrigger(string trigger) { 167 this.triggers ~= trigger; 168 this.rebuild(); 169 } 170 171 /// Sets this command's group 172 void setGroup(string group) { 173 this.group = group; 174 this.rebuild(); 175 } 176 177 /// Sets whether this command uses regex matching 178 void setRegex(bool rgx) { 179 this.useRegex = rgx; 180 this.rebuild(); 181 } 182 183 /// Rebuilds the locally cached regex 184 private void rebuild() { 185 if (this.useRegex) { 186 this.rgx = regex(this.triggers.join("|")); 187 } else { 188 // Append space to grouping 189 group = (this.group != "" ? this.group ~ " " : ""); 190 this.rgx = regex(this.triggers.map!((x) => "^" ~ group ~ x).join("|") ~ "( (.*)$|$)", "s"); 191 } 192 } 193 194 /// Returns a Regex capture group matched against the command's regex. 195 Captures!string match(string msg) { 196 return msg.matchFirst(this.rgx); 197 } 198 199 /// Returns the command name (always the first trigger in the list). 200 @property string name() { 201 return this.triggers[0]; 202 } 203 204 /// Call a command event 205 void call(CommandEvent e) { 206 foreach (prefunc; this.pre) { 207 if (!prefunc(e)) return; 208 } 209 this.func(e); 210 } 211 } 212 213 /** 214 Special event encapsulating MessageCreates, containing specific Bot utilties 215 and functionality. 216 */ 217 class CommandEvent { 218 CommandObject cmd; 219 MessageCreate event; 220 Message msg; 221 222 /** The message contents */ 223 string contents; 224 225 /** Array of arguments */ 226 string[] args; 227 228 this(MessageCreate event) { 229 this.event = event; 230 this.msg = event.message; 231 } 232 233 /// Returns arguments as a single string 234 @property string cleanedContents() { 235 return this.args.join(" "); 236 } 237 238 deprecated("use CommandEvent.args.length check") 239 bool has(ushort index) { 240 return (index < this.args.length); 241 } 242 243 deprecated("use CommandEvent.args[]") 244 string arg(ushort index) { 245 return this.args[index]; 246 } 247 248 /// Returns mentions for this command, sans the bot 249 @property UserMap mentions() { 250 return this.msg.mentions.filter((k, v) => k != this.event.client.me.id); 251 } 252 } 253 254 /** 255 The Commandable template is a virtual implementation which handles the command 256 UDAs, storing them within a local "commands" mapping. 257 */ 258 mixin template Commandable() { 259 CommandObject[string] commands; 260 261 /// Load commands 262 void loadCommands(T)() { 263 import std.traits; 264 265 //Find the function associated with each Command 266 foreach (symbol; getSymbolsByUDA!(T, CommandConfig)) { 267 static if (isFunction!symbol) { 268 //Perform some sanity checks 269 static assert(getUDAs!(symbol, Command).length == 1, "Each function must have one @Command UDA."); 270 static assert(getUDAs!(symbol, Description).length <= 1, "Each function may have only one @Description UDA."); 271 static assert(getUDAs!(symbol, RegEx).length <= 1, "Each function may have only one @RegEx UDA."); 272 static assert(getUDAs!(symbol, Group).length <= 1, "Each function may have only one @Group UDA."); 273 static assert(getUDAs!(symbol, CommandLevel).length <= 1, "Each function may have only one @CommandLevel UDA."); 274 275 //Get the UDAs themselves for each Command 276 foreach(uda; getUDAs!(symbol, Command)){ 277 //Display the commands and associated methods at build time 278 pragma(msg, uda.stringof, "\t", __traits(identifier, symbol)); 279 280 //Cast the symbol to the child plugin type inheriting from Plugin 281 auto _symbol = mixin("&(cast(T)this)." ~ __traits(identifier, symbol)); 282 283 //Register the function for its command triggers 284 this.registerCommand(new CommandObject(_symbol, getUDAs!(symbol, CommandConfig))); 285 } 286 } 287 } 288 } 289 290 /// Register a command from a CommandObject 291 CommandObject registerCommand(CommandObject obj) { 292 this.commands[obj.name] = obj; 293 return obj; 294 } 295 }