1 /// Provides collectors, like MessageCollector. 2 module dcord.types.collectors; 3 4 import dcord.bot.plugin, dcord.types, dcord.core, std.typecons, std.variant, std.stdio, std.conv; 5 import vibe.core.core; 6 7 /// MessageCollector is a feature-complete message collector with timeouts, callbacks, and filters, inspired by Discord.js. It can be used for listening to user input in seperate messages 8 class MessageCollector: Plugin { 9 /// The channel the MessageCollector is for. Only listens to messages in this channel 10 Channel chan; 11 /// The callback to execute upon each message, as a delegate 12 void delegate(Message) callback; 13 /// Whether the MessageCollector is finished or not, used to track state internally 14 bool done; 15 /// The maximum amount of messages to listen to 16 int cap; 17 /// The number of messages listened to so far, used to track state internally 18 long messagesSoFar; 19 /// An array of messages which is appended to every time a message is recieved, provided to the ending callback when done 20 Message[] messages; 21 /// The time the collector should run for, represented as a Duration object. The collector ends, and calls the ending callback, when this time is up 22 Nullable!(Duration) timeout; 23 /// The time the collector should wait after a message. The collector ends, and calls the ending callback, when this time is up 24 Nullable!(Duration) idleTimeout; 25 /// A Vibe.d timer coresponding to the timeout variable, used to track timeout internally 26 Nullable!(Timer) timeoutTimer; 27 /// A Vibe.d Timer coresponding to the idleTimeout variable, used to track idle timeout internally - resetted after each message recieved 28 Nullable!(Timer) idleTimeoutTimer; 29 /// An optional (delegate) filter to check against; should return a bool, and take a Message; can be passed to the constructor. Example: `m => m.author.id == 1234567890` 30 Nullable!(bool delegate(Message)) filter; 31 /// The delegate to run when end() is called, passed to the Vibe.d timer upon creation; is @safe, but the delegate passed to onEnd does not need to be @safe, as it constructs a @trusted one from whatever's passed to it 32 Nullable!(void delegate(Message[]) @safe) endCallback; 33 34 /** 35 Listen for message create events, internally. This is where the bulk of the logic is. Shouldn't be overloaded or overwritten 36 Params: 37 event = a MessageCreate event 38 */ 39 @Listener!(MessageCreate, EmitterOrder.UNSPECIFIED) 40 void onMessageCreate(MessageCreate event) { 41 if(!this.done) { // only execute if the collector is not finished 42 if(event.message.channel.id == this.chan.id) { // make sure that it's in the same channel 43 if(event.message.author.id != event.message.client.state.me.id) { // bot responding to it's own message is a disaster 44 if(!this.filter.isNull()) 45 if(!filter.get()(event.message)) return; // we can return early if it doesn't match the filter, if one exists. 46 47 this.messages ~= event.message; 48 if(this.cap != 0) { 49 if(this.messagesSoFar > this.cap) { 50 end(this.messages); 51 } else { 52 this.messagesSoFar++; 53 } 54 if(this.messagesSoFar <= this.cap) { 55 this.callback(event.message); 56 if(!this.idleTimeoutTimer.isNull()) { // we only need to do this if the idle timeout timer exists (aka the idle timeout option was passed. 57 this.idleTimeoutTimer.get.stop(); // stop and reset the stopwatch 58 this.idleTimeoutTimer.get.rearm(this.idleTimeout.get); // start the stopwatch 59 } 60 } 61 } else if(this.cap == 0) { // no cap 62 this.callback(event.message); 63 } else { 64 throw new Error("The world is broken... your cap is neither equal to zero nor not equal to zero. This is probably a bug, please report it.."); 65 } 66 } 67 } 68 } 69 } 70 71 /** 72 Class constructor. 73 Params: 74 chan = a Channel object to listen for messages in 75 filter = a filter delegate that accepts a message and returns a bool; defaults to null. Example: `m => m.author.id == 1234567890` 76 opts = an associative array of options, with values being wrapped in the `Opts` type and keys being strings; defaults to null. Available options are: `cap`, `timeout`, and `idleTimeout` 77 */ 78 this(Channel chan, bool delegate(Message) filter=null, Opts[string] opts=null) { 79 // TODO: possible migration to `sumtype` 80 this.chan = chan; 81 if("cap" in opts) this.cap = *(opts["cap"].peek!int); 82 else this.cap = 0; 83 84 if("timeout" in opts) { 85 this.timeout = *(opts["timeout"].peek!Duration); 86 this.timeoutTimer = createTimer(delegate() @safe { 87 try { 88 this.end(this.messages); 89 } catch(Exception t) { // stfu 90 try this.trustedError(t); catch(Exception e) {} 91 } 92 }); // create the timer 93 this.timeoutTimer.get.rearm(this.timeout.get); // arm the timer 94 } 95 96 if("idleTimeout" in opts) { 97 this.idleTimeout = *(opts["idleTimeout"].peek!Duration); 98 this.idleTimeoutTimer = createTimer(delegate() @safe { 99 try { 100 this.end(this.messages); 101 } catch(Exception t) { // stfu 102 try this.trustedError(t); catch(Exception e) {} 103 } 104 }); // create the timer 105 this.idleTimeoutTimer.get.rearm(this.idleTimeout.get); // arm the timer 106 } 107 this.filter = filter; 108 } 109 110 /** 111 Class constructor. 112 Params: 113 chan = a Channel object to listen for messages in 114 filter = a filter delegate that accepts a message and returns a bool; defaults to null. Example: `m => m.author.id == 1234567890` 115 */ 116 this(Channel chan, bool delegate(Message) filter=null) { 117 this.chan = chan; 118 this.callback = callback; 119 this.cap = 0; // == no cap 120 this.filter = filter; 121 } 122 123 /** 124 Set the callback to be run upon each message 125 Params: 126 callback = a void delegate(Message), assigned to this.callback 127 */ 128 void onMessage(void delegate(Message) callback) { 129 this.callback = callback; 130 } 131 132 /** 133 Set the callback to be run at the end, when this.end() is called 134 Params: 135 callback = a void delegate(Message[]), assigned to this.callback; automatically wrapped in a @trusted delegate to stop Vibe.d from complaining that the callback isn't @safe 136 */ 137 void onEnd(void delegate(Message[]) callback) { 138 this.endCallback = delegate(Message[] m) @trusted { 139 callback(m); 140 }; 141 } 142 143 /** 144 End the callback. 145 Params: 146 messages = an array of Message objects for the ending callback; these are stored in this.messages 147 */ 148 void end(Message[] messages) @trusted { 149 this.done = true; 150 if(!this.endCallback.isNull()) this.endCallback.get()(messages); 151 } 152 153 /// Simulate throwing an error by writing to stderr, but as a @trusted function that can be called within @safe functions. Used in ending callback; Vibe.d expects this to be a `nothrow @safe` delegate, internally 154 private void trustedError(T...)(T args) @trusted { 155 stderr.writeln(args); 156 } 157 }