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