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