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 }