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 modulekameloso.plugins.common.core;
53 54 private:
55 56 importkameloso.thread : CarryingFiber;
57 importdialect.defs;
58 importstd.traits : ParameterStorageClass;
59 importstd.typecons : Flag, No, Yes;
60 importcore.thread : Fiber;
61 62 public:
63 64 65 // IRCPlugin66 /++
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 abstractclassIRCPlugin76 {
77 @safe:
78 79 private:
80 importkameloso.thread : Sendable;
81 importstd.array : Appender;
82 83 public:
84 // CommandMetadata85 /++
86 Metadata about a [IRCEventHandler.Command]- and/or
87 [IRCEventHandler.Regex]-annotated event handler.
88 89 See_Also:
90 [IRCPlugin.commands]
91 +/92 staticstructCommandMetadata93 {
94 // policy95 /++
96 Prefix policy of this command.
97 +/98 PrefixPolicypolicy;
99 100 // description101 /++
102 Description about what the command does, in natural language.
103 +/104 stringdescription;
105 106 // syntaxes107 /++
108 Syntaxes on how to use the command.
109 +/110 string[] syntaxes;
111 112 // hidden113 /++
114 Whether or not the command should be hidden from view (but still
115 possible to trigger).
116 +/117 boolhidden;
118 119 // isRegex120 /++
121 Whether or not the command is based on an `IRCEventHandler.Regex`.
122 +/123 boolisRegex;
124 125 // this126 /++
127 Constructor taking an [IRCEventHandler.Command].
128 129 Do not touch [syntaxes]; populate them at the call site.
130 +/131 this(constIRCEventHandler.Commandcommand) pure @safenothrow @nogc132 {
133 this.policy = command.policy;
134 this.description = command.description;
135 this.hidden = command.hidden;
136 //this.isRegex = false;137 }
138 139 // this140 /++
141 Constructor taking an [IRCEventHandler.Regex].
142 143 Do not touch [syntaxes]; populate them at the call site.
144 +/145 this(constIRCEventHandler.Regexregex) pure @safenothrow @nogc146 {
147 this.policy = regex.policy;
148 this.description = regex.description;
149 this.hidden = regex.hidden;
150 this.isRegex = true;
151 }
152 }
153 154 // state155 /++
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 IRCPluginStatestate;
160 161 // postprocess162 /++
163 Allows a plugin to modify an event post-parsing.
164 +/165 voidpostprocess(refIRCEventevent) @system;
166 167 // onEvent168 /++
169 Called to let the plugin react to a new event, parsed from the server.
170 +/171 voidonEvent(constrefIRCEventevent) @system;
172 173 // initResources174 /++
175 Called when the plugin is requested to initialise its disk resources.
176 +/177 voidinitResources() @system;
178 179 // deserialiseConfigFrom180 /++
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 voiddeserialiseConfigFrom(
188 conststringconfigFile,
189 outstring[][string] missingEntries,
190 outstring[][string] invalidEntries);
191 192 // serialiseConfigInto193 /++
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 boolserialiseConfigInto(refAppender!(char[]) sink) const;
200 201 // setSettingByName202 /++
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 boolsetSettingByName(conststringsetting, conststringvalue);
209 210 // setup211 /++
212 Called at program start but before connection has been established.
213 +/214 voidsetup() @system;
215 216 // start217 /++
218 Called when connection has been established.
219 +/220 voidstart() @system;
221 222 // printSettings223 /++
224 Called when we want a plugin to print its [Settings]-annotated struct of settings.
225 +/226 voidprintSettings() @systemconst;
227 228 // teardown229 /++
230 Called during shutdown of a connection; a plugin's would-be destructor.
231 +/232 voidteardown() @system;
233 234 // name235 /++
236 Returns the name of the plugin.
237 238 Returns:
239 The string name of the plugin.
240 +/241 stringname() @propertyconstpurenothrow @nogc;
242 243 // commands244 /++
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() purenothrow @propertyconst;
251 252 // channelSpecificCommands253 /++
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(conststring) @system;
261 262 // reload263 /++
264 Reloads the plugin, where such is applicable.
265 266 Whatever this does is implementation-defined.
267 +/268 voidreload() @system;
269 270 // onBusMessage271 /++
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 voidonBusMessage(conststringheader, sharedSendablecontent) @system;
278 279 // isEnabled280 /++
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 boolisEnabled() const @propertypurenothrow @nogc;
287 }
288 289 290 // IRCPluginImpl291 /++
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 mixintemplateIRCPluginImpl(
320 Flag!"debug_"debug_ = No.debug_,
321 stringmodule_ = __MODULE__)
322 {
323 privateimportkameloso.plugins.common.core : FilterResult, IRCEventHandler, IRCPluginState, Permissions;
324 privateimportdialect.defs : IRCEvent, IRCServer, IRCUser;
325 privateimportlu.traits : getSymbolsByUDA;
326 privateimportstd.array : Appender;
327 privateimportstd.meta : AliasSeq;
328 privateimportstd.traits : getUDAs;
329 privateimportcore.thread : Fiber;
330 331 staticif (__traits(compiles, { alias_ = this.hasIRCPluginImpl; }))
332 {
333 importstd.format : format;
334 335 enumpattern = "Double mixin of `%s` in `%s`";
336 enummessage = pattern.format("IRCPluginImpl", typeof(this).stringof);
337 staticassert(0, message);
338 }
339 else340 {
341 /++
342 Marker declaring that [kameloso.plugins.common.core.IRCPluginImpl|IRCPluginImpl]
343 has been mixed in.
344 +/345 privateenumhasIRCPluginImpl = true;
346 }
347 348 mixin("private static import thisModule = ", module_, ";");
349 350 // Introspection351 /++
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 staticstructIntrospection357 {
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 aliasallEventHandlerFunctionsInModule = 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 staticimmutableallEventHandlerUDAsInModule = ()
371 {
372 IRCEventHandler[] udas;
373 udas.length = allEventHandlerFunctionsInModule.length;
374 375 foreach (immutablei, fun; allEventHandlerFunctionsInModule)
376 {
377 enumfqn = module_ ~ '.' ~ __traits(identifier, allEventHandlerFunctionsInModule[i]);
378 udas[i] = getUDAs!(fun, IRCEventHandler)[0];
379 udas[i].fqn = fqn;
380 debugudaSanityCheckCTFE(udas[i]);
381 udas[i].generateTypemap();
382 }
383 384 returnudas;
385 }();
386 }
387 388 @safe:
389 390 // isEnabled391 /++
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 overridepublicboolisEnabled() const @propertypurenothrow @nogc404 {
405 importkameloso.traits : udaIndexOf;
406 407 boolretval = true;
408 409 top:
410 foreach (immutablei, _; this.tupleof)
411 {
412 staticif (is(typeof(this.tupleof[i]) == struct))
413 {
414 enumtypeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
415 enumvalueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
416 417 staticif ((typeUDAIndex != -1) || (valueUDAIndex != -1))
418 {
419 foreach (immutablen, _2; this.tupleof[i].tupleof)
420 {
421 enumenablerUDAIndex = udaIndexOf!(this.tupleof[i].tupleof[n], Enabler);
422 423 staticif (enablerUDAIndex != -1)
424 {
425 aliasThisEnabler = typeof(this.tupleof[i].tupleof[n]);
426 427 staticif (!is(ThisEnabler : bool))
428 {
429 importstd.format : format;
430 importstd.traits : Unqual;
431 432 aliasUnqualThis = Unqual!(typeof(this));
433 enumpattern = "`%s` has a non-bool `Enabler`: `%s %s`";
434 enummessage = pattern.format(
435 UnqualThis.stringof,
436 ThisEnabler.stringof,
437 __traits(identifier, this.tupleof[i].tupleof[n]));
438 staticassert(0, message);
439 }
440 441 retval = this.tupleof[i].tupleof[n];
442 breaktop;
443 }
444 }
445 }
446 }
447 }
448 449 returnretval;
450 }
451 452 // allow453 /++
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 privateFilterResultallow(constrefIRCEventevent, constPermissionspermissionsRequired)
471 {
472 importkameloso.plugins.common.core : allowImpl;
473 returnallowImpl(this, event, permissionsRequired);
474 }
475 476 // onEvent477 /++
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 overridepublicvoidonEvent(constrefIRCEventevent) @system495 {
496 onEventImpl(event);
497 }
498 499 // onEventImpl500 /++
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 privatevoidonEventImpl(/*const ref*/IRCEventorigEvent) @system526 {
527 importkameloso.plugins.common.core : Timing;
528 529 // udaSanityCheckMinimal530 /++
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 debug536 staticbooludaSanityCheckMinimal(aliasfun, IRCEventHandleruda)()
537 {
538 staticif ((uda._permissionsRequired != Permissions.ignore) &&
539 !__traits(compiles, { alias_ = .hasMinimalAuthentication; }))
540 {
541 importstd.format : format;
542 543 enumpattern = "`%s` is missing a `MinimalAuthentication` " ~
544 "mixin (needed for `Permissions` checks)";
545 enummessage = pattern.format(module_);
546 staticassert(0, message);
547 }
548 549 returntrue;
550 }
551 552 // call553 /++
554 Calls the passed function pointer, appropriately.
555 +/556 voidcall(boolinFiber, Fun)(scopeFunfun, constrefIRCEventevent) scope557 {
558 importlu.traits : TakesParams;
559 importstd.traits : ParameterStorageClass, ParameterStorageClassTuple, Parameters, arity;
560 561 staticif (
562 TakesParams!(fun, typeof(this), IRCEvent) ||
563 TakesParams!(fun, IRCPlugin, IRCEvent))
564 {
565 debug566 {
567 staticassert(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 elsestaticif (
577 TakesParams!(fun, typeof(this)) ||
578 TakesParams!(fun, IRCPlugin))
579 {
580 fun(this);
581 }
582 elsestaticif (TakesParams!(fun, IRCEvent))
583 {
584 debug585 {
586 staticassert(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 elsestaticif (arity!fun == 0)
596 {
597 fun();
598 }
599 else600 {
601 importstd.format : format;
602 603 enumpattern = "`%s` has an event handler with an unsupported function signature: `%s`";
604 enummessage = pattern.format(module_, Fun.stringof);
605 staticassert(0, message);
606 }
607 }
608 609 // NextStep610 /++
611 Signal up the callstack of what to do next.
612 +/613 enumNextStep614 {
615 unset,
616 continue_,
617 repeat,
618 return_,
619 }
620 621 // process622 /++
623 Process a function.
624 +/625 autoprocess(boolverbose, boolinFiber, boolhasRegexes, Fun)
626 (scopeFunfun,
627 conststringfunName,
628 constIRCEventHandleruda,
629 refIRCEventevent) scope630 {
631 importstd.algorithm.searching : canFind;
632 633 staticif (verbose)
634 {
635 importlu.conv : Enum;
636 importstd.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 boolchannelMatch;
646 647 if (uda._channelPolicy == ChannelPolicy.home)
648 {
649 channelMatch = state.bot.homeChannels.canFind(event.channel);
650 }
651 elseif (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 staticif (verbose)
663 {
664 writeln(" ...ignore non-matching channel ", event.channel);
665 if (state.settings.flush) stdout.flush();
666 }
667 668 // channel policy does not match669 returnNextStep.continue_; // next fun670 }
671 }
672 673 // Snapshot content and aux for later restoration674 immutableorigContent = event.content; // don't strip675 typeof(IRCEvent.aux) origAux;
676 boolauxDirty;
677 678 scope(exit)
679 {
680 // Restore content and aux as they may have been altered681 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 importlu.string : strippedLeft;
692 693 if (state.settings.observerMode)
694 {
695 // Skip all commands696 returnNextStep.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 but704 // `event.content` is empty; cannot possibly be of interest.705 returnNextStep.continue_; // next function706 }
707 }
708 709 /// Whether or not a Command or Regex matched.710 boolcommandMatch;
711 712 // Evaluate each Command UDAs with the current event713 if (uda.commands.length)
714 {
715 commandForeach:
716 foreach (constcommand; uda.commands)
717 {
718 staticif (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 staticif (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 command733 continuecommandForeach;
734 }
735 else736 {
737 importlu.string : nom;
738 importstd.typecons : No, Yes;
739 importstd.uni : toLower;
740 741 immutablethisCommand = event.content742 .nom!(Yes.inherit, Yes.decode)(' ');
743 744 if (thisCommand.toLower == command._word.toLower)
745 {
746 staticif (verbose)
747 {
748 writeln(" ...command matches!");
749 if (state.settings.flush) stdout.flush();
750 }
751 752 if (!auxDirty)
753 {
754 origAux = event.aux; // copies755 auxDirty = true;
756 }
757 758 event.aux[$-1] = thisCommand;
759 commandMatch = true;
760 breakcommandForeach;
761 }
762 else763 {
764 // Restore content to pre-nom state765 event.content = origContent;
766 }
767 }
768 }
769 }
770 771 // Iff no match from Commands, evaluate Regexes772 staticif (hasRegexes)
773 {
774 if (/*uda.regexes.length &&*/ !commandMatch)
775 {
776 regexForeach:
777 foreach (constregex; uda.regexes)
778 {
779 staticif (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 staticif (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 regex794 continueregexForeach;
795 }
796 else797 {
798 try799 {
800 importstd.regex : matchFirst;
801 802 consthits = event.content.matchFirst(regex.engine);
803 804 if (!hits.empty)
805 {
806 staticif (verbose)
807 {
808 writeln(" ...expression matches!");
809 if (state.settings.flush) stdout.flush();
810 }
811 812 if (!auxDirty)
813 {
814 origAux = event.aux; // copies815 auxDirty = true;
816 }
817 818 event.aux[$-1] = hits[0];
819 commandMatch = true;
820 breakregexForeach;
821 }
822 else823 {
824 staticif (verbose)
825 {
826 enumpattern = ` ...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 (Exceptione)
833 {
834 staticif (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; skip851 staticif (verbose)
852 {
853 writeln(" ...no Command nor Regex match; continue funloop");
854 if (state.settings.flush) stdout.flush();
855 }
856 857 returnNextStep.continue_; // next function858 }
859 }
860 861 if (uda._permissionsRequired != Permissions.ignore)
862 {
863 staticif (verbose)
864 {
865 writeln(" ...Permissions.",
866 Enum!Permissions.toString(uda._permissionsRequired));
867 if (state.settings.flush) stdout.flush();
868 }
869 870 immutableresult = this.allow(event, uda._permissionsRequired);
871 872 staticif (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 down881 }
882 elseif (result == FilterResult.whois)
883 {
884 importkameloso.plugins.common.misc : enqueue;
885 importlu.traits : TakesParams;
886 importstd.traits : arity;
887 888 staticif (verbose)
889 {
890 writefln(" ...%s WHOIS", typeof(this).stringof);
891 if (state.settings.flush) stdout.flush();
892 }
893 894 staticif (
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 IRCPlugin903 // now despite typeof(this) being a subclass...904 enqueue(this, event, uda._permissionsRequired, uda._fiber, fun, funName);
905 returnuda._chainable ? NextStep.continue_ : NextStep.return_;
906 }
907 else908 {
909 importstd.format : format;
910 911 enumpattern = "`%s` has an event handler with an unsupported function signature: `%s`";
912 enummessage = pattern.format(module_, Fun.stringof);
913 staticassert(0, message);
914 }
915 }
916 else/*if (result == FilterResult.fail)*/917 {
918 returnuda._chainable ? NextStep.continue_ : NextStep.return_;
919 }
920 }
921 922 staticif (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 staticif (Fun.stringof[$-5..$] == "@safe")
936 {
937 enummessage = "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 else946 {
947 aliasSystemFun = Fun;
948 }
949 950 staticif (inFiber)
951 {
952 importkameloso.constants : BufferSize;
953 importkameloso.thread : CarryingFiber;
954 importcore.thread : Fiber;
955 956 autofiber = newCarryingFiber!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 destroy965 destroy(fiber);
966 }
967 }
968 else969 {
970 call!(inFiber, SystemFun)(fun, event);
971 }
972 973 if (uda._chainable)
974 {
975 // onEvent found an event and triggered a function, but976 // it's Chainable and there may be more, so keep looking.977 returnNextStep.continue_;
978 }
979 else980 {
981 // The triggered function is not Chainable so return and982 // let the main loop continue with the next plugin.983 returnNextStep.return_;
984 }
985 }
986 987 // tryProcess988 /++
989 Try a function.
990 +/991 autotryProcess(size_ti)(refIRCEventevent)
992 {
993 immutableuda = this.Introspection.allEventHandlerUDAsInModule[i];
994 aliasfun = this.Introspection.allEventHandlerFunctionsInModule[i];
995 debugstaticassert(udaSanityCheckMinimal!(fun, uda), "0");
996 997 enumverbose = (uda._verbose || debug_);
998 enumfunName = 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 down1007 }
1008 elseif (event.type >= uda.acceptedEventTypeMap.length)
1009 {
1010 // Out of bounds, cannot possibly be an accepted type1011 returnNextStep.continue_;
1012 }
1013 elseif (uda.acceptedEventTypeMap[event.type])
1014 {
1015 // Drop down1016 }
1017 else1018 {
1019 returnNextStep.continue_;
1020 }
1021 1022 try1023 {
1024 immutablenext = 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 returnNextStep.continue_;
1036 }
1037 elseif (next == NextStep.repeat)
1038 {
1039 // only repeat once so we don't endlessly loop1040 immutablenewNext = process!
1041 (verbose,
1042 cast(bool)uda._fiber,
1043 cast(bool)uda.regexes.length)
1044 (&fun,
1045 funName,
1046 uda,
1047 event);
1048 returnnewNext;
1049 }
1050 elseif (next == NextStep.return_)
1051 {
1052 returnNextStep.return_;
1053 }
1054 else/*if (next == NextStep.unset)*/1055 {
1056 assert(0, "`IRCPluginImpl.onEventImpl.process` returned `Next.unset`");
1057 }
1058 }
1059 catch (Exceptione)
1060 {
1061 importkameloso.plugins.common.core : sanitiseEvent;
1062 importstd.utf : UTFException;
1063 importcore.exception : UnicodeException;
1064 1065 /*enum pattern = "tryProcess some exception on <l>%s</>: <l>%s";
1066 logger.warningf(pattern, funName, e);*/1067 1068 immutableisRecoverableException =
1069 (cast(UnicodeException)e !isnull) ||
1070 (cast(UTFException)e !isnull);
1071 1072 if (!isRecoverableException) throwe;
1073 1074 sanitiseEvent(event);
1075 1076 // Copy-paste, not much we can do otherwise1077 immutablenext = 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 returnNextStep.continue_;
1089 }
1090 elseif (next == NextStep.repeat)
1091 {
1092 // only repeat once so we don't endlessly loop1093 immutablenewNext = process!
1094 (verbose,
1095 cast(bool)uda._fiber,
1096 cast(bool)uda.regexes.length)
1097 (&fun,
1098 funName,
1099 uda,
1100 event);
1101 returnnewNext;
1102 }
1103 elseif (next == NextStep.return_)
1104 {
1105 returnNextStep.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 staticif (!this.Introspection.allEventHandlerFunctionsInModule.length)
1118 {
1119 version(unittest)
1120 {
1121 // Skip event handler checks when unittesting, as it triggers1122 // unittests in common/core.d1123 }
1124 else1125 {
1126 importstd.algorithm.searching : endsWith;
1127 1128 staticif (module_.endsWith(".stub"))
1129 {
1130 // Defined to be empty1131 }
1132 else1133 {
1134 enumnoEventHandlerMessage = "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 // funIndexByTiming1144 /++
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 autofunIndexByTiming(constTimingtiming) scope1150 {
1151 assert(__ctfe, "funIndexByTiming called outside CTFE");
1152 1153 size_t[] indexes;
1154 1155 foreach (immutablei; 0..this.Introspection.allEventHandlerUDAsInModule.length)
1156 {
1157 if (this.Introspection.allEventHandlerUDAsInModule[i]._when == timing) indexes ~= i;
1158 }
1159 1160 returnindexes;
1161 }
1162 1163 /+
1164 Build index arrays, either as enums or static immutables.
1165 +/1166 staticimmutablesetupFunIndexes = funIndexByTiming(Timing.setup);
1167 staticimmutableearlyFunIndexes = funIndexByTiming(Timing.early);
1168 staticimmutablenormalFunIndexes = funIndexByTiming(Timing.untimed);
1169 staticimmutablelateFunIndexes = funIndexByTiming(Timing.late);
1170 staticimmutablecleanupFunIndexes = 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 staticif (__traits(compiles, { alias_ = .hasMinimalAuthentication; }))
1178 {
1179 staticif (!earlyFunIndexes.length)
1180 {
1181 importstd.format : format;
1182 1183 enumpattern = "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 immutablemessage = pattern.format(module_);
1187 staticassert(0, message);
1188 }
1189 }
1190 1191 staticif (__traits(compiles, { alias_ = .hasUserAwareness; }))
1192 {
1193 staticif (!cleanupFunIndexes.length)
1194 {
1195 importstd.format : format;
1196 1197 enumpattern = "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 immutablemessage = pattern.format(module_);
1201 staticassert(0, message);
1202 }
1203 }
1204 1205 staticif (__traits(compiles, { alias_ = .hasChannelAwareness; }))
1206 {
1207 staticif (!lateFunIndexes.length)
1208 {
1209 importstd.format : format;
1210 1211 enumpattern = "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 immutablemessage = pattern.format(module_);
1215 staticassert(0, message);
1216 }
1217 }
1218 1219 aliasallFunIndexes = 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 staticforeach (immutablei; funIndexes)
1234 {{
1235 immutablenext = tryProcess!i(origEvent);
1236 1237 if (next == NextStep.return_)
1238 {
1239 // return_; end loop, proceed with next index alias1240 continuealiasLoop;
1241 }
1242 /*else if (next == NextStep.continue_)
1243 {
1244 // continue_; iterate to next function within this alias
1245 }*/1246 elseif (next == NextStep.repeat)
1247 {
1248 immutablenewNext = tryProcess!i(origEvent);
1249 1250 // Only repeat once1251 if (newNext == NextStep.return_)
1252 {
1253 // as above, end index loop1254 continuealiasLoop;
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 publicthis(IRCPluginStatestate) @system1276 {
1277 importlu.traits : isSerialisable;
1278 1279 enumnumEventTypes = __traits(allMembers, IRCEvent.Type).length;
1280 1281 // Inherit select members of state by zeroing out what we don't want1282 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; // keep1294 this.state.updates = IRCPluginState.Update.nothing;
1295 1296 foreach (immutablei, refmember; this.tupleof)
1297 {
1298 staticif (isSerialisable!member)
1299 {
1300 importkameloso.traits : udaIndexOf;
1301 1302 enumresourceUDAIndex = udaIndexOf!(this.tupleof[i], Resource);
1303 enumconfigurationUDAIndex = udaIndexOf!(this.tupleof[i], Configuration);
1304 aliasattrs = __traits(getAttributes, this.tupleof[i]);
1305 1306 staticif (resourceUDAIndex != -1)
1307 {
1308 importstd.path : buildNormalizedPath;
1309 1310 staticif (is(typeof(attrs[resourceUDAIndex])))
1311 {
1312 member = buildNormalizedPath(
1313 state.settings.resourceDirectory,
1314 attrs[resourceUDAIndex].subdirectory,
1315 member);
1316 }
1317 else1318 {
1319 member = buildNormalizedPath(state.settings.resourceDirectory, member);
1320 }
1321 }
1322 elsestaticif (configurationUDAIndex != -1)
1323 {
1324 importstd.path : buildNormalizedPath;
1325 1326 staticif (is(typeof(attrs[configurationUDAIndex])))
1327 {
1328 member = buildNormalizedPath(
1329 state.settings.configDirectory,
1330 attrs[configurationUDAIndex].subdirectory,
1331 member);
1332 }
1333 else1334 {
1335 member = buildNormalizedPath(state.settings.configDirectory, member);
1336 }
1337 }
1338 }
1339 }
1340 1341 staticif (__traits(compiles, { alias_ = .initialise; }))
1342 {
1343 importlu.traits : TakesParams;
1344 1345 staticif (TakesParams!(.initialise, typeof(this)))
1346 {
1347 .initialise(this);
1348 }
1349 else1350 {
1351 importstd.format : format;
1352 1353 enumpattern = "`%s.initialise` has an unsupported function signature: `%s`";
1354 enummessage = pattern.format(module_, typeof(.initialise).stringof);
1355 staticassert(0, message);
1356 }
1357 }
1358 }
1359 1360 // postprocess1361 /++
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 overridepublicvoidpostprocess(refIRCEventevent) @system1369 {
1370 staticif (__traits(compiles, { alias_ = .postprocess; }))
1371 {
1372 importlu.traits : TakesParams;
1373 1374 if (!this.isEnabled) return;
1375 1376 staticif (TakesParams!(.postprocess, typeof(this), IRCEvent))
1377 {
1378 importstd.traits : ParameterStorageClass, ParameterStorageClassTuple;
1379 1380 aliasSC = ParameterStorageClass;
1381 aliasparamClasses = ParameterStorageClassTuple!(.postprocess);
1382 1383 staticif (paramClasses[1] & SC.ref_)
1384 {
1385 .postprocess(this, event);
1386 }
1387 else1388 {
1389 importstd.format : format;
1390 1391 enumpattern = "`%s.postprocess` does not take its " ~
1392 "`IRCEvent` parameter by `ref`";
1393 enummessage = pattern.format(module_,);
1394 staticassert(0, message);
1395 }
1396 }
1397 else1398 {
1399 importstd.format : format;
1400 1401 enumpattern = "`%s.postprocess` has an unsupported function signature: `%s`";
1402 enummessage = pattern.format(module_, typeof(.postprocess).stringof);
1403 staticassert(0, message);
1404 }
1405 }
1406 }
1407 1408 // initResources1409 /++
1410 Writes plugin resources to disk, creating them if they don't exist.
1411 +/1412 overridepublicvoidinitResources() @system1413 {
1414 staticif (__traits(compiles, { alias_ = .initResources; }))
1415 {
1416 importlu.traits : TakesParams;
1417 1418 if (!this.isEnabled) return;
1419 1420 staticif (TakesParams!(.initResources, typeof(this)))
1421 {
1422 .initResources(this);
1423 }
1424 else1425 {
1426 importstd.format : format;
1427 1428 enumpattern = "`%s.initResources` has an unsupported function signature: `%s`";
1429 enummessage = pattern.format(module_, typeof(.initResources).stringof);
1430 staticassert(0, message);
1431 }
1432 }
1433 }
1434 1435 // deserialiseConfigFrom1436 /++
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 overridepublicvoiddeserialiseConfigFrom(
1453 conststringconfigFile,
1454 outstring[][string] missingEntries,
1455 outstring[][string] invalidEntries)
1456 {
1457 importkameloso.configreader : readConfigInto;
1458 importkameloso.traits : udaIndexOf;
1459 importlu.meld : meldInto;
1460 1461 foreach (immutablei, refsymbol; this.tupleof)
1462 {
1463 staticif (is(typeof(this.tupleof[i]) == struct))
1464 {
1465 enumtypeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1466 enumvalueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1467 1468 staticif ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1469 {
1470 if (symbol != typeof(symbol).init)
1471 {
1472 // This symbol has had configuration applied to it already1473 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 // setSettingByName1490 /++
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 overridepublicboolsetSettingByName(conststringsetting, conststringvalue)
1519 {
1520 importkameloso.traits : udaIndexOf;
1521 importlu.objmanip : setMemberByName;
1522 1523 boolsuccess;
1524 1525 foreach (immutablei, refsymbol; this.tupleof)
1526 {
1527 staticif (is(typeof(this.tupleof[i]) == struct))
1528 {
1529 enumtypeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1530 enumvalueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1531 1532 staticif ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1533 {
1534 success = symbol.setMemberByName(setting, value);
1535 break;
1536 }
1537 }
1538 }
1539 1540 returnsuccess;
1541 }
1542 1543 // printSettings1544 /++
1545 Prints the plugin's [kameloso.plugins.common.core.Settings|Settings]-annotated settings struct.
1546 +/1547 overridepublicvoidprintSettings() const1548 {
1549 importkameloso.printing : printObject;
1550 importkameloso.traits : udaIndexOf;
1551 1552 foreach (immutablei, constrefsymbol; this.tupleof)
1553 {
1554 staticif (is(typeof(this.tupleof[i]) == struct))
1555 {
1556 enumtypeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1557 enumvalueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1558 1559 staticif ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1560 {
1561 importstd.typecons : No, Yes;
1562 printObject!(No.all)(symbol);
1563 break;
1564 }
1565 }
1566 }
1567 }
1568 1569 // serialiseConfigInto1570 /++
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 overridepublicboolserialiseConfigInto(refAppender!(char[]) sink) const1589 {
1590 importkameloso.traits : udaIndexOf;
1591 1592 booldidSomething;
1593 1594 foreach (immutablei, refsymbol; this.tupleof)
1595 {
1596 staticif (is(typeof(this.tupleof[i]) == struct))
1597 {
1598 enumtypeUDAIndex = udaIndexOf!(typeof(this.tupleof[i]), Settings);
1599 enumvalueUDAIndex = udaIndexOf!(this.tupleof[i], Settings);
1600 1601 staticif ((typeUDAIndex != -1) || (valueUDAIndex != -1))
1602 {
1603 importlu.serialisation : serialise;
1604 1605 sink.serialise(symbol);
1606 didSomething = true;
1607 break;
1608 }
1609 }
1610 }
1611 1612 returndidSomething;
1613 }
1614 1615 // setup, start, reload, teardown1616 /+
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 staticforeach (immutablefunName; 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 // name1661 /++
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 overridepublicstringname() @propertyconstpurenothrow @nogc1669 {
1670 importlu.string : beginsWith;
1671 1672 enummodulePrefix = "kameloso.plugins.";
1673 1674 staticif (module_.beginsWith(modulePrefix))
1675 {
1676 importstd.string : indexOf;
1677 1678 stringslice = module_[modulePrefix.length..$]; // mutable1679 immutabledotPos = slice.indexOf('.');
1680 if (dotPos == -1) returnslice;
1681 return (slice[dotPos+1..$] == "base") ? slice[0..dotPos] : slice[dotPos+1..$];
1682 }
1683 else1684 {
1685 importstd.format : format;
1686 1687 enumpattern = "Plugin module `%s` is not under `kameloso.plugins`";
1688 enummessage = pattern.format(module_);
1689 staticassert(0, message);
1690 }
1691 }
1692 1693 // channelSpecificCommands1694 /++
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 overridepublicIRCPlugin.CommandMetadata[string] channelSpecificCommands(conststringchannelName) @system1706 {
1707 returnnull;
1708 }
1709 1710 // commands1711 /++
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 overridepublicIRCPlugin.CommandMetadata[string] commands() purenothrow @propertyconst1727 {
1728 returncommandsImpl();
1729 }
1730 1731 // commandsImpl1732 /++
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 privateIRCPlugin.CommandMetadata[string] commandsImpl() purenothrow @propertyconst1752 {
1753 enumctCommandsEnumLiteral =
1754 {
1755 importkameloso.plugins.common.core : IRCEventHandler;
1756 importstd.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 immutableuda = getUDAs!(fun, IRCEventHandler)[0];
1765 1766 staticforeach (immutablecommand; uda.commands)
1767 {{
1768 enumkey = command._word;
1769 commandAA[key] = IRCPlugin.CommandMetadata(command);
1770 1771 staticif (command._hidden)
1772 {
1773 // Just ignore1774 }
1775 elsestaticif (command._description.length)
1776 {
1777 staticif (command._policy == PrefixPolicy.nickname)
1778 {
1779 importlu.string : beginsWith;
1780 1781 staticif (command.syntaxes.length)
1782 {
1783 foreach (immutablesyntax; command.syntaxes)
1784 {
1785 if (syntax.beginsWith("$bot"))
1786 {
1787 // Syntax is already prefixed1788 commandAA[key].syntaxes ~= syntax;
1789 }
1790 else1791 {
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 else1799 {
1800 // Define an empty nickname: command syntax1801 // to give hint about the nickname prefix1802 commandAA[key].syntaxes ~= "$bot: $command";
1803 }
1804 }
1805 else1806 {
1807 staticif (command.syntaxes.length)
1808 {
1809 commandAA[key].syntaxes ~= command.syntaxes.dup;
1810 }
1811 else1812 {
1813 commandAA[key].syntaxes ~= "$command";
1814 }
1815 }
1816 }
1817 else/*static if (!command._hidden && !command._description.length)*/1818 {
1819 importstd.format : format;
1820 1821 enumfqn = module_ ~ '.' ~ __traits(identifier, fun);
1822 enumpattern = "Warning: `%s` non-hidden command word \"%s\" is missing a description";
1823 enummessage = pattern.format(fqn, command._word);
1824 pragma(msg, message);
1825 }
1826 }}
1827 1828 staticforeach (immutableregex; uda.regexes)
1829 {{
1830 enumkey = `r"` ~ regex._expression ~ `"`;
1831 commandAA[key] = IRCPlugin.CommandMetadata(regex);
1832 1833 staticif (regex._description.length)
1834 {
1835 staticif (regex._policy == PrefixPolicy.direct)
1836 {
1837 commandAA[key].syntaxes ~= regex._expression;
1838 }
1839 elsestaticif (regex._policy == PrefixPolicy.prefixed)
1840 {
1841 commandAA[key].syntaxes ~= "$prefix" ~ regex._expression;
1842 }
1843 elsestaticif (regex._policy == PrefixPolicy.nickname)
1844 {
1845 commandAA[key].syntaxes ~= "$nickname: " ~ regex._expression;
1846 }
1847 }
1848 elsestaticif (!regex._hidden)
1849 {
1850 importstd.format : format;
1851 1852 enumfqn = module_ ~ '.' ~ __traits(identifier, fun);
1853 enumpattern = "Warning: `%s` non-hidden expression \"%s\" is missing a description";
1854 enummessage = pattern.format(fqn, regex._expression);
1855 pragma(msg, message);
1856 }
1857 }}
1858 }
1859 1860 returncommandAA;
1861 }();
1862 1863 // This is an associative array literal. We can't make it static immutable1864 // because of AAs' runtime-ness. We could make it runtime immutable once1865 // and then just the address, but this is really not a hotspot.1866 // So just let it allocate when it wants.1867 returnthis.isEnabled ? ctCommandsEnumLiteral : null;
1868 }
1869 1870 privateimportkameloso.thread : Sendable;
1871 1872 // onBusMessage1873 /++
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 overridepublicvoidonBusMessage(conststringheader, sharedSendablecontent) @system1882 {
1883 staticif (__traits(compiles, { alias_ = .onBusMessage; }))
1884 {
1885 importlu.traits : TakesParams;
1886 1887 staticif (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 else1896 {
1897 importstd.format : format;
1898 1899 enumpattern = "`%s.onBusMessage` has an unsupported function signature: `%s`";
1900 enummessage = pattern.format(module_, typeof(.onBusMessage).stringof);
1901 staticassert(0, message);
1902 }
1903 }
1904 }
1905 }
1906 1907 1908 // prefixPolicyMatches1909 /++
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 autoprefixPolicyMatches(boolverbose)
1927 (refIRCEventevent,
1928 constPrefixPolicypolicy,
1929 constIRCPluginStatestate)
1930 {
1931 importkameloso.string : stripSeparatedPrefix;
1932 importlu.string : beginsWith;
1933 importstd.typecons : No, Yes;
1934 1935 staticif (verbose)
1936 {
1937 importstd.stdio : writefln, writeln;
1938 writeln("...prefixPolicyMatches! policy:", policy);
1939 }
1940 1941 boolstrippedDisplayName;
1942 1943 with (PrefixPolicy)
1944 finalswitch (policy)
1945 {
1946 casedirect:
1947 staticif (verbose)
1948 {
1949 writeln("direct, so just passes.");
1950 }
1951 returntrue;
1952 1953 caseprefixed:
1954 if (state.settings.prefix.length && event.content.beginsWith(state.settings.prefix))
1955 {
1956 staticif (verbose)
1957 {
1958 writefln("starts with prefix (%s)", state.settings.prefix);
1959 }
1960 1961 event.content = event.content[state.settings.prefix.length..$];
1962 }
1963 else1964 {
1965 staticif (verbose)
1966 {
1967 writeln("did not start with prefix but falling back to nickname check");
1968 }
1969 1970 gotocasenickname;
1971 }
1972 break;
1973 1974 casenickname:
1975 if (event.content.beginsWith('@'))
1976 {
1977 staticif (verbose)
1978 {
1979 writeln("stripped away prepended '@'");
1980 }
1981 1982 // Using @name to refer to someone is not1983 // uncommon; allow for it and strip it away1984 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 staticif (verbose)
1994 {
1995 writeln("begins with displayName! stripping it");
1996 }
1997 1998 event.content = event.content1999 .stripSeparatedPrefix(state.client.displayName, Yes.demandSeparatingChars);
2000 2001 if (state.settings.prefix.length && event.content.beginsWith(state.settings.prefix))
2002 {
2003 staticif (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 down2013 }
2014 }
2015 2016 if (strippedDisplayName)
2017 {
2018 // Already did something2019 }
2020 elseif (event.content.beginsWith(state.client.nickname))
2021 {
2022 staticif (verbose)
2023 {
2024 writeln("begins with nickname! stripping it");
2025 }
2026 2027 event.content = event.content2028 .stripSeparatedPrefix(state.client.nickname, Yes.demandSeparatingChars);
2029 2030 if (state.settings.prefix.length && event.content.beginsWith(state.settings.prefix))
2031 {
2032 staticif (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 down2040 }
2041 elseif (event.type == IRCEvent.Type.QUERY)
2042 {
2043 staticif (verbose)
2044 {
2045 writeln("doesn't begin with nickname but it's a QUERY");
2046 }
2047 // Drop down2048 }
2049 else2050 {
2051 staticif (verbose)
2052 {
2053 writeln("nickname required but not present... returning false.");
2054 }
2055 returnfalse;
2056 }
2057 break;
2058 }
2059 2060 staticif (verbose)
2061 {
2062 writeln("policy checks out!");
2063 }
2064 2065 returntrue;
2066 }
2067 2068 2069 // filterSender2070 /++
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 autofilterSender(
2088 constrefIRCEventevent,
2089 constPermissionspermissionsRequired,
2090 constboolpreferHostmasks) @safe2091 {
2092 importkameloso.constants : Timeout;
2093 2094 version(WithPersistenceService) {}
2095 else2096 {
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 immutableclass_ = event.sender.class_;
2102 2103 if (class_ == IRCUser.Class.blacklist) returnFilterResult.fail;
2104 2105 immutabletimediff = (event.time - event.sender.updated);
2106 2107 // In hostmasks mode there's zero point to WHOIS a sender, as the instigating2108 // event will have the hostmask embedded in it, always.2109 immutablewhoisExpired = !preferHostmasks && (timediff > Timeout.whoisRetry);
2110 2111 if (event.sender.account.length)
2112 {
2113 immutableisAdmin = (class_ == IRCUser.Class.admin); // Trust in Persistence2114 immutableisStaff = (class_ == IRCUser.Class.staff);
2115 immutableisOperator = (class_ == IRCUser.Class.operator);
2116 immutableisElevated = (class_ == IRCUser.Class.elevated);
2117 immutableisWhitelisted = (class_ == IRCUser.Class.whitelist);
2118 immutableisAnyone = (class_ == IRCUser.Class.anyone);
2119 2120 if (isAdmin)
2121 {
2122 returnFilterResult.pass;
2123 }
2124 elseif (isStaff && (permissionsRequired <= Permissions.staff))
2125 {
2126 returnFilterResult.pass;
2127 }
2128 elseif (isOperator && (permissionsRequired <= Permissions.operator))
2129 {
2130 returnFilterResult.pass;
2131 }
2132 elseif (isElevated && (permissionsRequired <= Permissions.elevated))
2133 {
2134 returnFilterResult.pass;
2135 }
2136 elseif (isWhitelisted && (permissionsRequired <= Permissions.whitelist))
2137 {
2138 returnFilterResult.pass;
2139 }
2140 elseif (/*event.sender.account.length &&*/permissionsRequired <= Permissions.registered)
2141 {
2142 returnFilterResult.pass;
2143 }
2144 elseif (isAnyone && (permissionsRequired <= Permissions.anyone))
2145 {
2146 returnwhoisExpired ? FilterResult.whois : FilterResult.pass;
2147 }
2148 elseif (permissionsRequired == Permissions.ignore)
2149 {
2150 /*assert(0, "`filterSender` saw a `Permissions.ignore` and the call " ~
2151 "to it could have been skipped");*/2152 returnFilterResult.pass;
2153 }
2154 else2155 {
2156 returnFilterResult.fail;
2157 }
2158 }
2159 else2160 {
2161 immutableisLogoutEvent = (event.type == IRCEvent.Type.ACCOUNT);
2162 2163 with (Permissions)
2164 finalswitch (permissionsRequired)
2165 {
2166 caseadmin:
2167 casestaff:
2168 caseoperator:
2169 caseelevated:
2170 casewhitelist:
2171 caseregistered:
2172 // Unknown sender; WHOIS if old result expired, otherwise fail2173 return (whoisExpired && !isLogoutEvent) ? FilterResult.whois : FilterResult.fail;
2174 2175 caseanyone:
2176 // Unknown sender; WHOIS if old result expired in mere curiosity, else just pass2177 return (whoisExpired && !isLogoutEvent) ? FilterResult.whois : FilterResult.pass;
2178 2179 caseignore:
2180 /*assert(0, "`filterSender` saw a `Permissions.ignore` and the call " ~
2181 "to it could have been skipped");*/2182 returnFilterResult.pass;
2183 }
2184 }
2185 }
2186 2187 2188 // allowImpl2189 /++
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 autoallowImpl(
2208 IRCPluginplugin,
2209 constrefIRCEventevent,
2210 constPermissionspermissionsRequired) pure @safe2211 {
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 just2221 // Permissions.ignore with an extra WHOIS for good measure.2222 // Also everyone is registered on Twitch, by definition.2223 returnFilterResult.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 // sanitiseEvent2236 /++
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 voidsanitiseEvent(refIRCEventevent)
2243 {
2244 importstd.encoding : sanitize;
2245 importstd.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 (immutablei, refaux; 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 // udaSanityCheckCTFE2277 /++
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 debug2287 voidudaSanityCheckCTFE(constIRCEventHandleruda)
2288 {
2289 importstd.format : format;
2290 2291 assert(__ctfe, "udaSanityCheckCTFE called outside CTFE");
2292 2293 staticif (__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 enumfix = "`";
2302 }
2303 else2304 {
2305 // Hopefully no need past 2.104... Update when 2.105 is out.2306 enumfix = string.init;
2307 }
2308 2309 if (!uda.acceptedEventTypes.length)
2310 {
2311 enumpattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2312 "but it is not declared to accept any `IRCEvent.Type`s";
2313 immutablemessage = pattern.format(uda.fqn).idup;
2314 assert(0, message);
2315 }
2316 2317 foreach (immutabletype; uda.acceptedEventTypes)
2318 {
2319 if (type == IRCEvent.Type.UNSET)
2320 {
2321 enumpattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2322 "accepting `IRCEvent.Type.UNSET`, which is not a valid event type";
2323 immutablemessage = pattern.format(uda.fqn).idup;
2324 assert(0, message);
2325 }
2326 elseif (type == IRCEvent.Type.PRIVMSG)
2327 {
2328 enumpattern = 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 immutablemessage = pattern.format(uda.fqn).idup;
2332 assert(0, message);
2333 }
2334 elseif (type == IRCEvent.Type.WHISPER)
2335 {
2336 enumpattern = 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 immutablemessage = pattern.format(uda.fqn).idup;
2340 assert(0, message);
2341 }
2342 elseif ((type == IRCEvent.Type.ANY) && (uda.channelPolicy != ChannelPolicy.any))
2343 {
2344 enumpattern = 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 immutablemessage = 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 importlu.conv : Enum;
2360 2361 enumpattern = 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 immutablemessage = 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 importlu.string : contains;
2375 2376 foreach (constcommand; uda.commands)
2377 {
2378 if (!command._word.length)
2379 {
2380 enumpattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2381 "listening for a `Command` with an empty (or unspecified) trigger word";
2382 immutablemessage = pattern.format(uda.fqn).idup;
2383 assert(0, message);
2384 }
2385 elseif (command._word.contains(' '))
2386 {
2387 enumpattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2388 "listening for a `Command` whose trigger " ~
2389 `word "%s" contains a space character`;
2390 immutablemessage = pattern.format(uda.fqn, command._word).idup;
2391 assert(0, message);
2392 }
2393 }
2394 }
2395 2396 if (uda.regexes.length)
2397 {
2398 foreach (constregex; uda.regexes)
2399 {
2400 importlu.string : contains;
2401 2402 if (!regex._expression.length)
2403 {
2404 enumpattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2405 "listening for a `Regex` with an empty (or unspecified) expression";
2406 immutablemessage = pattern.format(uda.fqn).idup;
2407 assert(0, message);
2408 }
2409 elseif (
2410 (regex._policy != PrefixPolicy.direct) &&
2411 regex._expression.contains(' '))
2412 {
2413 enumpattern = fix ~ "`%s` is annotated with an `IRCEventHandler` " ~
2414 "listening for a non-`PrefixPolicy.direct`-annotated " ~
2415 "`Regex` with an expression containing spaces";
2416 immutablemessage = 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 module2423 /*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 // assertSaneStorageClasses2437 /++
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 autoassertSaneStorageClasses(
2457 constParameterStorageClassstorageClass,
2458 constboolparamIsConst,
2459 constboolinFiber,
2460 conststringmodule_,
2461 conststringtypestring)
2462 {
2463 importstd.format : format;
2464 2465 staticif (__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 enumfix = "`";
2474 }
2475 else2476 {
2477 // Hopefully no need past 2.104... Update when 2.105 is out.2478 enumfix = string.init;
2479 }
2480 2481 if (inFiber)
2482 {
2483 if (storageClass & ParameterStorageClass.ref_)
2484 {
2485 enumpattern = 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 immutablemessage = pattern.format(module_, typestring).idup;
2489 assert(0, message);
2490 }
2491 }
2492 elseif (!paramIsConst)
2493 {
2494 if (
2495 (storageClass & ParameterStorageClass.ref_) ||
2496 (storageClass & ParameterStorageClass.out_))
2497 {
2498 enumpattern = 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 immutablemessage = pattern.format(module_, typestring).idup;
2502 assert(0, message);
2503 }
2504 }
2505 2506 returntrue;
2507 }
2508 2509 2510 // IRCPluginState2511 /++
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 structIRCPluginState2524 {
2525 private:
2526 importkameloso.pods : ConnectionSettings, CoreSettings, IRCBot;
2527 importkameloso.thread : ScheduledDelegate, ScheduledFiber;
2528 importstd.concurrency : Tid;
2529 importcore.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 // Update2539 /++
2540 Bitfield enum of what member of an instance of `IRCPluginState` was updated (if any).
2541 +/2542 enumUpdate2543 {
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 // client2571 /++
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 IRCClientclient;
2576 2577 // server2578 /++
2579 The current [dialect.defs.IRCServer|IRCServer], containing information
2580 pertaining to the bot in the context of an IRC server.
2581 +/2582 IRCServerserver;
2583 2584 // bot2585 /++
2586 The current [kameloso.pods.IRCBot|IRCBot], containing information
2587 pertaining to the bot in the context of an IRC bot.
2588 +/2589 IRCBotbot;
2590 2591 // settings2592 /++
2593 The current program-wide [kameloso.pods.CoreSettings|CoreSettings].
2594 +/2595 CoreSettingssettings;
2596 2597 // connSettings2598 /++
2599 The current program-wide [kameloso.pods.ConnectionSettings|ConnectionSettings].
2600 +/2601 ConnectionSettingsconnSettings;
2602 2603 // mainThread2604 /++
2605 Thread ID to the main thread.
2606 +/2607 TidmainThread;
2608 2609 // users2610 /++
2611 Hashmap of IRC user details.
2612 +/2613 IRCUser[string] users;
2614 2615 // channels2616 /++
2617 Hashmap of IRC channels.
2618 +/2619 IRCChannel[string] channels;
2620 2621 // pendingReplays2622 /++
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 // hasReplays2632 /++
2633 Whether or not [pendingReplays] has elements (i.e. is not empty).
2634 +/2635 boolhasPendingReplays;
2636 2637 // readyReplays2638 /++
2639 [Replay]s primed and ready to be replayed.
2640 +/2641 Replay[] readyReplays;
2642 2643 // awaitingFibers2644 /++
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 // awaitingDelegates2651 /++
2652 The list of awaiting `void delegate(IRCEvent)` delegates, keyed by
2653 [dialect.defs.IRCEvent.Type|IRCEvent.Type].
2654 +/2655 voiddelegate(IRCEvent)[][] awaitingDelegates;
2656 2657 // scheduledFibers2658 /++
2659 The list of scheduled [core.thread.fiber.Fiber|Fiber], UNIX time tuples.
2660 +/2661 ScheduledFiber[] scheduledFibers;
2662 2663 // scheduledDelegates2664 /++
2665 The list of scheduled delegate, UNIX time tuples.
2666 +/2667 ScheduledDelegate[] scheduledDelegates;
2668 2669 // nextScheduledTimestamp2670 /++
2671 The UNIX timestamp of when the next scheduled
2672 [kameloso.thread.ScheduledFiber|ScheduledFiber] or delegate should be triggered.
2673 +/2674 longnextScheduledTimestamp = long.max;
2675 2676 // updateSchedule2677 /++
2678 Updates the saved UNIX timestamp of when the next scheduled
2679 [core.thread.fiber.Fiber|Fiber] or delegate should be triggered.
2680 +/2681 voidupdateSchedule() purenothrow @nogc2682 {
2683 // Reset the next timestamp to an invalid value, then update it as we2684 // iterate the fibers' and delegates' labels.2685 2686 nextScheduledTimestamp = long.max;
2687 2688 foreach (constscheduledFiber; scheduledFibers)
2689 {
2690 if (scheduledFiber.timestamp < nextScheduledTimestamp)
2691 {
2692 nextScheduledTimestamp = scheduledFiber.timestamp;
2693 }
2694 }
2695 2696 foreach (constscheduledDg; scheduledDelegates)
2697 {
2698 if (scheduledDg.timestamp < nextScheduledTimestamp)
2699 {
2700 nextScheduledTimestamp = scheduledDg.timestamp;
2701 }
2702 }
2703 }
2704 2705 // previousWhoisTimestamps2706 /++
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 // updates2714 /++
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 Updateupdates;
2729 2730 // abort2731 /++
2732 Pointer to the global abort flag.
2733 +/2734 bool* abort;
2735 2736 // connectionID2737 /++
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 autoconnectionID() const2746 {
2747 return_connectionID;
2748 }
2749 2750 // this2751 /++
2752 Constructor taking a connection ID `uint`.
2753 +/2754 this(constuintconnectionID)
2755 {
2756 this._connectionID = connectionID;
2757 }
2758 2759 // specialRequests2760 /++
2761 This plugin's array of [SpecialRequest]s.
2762 +/2763 SpecialRequest[] specialRequests;
2764 }
2765 2766 2767 // Replay2768 /++
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 structReplay2773 {
2774 // caller2775 /++
2776 Name of the caller function or similar context.
2777 +/2778 stringcaller;
2779 2780 // event2781 /++
2782 Stored [dialect.defs.IRCEvent|IRCEvent] to replay.
2783 +/2784 IRCEventevent;
2785 2786 // permissionsRequired2787 /++
2788 [Permissions] required by the function to replay.
2789 +/2790 PermissionspermissionsRequired;
2791 2792 // dg2793 /++
2794 Delegate, whose context includes the plugin to whom this [Replay] relates.
2795 +/2796 voiddelegate(Replay) dg;
2797 2798 // timestamp2799 /++
2800 When this request was issued.
2801 +/2802 longtimestamp;
2803 2804 /++
2805 Creates a new [Replay] with a timestamp of the current time.
2806 +/2807 this(
2808 voiddelegate(Replay) dg,
2809 constrefIRCEventevent,
2810 constPermissionspermissionsRequired,
2811 conststringcaller)
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 // filterResult2823 /++
2824 The tristate results from comparing a username with the admin or
2825 whitelist/elevated/operator/staff lists.
2826 +/2827 enumFilterResult2828 {
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 // PrefixPolicy2848 /++
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 enumPrefixPolicy2853 {
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 // ChannelPolicy2881 /++
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 enumChannelPolicy2886 {
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 // Permissions2907 /++
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 enumPermissions2918 {
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 // Timing2975 /++
2976 Declaration of what order event handler function should be given with respects
2977 to other functions in the same plugin module.
2978 +/2979 enumTiming2980 {
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 // IRCEventHandler3009 /++
3010 Aggregate to annotate event handler functions with, to control what they do
3011 and how they work.
3012 +/3013 structIRCEventHandler3014 {
3015 private:
3016 importkameloso.traits : UnderscoreOpDispatcher;
3017 3018 public:
3019 // acceptedEventTypes3020 /++
3021 Array of types of [dialect.defs.IRCEvent] that the annotated event
3022 handler function should accept.
3023 +/3024 IRCEvent.Type[] acceptedEventTypes;
3025 3026 // _onEvent3027 /++
3028 Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3029 [acceptedEventTypes] but by the name `onEvent`.
3030 +/3031 alias_onEvent = acceptedEventTypes;
3032 3033 // _permissionsRequired3034 /++
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 // _channelPolicy3041 /++
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 // commands3048 /++
3049 Array of [IRCEventHandler.Command]s the bot should pick up and listen for.
3050 +/3051 Command[] commands;
3052 3053 // _addCommand3054 /++
3055 Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3056 [commands] but by the name `addCommand`.
3057 +/3058 alias_addCommand = commands;
3059 3060 // regexes3061 /++
3062 Array of [IRCEventHandler.Regex]es the bot should pick up and listen for.
3063 +/3064 Regex[] regexes;
3065 3066 // _addRegex3067 /++
3068 Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3069 [regexes] but by the name `addRegex`.
3070 +/3071 alias_addRegex = regexes;
3072 3073 // _chainable3074 /++
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 // _verbose3082 /++
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 // _when3089 /++
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 // _fiber3096 /++
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 // acceptedEventTypeMap3103 /++
3104 Array of accepted [dialect.defs.IRCEvent.Type|IRCEvent.Type]s.
3105 +/3106 bool[] acceptedEventTypeMap;
3107 3108 // generateTypemap3109 /++
3110 Generates [acceptedEventTypeMap] from [acceptedEventTypes].
3111 +/3112 voidgenerateTypemap() pure @safenothrow3113 {
3114 assert(__ctfe, "generateTypemap called outside CTFE");
3115 3116 foreach (immutabletype; acceptedEventTypes)
3117 {
3118 if (type >= acceptedEventTypeMap.length) acceptedEventTypeMap.length = type+1;
3119 acceptedEventTypeMap[type] = true;
3120 }
3121 }
3122 3123 mixinUnderscoreOpDispatcher;
3124 3125 // fqn3126 /++
3127 Fully qualified name of the function the annotated [IRCEventHandler] is attached to.
3128 +/3129 stringfqn;
3130 3131 // Command3132 /++
3133 Embodies the notion of a chat command, e.g. `!hello`.
3134 +/3135 staticstructCommand3136 {
3137 // _policy3138 /++
3139 In what way the message is required to start for the annotated function to trigger.
3140 +/3141 PrefixPolicy_policy = PrefixPolicy.prefixed;
3142 3143 // _word3144 /++
3145 The command word, without spaces.
3146 +/3147 string_word;
3148 3149 // _description3150 /++
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 // _hidden3160 /++
3161 Whether this is a hidden command or if it should show up in help listings.
3162 +/3163 bool_hidden;
3164 3165 // syntaxes3166 /++
3167 Command usage syntax help strings.
3168 +/3169 string[] syntaxes;
3170 3171 // _addSyntax3172 /++
3173 Alias to make [kameloso.traits.UnderscoreOpDispatcher] redirect calls to
3174 [syntaxes] but by the name `addSyntax`.
3175 +/3176 alias_addSyntax = syntaxes;
3177 3178 mixinUnderscoreOpDispatcher;
3179 }
3180 3181 // Regex3182 /++
3183 Embodies the notion of a chat command regular expression, e.g. `![Hh]ello+`.
3184 +/3185 staticstructRegex3186 {
3187 private:
3188 importstd.regex : StdRegex = Regex;
3189 3190 public:
3191 // _policy3192 /++
3193 In what way the message is required to start for the annotated function to trigger.
3194 +/3195 PrefixPolicy_policy = PrefixPolicy.direct;
3196 3197 // engine3198 /++
3199 Regex engine to match incoming messages with.
3200 +/3201 StdRegex!charengine;
3202 3203 // _expression3204 /++
3205 The regular expression in string form.
3206 +/3207 string_expression;
3208 3209 // _description3210 /++
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 // _hidden3220 /++
3221 Whether this is a hidden command or if it should show up in help listings.
3222 +/3223 bool_hidden;
3224 3225 // _expression3226 /++
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 refautoexpression()(conststringexpression)
3247 {
3248 importstd.regex : regex;
3249 3250 this._expression = expression;
3251 this.engine = expression.regex;
3252 returnthis;
3253 }
3254 3255 mixinUnderscoreOpDispatcher;
3256 }
3257 }
3258 3259 3260 // SpecialRequest3261 /++
3262 Embodies the notion of a special request a plugin issues to the main thread.
3263 +/3264 interfaceSpecialRequest3265 {
3266 private:
3267 importcore.thread : Fiber;
3268 3269 public:
3270 // context3271 /++
3272 String context of the request.
3273 +/3274 stringcontext();
3275 3276 // fiber3277 /++
3278 Fiber embedded into the request.
3279 +/3280 Fiberfiber();
3281 }
3282 3283 3284 // SpecialRequestImpl3285 /++
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 finalclassSpecialRequestImpl(T) : SpecialRequest3295 {
3296 private:
3297 importkameloso.thread : CarryingFiber;
3298 importcore.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 // this3312 /++
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(stringcontext, CarryingFiber!Tfiber)
3320 {
3321 this._context = context;
3322 this._fiber = fiber;
3323 }
3324 3325 // this3326 /++
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(stringcontext, voiddelegate() dg)
3334 {
3335 importkameloso.constants : BufferSize;
3336 3337 this._context = context;
3338 this._fiber = newCarryingFiber!T(dg, BufferSize.fiberStack);
3339 }
3340 3341 // context3342 /++
3343 String context of the request. May be anything; highly request-specific.
3344 3345 Returns:
3346 A string.
3347 +/3348 stringcontext()
3349 {
3350 return_context;
3351 }
3352 3353 // fiber3354 /++
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 Fiberfiber()
3362 {
3363 return_fiber;
3364 }
3365 }
3366 3367 3368 // specialRequest3369 /++
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 SpecialRequestspecialRequest(T)(conststringcontext, CarryingFiber!Tfiber)
3382 {
3383 returnnewSpecialRequestImpl!T(context, fiber);
3384 }
3385 3386 3387 // specialRequest3388 /++
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 SpecialRequestspecialRequest(T)(conststringcontext, voiddelegate() dg)
3401 {
3402 returnnewSpecialRequestImpl!T(context, dg);
3403 }
3404 3405 3406 // Settings3407 /++
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 enumSettings;
3413 3414 3415 // Resource3416 /++
3417 Annotation denoting that a variable is the basename of a resource file or directory.
3418 +/3419 structResource3420 {
3421 /++
3422 Subdirectory in which to put the annotated filename.
3423 +/3424 stringsubdirectory;
3425 }
3426 3427 3428 // Configuration3429 /++
3430 Annotation denoting that a variable is the basename of a configuration
3431 file or directory.
3432 +/3433 structConfiguration3434 {
3435 /++
3436 Subdirectory in which to put the annotated filename.
3437 +/3438 stringsubdirectory;
3439 }
3440 3441 3442 // Enabler3443 /++
3444 Annotation denoting that a variable enables and disables a plugin.
3445 +/3446 enumEnabler;