1 /++
2     Contains the definition of an [IRCPlugin] and its ancilliaries, as well as
3     mixins to fully implement it.
4 
5     Event handlers can then be module-level functions, annotated with
6     [dialect.defs.IRCEvent.Type|IRCEvent.Type]s.
7 
8     Example:
9     ---
10     import kameloso.plugins.common.core;
11     import kameloso.plugins.common.awareness;
12 
13     @(IRCEventHandler()
14         .onEvent(IRCEvent.Type.CHAN)
15         .permissionsRequired(Permissions.anyone)
16         .channelPolicy(ChannelPolicy.home)
17         .addCommand(
18             IRCEventHandler.Command()
19                 .word("foo")
20                 .policy(PrefixPolicy.prefixed)
21         )
22     )
23     void onFoo(FooPlugin plugin, const ref IRCEvent event)
24     {
25         // ...
26     }
27 
28     mixin UserAwareness;
29     mixin ChannelAwareness;
30     mixin PluginRegistration!FooPlugin;
31 
32     final class FooPlugin : IRCPlugin
33     {
34         // ...
35 
36         mixin IRCPluginImpl;
37     }
38     ---
39 
40     See_Also:
41         [kameloso.plugins.common.misc],
42         [kameloso.plugins.common.awareness],
43         [kameloso.plugins.common.delayawait],
44         [kameloso.plugins.common.mixins],
45 
46     Copyright: [JR](https://github.com/zorael)
47     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
48 
49     Authors:
50         [JR](https://github.com/zorael)
51  +/
52 module kameloso.plugins.common.core;
53 
54 private:
55 
56 import kameloso.thread : CarryingFiber;
57 import dialect.defs;
58 import std.traits : ParameterStorageClass;
59 import std.typecons : Flag, No, Yes;
60 import core.thread : Fiber;
61 
62 public:
63 
64 
65 // IRCPlugin
66 /++
67     Abstract IRC plugin class.
68 
69     This is currently shared with all `service`-class "plugins".
70 
71     See_Also:
72         [IRCPluginImpl]
73         [IRCPluginState]
74  +/
75 abstract class IRCPlugin
76 {
77 @safe:
78 
79 private:
80     import kameloso.thread : Sendable;
81     import std.array : Appender;
82 
83 public:
84     // CommandMetadata
85     /++
86         Metadata about a [IRCEventHandler.Command]- and/or
87         [IRCEventHandler.Regex]-annotated event handler.
88 
89         See_Also:
90             [IRCPlugin.commands]
91      +/
92     static struct CommandMetadata
93     {
94         // policy
95         /++
96             Prefix policy of this command.
97          +/
98         PrefixPolicy policy;
99 
100         // description
101         /++
102             Description about what the command does, in natural language.
103          +/
104         string description;
105 
106         // syntaxes
107         /++
108             Syntaxes on how to use the command.
109          +/
110         string[] syntaxes;
111 
112         // hidden
113         /++
114             Whether or not the command should be hidden from view (but still
115             possible to trigger).
116          +/
117         bool hidden;
118 
119         // isRegex
120         /++
121             Whether or not the command is based on an `IRCEventHandler.Regex`.
122          +/
123         bool isRegex;
124 
125         // this
126         /++
127             Constructor taking an [IRCEventHandler.Command].
128 
129             Do not touch [syntaxes]; populate them at the call site.
130          +/
131         this(const IRCEventHandler.Command command) pure @safe nothrow @nogc
132         {
133             this.policy = command.policy;
134             this.description = command.description;
135             this.hidden = command.hidden;
136             //this.isRegex = false;
137         }
138 
139         // this
140         /++
141             Constructor taking an [IRCEventHandler.Regex].
142 
143             Do not touch [syntaxes]; populate them at the call site.
144          +/
145         this(const IRCEventHandler.Regex regex) pure @safe nothrow @nogc
146         {
147             this.policy = regex.policy;
148             this.description = regex.description;
149             this.hidden = regex.hidden;
150             this.isRegex = true;
151         }
152     }
153 
154     // state
155     /++
156         An [IRCPluginState] instance containing variables and arrays that represent
157         the current state of the plugin. Should generally be passed by reference.
158      +/
159     IRCPluginState state;
160 
161     // postprocess
162     /++
163         Allows a plugin to modify an event post-parsing.
164      +/
165     void postprocess(ref IRCEvent event) @system;
166 
167     // onEvent
168     /++
169         Called to let the plugin react to a new event, parsed from the server.
170      +/
171     void onEvent(const ref IRCEvent event) @system;
172 
173     // initResources
174     /++
175         Called when the plugin is requested to initialise its disk resources.
176      +/
177     void initResources() @system;
178 
179     // deserialiseConfigFrom
180     /++
181         Reads serialised configuration text into the plugin's settings struct.
182 
183         Stores an associative array of `string[]`s of missing entries in its
184         first `out string[][string]` parameter, and the invalid encountered
185         entries in the second.
186      +/
187     void deserialiseConfigFrom(
188         const string configFile,
189         out string[][string] missingEntries,
190         out string[][string] invalidEntries);
191 
192     // serialiseConfigInto
193     /++
194         Called to let the plugin contribute settings when writing the configuration file.
195 
196         Returns:
197             Boolean of whether something was added.
198      +/
199     bool serialiseConfigInto(ref Appender!(char[]) sink) const;
200 
201     // setSettingByName
202     /++
203         Called when we want to change a setting by its string name.
204 
205         Returns:
206             Boolean of whether the set succeeded or not.
207      +/
208     bool setSettingByName(const string setting, const string value);
209 
210     // setup
211     /++
212         Called at program start but before connection has been established.
213      +/
214     void setup() @system;
215 
216     // start
217     /++
218         Called when connection has been established.
219      +/
220     void start() @system;
221 
222     // printSettings
223     /++
224         Called when we want a plugin to print its [Settings]-annotated struct of settings.
225      +/
226     void printSettings() @system const;
227 
228     // teardown
229     /++
230         Called during shutdown of a connection; a plugin's would-be destructor.
231      +/
232     void teardown() @system;
233 
234     // name
235     /++
236         Returns the name of the plugin.
237 
238         Returns:
239             The string name of the plugin.
240      +/
241     string name() @property const pure nothrow @nogc;
242 
243     // commands
244     /++
245         Returns an array of the descriptions of the commands a plugin offers.
246 
247         Returns:
248             An associative [IRCPlugin.CommandMetadata] array keyed by string.
249      +/
250     CommandMetadata[string] commands() pure nothrow @property const;
251 
252     // channelSpecificCommands
253     /++
254         Returns an array of the descriptions of the channel-specific commands a
255         plugin offers.
256 
257         Returns:
258             An associative [IRCPlugin.CommandMetadata] array keyed by string.
259      +/
260     CommandMetadata[string] channelSpecificCommands(const string) @system;
261 
262     // reload
263     /++
264         Reloads the plugin, where such is applicable.
265 
266         Whatever this does is implementation-defined.
267      +/
268     void reload() @system;
269 
270     // onBusMessage
271     /++
272         Called when a bus message arrives from another plugin.
273 
274         It is passed to all plugins and it is up to the receiving plugin to
275         discard those not meant for it by examining the value of the `header` argument.
276      +/
277     void onBusMessage(const string header, shared Sendable content) @system;
278 
279     // isEnabled
280     /++
281         Returns whether or not the plugin is enabled in its settings.
282 
283         Returns:
284             `true` if the plugin should listen to events, `false` if not.
285      +/
286     bool isEnabled() const @property pure nothrow @nogc;
287 }
288 
289 
290 // IRCPluginImpl
291 /++
292     Mixin that fully implements an [kameloso.plugins.common.core.IRCPlugin|IRCPlugin].
293 
294     Uses compile-time introspection to call module-level functions to extend behaviour.
295 
296     With UFCS, transparently emulates all such as being member methods of the
297     mixing-in class.
298 
299     Example:
300     ---
301     final class MyPlugin : IRCPlugin
302     {
303         MyPluginSettings myPluginSettings;  // type should be annotated @Settings at declaration
304 
305         // ...implementation...
306 
307         mixin IRCPluginImpl;
308     }
309     ---
310 
311     Params:
312         debug_ = Enables some debug output.
313         module_ = Name of the current module. Should never be specified and always
314             be left to its `__MODULE__` default value. Here be dragons.
315 
316     See_Also:
317         [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]
318  +/
319 mixin template IRCPluginImpl(
320     Flag!"debug_" debug_ = No.debug_,
321     string module_ = __MODULE__)
322 {
323     private import kameloso.plugins.common.core : FilterResult, IRCEventHandler, IRCPluginState, Permissions;
324     private import dialect.defs : IRCEvent, IRCServer, IRCUser;
325     private import lu.traits : getSymbolsByUDA;
326     private import std.array : Appender;
327     private import std.meta : AliasSeq;
328     private import std.traits : getUDAs;
329     private import core.thread : Fiber;
330 
331     static if (__traits(compiles, { alias _ = this.hasIRCPluginImpl; }))
332     {
333         import std.format : format;
334 
335         enum pattern = "Double mixin of `%s` in `%s`";
336         enum message = pattern.format("IRCPluginImpl", typeof(this).stringof);
337         static assert(0, message);
338     }
339     else
340     {
341         /++
342             Marker declaring that [kameloso.plugins.common.core.IRCPluginImpl|IRCPluginImpl]
343             has been mixed in.
344          +/
345         private enum hasIRCPluginImpl = true;
346     }
347 
348     mixin("private static import thisModule = ", module_, ";");
349 
350     // Introspection
351     /++
352         Namespace for the alias sequences of all event handler functions in this
353         module, as well as the one of all [kameloso.plugins.common.core.IRCEventHandler|IRCEventHandler]
354         annotations in the module.
355      +/
356     static struct Introspection
357     {
358         /++
359             Alias sequence of all top-level symbols annotated with
360             [kameloso.plugins.common.core.IRCEventHandler|IRCEventHandler]s
361             in this module.
362          +/
363         alias allEventHandlerFunctionsInModule = getSymbolsByUDA!(thisModule, IRCEventHandler);
364 
365         /++
366             Alias sequence of all
367             [kameloso.plugins.common.core.IRCEventHandler|IRCEventHandler]s
368             that are annotations of the symbols in [allEventHandlerFunctionsInModule].
369          +/
370         static immutable allEventHandlerUDAsInModule = ()
371         {
372             IRCEventHandler[] udas;
373             udas.length = allEventHandlerFunctionsInModule.length;
374 
375             foreach (immutable i, fun; allEventHandlerFunctionsInModule)
376             {
377                 enum fqn = module_ ~ '.'  ~ __traits(identifier, allEventHandlerFunctionsInModule[i]);
378                 udas[i] = getUDAs!(fun, IRCEventHandler)[0];
379                 udas[i].fqn = fqn;
380                 debug udaSanityCheckCTFE(udas[i]);
381                 udas[i].generateTypemap();
382             }
383 
384             return udas;
385         }();
386     }
387 
388     @safe:
389 
390     // isEnabled
391     /++
392         Introspects the current plugin, looking for a
393         [kameloso.plugins.common.core.Settings|Settings]-annotated struct
394         member that has a bool annotated with [kameloso.plugins.common.core.Enabler|Enabler],
395         which denotes it as the bool that toggles a plugin on and off.
396 
397         It then returns its value.
398 
399         Returns:
400             `true` if the plugin is deemed enabled (or cannot be disabled),
401             `false` if not.
402      +/
403     override public bool isEnabled() const @property pure nothrow @nogc
404     {
405         import kameloso.traits : udaIndexOf;
406 
407         bool retval = true;
408 
409         top:
410         foreach (immutable i, _; this.tupleof)
411         {
412             static if (is(typeof(this.tupleof[i]) == struct))
413             {
414                 enum typeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
415                 enum valueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
416 
417                 static if ((typeUDAIndex != -1) || (valueUDAIndex != -1))
418                 {
419                     foreach (immutable n, _2; this.tupleof[i].tupleof)
420                     {
421                         enum enablerUDAIndex = udaIndexOf!(this.tupleof[i].tupleof[n], Enabler);
422 
423                         static if (enablerUDAIndex != -1)
424                         {
425                             alias ThisEnabler = typeof(this.tupleof[i].tupleof[n]);
426 
427                             static if (!is(ThisEnabler : bool))
428                             {
429                                 import std.format : format;
430                                 import std.traits : Unqual;
431 
432                                 alias UnqualThis = Unqual!(typeof(this));
433                                 enum pattern = "`%s` has a non-bool `Enabler`: `%s %s`";
434                                 enum message = pattern.format(
435                                     UnqualThis.stringof,
436                                     ThisEnabler.stringof,
437                                     __traits(identifier, this.tupleof[i].tupleof[n]));
438                                 static assert(0, message);
439                             }
440 
441                             retval = this.tupleof[i].tupleof[n];
442                             break top;
443                         }
444                     }
445                 }
446             }
447         }
448 
449         return retval;
450     }
451 
452     // allow
453     /++
454         Judges whether an event may be triggered, based on the event itself and
455         the annotated required [kameloso.plugins.common.core.Permissions|Permissions] of the
456         handler in question. Wrapper function that merely calls
457         [kameloso.plugins.common.core.allowImpl].
458         The point behind it is to make something that can be overridden and still
459         allow it to call the original logic (below).
460 
461         Params:
462             event = [dialect.defs.IRCEvent|IRCEvent] to allow, or not.
463             permissionsRequired = Required [kameloso.plugins.common.core.Permissions|Permissions]
464                 of the handler in question.
465 
466         Returns:
467             `true` if the event should be allowed to trigger, `false` if not.
468      +/
469     pragma(inline, true)
470     private FilterResult allow(const ref IRCEvent event, const Permissions permissionsRequired)
471     {
472         import kameloso.plugins.common.core : allowImpl;
473         return allowImpl(this, event, permissionsRequired);
474     }
475 
476     // onEvent
477     /++
478         Forwards the supplied [dialect.defs.IRCEvent|IRCEvent] to
479         [kameloso.plugins.common.core.IRCPluginImpl.onEventImpl|IRCPluginImpl.onEventImpl].
480 
481         This is made a separate function to allow plugins to override it and
482         insert their own code, while still leveraging
483         [kameloso.plugins.common.core.IRCPluginImpl.onEventImpl|IRCPluginImpl.onEventImpl]
484         for the actual dirty work.
485 
486         Params:
487             event = Parsed [dialect.defs.IRCEvent|IRCEvent] to pass onto
488                 [kameloso.plugins.common.core.IRCPluginImpl.onEventImpl|IRCPluginImpl.onEventImpl].
489 
490         See_Also:
491             [kameloso.plugins.common.core.IRCPluginImpl.onEventImpl|IRCPluginImpl.onEventImpl]
492      +/
493     pragma(inline, true)
494     override public void onEvent(const ref IRCEvent event) @system
495     {
496         onEventImpl(event);
497     }
498 
499     // onEventImpl
500     /++
501         Pass on the supplied [dialect.defs.IRCEvent|IRCEvent] to module-level functions
502         annotated with an [kameloso.plugins.common.core.IRCEventHandler|IRCEventHandler],
503         registered with the matching [dialect.defs.IRCEvent.Type|IRCEvent.Type]s.
504 
505         It also does checks for
506         [kameloso.plugins.common.core.ChannelPolicy|ChannelPolicy],
507         [kameloso.plugins.common.core.Permissions|Permissions],
508         [kameloso.plugins.common.core.PrefixPolicy|PrefixPolicy],
509         [kameloso.plugins.common.core.IRCEventHandler.Command|IRCEventHandler.Command],
510         [kameloso.plugins.common.core.IRCEventHandler.Regex|IRCEventHandler.Regex],
511         `chainable` settings etc; where such is applicable.
512 
513         This function is private, but since it's part of a mixin template it will
514         be visible at the mixin site. Plugins can as such override
515         [kameloso.plugins.common.core.IRCPlugin.onEvent|IRCPlugin.onEvent] with
516         their own code and invoke [onEventImpl] as a fallback.
517 
518         Params:
519             origEvent = Parsed [dialect.defs.IRCEvent|IRCEvent] to dispatch to
520                 event handlers, taken by value so we have an object we can modify.
521 
522         See_Also:
523             [kameloso.plugins.common.core.IRCPluginImpl.onEvent|IRCPluginImpl.onEvent]
524      +/
525     private void onEventImpl(/*const ref*/ IRCEvent origEvent) @system
526     {
527         import kameloso.plugins.common.core : Timing;
528 
529         // udaSanityCheckMinimal
530         /++
531             Verifies that some annotations are as expected.
532             Most of the verification is done in
533             [kameloso.plugins.common.core.udaSanityCheckCTFE|udaSanityCheckCTFE].
534          +/
535         debug
536         static bool udaSanityCheckMinimal(alias fun, IRCEventHandler uda)()
537         {
538             static if ((uda._permissionsRequired != Permissions.ignore) &&
539                 !__traits(compiles, { alias _ = .hasMinimalAuthentication; }))
540             {
541                 import std.format : format;
542 
543                 enum pattern = "`%s` is missing a `MinimalAuthentication` " ~
544                     "mixin (needed for `Permissions` checks)";
545                 enum message = pattern.format(module_);
546                 static assert(0, message);
547             }
548 
549             return true;
550         }
551 
552         // call
553         /++
554             Calls the passed function pointer, appropriately.
555          +/
556         void call(bool inFiber, Fun)(scope Fun fun, const ref IRCEvent event) scope
557         {
558             import lu.traits : TakesParams;
559             import std.traits : ParameterStorageClass, ParameterStorageClassTuple, Parameters, arity;
560 
561             static if (
562                 TakesParams!(fun, typeof(this), IRCEvent) ||
563                 TakesParams!(fun, IRCPlugin, IRCEvent))
564             {
565                 debug
566                 {
567                     static assert(assertSaneStorageClasses(
568                         ParameterStorageClassTuple!fun[1],
569                         is(Parameters!fun[1] == const),
570                         inFiber,
571                         module_,
572                         Fun.stringof), "0");
573                 }
574                 fun(this, event);
575             }
576             else static if (
577                 TakesParams!(fun, typeof(this)) ||
578                 TakesParams!(fun, IRCPlugin))
579             {
580                 fun(this);
581             }
582             else static if (TakesParams!(fun, IRCEvent))
583             {
584                 debug
585                 {
586                     static assert(assertSaneStorageClasses(
587                         ParameterStorageClassTuple!fun[0],
588                         is(Parameters!fun[0] == const),
589                         inFiber,
590                         module_,
591                         Fun.stringof), "0");
592                 }
593                 fun(event);
594             }
595             else static if (arity!fun == 0)
596             {
597                 fun();
598             }
599             else
600             {
601                 import std.format : format;
602 
603                 enum pattern = "`%s` has an event handler with an unsupported function signature: `%s`";
604                 enum message = pattern.format(module_, Fun.stringof);
605                 static assert(0, message);
606             }
607         }
608 
609         // NextStep
610         /++
611             Signal up the callstack of what to do next.
612          +/
613         enum NextStep
614         {
615             unset,
616             continue_,
617             repeat,
618             return_,
619         }
620 
621         // process
622         /++
623             Process a function.
624          +/
625         auto process(bool verbose, bool inFiber, bool hasRegexes, Fun)
626             (scope Fun fun,
627             const string funName,
628             const IRCEventHandler uda,
629             ref IRCEvent event) scope
630         {
631             import std.algorithm.searching : canFind;
632 
633             static if (verbose)
634             {
635                 import lu.conv : Enum;
636                 import std.stdio : stdout, writeln, writefln;
637 
638                 writeln("-- ", funName, " @ ", Enum!(IRCEvent.Type).toString(event.type));
639                 writeln("   ...", Enum!ChannelPolicy.toString(uda._channelPolicy));
640                 if (state.settings.flush) stdout.flush();
641             }
642 
643             if (event.channel.length)
644             {
645                 bool channelMatch;
646 
647                 if (uda._channelPolicy == ChannelPolicy.home)
648                 {
649                     channelMatch = state.bot.homeChannels.canFind(event.channel);
650                 }
651                 else if (uda._channelPolicy == ChannelPolicy.guest)
652                 {
653                     channelMatch = !state.bot.homeChannels.canFind(event.channel);
654                 }
655                 else /*if (channelPolicy == ChannelPolicy.any)*/
656                 {
657                     channelMatch = true;
658                 }
659 
660                 if (!channelMatch)
661                 {
662                     static if (verbose)
663                     {
664                         writeln("   ...ignore non-matching channel ", event.channel);
665                         if (state.settings.flush) stdout.flush();
666                     }
667 
668                     // channel policy does not match
669                     return NextStep.continue_;  // next fun
670                 }
671             }
672 
673             // Snapshot content and aux for later restoration
674             immutable origContent = event.content;  // don't strip
675             typeof(IRCEvent.aux) origAux;
676             bool auxDirty;
677 
678             scope(exit)
679             {
680                 // Restore content and aux as they may have been altered
681                 event.content = origContent;
682 
683                 if (auxDirty)
684                 {
685                     event.aux = origAux;
686                 }
687             }
688 
689             if (uda.commands.length || uda.regexes.length)
690             {
691                 import lu.string : strippedLeft;
692 
693                 if (state.settings.observerMode)
694                 {
695                     // Skip all commands
696                     return NextStep.continue_;
697                 }
698 
699                 event.content = event.content.strippedLeft;
700 
701                 if (!event.content.length)
702                 {
703                     // Event has a Command or a Regex set up but
704                     // `event.content` is empty; cannot possibly be of interest.
705                     return NextStep.continue_;  // next function
706                 }
707             }
708 
709             /// Whether or not a Command or Regex matched.
710             bool commandMatch;
711 
712             // Evaluate each Command UDAs with the current event
713             if (uda.commands.length)
714             {
715                 commandForeach:
716                 foreach (const command; uda.commands)
717                 {
718                     static if (verbose)
719                     {
720                         writefln(`   ...Command "%s"`, command._word);
721                         if (state.settings.flush) stdout.flush();
722                     }
723 
724                     if (!event.prefixPolicyMatches!verbose(command._policy, state))
725                     {
726                         static if (verbose)
727                         {
728                             writeln("   ...policy doesn't match; continue next Command");
729                             if (state.settings.flush) stdout.flush();
730                         }
731 
732                         // Do nothing, proceed to next command
733                         continue commandForeach;
734                     }
735                     else
736                     {
737                         import lu.string : nom;
738                         import std.typecons : No, Yes;
739                         import std.uni : toLower;
740 
741                         immutable thisCommand = event.content
742                             .nom!(Yes.inherit, Yes.decode)(' ');
743 
744                         if (thisCommand.toLower == command._word.toLower)
745                         {
746                             static if (verbose)
747                             {
748                                 writeln("   ...command matches!");
749                                 if (state.settings.flush) stdout.flush();
750                             }
751 
752                             if (!auxDirty)
753                             {
754                                 origAux = event.aux;  // copies
755                                 auxDirty = true;
756                             }
757 
758                             event.aux[$-1] = thisCommand;
759                             commandMatch = true;
760                             break commandForeach;
761                         }
762                         else
763                         {
764                             // Restore content to pre-nom state
765                             event.content = origContent;
766                         }
767                     }
768                 }
769             }
770 
771             // Iff no match from Commands, evaluate Regexes
772             static if (hasRegexes)
773             {
774                 if (/*uda.regexes.length &&*/ !commandMatch)
775                 {
776                     regexForeach:
777                     foreach (const regex; uda.regexes)
778                     {
779                         static if (verbose)
780                         {
781                             writefln(`   ...Regex r"%s"`, regex._expression);
782                             if (state.settings.flush) stdout.flush();
783                         }
784 
785                         if (!event.prefixPolicyMatches!verbose(regex._policy, state))
786                         {
787                             static if (verbose)
788                             {
789                                 writeln("   ...policy doesn't match; continue next Regex");
790                                 if (state.settings.flush) stdout.flush();
791                             }
792 
793                             // Do nothing, proceed to next regex
794                             continue regexForeach;
795                         }
796                         else
797                         {
798                             try
799                             {
800                                 import std.regex : matchFirst;
801 
802                                 const hits = event.content.matchFirst(regex.engine);
803 
804                                 if (!hits.empty)
805                                 {
806                                     static if (verbose)
807                                     {
808                                         writeln("   ...expression matches!");
809                                         if (state.settings.flush) stdout.flush();
810                                     }
811 
812                                     if (!auxDirty)
813                                     {
814                                         origAux = event.aux;  // copies
815                                         auxDirty = true;
816                                     }
817 
818                                     event.aux[$-1] = hits[0];
819                                     commandMatch = true;
820                                     break regexForeach;
821                                 }
822                                 else
823                                 {
824                                     static if (verbose)
825                                     {
826                                         enum pattern = `   ...matching "%s" against expression "%s" failed.`;
827                                         writefln(pattern, event.content, regex._expression);
828                                         if (state.settings.flush) stdout.flush();
829                                     }
830                                 }
831                             }
832                             catch (Exception e)
833                             {
834                                 static if (verbose)
835                                 {
836                                     writeln("   ...Regex exception: ", e.msg);
837                                     version(PrintStacktraces) writeln(e);
838                                     if (state.settings.flush) stdout.flush();
839                                 }
840                             }
841                         }
842                     }
843                 }
844             }
845 
846             if (uda.commands.length || uda.regexes.length)
847             {
848                 if (!commandMatch)
849                 {
850                     // {Command,Regex} exist but neither matched; skip
851                     static if (verbose)
852                     {
853                         writeln("   ...no Command nor Regex match; continue funloop");
854                         if (state.settings.flush) stdout.flush();
855                     }
856 
857                     return NextStep.continue_; // next function
858                 }
859             }
860 
861             if (uda._permissionsRequired != Permissions.ignore)
862             {
863                 static if (verbose)
864                 {
865                     writeln("   ...Permissions.",
866                         Enum!Permissions.toString(uda._permissionsRequired));
867                     if (state.settings.flush) stdout.flush();
868                 }
869 
870                 immutable result = this.allow(event, uda._permissionsRequired);
871 
872                 static if (verbose)
873                 {
874                     writeln("   ...allow result is ", Enum!FilterResult.toString(result));
875                     if (state.settings.flush) stdout.flush();
876                 }
877 
878                 if (result == FilterResult.pass)
879                 {
880                     // Drop down
881                 }
882                 else if (result == FilterResult.whois)
883                 {
884                     import kameloso.plugins.common.misc : enqueue;
885                     import lu.traits : TakesParams;
886                     import std.traits : arity;
887 
888                     static if (verbose)
889                     {
890                         writefln("   ...%s WHOIS", typeof(this).stringof);
891                         if (state.settings.flush) stdout.flush();
892                     }
893 
894                     static if (
895                         TakesParams!(fun, typeof(this), IRCEvent) ||
896                         TakesParams!(fun, IRCPlugin, IRCEvent) ||
897                         TakesParams!(fun, typeof(this)) ||
898                         TakesParams!(fun, IRCPlugin) ||
899                         TakesParams!(fun, IRCEvent) ||
900                         (arity!fun == 0))
901                     {
902                         // Unsure why we need to specifically specify IRCPlugin
903                         // now despite typeof(this) being a subclass...
904                         enqueue(this, event, uda._permissionsRequired, uda._fiber, fun, funName);
905                         return uda._chainable ? NextStep.continue_ : NextStep.return_;
906                     }
907                     else
908                     {
909                         import std.format : format;
910 
911                         enum pattern = "`%s` has an event handler with an unsupported function signature: `%s`";
912                         enum message = pattern.format(module_, Fun.stringof);
913                         static assert(0, message);
914                     }
915                 }
916                 else /*if (result == FilterResult.fail)*/
917                 {
918                     return uda._chainable ? NextStep.continue_ : NextStep.return_;
919                 }
920             }
921 
922             static if (verbose)
923             {
924                 writeln("   ...calling!");
925                 if (state.settings.flush) stdout.flush();
926             }
927 
928             /+
929                 This casts any @safe event handler functions to @system.
930                 It should no longer be necessary since we removed the `@safe:`
931                 from the top of all modules with handler functions (including
932                 `awareness.d`), but it's free, so keep it here in case we add
933                 something later and accidentally make it @safe.
934              +/
935             static if (Fun.stringof[$-5..$] == "@safe")
936             {
937                 enum message = "Warning: `" ~ module_ ~ "` has a `" ~ Fun.stringof[0..$-6] ~
938                     "` event handler annotated `@safe`, either directly or via mixins, " ~
939                     "which incurs unnecessary template instantiations. " ~
940                     "It was cast to `@system`, but consider revising source";
941                 pragma(msg, message);
942 
943                 mixin("alias SystemFun = " ~ Fun.stringof[0..$-6] ~ " @system;");
944             }
945             else
946             {
947                 alias SystemFun = Fun;
948             }
949 
950             static if (inFiber)
951             {
952                 import kameloso.constants : BufferSize;
953                 import kameloso.thread : CarryingFiber;
954                 import core.thread : Fiber;
955 
956                 auto fiber = new CarryingFiber!IRCEvent(
957                     () => call!(inFiber, SystemFun)(fun, event),
958                     BufferSize.fiberStack);
959                 fiber.payload = event;
960                 fiber.call();
961 
962                 if (fiber.state == Fiber.State.TERM)
963                 {
964                     // Ended immediately, so just destroy
965                     destroy(fiber);
966                 }
967             }
968             else
969             {
970                 call!(inFiber, SystemFun)(fun, event);
971             }
972 
973             if (uda._chainable)
974             {
975                 // onEvent found an event and triggered a function, but
976                 // it's Chainable and there may be more, so keep looking.
977                 return NextStep.continue_;
978             }
979             else
980             {
981                 // The triggered function is not Chainable so return and
982                 // let the main loop continue with the next plugin.
983                 return NextStep.return_;
984             }
985         }
986 
987         // tryProcess
988         /++
989             Try a function.
990          +/
991         auto tryProcess(size_t i)(ref IRCEvent event)
992         {
993             immutable uda = this.Introspection.allEventHandlerUDAsInModule[i];
994             alias fun = this.Introspection.allEventHandlerFunctionsInModule[i];
995             debug static assert(udaSanityCheckMinimal!(fun, uda), "0");
996 
997             enum verbose = (uda._verbose || debug_);
998             enum funName = module_ ~ '.' ~ __traits(identifier, fun);
999 
1000             /+
1001                 Return if the event handler does not accept this type of event.
1002              +/
1003             if ((uda.acceptedEventTypeMap.length >= IRCEvent.Type.ANY) &&
1004                 uda.acceptedEventTypeMap[IRCEvent.Type.ANY])
1005             {
1006                 // ANY; drop down
1007             }
1008             else if (event.type >= uda.acceptedEventTypeMap.length)
1009             {
1010                 // Out of bounds, cannot possibly be an accepted type
1011                 return NextStep.continue_;
1012             }
1013             else if (uda.acceptedEventTypeMap[event.type])
1014             {
1015                 // Drop down
1016             }
1017             else
1018             {
1019                 return NextStep.continue_;
1020             }
1021 
1022             try
1023             {
1024                 immutable next = process!
1025                     (verbose,
1026                     cast(bool)uda._fiber,
1027                     cast(bool)uda.regexes.length)
1028                     (&fun,
1029                     funName,
1030                     uda,
1031                     event);
1032 
1033                 if (next == NextStep.continue_)
1034                 {
1035                     return NextStep.continue_;
1036                 }
1037                 else if (next == NextStep.repeat)
1038                 {
1039                     // only repeat once so we don't endlessly loop
1040                     immutable newNext = process!
1041                         (verbose,
1042                         cast(bool)uda._fiber,
1043                         cast(bool)uda.regexes.length)
1044                         (&fun,
1045                         funName,
1046                         uda,
1047                         event);
1048                     return newNext;
1049                 }
1050                 else if (next == NextStep.return_)
1051                 {
1052                     return NextStep.return_;
1053                 }
1054                 else /*if (next == NextStep.unset)*/
1055                 {
1056                     assert(0, "`IRCPluginImpl.onEventImpl.process` returned `Next.unset`");
1057                 }
1058             }
1059             catch (Exception e)
1060             {
1061                 import kameloso.plugins.common.core : sanitiseEvent;
1062                 import std.utf : UTFException;
1063                 import core.exception : UnicodeException;
1064 
1065                 /*enum pattern = "tryProcess some exception on <l>%s</>: <l>%s";
1066                 logger.warningf(pattern, funName, e);*/
1067 
1068                 immutable isRecoverableException =
1069                     (cast(UnicodeException)e !is null) ||
1070                     (cast(UTFException)e !is null);
1071 
1072                 if (!isRecoverableException) throw e;
1073 
1074                 sanitiseEvent(event);
1075 
1076                 // Copy-paste, not much we can do otherwise
1077                 immutable next = process!
1078                     (verbose,
1079                     cast(bool)uda._fiber,
1080                     cast(bool)uda.regexes.length)
1081                     (&fun,
1082                     funName,
1083                     uda,
1084                     event);
1085 
1086                 if (next == NextStep.continue_)
1087                 {
1088                     return NextStep.continue_;
1089                 }
1090                 else if (next == NextStep.repeat)
1091                 {
1092                     // only repeat once so we don't endlessly loop
1093                     immutable newNext = process!
1094                         (verbose,
1095                         cast(bool)uda._fiber,
1096                         cast(bool)uda.regexes.length)
1097                         (&fun,
1098                         funName,
1099                         uda,
1100                         event);
1101                     return newNext;
1102                 }
1103                 else if (next == NextStep.return_)
1104                 {
1105                     return NextStep.return_;
1106                 }
1107                 else /*if (next == NextStep.unset)*/
1108                 {
1109                     assert(0, "`IRCPluginImpl.onEventImpl.process` returned `Next.unset`");
1110                 }
1111             }
1112         }
1113 
1114         /+
1115             Perform some sanity checks to make sure nothing is broken.
1116          +/
1117         static if (!this.Introspection.allEventHandlerFunctionsInModule.length)
1118         {
1119             version(unittest)
1120             {
1121                 // Skip event handler checks when unittesting, as it triggers
1122                 // unittests in common/core.d
1123             }
1124             else
1125             {
1126                 import std.algorithm.searching : endsWith;
1127 
1128                 static if (module_.endsWith(".stub"))
1129                 {
1130                     // Defined to be empty
1131                 }
1132                 else
1133                 {
1134                     enum noEventHandlerMessage = "Warning: Module `" ~ module_ ~
1135                         "` mixes in `IRCPluginImpl`, but there " ~
1136                         "seem to be no module-level event handlers. " ~
1137                         "Verify `IRCEventHandler` annotations";
1138                     pragma(msg, noEventHandlerMessage);
1139                 }
1140             }
1141         }
1142 
1143         // funIndexByTiming
1144         /++
1145             Populates an array with indices of functions in `allEventHandlerUDAsInModule`
1146             that were annotated with an [IRCEventHandler] with a [Timing] matching
1147             the one supplied.
1148          +/
1149         auto funIndexByTiming(const Timing timing) scope
1150         {
1151             assert(__ctfe, "funIndexByTiming called outside CTFE");
1152 
1153             size_t[] indexes;
1154 
1155             foreach (immutable i; 0..this.Introspection.allEventHandlerUDAsInModule.length)
1156             {
1157                 if (this.Introspection.allEventHandlerUDAsInModule[i]._when == timing) indexes ~= i;
1158             }
1159 
1160             return indexes;
1161         }
1162 
1163         /+
1164             Build index arrays, either as enums or static immutables.
1165          +/
1166         static immutable setupFunIndexes = funIndexByTiming(Timing.setup);
1167         static immutable earlyFunIndexes = funIndexByTiming(Timing.early);
1168         static immutable normalFunIndexes = funIndexByTiming(Timing.untimed);
1169         static immutable lateFunIndexes = funIndexByTiming(Timing.late);
1170         static immutable cleanupFunIndexes = funIndexByTiming(Timing.cleanup);
1171 
1172         /+
1173             It seems we can't trust mixed-in awareness functions to actually get
1174             detected, depending on how late in the module the site of mixin is.
1175             So statically assert we found some.
1176          +/
1177         static if (__traits(compiles, { alias _ = .hasMinimalAuthentication; }))
1178         {
1179             static if (!earlyFunIndexes.length)
1180             {
1181                 import std.format : format;
1182 
1183                 enum pattern = "Module `%s` mixes in `MinimalAuthentication`, " ~
1184                     "yet no `Timing.early` functions were found during introspection. " ~
1185                     "Try moving the mixin site to earlier in the module";
1186                 immutable message = pattern.format(module_);
1187                 static assert(0, message);
1188             }
1189         }
1190 
1191         static if (__traits(compiles, { alias _ = .hasUserAwareness; }))
1192         {
1193             static if (!cleanupFunIndexes.length)
1194             {
1195                 import std.format : format;
1196 
1197                 enum pattern = "Module `%s` mixes in `UserAwareness`, " ~
1198                     "yet no `Timing.cleanup` functions were found during introspection. " ~
1199                     "Try moving the mixin site to earlier in the module";
1200                 immutable message = pattern.format(module_);
1201                 static assert(0, message);
1202             }
1203         }
1204 
1205         static if (__traits(compiles, { alias _ = .hasChannelAwareness; }))
1206         {
1207             static if (!lateFunIndexes.length)
1208             {
1209                 import std.format : format;
1210 
1211                 enum pattern = "Module `%s` mixes in `ChannelAwareness`, " ~
1212                     "yet no `Timing.late` functions were found during introspection. " ~
1213                     "Try moving the mixin site to earlier in the module";
1214                 immutable message = pattern.format(module_);
1215                 static assert(0, message);
1216             }
1217         }
1218 
1219         alias allFunIndexes = AliasSeq!(
1220             setupFunIndexes,
1221             earlyFunIndexes,
1222             normalFunIndexes,
1223             lateFunIndexes,
1224             cleanupFunIndexes,
1225         );
1226 
1227         /+
1228             Process all functions.
1229          +/
1230         aliasLoop:
1231         foreach (funIndexes; allFunIndexes)
1232         {
1233             static foreach (immutable i; funIndexes)
1234             {{
1235                 immutable next = tryProcess!i(origEvent);
1236 
1237                 if (next == NextStep.return_)
1238                 {
1239                     // return_; end loop, proceed with next index alias
1240                     continue aliasLoop;
1241                 }
1242                 /*else if (next == NextStep.continue_)
1243                 {
1244                     // continue_; iterate to next function within this alias
1245                 }*/
1246                 else if (next == NextStep.repeat)
1247                 {
1248                     immutable newNext = tryProcess!i(origEvent);
1249 
1250                     // Only repeat once
1251                     if (newNext == NextStep.return_)
1252                     {
1253                         // as above, end index loop
1254                         continue aliasLoop;
1255                     }
1256                 }
1257             }}
1258         }
1259     }
1260 
1261     // this(IRCPluginState)
1262     /++
1263         Basic constructor for a plugin.
1264 
1265         It passes execution to the module-level `initialise` if it exists.
1266 
1267         There's no point in checking whether the plugin is enabled or not, as it
1268         will only be possible to change the setting after having created the
1269         plugin (and serialised settings into it).
1270 
1271         Params:
1272             state = The aggregate of all plugin state variables, making
1273                 this the "original state" of the plugin.
1274      +/
1275     public this(IRCPluginState state) @system
1276     {
1277         import lu.traits : isSerialisable;
1278 
1279         enum numEventTypes = __traits(allMembers, IRCEvent.Type).length;
1280 
1281         // Inherit select members of state by zeroing out what we don't want
1282         this.state = state;
1283         this.state.awaitingFibers = null;
1284         this.state.awaitingFibers.length = numEventTypes;
1285         this.state.awaitingDelegates = null;
1286         this.state.awaitingDelegates.length = numEventTypes;
1287         this.state.pendingReplays = null;
1288         this.state.hasPendingReplays = false;
1289         this.state.readyReplays = null;
1290         this.state.scheduledFibers = null;
1291         this.state.scheduledDelegates = null;
1292         this.state.nextScheduledTimestamp = long.max;
1293         //this.state.previousWhoisTimestamps = null;  // keep
1294         this.state.updates = IRCPluginState.Update.nothing;
1295 
1296         foreach (immutable i, ref member; this.tupleof)
1297         {
1298             static if (isSerialisable!member)
1299             {
1300                 import kameloso.traits : udaIndexOf;
1301 
1302                 enum resourceUDAIndex = udaIndexOf!(this.tupleof[i], Resource);
1303                 enum configurationUDAIndex = udaIndexOf!(this.tupleof[i], Configuration);
1304                 alias attrs = __traits(getAttributes, this.tupleof[i]);
1305 
1306                 static if (resourceUDAIndex != -1)
1307                 {
1308                     import std.path : buildNormalizedPath;
1309 
1310                     static if (is(typeof(attrs[resourceUDAIndex])))
1311                     {
1312                         member = buildNormalizedPath(
1313                             state.settings.resourceDirectory,
1314                             attrs[resourceUDAIndex].subdirectory,
1315                             member);
1316                     }
1317                     else
1318                     {
1319                         member = buildNormalizedPath(state.settings.resourceDirectory, member);
1320                     }
1321                 }
1322                 else static if (configurationUDAIndex != -1)
1323                 {
1324                     import std.path : buildNormalizedPath;
1325 
1326                     static if (is(typeof(attrs[configurationUDAIndex])))
1327                     {
1328                         member = buildNormalizedPath(
1329                             state.settings.configDirectory,
1330                             attrs[configurationUDAIndex].subdirectory,
1331                             member);
1332                     }
1333                     else
1334                     {
1335                         member = buildNormalizedPath(state.settings.configDirectory, member);
1336                     }
1337                 }
1338             }
1339         }
1340 
1341         static if (__traits(compiles, { alias _ = .initialise; }))
1342         {
1343             import lu.traits : TakesParams;
1344 
1345             static if (TakesParams!(.initialise, typeof(this)))
1346             {
1347                 .initialise(this);
1348             }
1349             else
1350             {
1351                 import std.format : format;
1352 
1353                 enum pattern = "`%s.initialise` has an unsupported function signature: `%s`";
1354                 enum message = pattern.format(module_, typeof(.initialise).stringof);
1355                 static assert(0, message);
1356             }
1357         }
1358     }
1359 
1360     // postprocess
1361     /++
1362         Lets a plugin modify an [dialect.defs.IRCEvent|IRCEvent] while it's begin
1363         constructed, before it's finalised and passed on to be handled.
1364 
1365         Params:
1366             event = The [dialect.defs.IRCEvent|IRCEvent] in flight.
1367      +/
1368     override public void postprocess(ref IRCEvent event) @system
1369     {
1370         static if (__traits(compiles, { alias _ = .postprocess; }))
1371         {
1372             import lu.traits : TakesParams;
1373 
1374             if (!this.isEnabled) return;
1375 
1376             static if (TakesParams!(.postprocess, typeof(this), IRCEvent))
1377             {
1378                 import std.traits : ParameterStorageClass, ParameterStorageClassTuple;
1379 
1380                 alias SC = ParameterStorageClass;
1381                 alias paramClasses = ParameterStorageClassTuple!(.postprocess);
1382 
1383                 static if (paramClasses[1] & SC.ref_)
1384                 {
1385                     .postprocess(this, event);
1386                 }
1387                 else
1388                 {
1389                     import std.format : format;
1390 
1391                     enum pattern = "`%s.postprocess` does not take its " ~
1392                         "`IRCEvent` parameter by `ref`";
1393                     enum message = pattern.format(module_,);
1394                     static assert(0, message);
1395                 }
1396             }
1397             else
1398             {
1399                 import std.format : format;
1400 
1401                 enum pattern = "`%s.postprocess` has an unsupported function signature: `%s`";
1402                 enum message = pattern.format(module_, typeof(.postprocess).stringof);
1403                 static assert(0, message);
1404             }
1405         }
1406     }
1407 
1408     // initResources
1409     /++
1410         Writes plugin resources to disk, creating them if they don't exist.
1411      +/
1412     override public void initResources() @system
1413     {
1414         static if (__traits(compiles, { alias _ = .initResources; }))
1415         {
1416             import lu.traits : TakesParams;
1417 
1418             if (!this.isEnabled) return;
1419 
1420             static if (TakesParams!(.initResources, typeof(this)))
1421             {
1422                 .initResources(this);
1423             }
1424             else
1425             {
1426                 import std.format : format;
1427 
1428                 enum pattern = "`%s.initResources` has an unsupported function signature: `%s`";
1429                 enum message = pattern.format(module_, typeof(.initResources).stringof);
1430                 static assert(0, message);
1431             }
1432         }
1433     }
1434 
1435     // deserialiseConfigFrom
1436     /++
1437         Loads configuration for this plugin from disk.
1438 
1439         This does not proxy a call but merely loads configuration from disk for
1440         all struct variables annotated [kameloso.plugins.common.core.Settings|Settings].
1441 
1442         "Returns" two associative arrays for missing entries and invalid
1443         entries via its two out parameters.
1444 
1445         Params:
1446             configFile = String of the configuration file to read.
1447             missingEntries = Out reference of an associative array of string arrays
1448                 of expected configuration entries that were missing.
1449             invalidEntries = Out reference of an associative array of string arrays
1450                 of unexpected configuration entries that did not belong.
1451      +/
1452     override public void deserialiseConfigFrom(
1453         const string configFile,
1454         out string[][string] missingEntries,
1455         out string[][string] invalidEntries)
1456     {
1457         import kameloso.configreader : readConfigInto;
1458         import kameloso.traits : udaIndexOf;
1459         import lu.meld : meldInto;
1460 
1461         foreach (immutable i, ref symbol; this.tupleof)
1462         {
1463             static if (is(typeof(this.tupleof[i]) == struct))
1464             {
1465                 enum typeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1466                 enum valueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1467 
1468                 static if ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1469                 {
1470                     if (symbol != typeof(symbol).init)
1471                     {
1472                         // This symbol has had configuration applied to it already
1473                         continue;
1474                     }
1475 
1476                     string[][string] theseMissingEntries;
1477                     string[][string] theseInvalidEntries;
1478 
1479                     configFile.readConfigInto(theseMissingEntries, theseInvalidEntries, symbol);
1480 
1481                     theseMissingEntries.meldInto(missingEntries);
1482                     theseInvalidEntries.meldInto(invalidEntries);
1483                     break;
1484                 }
1485             }
1486         }
1487     }
1488 
1489     // setSettingByName
1490     /++
1491         Change a plugin's [kameloso.plugins.common.core.Settings|Settings]-annotated
1492         settings struct member by their string name.
1493 
1494         This is used to allow for command-line argument to set any plugin's
1495         setting by only knowing its name.
1496 
1497         Example:
1498         ---
1499         @Settings struct FooSettings
1500         {
1501             int bar;
1502         }
1503 
1504         FooSettings settings;
1505 
1506         setSettingByName("bar", 42);
1507         assert(settings.bar == 42);
1508         ---
1509 
1510         Params:
1511             setting = String name of the struct member to set.
1512             value = String value to set it to (after converting it to the
1513                 correct type).
1514 
1515         Returns:
1516             `true` if a member was found and set, `false` otherwise.
1517      +/
1518     override public bool setSettingByName(const string setting, const string value)
1519     {
1520         import kameloso.traits : udaIndexOf;
1521         import lu.objmanip : setMemberByName;
1522 
1523         bool success;
1524 
1525         foreach (immutable i, ref symbol; this.tupleof)
1526         {
1527             static if (is(typeof(this.tupleof[i]) == struct))
1528             {
1529                 enum typeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1530                 enum valueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1531 
1532                 static if ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1533                 {
1534                     success = symbol.setMemberByName(setting, value);
1535                     break;
1536                 }
1537             }
1538         }
1539 
1540         return success;
1541     }
1542 
1543     // printSettings
1544     /++
1545         Prints the plugin's [kameloso.plugins.common.core.Settings|Settings]-annotated settings struct.
1546      +/
1547     override public void printSettings() const
1548     {
1549         import kameloso.printing : printObject;
1550         import kameloso.traits : udaIndexOf;
1551 
1552         foreach (immutable i, const ref symbol; this.tupleof)
1553         {
1554             static if (is(typeof(this.tupleof[i]) == struct))
1555             {
1556                 enum typeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1557                 enum valueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1558 
1559                 static if ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1560                 {
1561                     import std.typecons : No, Yes;
1562                     printObject!(No.all)(symbol);
1563                     break;
1564                 }
1565             }
1566         }
1567     }
1568 
1569     // serialiseConfigInto
1570     /++
1571         Gathers the configuration text the plugin wants to contribute to the
1572         configuration file.
1573 
1574         Example:
1575         ---
1576         Appender!(char[]) sink;
1577         sink.reserve(128);
1578         serialiseConfigInto(sink);
1579         ---
1580 
1581         Params:
1582             sink = Reference [std.array.Appender|Appender] to fill with plugin-specific
1583                 settings text.
1584 
1585         Returns:
1586             `true` if something was serialised into the passed sink; `false` if not.
1587      +/
1588     override public bool serialiseConfigInto(ref Appender!(char[]) sink) const
1589     {
1590         import kameloso.traits : udaIndexOf;
1591 
1592         bool didSomething;
1593 
1594         foreach (immutable i, ref symbol; this.tupleof)
1595         {
1596             static if (is(typeof(this.tupleof[i]) == struct))
1597             {
1598                 enum typeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1599                 enum valueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1600 
1601                 static if ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1602                 {
1603                     import lu.serialisation : serialise;
1604 
1605                     sink.serialise(symbol);
1606                     didSomething = true;
1607                     break;
1608                 }
1609             }
1610         }
1611 
1612         return didSomething;
1613     }
1614 
1615     // setup, start, reload, teardown
1616     /+
1617         Generates functions `setup`, `start`, `reload` and `teardown`. These
1618         merely pass on calls to module-level `.setup`, `.start`, `.reload` and
1619         `.teardown`, where such is available.
1620 
1621         `setup` runs early pre-connect routines.
1622 
1623         `start` runs early post-connect routines, immediately after connection
1624         has been established.
1625 
1626         `reload` Reloads the plugin, where such makes sense. What this means is
1627         implementation-defined.
1628 
1629         `teardown` de-initialises the plugin.
1630      +/
1631     static foreach (immutable funName; AliasSeq!("setup", "start", "reload", "teardown"))
1632     {
1633         mixin(`
1634         /++
1635             Automatically generated function.
1636          +/
1637         override public void ` ~ funName ~ `() @system
1638         {
1639             static if (__traits(compiles, { alias _ = .` ~ funName ~ `; }))
1640             {
1641                 import lu.traits : TakesParams;
1642 
1643                 if (!this.isEnabled) return;
1644 
1645                 static if (TakesParams!(.` ~ funName ~ `, typeof(this)))
1646                 {
1647                     .` ~ funName ~ `(this);
1648                 }
1649                 else
1650                 {
1651                     import std.format : format;
1652                     ` ~ "enum pattern = \"`%s.%s` has an unsupported function signature: `%s`\";
1653                     enum message = pattern.format(module_, \"" ~ funName ~ `", typeof(.` ~ funName ~ `).stringof);
1654                     static assert(0, message);
1655                 }
1656             }
1657         }`);
1658     }
1659 
1660     // name
1661     /++
1662         Returns the name of the plugin. (Technically it's the name of the module.)
1663 
1664         Returns:
1665             The module name of the mixing-in class.
1666      +/
1667     pragma(inline, true)
1668     override public string name() @property const pure nothrow @nogc
1669     {
1670         import lu.string : beginsWith;
1671 
1672         enum modulePrefix = "kameloso.plugins.";
1673 
1674         static if (module_.beginsWith(modulePrefix))
1675         {
1676             import std.string : indexOf;
1677 
1678             string slice = module_[modulePrefix.length..$];  // mutable
1679             immutable dotPos = slice.indexOf('.');
1680             if (dotPos == -1) return slice;
1681             return (slice[dotPos+1..$] == "base") ? slice[0..dotPos] : slice[dotPos+1..$];
1682         }
1683         else
1684         {
1685             import std.format : format;
1686 
1687             enum pattern = "Plugin module `%s` is not under `kameloso.plugins`";
1688             enum message = pattern.format(module_);
1689             static assert(0, message);
1690         }
1691     }
1692 
1693     // channelSpecificCommands
1694     /++
1695         Compile a list of our a plugin's oneliner commands.
1696 
1697         Params:
1698             channelName = Name of channel whose commands we want to summarise.
1699 
1700         Returns:
1701             An associative array of
1702             [kameloso.plugins.common.core.IRCPlugin.CommandMetadata|IRCPlugin.CommandMetadata]s,
1703             one for each soft command active in the passed channel.
1704      +/
1705     override public IRCPlugin.CommandMetadata[string] channelSpecificCommands(const string channelName) @system
1706     {
1707         return null;
1708     }
1709 
1710     // commands
1711     /++
1712         Forwards to [kameloso.plugins.common.core.IRCPluginImpl.commandsImpl|IRCPluginImpl.commandsImpl].
1713 
1714         This is made a separate function to allow plugins to override it and
1715         insert their own code, while still leveraging
1716         [kameloso.plugins.common.core.IRCPluginImpl.commandsImpl|IRCPluginImpl.commandsImpl]
1717         for the actual dirty work.
1718 
1719         Returns:
1720             Associative array of tuples of all command metadata (descriptions,
1721             syntaxes, and whether they are hidden), keyed by
1722             [kameloso.plugins.common.core.IRCEventHandler.Command.word|IRCEventHandler.Command.word]s
1723             and [kameloso.plugins.common.core.IRCEventHandler.Regex.expression|IRCEventHandler.Regex.expression]s.
1724      +/
1725     pragma(inline, true)
1726     override public IRCPlugin.CommandMetadata[string] commands() pure nothrow @property const
1727     {
1728         return commandsImpl();
1729     }
1730 
1731     // commandsImpl
1732     /++
1733         Collects all [kameloso.plugins.common.core.IRCEventHandler.Command|IRCEventHandler.Command]
1734         command words and [kameloso.plugins.common.core.IRCEventHandler.Regex|IRCEventHandler.Regex]
1735         regex expressions that this plugin offers at compile time, then at runtime
1736         returns them alongside their descriptions and their visibility, as an associative
1737         array of [kameloso.plugins.common.core.IRCPlugin.CommandMetadata|IRCPlugin.CommandMetadata]s
1738         keyed by command name strings.
1739 
1740         This function is private, but since it's part of a mixin template it will
1741         be visible at the mixin site. Plugins can as such override
1742         [kameloso.plugins.common.core.IRCPlugin.commands|IRCPlugin.commands] with
1743         their own code and invoke [commandsImpl] as a fallback.
1744 
1745         Returns:
1746             Associative array of tuples of all command metadata (descriptions,
1747             syntaxes, and whether they are hidden), keyed by
1748             [kameloso.plugins.common.core.IRCEventHandler.Command.word|IRCEventHandler.Command.word]s
1749             and [kameloso.plugins.common.core.IRCEventHandler.Regex.expression|IRCEventHandler.Regex.expression]s.
1750      +/
1751     private IRCPlugin.CommandMetadata[string] commandsImpl() pure nothrow @property const
1752     {
1753         enum ctCommandsEnumLiteral =
1754         {
1755             import kameloso.plugins.common.core : IRCEventHandler;
1756             import std.traits : getUDAs;
1757 
1758             assert(__ctfe, "ctCommandsEnumLiteral called outside CTFE");
1759 
1760             IRCPlugin.CommandMetadata[string] commandAA;
1761 
1762             foreach (fun; this.Introspection.allEventHandlerFunctionsInModule)
1763             {
1764                 immutable uda = getUDAs!(fun, IRCEventHandler)[0];
1765 
1766                 static foreach (immutable command; uda.commands)
1767                 {{
1768                     enum key = command._word;
1769                     commandAA[key] = IRCPlugin.CommandMetadata(command);
1770 
1771                     static if (command._hidden)
1772                     {
1773                         // Just ignore
1774                     }
1775                     else static if (command._description.length)
1776                     {
1777                         static if (command._policy == PrefixPolicy.nickname)
1778                         {
1779                             import lu.string : beginsWith;
1780 
1781                             static if (command.syntaxes.length)
1782                             {
1783                                 foreach (immutable syntax; command.syntaxes)
1784                                 {
1785                                     if (syntax.beginsWith("$bot"))
1786                                     {
1787                                         // Syntax is already prefixed
1788                                         commandAA[key].syntaxes ~= syntax;
1789                                     }
1790                                     else
1791                                     {
1792                                         // Prefix the command with the bot's nickname,
1793                                         // as that's how it's actually used.
1794                                         commandAA[key].syntaxes ~= "$bot: " ~ syntax;
1795                                     }
1796                                 }
1797                             }
1798                             else
1799                             {
1800                                 // Define an empty nickname: command syntax
1801                                 // to give hint about the nickname prefix
1802                                 commandAA[key].syntaxes ~= "$bot: $command";
1803                             }
1804                         }
1805                         else
1806                         {
1807                             static if (command.syntaxes.length)
1808                             {
1809                                 commandAA[key].syntaxes ~= command.syntaxes.dup;
1810                             }
1811                             else
1812                             {
1813                                 commandAA[key].syntaxes ~= "$command";
1814                             }
1815                         }
1816                     }
1817                     else /*static if (!command._hidden && !command._description.length)*/
1818                     {
1819                         import std.format : format;
1820 
1821                         enum fqn = module_ ~ '.' ~ __traits(identifier, fun);
1822                         enum pattern = "Warning: `%s` non-hidden command word \"%s\" is missing a description";
1823                         enum message = pattern.format(fqn, command._word);
1824                         pragma(msg, message);
1825                     }
1826                 }}
1827 
1828                 static foreach (immutable regex; uda.regexes)
1829                 {{
1830                     enum key = `r"` ~ regex._expression ~ `"`;
1831                     commandAA[key] = IRCPlugin.CommandMetadata(regex);
1832 
1833                     static if (regex._description.length)
1834                     {
1835                         static if (regex._policy == PrefixPolicy.direct)
1836                         {
1837                             commandAA[key].syntaxes ~= regex._expression;
1838                         }
1839                         else static if (regex._policy == PrefixPolicy.prefixed)
1840                         {
1841                             commandAA[key].syntaxes ~= "$prefix" ~ regex._expression;
1842                         }
1843                         else static if (regex._policy == PrefixPolicy.nickname)
1844                         {
1845                             commandAA[key].syntaxes ~= "$nickname: " ~ regex._expression;
1846                         }
1847                     }
1848                     else static if (!regex._hidden)
1849                     {
1850                         import std.format : format;
1851 
1852                         enum fqn = module_ ~ '.' ~ __traits(identifier, fun);
1853                         enum pattern = "Warning: `%s` non-hidden expression \"%s\" is missing a description";
1854                         enum message = pattern.format(fqn, regex._expression);
1855                         pragma(msg, message);
1856                     }
1857                 }}
1858             }
1859 
1860             return commandAA;
1861         }();
1862 
1863         // This is an associative array literal. We can't make it static immutable
1864         // because of AAs' runtime-ness. We could make it runtime immutable once
1865         // and then just the address, but this is really not a hotspot.
1866         // So just let it allocate when it wants.
1867         return this.isEnabled ? ctCommandsEnumLiteral : null;
1868     }
1869 
1870     private import kameloso.thread : Sendable;
1871 
1872     // onBusMessage
1873     /++
1874         Proxies a bus message to the plugin, to let it handle it (or not).
1875 
1876         Params:
1877             header = String header for plugins to examine and decide if the
1878                 message was meant for them.
1879             content = Wildcard content, to be cast to concrete types if the header matches.
1880      +/
1881     override public void onBusMessage(const string header, shared Sendable content) @system
1882     {
1883         static if (__traits(compiles, { alias _ = .onBusMessage; }))
1884         {
1885             import lu.traits : TakesParams;
1886 
1887             static if (TakesParams!(.onBusMessage, typeof(this), string, Sendable))
1888             {
1889                 .onBusMessage(this, header, content);
1890             }
1891             /*else static if (TakesParams!(.onBusMessage, typeof(this), string))
1892             {
1893                 .onBusMessage(this, header);
1894             }*/
1895             else
1896             {
1897                 import std.format : format;
1898 
1899                 enum pattern = "`%s.onBusMessage` has an unsupported function signature: `%s`";
1900                 enum message = pattern.format(module_, typeof(.onBusMessage).stringof);
1901                 static assert(0, message);
1902             }
1903         }
1904     }
1905 }
1906 
1907 
1908 // prefixPolicyMatches
1909 /++
1910     Evaluates whether or not the message in an event satisfies the [PrefixPolicy]
1911     specified, as fetched from a [IRCEventHandler.Command] or [IRCEventHandler.Regex] UDA.
1912 
1913     If it doesn't match, the [IRCPluginImpl.onEventImpl] routine shall consider
1914     the UDA as not matching and continue with the next one.
1915 
1916     Params:
1917         verbose = Whether or not to output verbose debug information to the local terminal.
1918         event = Reference to the mutable [dialect.defs.IRCEvent|IRCEvent] we're considering.
1919         policy = Policy to apply.
1920         state = The calling [IRCPlugin]'s [IRCPluginState].
1921 
1922     Returns:
1923         `true` if the message is in a context where the event matches the
1924         `policy`, `false` if not.
1925  +/
1926 auto prefixPolicyMatches(bool verbose)
1927     (ref IRCEvent event,
1928     const PrefixPolicy policy,
1929     const IRCPluginState state)
1930 {
1931     import kameloso.string : stripSeparatedPrefix;
1932     import lu.string : beginsWith;
1933     import std.typecons : No, Yes;
1934 
1935     static if (verbose)
1936     {
1937         import std.stdio : writefln, writeln;
1938         writeln("...prefixPolicyMatches! policy:", policy);
1939     }
1940 
1941     bool strippedDisplayName;
1942 
1943     with (PrefixPolicy)
1944     final switch (policy)
1945     {
1946     case direct:
1947         static if (verbose)
1948         {
1949             writeln("direct, so just passes.");
1950         }
1951         return true;
1952 
1953     case prefixed:
1954         if (state.settings.prefix.length && event.content.beginsWith(state.settings.prefix))
1955         {
1956             static if (verbose)
1957             {
1958                 writefln("starts with prefix (%s)", state.settings.prefix);
1959             }
1960 
1961             event.content = event.content[state.settings.prefix.length..$];
1962         }
1963         else
1964         {
1965             static if (verbose)
1966             {
1967                 writeln("did not start with prefix but falling back to nickname check");
1968             }
1969 
1970             goto case nickname;
1971         }
1972         break;
1973 
1974     case nickname:
1975         if (event.content.beginsWith('@'))
1976         {
1977             static if (verbose)
1978             {
1979                 writeln("stripped away prepended '@'");
1980             }
1981 
1982             // Using @name to refer to someone is not
1983             // uncommon; allow for it and strip it away
1984             event.content = event.content[1..$];
1985         }
1986 
1987         version(TwitchSupport)
1988         {
1989             if ((state.server.daemon == IRCServer.Daemon.twitch) &&
1990                 state.client.displayName.length &&
1991                 event.content.beginsWith(state.client.displayName))
1992             {
1993                 static if (verbose)
1994                 {
1995                     writeln("begins with displayName! stripping it");
1996                 }
1997 
1998                 event.content = event.content
1999                     .stripSeparatedPrefix(state.client.displayName, Yes.demandSeparatingChars);
2000 
2001                 if (state.settings.prefix.length && event.content.beginsWith(state.settings.prefix))
2002                 {
2003                     static if (verbose)
2004                     {
2005                         writefln("further starts with prefix (%s)", state.settings.prefix);
2006                     }
2007 
2008                     event.content = event.content[state.settings.prefix.length..$];
2009                 }
2010 
2011                 strippedDisplayName = true;
2012                 // Drop down
2013             }
2014         }
2015 
2016         if (strippedDisplayName)
2017         {
2018             // Already did something
2019         }
2020         else if (event.content.beginsWith(state.client.nickname))
2021         {
2022             static if (verbose)
2023             {
2024                 writeln("begins with nickname! stripping it");
2025             }
2026 
2027             event.content = event.content
2028                 .stripSeparatedPrefix(state.client.nickname, Yes.demandSeparatingChars);
2029 
2030             if (state.settings.prefix.length && event.content.beginsWith(state.settings.prefix))
2031             {
2032                 static if (verbose)
2033                 {
2034                     writefln("further starts with prefix (%s)", state.settings.prefix);
2035                 }
2036 
2037                 event.content = event.content[state.settings.prefix.length..$];
2038             }
2039             // Drop down
2040         }
2041         else if (event.type == IRCEvent.Type.QUERY)
2042         {
2043             static if (verbose)
2044             {
2045                 writeln("doesn't begin with nickname but it's a QUERY");
2046             }
2047             // Drop down
2048         }
2049         else
2050         {
2051             static if (verbose)
2052             {
2053                 writeln("nickname required but not present... returning false.");
2054             }
2055             return false;
2056         }
2057         break;
2058     }
2059 
2060     static if (verbose)
2061     {
2062         writeln("policy checks out!");
2063     }
2064 
2065     return true;
2066 }
2067 
2068 
2069 // filterSender
2070 /++
2071     Decides if a sender meets a [Permissions] and is allowed to trigger an event
2072     handler, or if a WHOIS query is needed to be able to tell.
2073 
2074     This requires the Persistence service to be active to work.
2075 
2076     Params:
2077         event = [dialect.defs.IRCEvent|IRCEvent] to filter.
2078         permissionsRequired = The [Permissions] context in which this user should be filtered.
2079         preferHostmasks = Whether to rely on hostmasks for user identification,
2080             or to use services account logins, which need to be issued WHOIS
2081             queries to divine.
2082 
2083     Returns:
2084         A [FilterResult] saying the event should `pass`, `fail`, or that more
2085         information about the sender is needed via a WHOIS call.
2086  +/
2087 auto filterSender(
2088     const ref IRCEvent event,
2089     const Permissions permissionsRequired,
2090     const bool preferHostmasks) @safe
2091 {
2092     import kameloso.constants : Timeout;
2093 
2094     version(WithPersistenceService) {}
2095     else
2096     {
2097         pragma(msg, "Warning: The Persistence service is not compiled in. " ~
2098             "Event triggers may or may not work. You get to keep the pieces.");
2099     }
2100 
2101     immutable class_ = event.sender.class_;
2102 
2103     if (class_ == IRCUser.Class.blacklist) return FilterResult.fail;
2104 
2105     immutable timediff = (event.time - event.sender.updated);
2106 
2107     // In hostmasks mode there's zero point to WHOIS a sender, as the instigating
2108     // event will have the hostmask embedded in it, always.
2109     immutable whoisExpired = !preferHostmasks && (timediff > Timeout.whoisRetry);
2110 
2111     if (event.sender.account.length)
2112     {
2113         immutable isAdmin = (class_ == IRCUser.Class.admin);  // Trust in Persistence
2114         immutable isStaff = (class_ == IRCUser.Class.staff);
2115         immutable isOperator = (class_ == IRCUser.Class.operator);
2116         immutable isElevated = (class_ == IRCUser.Class.elevated);
2117         immutable isWhitelisted = (class_ == IRCUser.Class.whitelist);
2118         immutable isAnyone = (class_ == IRCUser.Class.anyone);
2119 
2120         if (isAdmin)
2121         {
2122             return FilterResult.pass;
2123         }
2124         else if (isStaff && (permissionsRequired <= Permissions.staff))
2125         {
2126             return FilterResult.pass;
2127         }
2128         else if (isOperator && (permissionsRequired <= Permissions.operator))
2129         {
2130             return FilterResult.pass;
2131         }
2132         else if (isElevated && (permissionsRequired <= Permissions.elevated))
2133         {
2134             return FilterResult.pass;
2135         }
2136         else if (isWhitelisted && (permissionsRequired <= Permissions.whitelist))
2137         {
2138             return FilterResult.pass;
2139         }
2140         else if (/*event.sender.account.length &&*/ permissionsRequired <= Permissions.registered)
2141         {
2142             return FilterResult.pass;
2143         }
2144         else if (isAnyone && (permissionsRequired <= Permissions.anyone))
2145         {
2146             return whoisExpired ? FilterResult.whois : FilterResult.pass;
2147         }
2148         else if (permissionsRequired == Permissions.ignore)
2149         {
2150             /*assert(0, "`filterSender` saw a `Permissions.ignore` and the call " ~
2151                 "to it could have been skipped");*/
2152             return FilterResult.pass;
2153         }
2154         else
2155         {
2156             return FilterResult.fail;
2157         }
2158     }
2159     else
2160     {
2161         immutable isLogoutEvent = (event.type == IRCEvent.Type.ACCOUNT);
2162 
2163         with (Permissions)
2164         final switch (permissionsRequired)
2165         {
2166         case admin:
2167         case staff:
2168         case operator:
2169         case elevated:
2170         case whitelist:
2171         case registered:
2172             // Unknown sender; WHOIS if old result expired, otherwise fail
2173             return (whoisExpired && !isLogoutEvent) ? FilterResult.whois : FilterResult.fail;
2174 
2175         case anyone:
2176             // Unknown sender; WHOIS if old result expired in mere curiosity, else just pass
2177             return (whoisExpired && !isLogoutEvent) ? FilterResult.whois : FilterResult.pass;
2178 
2179         case ignore:
2180             /*assert(0, "`filterSender` saw a `Permissions.ignore` and the call " ~
2181                 "to it could have been skipped");*/
2182             return FilterResult.pass;
2183         }
2184     }
2185 }
2186 
2187 
2188 // allowImpl
2189 /++
2190     Judges whether an event may be triggered, based on the event itself and
2191     the annotated [kameloso.plugins.common.core.Permissions|Permissions] of the
2192     handler in question. Implementation function.
2193 
2194     Params:
2195         plugin = The [IRCPlugin] this relates to.
2196         event = [dialect.defs.IRCEvent|IRCEvent] to allow, or not.
2197         permissionsRequired = Required [kameloso.plugins.common.core.Permissions|Permissions]
2198             of the handler in question.
2199 
2200     Returns:
2201         [FilterResult.pass] if the event should be allowed to trigger,
2202         [FilterResult.whois] if not.
2203 
2204     See_Also:
2205         [filterSender]
2206  +/
2207 auto allowImpl(
2208     IRCPlugin plugin,
2209     const ref IRCEvent event,
2210     const Permissions permissionsRequired) pure @safe
2211 {
2212     version(TwitchSupport)
2213     {
2214         if (plugin.state.server.daemon == IRCServer.Daemon.twitch)
2215         {
2216             if (((permissionsRequired == Permissions.anyone) ||
2217                 (permissionsRequired == Permissions.registered)) &&
2218                 (event.sender.class_ != IRCUser.Class.blacklist))
2219             {
2220                 // We can't WHOIS on Twitch, and Permissions.anyone is just
2221                 // Permissions.ignore with an extra WHOIS for good measure.
2222                 // Also everyone is registered on Twitch, by definition.
2223                 return FilterResult.pass;
2224             }
2225         }
2226     }
2227 
2228     // Permissions.ignore always passes, even for Class.blacklist.
2229     return (permissionsRequired == Permissions.ignore) ?
2230         FilterResult.pass :
2231         filterSender(event, permissionsRequired, plugin.state.settings.preferHostmasks);
2232 }
2233 
2234 
2235 // sanitiseEvent
2236 /++
2237     Sanitise event, used upon UTF/Unicode exceptions.
2238 
2239     Params:
2240         event = Reference to the mutable [dialect.defs.IRCEvent|IRCEvent] to sanitise.
2241  +/
2242 void sanitiseEvent(ref IRCEvent event)
2243 {
2244     import std.encoding : sanitize;
2245     import std.range : only;
2246 
2247     event.raw = sanitize(event.raw);
2248     event.channel = sanitize(event.channel);
2249     event.content = sanitize(event.content);
2250     event.tags = sanitize(event.tags);
2251     event.errors = sanitize(event.errors);
2252     event.errors ~= event.errors.length ? " | Sanitised" : "Sanitised";
2253 
2254     foreach (immutable i, ref aux; event.aux)
2255     {
2256         aux = sanitize(aux);
2257     }
2258 
2259     foreach (user; only(&event.sender, &event.target))
2260     {
2261         user.nickname = sanitize(user.nickname);
2262         user.ident = sanitize(user.ident);
2263         user.address = sanitize(user.address);
2264         user.account = sanitize(user.account);
2265 
2266         version(TwitchSupport)
2267         {
2268             user.displayName = sanitize(user.displayName);
2269             user.badges = sanitize(user.badges);
2270             user.colour = sanitize(user.colour);
2271         }
2272     }
2273 }
2274 
2275 
2276 // udaSanityCheckCTFE
2277 /++
2278     Sanity-checks a plugin's [IRCEventHandler]s at compile time.
2279 
2280     Params:
2281         uda = The [IRCEventHandler] UDA to check.
2282 
2283     Throws:
2284         Asserts `0` if the UDA is deemed malformed.
2285  +/
2286 debug
2287 void udaSanityCheckCTFE(const IRCEventHandler uda)
2288 {
2289     import std.format : format;
2290 
2291     assert(__ctfe, "udaSanityCheckCTFE called outside CTFE");
2292 
2293     static if (__VERSION__ <= 2104L)
2294     {
2295         /++
2296             There's something wrong with how the assert message is printed from CTFE.
2297             Work around it somewhat by prepending a backtick.
2298 
2299             https://issues.dlang.org/show_bug.cgi?id=24036
2300          +/
2301         enum fix = "`";
2302     }
2303     else
2304     {
2305         // Hopefully no need past 2.104... Update when 2.105 is out.
2306         enum fix = string.init;
2307     }
2308 
2309     if (!uda.acceptedEventTypes.length)
2310     {
2311         enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2312             "but it is not declared to accept any `IRCEvent.Type`s";
2313         immutable message = pattern.format(uda.fqn).idup;
2314         assert(0, message);
2315     }
2316 
2317     foreach (immutable type; uda.acceptedEventTypes)
2318     {
2319         if (type == IRCEvent.Type.UNSET)
2320         {
2321             enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2322                 "accepting `IRCEvent.Type.UNSET`, which is not a valid event type";
2323             immutable message = pattern.format(uda.fqn).idup;
2324             assert(0, message);
2325         }
2326         else if (type == IRCEvent.Type.PRIVMSG)
2327         {
2328             enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2329                 "accepting `IRCEvent.Type.PRIVMSG`, which is not a valid event type. " ~
2330                 "Use `IRCEvent.Type.CHAN` and/or `IRCEvent.Type.QUERY` instead";
2331             immutable message = pattern.format(uda.fqn).idup;
2332             assert(0, message);
2333         }
2334         else if (type == IRCEvent.Type.WHISPER)
2335         {
2336             enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2337                 "accepting `IRCEvent.Type.WHISPER`, which is not a valid event type. " ~
2338                 "Use `IRCEvent.Type.QUERY` instead";
2339             immutable message = pattern.format(uda.fqn).idup;
2340             assert(0, message);
2341         }
2342         else if ((type == IRCEvent.Type.ANY) && (uda.channelPolicy != ChannelPolicy.any))
2343         {
2344             enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2345                 "accepting `IRCEvent.Type.ANY` and is at the same time not annotated " ~
2346                 "`ChannelPolicy.any`, which is the only accepted combination";
2347             immutable message = pattern.format(uda.fqn).idup;
2348             assert(0, message);
2349         }
2350 
2351         if (uda.commands.length || uda.regexes.length)
2352         {
2353             if (
2354                 (type != IRCEvent.Type.CHAN) &&
2355                 (type != IRCEvent.Type.QUERY) &&
2356                 (type != IRCEvent.Type.SELFCHAN) &&
2357                 (type != IRCEvent.Type.SELFQUERY))
2358             {
2359                 import lu.conv : Enum;
2360 
2361                 enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2362                     "listening for a `Command` and/or `Regex`, but is at the " ~
2363                     "same time accepting non-message `IRCEvent.Type.%s events`";
2364                 immutable message = pattern.format(
2365                     uda.fqn,
2366                     Enum!(IRCEvent.Type).toString(type)).idup;
2367                 assert(0, message);
2368             }
2369         }
2370     }
2371 
2372     if (uda.commands.length)
2373     {
2374         import lu.string : contains;
2375 
2376         foreach (const command; uda.commands)
2377         {
2378             if (!command._word.length)
2379             {
2380                 enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2381                     "listening for a `Command` with an empty (or unspecified) trigger word";
2382                 immutable message = pattern.format(uda.fqn).idup;
2383                 assert(0, message);
2384             }
2385             else if (command._word.contains(' '))
2386             {
2387                 enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2388                     "listening for a `Command` whose trigger " ~
2389                     `word "%s" contains a space character`;
2390                 immutable message = pattern.format(uda.fqn, command._word).idup;
2391                 assert(0, message);
2392             }
2393         }
2394     }
2395 
2396     if (uda.regexes.length)
2397     {
2398         foreach (const regex; uda.regexes)
2399         {
2400             import lu.string : contains;
2401 
2402             if (!regex._expression.length)
2403             {
2404                 enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2405                     "listening for a `Regex` with an empty (or unspecified) expression";
2406                 immutable message = pattern.format(uda.fqn).idup;
2407                 assert(0, message);
2408             }
2409             else if (
2410                 (regex._policy != PrefixPolicy.direct) &&
2411                 regex._expression.contains(' '))
2412             {
2413                 enum pattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2414                     "listening for a non-`PrefixPolicy.direct`-annotated " ~
2415                     "`Regex` with an expression containing spaces";
2416                 immutable message = pattern.format(uda.fqn).idup;
2417                 assert(0, message);
2418             }
2419         }
2420     }
2421 
2422     // The below is done inside onEventImpl as it needs template access to the module
2423     /*if ((uda._permissionsRequired != Permissions.ignore) &&
2424         !__traits(compiles, { alias _ = .hasMinimalAuthentication; }))
2425     {
2426         import std.format : format;
2427 
2428         enum pattern = "`%s` is missing a `MinimalAuthentication` " ~
2429             "mixin (needed for `Permissions` checks)";
2430         immutable message = pattern.format(module_);
2431         assert(0, message);
2432     }*/
2433 }
2434 
2435 
2436 // assertSaneStorageClasses
2437 /++
2438     Statically asserts that a parameter storage class is not `ref`
2439     if `inFiber`, and neither `ref` nor `out` if not `inFiber`.
2440 
2441     Take the storage class as a template parameter and statically
2442     assert inside this function, unlike how `udaSanityCheck` returns
2443     false on failure, so we can format and print the error message
2444     once here (instead of at all call sites upon receiving false).
2445 
2446     Params:
2447         storageClass = The storage class of the parameter.
2448         paramIsConst = Whether or not the parameter is `const`.
2449         inFiber = Whether or not the event handler is annotated `.fiber(true)`.
2450         module_ = The module name of the plugin.
2451         typestring = The signature string of the function.
2452 
2453     Returns:
2454         `true` if the storage class is valid; asserts `0` if not.
2455  +/
2456 auto assertSaneStorageClasses(
2457     const ParameterStorageClass storageClass,
2458     const bool paramIsConst,
2459     const bool inFiber,
2460     const string module_,
2461     const string typestring)
2462 {
2463     import std.format : format;
2464 
2465     static if (__VERSION__ <= 2104L)
2466     {
2467         /++
2468             There's something wrong with how the assert message is printed from CTFE.
2469             Work around it somewhat by prepending a backtick.
2470 
2471             https://issues.dlang.org/show_bug.cgi?id=24036
2472          +/
2473         enum fix = "`";
2474     }
2475     else
2476     {
2477         // Hopefully no need past 2.104... Update when 2.105 is out.
2478         enum fix = string.init;
2479     }
2480 
2481     if (inFiber)
2482     {
2483         if (storageClass & ParameterStorageClass.ref_)
2484         {
2485             enum pattern = fix ~ "`%s` has a `%s` event handler annotated `.fiber(true)` " ~
2486                 "that takes an `IRCEvent` by `ref`, which is a combination prone " ~
2487                 "to memory corruption. Pass by value instead";
2488             immutable message = pattern.format(module_, typestring).idup;
2489             assert(0, message);
2490         }
2491     }
2492     else if (!paramIsConst)
2493     {
2494         if (
2495             (storageClass & ParameterStorageClass.ref_) ||
2496             (storageClass & ParameterStorageClass.out_))
2497         {
2498             enum pattern = fix ~ "`%s` has a `%s` event handler that takes an " ~
2499                 "`IRCEvent` of an unsupported storage class; " ~
2500                 "may not be mutable `ref` or `out`";
2501             immutable message = pattern.format(module_, typestring).idup;
2502             assert(0, message);
2503         }
2504     }
2505 
2506     return true;
2507 }
2508 
2509 
2510 // IRCPluginState
2511 /++
2512     An aggregate of all variables that make up the common state of plugins.
2513 
2514     This neatly tidies up the amount of top-level variables in each plugin
2515     module. This allows for making more or less all functions top-level
2516     functions, since any state could be passed to it with variables of this type.
2517 
2518     Plugin-specific state should be kept inside the [IRCPlugin] subclass itself.
2519 
2520     See_Also:
2521         [IRCPlugin]
2522  +/
2523 struct IRCPluginState
2524 {
2525 private:
2526     import kameloso.pods : ConnectionSettings, CoreSettings, IRCBot;
2527     import kameloso.thread : ScheduledDelegate, ScheduledFiber;
2528     import std.concurrency : Tid;
2529     import core.thread : Fiber;
2530 
2531     /++
2532         Numeric ID of the current connection, to disambiguate between multiple
2533         connections in one program run. Private value.
2534      +/
2535     uint _connectionID;
2536 
2537 public:
2538     // Update
2539     /++
2540         Bitfield enum of what member of an instance of `IRCPluginState` was updated (if any).
2541      +/
2542     enum Update
2543     {
2544         /++
2545             Nothing marked as updated. Initial value.
2546          +/
2547         nothing  = 0,
2548 
2549         /++
2550             [IRCPluginState.bot] was marked as updated.
2551          +/
2552         bot      = 1 << 0,
2553 
2554         /++
2555             [IRCPluginState.client] was marked as updated.
2556          +/
2557         client   = 1 << 1,
2558 
2559         /++
2560             [IRCPluginState.server] was marked as updated.
2561          +/
2562         server   = 1 << 2,
2563 
2564         /++
2565             [IRCPluginState.settings] was marked as updated.
2566          +/
2567         settings = 1 << 3,
2568     }
2569 
2570     // client
2571     /++
2572         The current [dialect.defs.IRCClient|IRCClient], containing information
2573         pertaining to the bot in the context of a client connected to an IRC server.
2574      +/
2575     IRCClient client;
2576 
2577     // server
2578     /++
2579         The current [dialect.defs.IRCServer|IRCServer], containing information
2580         pertaining to the bot in the context of an IRC server.
2581      +/
2582     IRCServer server;
2583 
2584     // bot
2585     /++
2586         The current [kameloso.pods.IRCBot|IRCBot], containing information
2587         pertaining to the bot in the context of an IRC bot.
2588      +/
2589     IRCBot bot;
2590 
2591     // settings
2592     /++
2593         The current program-wide [kameloso.pods.CoreSettings|CoreSettings].
2594      +/
2595     CoreSettings settings;
2596 
2597     // connSettings
2598     /++
2599         The current program-wide [kameloso.pods.ConnectionSettings|ConnectionSettings].
2600      +/
2601     ConnectionSettings connSettings;
2602 
2603     // mainThread
2604     /++
2605         Thread ID to the main thread.
2606      +/
2607     Tid mainThread;
2608 
2609     // users
2610     /++
2611         Hashmap of IRC user details.
2612      +/
2613     IRCUser[string] users;
2614 
2615     // channels
2616     /++
2617         Hashmap of IRC channels.
2618      +/
2619     IRCChannel[string] channels;
2620 
2621     // pendingReplays
2622     /++
2623         Queued [dialect.defs.IRCEvent|IRCEvent]s to replay.
2624 
2625         The main loop iterates this after processing all on-event functions so
2626         as to know what nicks the plugin wants a WHOIS for. After the WHOIS
2627         response returns, the event bundled with the [Replay] will be replayed.
2628      +/
2629     Replay[][string] pendingReplays;
2630 
2631     // hasReplays
2632     /++
2633         Whether or not [pendingReplays] has elements (i.e. is not empty).
2634      +/
2635     bool hasPendingReplays;
2636 
2637     // readyReplays
2638     /++
2639         [Replay]s primed and ready to be replayed.
2640      +/
2641     Replay[] readyReplays;
2642 
2643     // awaitingFibers
2644     /++
2645         The list of awaiting [core.thread.fiber.Fiber|Fiber]s, keyed by
2646         [dialect.defs.IRCEvent.Type|IRCEvent.Type].
2647      +/
2648     Fiber[][] awaitingFibers;
2649 
2650     // awaitingDelegates
2651     /++
2652         The list of awaiting `void delegate(IRCEvent)` delegates, keyed by
2653         [dialect.defs.IRCEvent.Type|IRCEvent.Type].
2654      +/
2655     void delegate(IRCEvent)[][] awaitingDelegates;
2656 
2657     // scheduledFibers
2658     /++
2659         The list of scheduled [core.thread.fiber.Fiber|Fiber], UNIX time tuples.
2660      +/
2661     ScheduledFiber[] scheduledFibers;
2662 
2663     // scheduledDelegates
2664     /++
2665         The list of scheduled delegate, UNIX time tuples.
2666      +/
2667     ScheduledDelegate[] scheduledDelegates;
2668 
2669     // nextScheduledTimestamp
2670     /++
2671         The UNIX timestamp of when the next scheduled
2672         [kameloso.thread.ScheduledFiber|ScheduledFiber] or delegate should be triggered.
2673      +/
2674     long nextScheduledTimestamp = long.max;
2675 
2676     // updateSchedule
2677     /++
2678         Updates the saved UNIX timestamp of when the next scheduled
2679         [core.thread.fiber.Fiber|Fiber] or delegate should be triggered.
2680      +/
2681     void updateSchedule() pure nothrow @nogc
2682     {
2683         // Reset the next timestamp to an invalid value, then update it as we
2684         // iterate the fibers' and delegates' labels.
2685 
2686         nextScheduledTimestamp = long.max;
2687 
2688         foreach (const scheduledFiber; scheduledFibers)
2689         {
2690             if (scheduledFiber.timestamp < nextScheduledTimestamp)
2691             {
2692                 nextScheduledTimestamp = scheduledFiber.timestamp;
2693             }
2694         }
2695 
2696         foreach (const scheduledDg; scheduledDelegates)
2697         {
2698             if (scheduledDg.timestamp < nextScheduledTimestamp)
2699             {
2700                 nextScheduledTimestamp = scheduledDg.timestamp;
2701             }
2702         }
2703     }
2704 
2705     // previousWhoisTimestamps
2706     /++
2707         A copy of the main thread's `previousWhoisTimestamps` associative arrays
2708         of UNIX timestamps of when someone had a WHOIS query aimed at them, keyed
2709         by nickname.
2710      +/
2711     long[string] previousWhoisTimestamps;
2712 
2713     // updates
2714     /++
2715         Bitfield of in what way the plugin state was altered during postprocessing
2716         or event handler execution.
2717 
2718         Example:
2719         ---
2720         if (state.updates & IRCPluginState.Update.bot)
2721         {
2722             // state.bot was marked as updated
2723             state.updates |= IRCPluginState.Update.server;
2724             // state.server now marked as updated
2725         }
2726         ---
2727      +/
2728     Update updates;
2729 
2730     // abort
2731     /++
2732         Pointer to the global abort flag.
2733      +/
2734     bool* abort;
2735 
2736     // connectionID
2737     /++
2738         Numeric ID of the current connection, to disambiguate between multiple
2739         connections in one program run. Accessor.
2740 
2741         Returns:
2742             The numeric ID of the current connection.
2743      +/
2744     pragma(inline, true)
2745     auto connectionID() const
2746     {
2747         return _connectionID;
2748     }
2749 
2750     // this
2751     /++
2752         Constructor taking a connection ID `uint`.
2753      +/
2754     this(const uint connectionID)
2755     {
2756         this._connectionID = connectionID;
2757     }
2758 
2759     // specialRequests
2760     /++
2761         This plugin's array of [SpecialRequest]s.
2762      +/
2763     SpecialRequest[] specialRequests;
2764 }
2765 
2766 
2767 // Replay
2768 /++
2769     Embodies the notion of an event to be replayed, once we know more about a user
2770     (meaning after a WHOIS query response).
2771  +/
2772 struct Replay
2773 {
2774     // caller
2775     /++
2776         Name of the caller function or similar context.
2777      +/
2778     string caller;
2779 
2780     // event
2781     /++
2782         Stored [dialect.defs.IRCEvent|IRCEvent] to replay.
2783      +/
2784     IRCEvent event;
2785 
2786     // permissionsRequired
2787     /++
2788         [Permissions] required by the function to replay.
2789      +/
2790     Permissions permissionsRequired;
2791 
2792     // dg
2793     /++
2794         Delegate, whose context includes the plugin to whom this [Replay] relates.
2795      +/
2796     void delegate(Replay) dg;
2797 
2798     // timestamp
2799     /++
2800         When this request was issued.
2801      +/
2802     long timestamp;
2803 
2804     /++
2805         Creates a new [Replay] with a timestamp of the current time.
2806      +/
2807     this(
2808         void delegate(Replay) dg,
2809         const ref IRCEvent event,
2810         const Permissions permissionsRequired,
2811         const string caller)
2812     {
2813         this.timestamp = event.time;
2814         this.dg = dg;
2815         this.event = event;
2816         this.permissionsRequired = permissionsRequired;
2817         this.caller = caller;
2818     }
2819 }
2820 
2821 
2822 // filterResult
2823 /++
2824     The tristate results from comparing a username with the admin or
2825     whitelist/elevated/operator/staff lists.
2826  +/
2827 enum FilterResult
2828 {
2829     /++
2830         The user is not allowed to trigger this function.
2831      +/
2832     fail,
2833 
2834     /++
2835         The user is allowed to trigger this function.
2836      +/
2837     pass,
2838 
2839     /++
2840         We don't know enough to say whether the user is allowed to trigger this
2841         function, so do a WHOIS query and act based on the results.
2842      +/
2843     whois,
2844 }
2845 
2846 
2847 // PrefixPolicy
2848 /++
2849     In what way the contents of a [dialect.defs.IRCEvent|IRCEvent] must start
2850     (be "prefixed") for an annotated function to be allowed to trigger.
2851  +/
2852 enum PrefixPolicy
2853 {
2854     /++
2855         The annotated event handler will not examine the [dialect.defs.IRCEvent.content|IRCEvent.content]
2856         member at all and will always trigger, as long as all other annotations match.
2857      +/
2858     direct,
2859 
2860     /++
2861         The annotated event handler will only trigger if the
2862         [dialect.defs.IRCEvent.content|IRCEvent.content] member starts with the
2863         [kameloso.pods.CoreSettings.prefix|CoreSettings.prefix] (e.g. "!").
2864         All other annotations must also match.
2865      +/
2866     prefixed,
2867 
2868     /++
2869         The annotated event handler will only trigger if the
2870         [dialect.defs.IRCEvent.content|IRCEvent.content] member starts with the
2871         bot's name, as if addressed to it.
2872 
2873         In [dialect.defs.IRCEvent.Type.QUERY|QUERY] events this instead behaves as
2874         [PrefixPolicy.direct].
2875      +/
2876     nickname,
2877 }
2878 
2879 
2880 // ChannelPolicy
2881 /++
2882     Whether an annotated function should be allowed to trigger on events in only
2883     home channels or in guest ones as well.
2884  +/
2885 enum ChannelPolicy
2886 {
2887     /++
2888         The annotated function will only be allowed to trigger if the event
2889         happened in a home channel, where applicable. Not all events carry channels.
2890      +/
2891     home,
2892 
2893     /++
2894         The annotated function will only be allowed to trigger if the event
2895         happened in a guest channel, where applicable. Not all events carry channels.
2896      +/
2897     guest,
2898 
2899     /++
2900         The annotated function will be allowed to trigger regardless of channel.
2901      +/
2902     any,
2903 }
2904 
2905 
2906 // Permissions
2907 /++
2908     What level of permissions is needed to trigger an event handler.
2909 
2910     In any event handler context, the triggering user has a *level of privilege*.
2911     This decides whether or not they are allowed to trigger the function.
2912     Put simply this is the "barrier of entry" for event handlers.
2913 
2914     Permissions are set on a per-channel basis and are stored in the "users.json"
2915     file in the resource directory.
2916  +/
2917 enum Permissions
2918 {
2919     /++
2920         Override privilege checks, allowing anyone to trigger the annotated function.
2921      +/
2922     ignore = 0,
2923 
2924     /++
2925         Anyone not explicitly blacklisted (with a
2926         [dialect.defs.IRCUser.Class.blacklist|IRCUser.Class.blacklist]
2927         classifier) may trigger the annotated function. As such, to know if they're
2928         blacklisted, unknown users will first be looked up with a WHOIS query
2929         before allowing the function to trigger.
2930      +/
2931     anyone = 10,
2932 
2933     /++
2934         Anyone logged onto services may trigger the annotated function.
2935      +/
2936     registered = 20,
2937 
2938     /++
2939         Only users with a [dialect.defs.IRCUser.Class.whitelist|IRCUser.Class.whitelist]
2940         classifier (or higher) may trigger the annotated function.
2941      +/
2942     whitelist = 30,
2943 
2944     /++
2945         Only users with a [dialect.defs.IRCUser.Class.elevated|IRCUser.Class.elevated]
2946         classifier (or higher) may trigger the annotated function.
2947      +/
2948     elevated = 40,
2949 
2950     /++
2951         Only users with a [dialect.defs.IRCUser.Class.operator|IRCUser.Class.operator]
2952         classifier (or higiher) may trigger the annotated function.
2953 
2954         Note: this does not mean IRC "+o" operators.
2955      +/
2956     operator = 50,
2957 
2958     /++
2959         Only users with a [dialect.defs.IRCUser.Class.staff|IRCUser.Class.staff]
2960         classifier (or higher) may trigger the annotated function.
2961 
2962         These are channel owners.
2963      +/
2964     staff = 60,
2965 
2966     /++
2967         Only users defined in the configuration file as an administrator may
2968         trigger the annotated function.
2969      +/
2970     admin = 100,
2971 }
2972 
2973 
2974 // Timing
2975 /++
2976     Declaration of what order event handler function should be given with respects
2977     to other functions in the same plugin module.
2978  +/
2979 enum Timing
2980 {
2981     /++
2982         No timing.
2983      +/
2984     untimed,
2985 
2986     /++
2987         To be executed during setup; the first thing to happen.
2988      +/
2989     setup,
2990 
2991     /++
2992         To be executed after setup but before normal event handlers.
2993      +/
2994     early,
2995 
2996     /++
2997         To be executed after normal event handlers.
2998      +/
2999     late,
3000 
3001     /++
3002         To be executed last before execution moves on to the next plugin.
3003      +/
3004     cleanup,
3005 }
3006 
3007 
3008 // IRCEventHandler
3009 /++
3010     Aggregate to annotate event handler functions with, to control what they do
3011     and how they work.
3012  +/
3013 struct IRCEventHandler
3014 {
3015 private:
3016     import kameloso.traits : UnderscoreOpDispatcher;
3017 
3018 public:
3019     // acceptedEventTypes
3020     /++
3021         Array of types of [dialect.defs.IRCEvent] that the annotated event
3022         handler function should accept.
3023      +/
3024     IRCEvent.Type[] acceptedEventTypes;
3025 
3026     // _onEvent
3027     /++
3028         Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3029         [acceptedEventTypes] but by the name `onEvent`.
3030      +/
3031     alias _onEvent = acceptedEventTypes;
3032 
3033     // _permissionsRequired
3034     /++
3035         Permissions required of instigating user, below which the annotated
3036         event handler function should not be triggered.
3037      +/
3038     Permissions _permissionsRequired = Permissions.ignore;
3039 
3040     // _channelPolicy
3041     /++
3042         What kind of channel the annotated event handler function may be
3043         triggered in; homes or mere guest channels.
3044      +/
3045     ChannelPolicy _channelPolicy = ChannelPolicy.home;
3046 
3047     // commands
3048     /++
3049         Array of [IRCEventHandler.Command]s the bot should pick up and listen for.
3050      +/
3051     Command[] commands;
3052 
3053     // _addCommand
3054     /++
3055         Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3056         [commands] but by the name `addCommand`.
3057      +/
3058     alias _addCommand = commands;
3059 
3060     // regexes
3061     /++
3062         Array of [IRCEventHandler.Regex]es the bot should pick up and listen for.
3063      +/
3064     Regex[] regexes;
3065 
3066     // _addRegex
3067     /++
3068         Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3069         [regexes] but by the name `addRegex`.
3070      +/
3071     alias _addRegex = regexes;
3072 
3073     // _chainable
3074     /++
3075         Whether or not the annotated event handler function should allow other
3076         functions to fire after it. If not set (default false), it will
3077         terminate and move on to the next plugin after the function returns.
3078      +/
3079     bool _chainable;
3080 
3081     // _verbose
3082     /++
3083         Whether or not additional information should be output to the local
3084         terminal as the function is (or is not) triggered.
3085      +/
3086     bool _verbose;
3087 
3088     // _when
3089     /++
3090         Special instruction related to the order of which event handler functions
3091         within a plugin module are triggered.
3092      +/
3093     Timing _when;
3094 
3095     // _fiber
3096     /++
3097         Whether or not the annotated event handler should be run from within a
3098         [core.thread.fiber.Fiber|Fiber].
3099      +/
3100     bool _fiber;
3101 
3102     // acceptedEventTypeMap
3103     /++
3104         Array of accepted [dialect.defs.IRCEvent.Type|IRCEvent.Type]s.
3105      +/
3106     bool[] acceptedEventTypeMap;
3107 
3108     // generateTypemap
3109     /++
3110         Generates [acceptedEventTypeMap] from [acceptedEventTypes].
3111      +/
3112     void generateTypemap() pure @safe nothrow
3113     {
3114         assert(__ctfe, "generateTypemap called outside CTFE");
3115 
3116         foreach (immutable type; acceptedEventTypes)
3117         {
3118             if (type >= acceptedEventTypeMap.length) acceptedEventTypeMap.length = type+1;
3119             acceptedEventTypeMap[type] = true;
3120         }
3121     }
3122 
3123     mixin UnderscoreOpDispatcher;
3124 
3125     // fqn
3126     /++
3127         Fully qualified name of the function the annotated [IRCEventHandler] is attached to.
3128      +/
3129     string fqn;
3130 
3131     // Command
3132     /++
3133         Embodies the notion of a chat command, e.g. `!hello`.
3134      +/
3135     static struct Command
3136     {
3137         // _policy
3138         /++
3139             In what way the message is required to start for the annotated function to trigger.
3140          +/
3141         PrefixPolicy _policy = PrefixPolicy.prefixed;
3142 
3143         // _word
3144         /++
3145             The command word, without spaces.
3146          +/
3147         string _word;
3148 
3149         // _description
3150         /++
3151             Describes the functionality of the event handler function the parent
3152             [IRCEventHandler] annotates, and by extension, this [IRCEventHandler.Command].
3153 
3154             Specifically this is used to describe functions triggered by
3155             [IRCEventHandler.Command]s, in the help listing routine in [kameloso.plugins.chatbot].
3156          +/
3157         string _description;
3158 
3159         // _hidden
3160         /++
3161             Whether this is a hidden command or if it should show up in help listings.
3162          +/
3163         bool _hidden;
3164 
3165         // syntaxes
3166         /++
3167             Command usage syntax help strings.
3168          +/
3169         string[] syntaxes;
3170 
3171         // _addSyntax
3172         /++
3173             Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3174             [syntaxes] but by the name `addSyntax`.
3175          +/
3176         alias _addSyntax = syntaxes;
3177 
3178         mixin UnderscoreOpDispatcher;
3179     }
3180 
3181     // Regex
3182     /++
3183         Embodies the notion of a chat command regular expression, e.g. `![Hh]ello+`.
3184      +/
3185     static struct Regex
3186     {
3187     private:
3188         import std.regex : StdRegex = Regex;
3189 
3190     public:
3191         // _policy
3192         /++
3193             In what way the message is required to start for the annotated function to trigger.
3194          +/
3195         PrefixPolicy _policy = PrefixPolicy.direct;
3196 
3197         // engine
3198         /++
3199             Regex engine to match incoming messages with.
3200          +/
3201         StdRegex!char engine;
3202 
3203         // _expression
3204         /++
3205             The regular expression in string form.
3206          +/
3207         string _expression;
3208 
3209         // _description
3210         /++
3211             Describes the functionality of the event handler function the parent
3212             [IRCEventHandler] annotates, and by extension, this [IRCEventHandler.Regex].
3213 
3214             Specifically this is used to describe functions triggered by
3215             [IRCEventHandler.Command]s, in the help listing routine in [kameloso.plugins.chatbot].
3216          +/
3217         string _description;
3218 
3219         // _hidden
3220         /++
3221             Whether this is a hidden command or if it should show up in help listings.
3222          +/
3223         bool _hidden;
3224 
3225         // _expression
3226         /++
3227             The regular expression this [IRCEventHandler.Regex] embodies, in string form.
3228 
3229             Upon setting this a regex engine is also created. Because of this extra step we
3230             cannot rely on [kameloso.traits.UnderscoreOpDispatcher|UnderscoreOpDispatcher]
3231             to redirect calls.
3232 
3233             Example:
3234             ---
3235             Regex()
3236                 .expression(r"(?:^|\s)MonkaS(?:$|\s)")
3237                 .description("Detects MonkaS.")
3238             ---
3239 
3240             Params:
3241                 expression = New regular expression string.
3242 
3243             Returns:
3244                 A `this` reference to the current struct instance.
3245          +/
3246         ref auto expression()(const string expression)
3247         {
3248             import std.regex : regex;
3249 
3250             this._expression = expression;
3251             this.engine = expression.regex;
3252             return this;
3253         }
3254 
3255         mixin UnderscoreOpDispatcher;
3256     }
3257 }
3258 
3259 
3260 // SpecialRequest
3261 /++
3262     Embodies the notion of a special request a plugin issues to the main thread.
3263  +/
3264 interface SpecialRequest
3265 {
3266 private:
3267     import core.thread : Fiber;
3268 
3269 public:
3270     // context
3271     /++
3272         String context of the request.
3273      +/
3274     string context();
3275 
3276     // fiber
3277     /++
3278         Fiber embedded into the request.
3279      +/
3280     Fiber fiber();
3281 }
3282 
3283 
3284 // SpecialRequestImpl
3285 /++
3286     Concrete implementation of a [SpecialRequest].
3287 
3288     The template parameter `T` defines that kind of
3289     [kameloso.thread.CarryingFiber|CarryingFiber] is embedded into it.
3290 
3291     Params:
3292         T = Type to instantiate the [kameloso.thread.CarryingFiber|CarryingFiber] with.
3293  +/
3294 final class SpecialRequestImpl(T) : SpecialRequest
3295 {
3296 private:
3297     import kameloso.thread : CarryingFiber;
3298     import core.thread : Fiber;
3299 
3300     /++
3301         Private context string.
3302      +/
3303     string _context;
3304 
3305     /++
3306         Private [kameloso.thread.CarryingFiber|CarryingFiber].
3307      +/
3308     CarryingFiber!T _fiber;
3309 
3310 public:
3311     // this
3312     /++
3313         Constructor.
3314 
3315         Params:
3316             context = String context of the request.
3317             fiber = [kameloso.thread.CarryingFiber|CarryingFiber] to embed into the request.
3318      +/
3319     this(string context, CarryingFiber!T fiber)
3320     {
3321         this._context = context;
3322         this._fiber = fiber;
3323     }
3324 
3325     // this
3326     /++
3327         Constructor.
3328 
3329         Params:
3330             context = String context of the request.
3331             dg = Delegate to create a [kameloso.thread.CarryingFiber|CarryingFiber] from.
3332      +/
3333     this(string context, void delegate() dg)
3334     {
3335         import kameloso.constants : BufferSize;
3336 
3337         this._context = context;
3338         this._fiber = new CarryingFiber!T(dg, BufferSize.fiberStack);
3339     }
3340 
3341     // context
3342     /++
3343         String context of the request. May be anything; highly request-specific.
3344 
3345         Returns:
3346             A string.
3347      +/
3348     string context()
3349     {
3350         return _context;
3351     }
3352 
3353     // fiber
3354     /++
3355         [kameloso.thread.CarryingFiber|CarryingFiber] embedded into the request.
3356 
3357         Returns:
3358             A [kameloso.thread.CarryingFiber|CarryingFiber] in the guise of a
3359             [core.thread.Fiber|Fiber].
3360      +/
3361     Fiber fiber()
3362     {
3363         return _fiber;
3364     }
3365 }
3366 
3367 
3368 // specialRequest
3369 /++
3370     Instantiates a [SpecialRequestImpl] in the guise of a [SpecialRequest]
3371     with the implicit type `T` as payload.
3372 
3373     Params:
3374         T = Type to instantiate [SpecialRequestImpl] with.
3375         context = String context of the request.
3376         fiber = [kameloso.thread.CarryingFiber|CarryingFiber] to embed into the request.
3377 
3378     Returns:
3379         A new [SpecialRequest] that is in actually a [SpecialRequestImpl].
3380  +/
3381 SpecialRequest specialRequest(T)(const string context, CarryingFiber!T fiber)
3382 {
3383     return new SpecialRequestImpl!T(context, fiber);
3384 }
3385 
3386 
3387 // specialRequest
3388 /++
3389     Instantiates a [SpecialRequestImpl] in the guise of a [SpecialRequest]
3390     with the explicit type `T` as payload.
3391 
3392     Params:
3393         T = Type to instantiate [SpecialRequestImpl] with.
3394         context = String context of the request.
3395         dg = Delegate to create a [kameloso.thread.CarryingFiber|CarryingFiber] from.
3396 
3397     Returns:
3398         A new [SpecialRequest] that is in actually a [SpecialRequestImpl].
3399  +/
3400 SpecialRequest specialRequest(T)(const string context, void delegate() dg)
3401 {
3402     return new SpecialRequestImpl!T(context, dg);
3403 }
3404 
3405 
3406 // Settings
3407 /++
3408     Annotation denoting that a struct variable or struct type is to be considered
3409     as housing settings for a plugin, and should thus be serialised and saved in
3410     the configuration file.
3411  +/
3412 enum Settings;
3413 
3414 
3415 // Resource
3416 /++
3417     Annotation denoting that a variable is the basename of a resource file or directory.
3418  +/
3419 struct Resource
3420 {
3421     /++
3422         Subdirectory in which to put the annotated filename.
3423      +/
3424     string subdirectory;
3425 }
3426 
3427 
3428 // Configuration
3429 /++
3430     Annotation denoting that a variable is the basename of a configuration
3431     file or directory.
3432  +/
3433 struct Configuration
3434 {
3435     /++
3436         Subdirectory in which to put the annotated filename.
3437      +/
3438     string subdirectory;
3439 }
3440 
3441 
3442 // Enabler
3443 /++
3444     Annotation denoting that a variable enables and disables a plugin.
3445  +/
3446 enum Enabler;