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 }