1 /++
2     Module for the main [Kameloso] instance struct.
3 
4     Copyright: [JR](https://github.com/zorael)
5     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
6 
7     Authors:
8         [JR](https://github.com/zorael)
9  +/
10 module kameloso.kameloso;
11 
12 private:
13 
14 import std.typecons : Flag, No, Yes;
15 
16 public:
17 
18 
19 // Kameloso
20 /++
21     State needed for the kameloso bot, aggregated in a struct for easier passing
22     by reference.
23  +/
24 struct Kameloso
25 {
26 private:
27     import kameloso.common : OutgoingLine, logger;
28     import kameloso.constants : BufferSize;
29     import kameloso.net : Connection;
30     import kameloso.plugins.common.core : IRCPlugin;
31     import kameloso.pods : ConnectionSettings, CoreSettings, IRCBot;
32     import dialect.defs : IRCClient, IRCServer;
33     import dialect.parsing : IRCParser;
34     import lu.container : Buffer;
35     import std.algorithm.comparison : among;
36     import std.datetime.systime : SysTime;
37 
38     // Throttle
39     /++
40         Aggregate of values and state needed to throttle outgoing messages.
41      +/
42     static struct Throttle
43     {
44         // t0
45         /++
46             Origo of x-axis (last sent message).
47          +/
48         SysTime t0;
49 
50         // m
51         /++
52             y at t0 (ergo y at x = 0, weight at last sent message).
53          +/
54         double m = 0.0;
55 
56         // increment
57         /++
58             Increment to y on sent message.
59          +/
60         enum increment = 1.0;
61 
62         // this(this)
63         /++
64             Don't copy this, just keep one instance.
65          +/
66         @disable this(this);
67 
68         // reset
69         /++
70             Resets the throttle values in-place.
71          +/
72         void reset()
73         {
74             t0 = SysTime.init;
75             m = 0.0;
76         }
77     }
78 
79     // State
80     /++
81         Transient state bool flags, aggregated in a struct.
82      +/
83     static struct StateFlags
84     {
85         // wantReceiveTimeoutShortened
86         /++
87             Set when the Socket read timeout was requested to be shortened.
88          +/
89         bool wantReceiveTimeoutShortened;
90 
91         // wantLiveSummary
92         /++
93             When this is set, the main loop should print a connection summary upon
94             the next iteration.
95          +/
96         bool wantLiveSummary;
97 
98         // askedToReconnect
99         /++
100             Set when the server asked us to reconnect (by way of a
101             [dialect.defs.IRCEvent.Type.RECONNECT|RECONNECT] event).
102          +/
103         bool askedToReconnect;
104 
105         // quitMessageSent
106         /++
107             Set when we have sent a QUIT message to the server.
108          +/
109         bool quitMessageSent;
110 
111         // askedToReexec
112         /++
113             Set when the user explicitly asked to re-exec in the middle of a session.
114          +/
115         bool askedToReexec;
116 
117         version(TwitchSupport)
118         {
119             // sawWelcome
120             /++
121                 Set when an [dialect.defs.IRCEvent.Type.RPL_WELCOME|RPL_WELCOME]
122                 event was encountered.
123              +/
124             bool sawWelcome;
125         }
126     }
127 
128     // _connectionID
129     /++
130         Numeric ID of the current connection, to disambiguate between multiple
131         connections in one program run. Private value.
132      +/
133     uint _connectionID;
134 
135 public:
136     // ctor
137     /++
138         Constructor taking an [args] string array.
139      +/
140     this(string[] args)
141     {
142         this.args = args;
143     }
144 
145     // flags
146     /++
147         Transient state flags of this [Kameloso] instance.
148      +/
149     StateFlags flags;
150 
151     // args
152     /++
153         Command-line arguments passed to the program.
154      +/
155     string[] args;
156 
157     // conn
158     /++
159         The [kameloso.net.Connection|Connection] that houses and wraps the socket
160         we use to connect to, write to and read from the server.
161      +/
162     Connection conn;
163 
164     // plugins
165     /++
166         A runtime array of all plugins. We iterate these when we have finished
167         parsing an [dialect.defs.IRCEvent|IRCEvent], and call the relevant event
168         handlers of each.
169      +/
170     IRCPlugin[] plugins;
171 
172     // settings
173     /++
174         The root copy of the program-wide settings.
175      +/
176     CoreSettings settings;
177 
178     // connSettings
179     /++
180         Settings relating to the connection between the bot and the IRC server.
181      +/
182     ConnectionSettings connSettings;
183 
184     // previousWhoisTimestamps
185     /++
186         An associative array o fwhen a nickname was last issued a WHOIS query for,
187         UNIX timestamps by nickname key, for hysteresis and rate-limiting.
188      +/
189     long[string] previousWhoisTimestamps;
190 
191     // parser
192     /++
193         Parser instance.
194      +/
195     IRCParser parser;
196 
197     // bot
198     /++
199         IRC bot values and state.
200      +/
201     IRCBot bot;
202 
203     // throttle
204     /++
205         Values and state needed to throttle sending messages.
206      +/
207     Throttle throttle;
208 
209     // abort
210     /++
211         When this is set by signal handlers, the program should exit. Other
212         parts of the program will be monitoring it.
213      +/
214     bool* abort;
215 
216     // outbuffer
217     /++
218         Buffer of outgoing message strings.
219 
220         The buffer size is "how many string pointers", now how many bytes. So
221         we can comfortably keep it arbitrarily high.
222      +/
223     Buffer!(OutgoingLine, No.dynamic, BufferSize.outbuffer) outbuffer;
224 
225     // backgroundBuffer
226     /++
227         Buffer of outgoing background message strings.
228 
229         The buffer size is "how many string pointers", now how many bytes. So
230         we can comfortably keep it arbitrarily high.
231      +/
232     Buffer!(OutgoingLine, No.dynamic, BufferSize.outbuffer) backgroundBuffer;
233 
234     // priorityBuffer
235     /++
236         Buffer of outgoing priority message strings.
237 
238         The buffer size is "how many string pointers", now how many bytes. So
239         we can comfortably keep it arbitrarily high.
240      +/
241     Buffer!(OutgoingLine, No.dynamic, BufferSize.priorityBuffer) priorityBuffer;
242 
243     // immediateBuffer
244     /++
245         Buffer of outgoing message strings to be sent immediately.
246 
247         The buffer size is "how many string pointers", now how many bytes. So
248         we can comfortably keep it arbitrarily high.
249      +/
250     Buffer!(OutgoingLine, No.dynamic, BufferSize.priorityBuffer) immediateBuffer;
251 
252     version(TwitchSupport)
253     {
254         // fastbuffer
255         /++
256             Buffer of outgoing fast message strings, used on Twitch servers.
257 
258             The buffer size is "how many string pointers", now how many bytes. So
259             we can comfortably keep it arbitrarily high.
260          +/
261         Buffer!(OutgoingLine, No.dynamic, BufferSize.outbuffer) fastbuffer;
262     }
263 
264     // missingConfigurationEntries
265     /++
266         Associative array of string arrays of expected configuration entries
267         that were missing.
268      +/
269     string[][string] missingConfigurationEntries;
270 
271     // invalidConfigurationEntries
272     /++
273         Associative array of string arrays of unexpected configuration entries
274         that did not belong.
275      +/
276     string[][string] invalidConfigurationEntries;
277 
278     // customSettings
279     /++
280         Custom settings specfied at the command line with the `--set` parameter.
281      +/
282     string[] customSettings;
283 
284     version(Callgrind)
285     {
286         // callgrindRunning
287         /++
288             Flag to keep record of whether or not the program is run under the
289             Callgrind profiler.
290 
291             Assume it is until proven otherwise.
292          +/
293         bool callgrindRunning = true;
294     }
295 
296     // this(this)
297     /// Never copy this.
298     @disable this(this);
299 
300     // connectionID
301     /++
302         Numeric ID of the current connection, to disambiguate between multiple
303         connections in one program run. Accessor.
304 
305         Returns:
306             The numeric ID of the current connection.
307      +/
308     pragma(inline, true)
309     auto connectionID() const
310     {
311         return _connectionID;
312     }
313 
314     // generateNewConnectionID
315     /++
316         Generates a new connection ID.
317 
318         Don't include the number 0, or it may collide with the default value of `static uint`.
319      +/
320     void generateNewConnectionID() @safe
321     {
322         import std.random : uniform;
323 
324         synchronized //()
325         {
326             immutable previous = _connectionID;
327 
328             do
329             {
330                 _connectionID = uniform(1, uint.max);
331             }
332             while (_connectionID == previous);
333         }
334     }
335 
336     // throttleline
337     /++
338         Takes one or more lines from the passed buffer and sends them to the server.
339 
340         Sends to the server in a throttled fashion, based on a simple
341         `y = k*x + m` graph.
342 
343         This is so we don't get kicked by the server for spamming, if a lot of
344         lines are to be sent at once.
345 
346         Params:
347             Buffer = Buffer type, generally [lu.container.Buffer].
348             buffer = Buffer instance.
349             dryRun = Whether or not to send anything or just do a dry run,
350                 incrementing the graph by [Throttle.increment].
351             sendFaster = On Twitch, whether or not we should throttle less and
352                 send messages faster. Useful in some situations when rate-limiting
353                 is more lax.
354             immediate = Whether or not the line should just be sent straight away,
355                 ignoring throttling.
356 
357         Returns:
358             The time remaining until the next message may be sent, so that we
359             can reschedule the next server read timeout to happen earlier.
360      +/
361     auto throttleline(Buffer)
362         (ref Buffer buffer,
363         const Flag!"dryRun" dryRun = No.dryRun,
364         const Flag!"sendFaster" sendFaster = No.sendFaster,
365         const Flag!"immediate" immediate = No.immediate)
366     {
367         import std.datetime.systime : Clock;
368 
369         alias t = throttle;
370 
371         immutable now = Clock.currTime;
372         if (t.t0 == SysTime.init) t.t0 = now;
373 
374         double k = -connSettings.messageRate;
375         double burst = connSettings.messageBurst;
376 
377         version(TwitchSupport)
378         {
379             if (parser.server.daemon == IRCServer.Daemon.twitch)
380             {
381                 import kameloso.constants : ConnectionDefaultFloats;
382 
383                 if (sendFaster)
384                 {
385                     k = -ConnectionDefaultFloats.messageRateTwitchFast;
386                     burst = ConnectionDefaultFloats.messageBurstTwitchFast;
387                 }
388                 else
389                 {
390                     k = -ConnectionDefaultFloats.messageRateTwitchSlow;
391                     burst = ConnectionDefaultFloats.messageBurstTwitchSlow;
392                 }
393             }
394         }
395 
396         while (!buffer.empty || dryRun)
397         {
398             if (!immediate)
399             {
400                 double x = (now - t.t0).total!"msecs"/1000.0;
401                 double y = k * x + t.m;
402 
403                 if (y < 0.0)
404                 {
405                     t.t0 = now;
406                     x = 0.0;
407                     y = 0.0;
408                     t.m = 0.0;
409                 }
410 
411                 if (y >= burst)
412                 {
413                     x = (now - t.t0).total!"msecs"/1000.0;
414                     y = k*x + t.m;
415                     return y;
416                 }
417 
418                 t.m = y + t.increment;
419                 t.t0 = now;
420             }
421 
422             if (dryRun) break;
423 
424             if (!settings.headless && (settings.trace || !buffer.front.quiet))
425             {
426                 bool printed;
427 
428                 version(Colours)
429                 {
430                     if (!settings.monochrome)
431                     {
432                         import kameloso.irccolours : mapEffects;
433                         logger.trace("--> ", buffer.front.line.mapEffects);
434                         printed = true;
435                     }
436                 }
437 
438                 if (!printed)
439                 {
440                     import kameloso.irccolours : stripEffects;
441                     logger.trace("--> ", buffer.front.line.stripEffects);
442                 }
443             }
444 
445             conn.sendline(buffer.front.line);
446             buffer.popFront();
447         }
448 
449         return 0.0;
450     }
451 
452     // initPlugins
453     /++
454         Resets and *minimally* initialises all plugins.
455 
456         It only initialises them to the point where they're aware of their
457         settings, and not far enough to have loaded any resources.
458 
459         Throws:
460             [kameloso.plugins.common.misc.IRCPluginSettingsException|IRCPluginSettingsException]
461             on failure to apply custom settings.
462      +/
463     void initPlugins() @system
464     {
465         import kameloso.plugins.common.core : IRCPluginState;
466         import kameloso.plugins.common.misc : applyCustomSettings;
467         import std.concurrency : thisTid;
468         static import kameloso.plugins;
469 
470         teardownPlugins();
471 
472         auto state = IRCPluginState(this.connectionID);
473         state.client = parser.client;
474         state.server = parser.server;
475         state.bot = this.bot;
476         state.mainThread = thisTid;
477         state.settings = settings;
478         state.connSettings = connSettings;
479         state.abort = abort;
480 
481         // Leverage kameloso.plugins.instantiatePlugins to construct all plugins.
482         plugins = kameloso.plugins.instantiatePlugins(state);
483 
484         foreach (plugin; plugins)
485         {
486             import lu.meld : meldInto;
487 
488             string[][string] theseMissingEntries;
489             string[][string] theseInvalidEntries;
490 
491             plugin.deserialiseConfigFrom(
492                 settings.configFile,
493                 theseMissingEntries,
494                 theseInvalidEntries);
495 
496             if (theseMissingEntries.length)
497             {
498                 theseMissingEntries.meldInto(this.missingConfigurationEntries);
499             }
500 
501             if (theseInvalidEntries.length)
502             {
503                 theseInvalidEntries.meldInto(this.invalidConfigurationEntries);
504             }
505         }
506 
507         immutable allCustomSuccess = plugins.applyCustomSettings(this.customSettings, settings);
508 
509         if (!allCustomSuccess)
510         {
511             import kameloso.plugins.common.misc : IRCPluginSettingsException;
512             throw new IRCPluginSettingsException("Some custom plugin settings could not be applied.");
513         }
514     }
515 
516     // issuePluginCallImpl
517     /++
518         Issues a call to all plugins, where such a call is one of "setup",
519         "start", "initResources" or "reload". This invokes their module-level
520         functions of the same name, where available.
521 
522         In the case of "initResources", the call does not care whether the
523         plugins are enabled, but in all other cases they are skipped if so.
524 
525         Params:
526             call = String name of call to issue to all plugins.
527      +/
528     private void issuePluginCallImpl(string call)()
529     if (call.among!("setup", "start", "reload", "initResources"))
530     {
531         foreach (plugin; plugins)
532         {
533             static if (call == "initResources")
534             {
535                 // Always init resources, even if the plugin is disabled
536                 mixin("plugin." ~ call ~ "();");
537             }
538             else
539             {
540                 if (!plugin.isEnabled) continue;
541 
542                 mixin("plugin." ~ call ~ "();");
543                 checkPluginForUpdates(plugin);
544             }
545         }
546     }
547 
548     // setupPlugins
549     /++
550         Sets up all plugins, calling any module-level `setup` functions.
551      +/
552     alias setupPlugins = issuePluginCallImpl!"setup";
553 
554     // initPluginResources
555     /++
556         Initialises all plugins' resource files.
557 
558         This merely calls
559         [kameloso.plugins.common.core.IRCPlugin.initResources|IRCPlugin.initResources]
560         on each plugin.
561      +/
562     alias initPluginResources = issuePluginCallImpl!"initResources";
563 
564     // startPlugins
565     /++
566         Starts all plugins by calling any module-level `start` functions.
567 
568         This happens after connection has been established.
569 
570         Don't start disabled plugins.
571      +/
572     alias startPlugins = issuePluginCallImpl!"start";
573 
574     // reloadPlugins
575     /++
576         Reloads all plugins by calling any module-level `reload` functions.
577 
578         What this actually does is up to the plugins.
579      +/
580     alias reloadPlugins = issuePluginCallImpl!"reload";
581 
582     // teardownPlugins
583     /++
584         Tears down all plugins, deinitialising them and having them save their
585         settings for a clean shutdown. Calls module-level `teardown` functions.
586 
587         Think of it as a plugin destructor.
588 
589         Don't teardown disabled plugins as they may not have been initialised fully.
590      +/
591     void teardownPlugins() @system
592     {
593         if (!plugins.length) return;
594 
595         foreach (plugin; plugins)
596         {
597             import std.exception : ErrnoException;
598             import core.thread : Fiber;
599 
600             if (!plugin.isEnabled) continue;
601 
602             try
603             {
604                 plugin.teardown();
605 
606                 foreach (scheduledFiber; plugin.state.scheduledFibers)
607                 {
608                     // All Fibers should be at HOLD state but be conservative
609                     if (scheduledFiber.fiber.state != Fiber.State.EXEC)
610                     {
611                         destroy(scheduledFiber.fiber);
612                     }
613                 }
614 
615                 plugin.state.scheduledFibers = null;
616 
617                 foreach (scheduledDelegate; plugin.state.scheduledDelegates)
618                 {
619                     destroy(scheduledDelegate.dg);
620                 }
621 
622                 plugin.state.scheduledDelegates = null;
623 
624                 foreach (immutable type, ref fibersForType; plugin.state.awaitingFibers)
625                 {
626                     foreach (fiber; fibersForType)
627                     {
628                         // As above
629                         if (fiber.state != Fiber.State.EXEC)
630                         {
631                             destroy(fiber);
632                         }
633                     }
634                 }
635 
636                 plugin.state.awaitingFibers = null;
637 
638                 foreach (immutable type, ref dgsForType; plugin.state.awaitingDelegates)
639                 {
640                     foreach (ref dg; dgsForType)
641                     {
642                         destroy(dg);
643                     }
644                 }
645 
646                 plugin.state.awaitingDelegates = null;
647             }
648             catch (ErrnoException e)
649             {
650                 import std.file : exists;
651                 import std.path : dirName;
652                 import core.stdc.errno : ENOENT;
653 
654                 if ((e.errno == ENOENT) && !settings.resourceDirectory.dirName.exists)
655                 {
656                     // The resource directory hasn't been created, don't panic
657                 }
658                 else
659                 {
660                     enum pattern = "ErrnoException when tearing down <l>%s</>: <l>%s";
661                     logger.warningf(pattern, plugin.name, e.msg);
662                     version(PrintStacktraces) logger.trace(e.info);
663                 }
664             }
665             catch (Exception e)
666             {
667                 enum pattern = "Exception when tearing down <l>%s</>: <l>%s";
668                 logger.warningf(pattern, plugin.name, e.msg);
669                 version(PrintStacktraces) logger.trace(e);
670             }
671 
672             destroy(plugin);
673         }
674 
675         // Zero out old plugins array
676         plugins = null;
677     }
678 
679     // checkPluginForUpdates
680     /++
681         Propagates updated bots, clients, servers and/or settings, to `this`,
682         [parser], and to all plugins.
683 
684         Params:
685             plugin = The plugin whose
686                 [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]s
687                 member structs to inspect for updates.
688      +/
689     void checkPluginForUpdates(IRCPlugin plugin)
690     {
691         alias Update = typeof(plugin.state.updates);
692 
693         if (plugin.state.updates & Update.bot)
694         {
695             // Something changed the bot; propagate
696             plugin.state.updates &= ~Update.bot;
697             propagate(plugin.state.bot);
698         }
699 
700         if (plugin.state.updates & Update.client)
701         {
702             // Something changed the client; propagate
703             plugin.state.updates &= ~Update.client;
704             propagate(plugin.state.client);
705         }
706 
707         if (plugin.state.updates & Update.server)
708         {
709             // Something changed the server; propagate
710             plugin.state.updates &= ~Update.server;
711             propagate(plugin.state.server);
712         }
713 
714         if (plugin.state.updates & Update.settings)
715         {
716             // Something changed the settings; propagate
717             plugin.state.updates &= ~Update.settings;
718             propagate(plugin.state.settings);
719             this.settings = plugin.state.settings;
720 
721             // This shouldn't be necessary since kameloso.common.settings points to this.settings
722             //*kameloso.common.settings = plugin.state.settings;
723         }
724 
725         assert((plugin.state.updates == Update.nothing),
726             "`IRCPluginState.updates` was not reset after checking and propagation");
727     }
728 
729     // propagate
730     /++
731         Propgates an updated struct, to `this`, [parser], and to each plugins'
732         [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]s, overwriting
733         existing such.
734 
735         Params:
736             thing = Struct object to propagate.
737      +/
738     //pragma(inline, true)
739     void propagate(Thing)(Thing thing) pure nothrow @nogc
740     if (is(Thing == struct))
741     {
742         import std.meta : AliasSeq;
743 
744         foreach (ref sym; AliasSeq!(this, parser))
745         {
746             foreach (immutable i, ref member; sym.tupleof)
747             {
748                 alias T = typeof(sym.tupleof[i]);
749 
750                 static if (is(T == Thing))
751                 {
752                     sym.tupleof[i] = thing;
753                     break;
754                 }
755             }
756         }
757 
758         foreach (plugin; plugins)
759         {
760             foreach (immutable i, ref member; plugin.state.tupleof)
761             {
762                 alias T = typeof(plugin.state.tupleof[i]);
763 
764                 static if (is(T == Thing))
765                 {
766                     plugin.state.tupleof[i] = thing;
767                     break;
768                 }
769             }
770         }
771     }
772 
773     // ConnectionHistoryEntry
774     /++
775         A record of a successful connection.
776      +/
777     static struct ConnectionHistoryEntry
778     {
779         /// UNIX time when this connection was established.
780         long startTime;
781 
782         /// UNIX time when this connection was lost.
783         long stopTime;
784 
785         /// How many events fired during this connection.
786         long numEvents;
787 
788         /// How many bytses were read during this connection.
789         ulong bytesReceived;
790     }
791 
792     // connectionHistory
793     /++
794         History records of established connections this execution run.
795      +/
796     ConnectionHistoryEntry[] connectionHistory;
797 }