1 /++ 2 Common functions used throughout the program, generic enough to be used in 3 several places, not fitting into any specific one. 4 5 See_Also: 6 [kameloso.kameloso], 7 [kameloso.main] 8 9 Copyright: [JR](https://github.com/zorael) 10 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 11 12 Authors: 13 [JR](https://github.com/zorael) 14 +/ 15 module kameloso.common; 16 17 private: 18 19 import kameloso.pods : CoreSettings; 20 import kameloso.logger : KamelosoLogger; 21 import dialect.defs : IRCClient; 22 import std.range.primitives : isOutputRange; 23 import std.stdio : stdout; 24 import std.typecons : Flag, No, Yes; 25 26 public: 27 28 version(unittest) 29 shared static this() 30 { 31 // This is technically before settings have been read. 32 // We need this for unittests. 33 logger = new KamelosoLogger( 34 No.monochrome, 35 No.brightTerminal, 36 No.headless, 37 Yes.flush); 38 39 // settings need instantiating too, for tag expansion and kameloso.printing. 40 settings = new CoreSettings; 41 } 42 43 44 // logger 45 /++ 46 Instance of a [kameloso.logger.KamelosoLogger|KamelosoLogger], providing 47 timestamped and coloured logging. 48 49 The member functions to use are `log`, `trace`, `info`, `warning`, `error`, 50 and `fatal`. It is not `__gshared`, so instantiate a thread-local 51 [kameloso.logger.KamelosoLogger|KamelosoLogger] if threading. 52 53 Having this here is unfortunate; ideally plugins should not use variables 54 from other modules, but unsure of any way to fix this other than to have 55 each plugin keep their own [kameloso.common.logger] pointer. 56 +/ 57 KamelosoLogger logger; 58 59 60 // initLogger 61 /++ 62 Initialises the [kameloso.logger.KamelosoLogger|KamelosoLogger] logger for 63 use in this thread. 64 65 It needs to be separately instantiated per thread, and even so there may be 66 race conditions. Plugins are encouraged to use 67 [kameloso.thread.ThreadMessage|ThreadMessage]s to log to screen from other threads. 68 69 Example: 70 --- 71 initLogger(No.monochrome, Yes.brightTerminal); 72 --- 73 74 Params: 75 monochrome = Whether the terminal is set to monochrome or not. 76 bright = Whether the terminal has a bright background or not. 77 headless = Whether the terminal is headless or not. 78 flush = Whether the terminal needs to manually flush standard out after writing to it. 79 +/ 80 void initLogger( 81 const Flag!"monochrome" monochrome, 82 const Flag!"brightTerminal" bright, 83 const Flag!"headless" headless, 84 const Flag!"flush" flush) @safe 85 out (; (logger !is null), "Failed to initialise logger") 86 { 87 import kameloso.logger : KamelosoLogger; 88 logger = new KamelosoLogger(monochrome, bright, headless, flush); 89 } 90 91 92 // settings 93 /++ 94 A [kameloso.pods.CoreSettings|CoreSettings] struct global, housing 95 certain runtime settings. 96 97 This will be accessed from other parts of the program, via 98 [kameloso.common.settings], so they know to use monochrome output or not. 99 It is a problem that needs solving. 100 +/ 101 CoreSettings* settings; 102 103 104 // printVersionInfo 105 /++ 106 Prints out the bot banner with the version number and GitHub URL, with the 107 passed colouring. 108 109 Example: 110 --- 111 printVersionInfo(Yes.colours); 112 --- 113 114 Params: 115 colours = Whether or not to tint output, default yes. A global monochrome 116 setting overrides this. 117 +/ 118 void printVersionInfo(const Flag!"colours" colours = Yes.colours) @safe 119 { 120 import kameloso.common : logger; 121 import kameloso.constants : KamelosoInfo; 122 import kameloso.logger : LogLevel; 123 import kameloso.terminal.colours.tags : expandTags; 124 import std.stdio : writefln; 125 126 version(TwitchSupport) enum twitchSupport = " (+twitch)"; 127 else enum twitchSupport = string.init; 128 129 immutable versionPattern = colours ? 130 "<l>kameloso IRC bot v%s%s, built with %s (%s) on %s</>".expandTags(LogLevel.off) : 131 "kameloso IRC bot v%s%s, built with %s (%s) on %s"; 132 133 writefln(versionPattern, 134 cast(string)KamelosoInfo.version_, 135 twitchSupport, 136 cast(string)KamelosoInfo.compiler, 137 cast(string)KamelosoInfo.compilerVersion, 138 cast(string)KamelosoInfo.built); 139 140 immutable gitClonePattern = colours ? 141 "$ git clone <i>%s.git</>".expandTags(LogLevel.off) : 142 "$ git clone %s.git"; 143 144 writefln(gitClonePattern, cast(string)KamelosoInfo.source); 145 } 146 147 148 // printStacktrace 149 /++ 150 Prints the current stacktrace to the terminal. 151 152 This is so we can get the stacktrace even outside a thrown Exception. 153 +/ 154 version(PrintStacktraces) 155 void printStacktrace() @system 156 { 157 import std.stdio : stdout, writeln; 158 import core.runtime : defaultTraceHandler; 159 160 writeln(defaultTraceHandler); 161 if (settings.flush) stdout.flush(); 162 } 163 164 165 // OutgoingLine 166 /++ 167 A string to be sent to the IRC server, along with whether the message 168 should be sent quietly or if it should be displayed in the terminal. 169 +/ 170 struct OutgoingLine 171 { 172 /// String line to send. 173 string line; 174 175 /// Whether this message should be sent quietly or verbosely. 176 bool quiet; 177 178 /// Constructor. 179 this(const string line, const Flag!"quiet" quiet = No.quiet) pure @safe nothrow @nogc 180 { 181 this.line = line; 182 this.quiet = quiet; 183 } 184 } 185 186 187 // findURLs 188 /++ 189 Finds URLs in a string, returning an array of them. Does not filter out duplicates. 190 191 Replacement for regex matching using much less memory when compiling 192 (around ~300mb). 193 194 To consider: does this need a `dstring`? 195 196 Example: 197 --- 198 // Replaces the following: 199 // enum stephenhay = `\bhttps?://[^\s/$.?#].[^\s]*`; 200 // static urlRegex = ctRegex!stephenhay; 201 202 string[] urls = findURL("blah https://google.com http://facebook.com httpx://wefpokwe"); 203 assert(urls.length == 2); 204 --- 205 206 Params: 207 line = String line to examine and find URLs in. 208 209 Returns: 210 A `string[]` array of found URLs. These include fragment identifiers. 211 +/ 212 auto findURLs(const string line) @safe pure 213 { 214 import lu.string : contains, nom, strippedRight; 215 import std.string : indexOf; 216 import std.typecons : Flag, No, Yes; 217 218 enum wordBoundaryTokens = ".,!?:"; 219 220 string[] hits; 221 string slice = line; // mutable 222 223 ptrdiff_t httpPos = slice.indexOf("http"); 224 225 while (httpPos != -1) 226 { 227 if ((httpPos > 0) && (slice[httpPos-1] != ' ')) 228 { 229 // Run-on http address (character before the 'h') 230 slice = slice[httpPos+4..$]; 231 httpPos = slice.indexOf("http"); 232 continue; 233 } 234 235 slice = slice[httpPos..$]; 236 237 if (slice.length < 11) 238 { 239 // Too short, minimum is "http://a.se".length 240 break; 241 } 242 else if ((slice[4] != ':') && (slice[4] != 's')) 243 { 244 // Not http or https, something else 245 // But could still be another link after this 246 slice = slice[5..$]; 247 httpPos = slice.indexOf("http"); 248 continue; 249 } 250 else if ((slice[7] == ' ') || (slice[8] == ' ')) 251 { 252 slice = slice[7..$]; 253 httpPos = slice.indexOf("http"); 254 continue; 255 } 256 else if (!slice.contains(' ') && 257 (slice[10..$].contains("http://") || 258 slice[10..$].contains("https://"))) 259 { 260 // There is a second URL in the middle of this one 261 break; 262 } 263 264 // nom until the next space if there is one, otherwise just inherit slice 265 // Also strip away common punctuation 266 immutable hit = slice.nom!(Yes.inherit)(' ').strippedRight(wordBoundaryTokens); 267 if (hit.contains('.')) hits ~= hit; 268 httpPos = slice.indexOf("http"); 269 } 270 271 return hits; 272 } 273 274 /// 275 unittest 276 { 277 import std.conv : to; 278 279 { 280 const urls = findURLs("http://google.com"); 281 assert((urls.length == 1), urls.to!string); 282 assert((urls[0] == "http://google.com"), urls[0]); 283 } 284 { 285 const urls = findURLs("blah https://a.com http://b.com shttps://c https://d.asdf.asdf.asdf "); 286 assert((urls.length == 3), urls.to!string); 287 assert((urls == [ "https://a.com", "http://b.com", "https://d.asdf.asdf.asdf" ]), urls.to!string); 288 } 289 { 290 const urls = findURLs("http:// http://asdf https:// asdfhttpasdf http://google.com"); 291 assert((urls.length == 1), urls.to!string); 292 } 293 { 294 const urls = findURLs("http://a.sehttp://a.shttp://a.http://http:"); 295 assert(!urls.length, urls.to!string); 296 } 297 { 298 const urls = findURLs("blahblah https://motorbörsen.se blhblah"); 299 assert(urls.length, urls.to!string); 300 } 301 { 302 // Let dlang-requests attempt complex URLs, don't validate more than necessary 303 const urls = findURLs("blahblah https://高所恐怖症。co.jp blhblah"); 304 assert(urls.length, urls.to!string); 305 } 306 { 307 const urls = findURLs("nyaa is now at https://nyaa.si, https://nyaa.si? " ~ 308 "https://nyaa.si. https://nyaa.si! and you should use it https://nyaa.si:"); 309 310 foreach (immutable url; urls) 311 { 312 assert((url == "https://nyaa.si"), url); 313 } 314 } 315 { 316 const urls = findURLs("https://google.se httpx://google.se https://google.se"); 317 assert((urls == [ "https://google.se", "https://google.se" ]), urls.to!string); 318 } 319 { 320 const urls = findURLs("https:// "); 321 assert(!urls.length, urls.to!string); 322 } 323 { 324 const urls = findURLs("http:// "); 325 assert(!urls.length, urls.to!string); 326 } 327 } 328 329 330 // errnoStrings 331 /++ 332 Reverse mapping of [core.stdc.errno.errno|errno] values to their string names. 333 334 Automatically generated by introspecting [core.stdc.errno]. 335 336 --- 337 string[134] errnoStrings; 338 339 foreach (immutable symname; __traits(allMembers, core.stdc.errno)) 340 { 341 static if (symname[0] == 'E') 342 { 343 immutable idx = __traits(getMember, core.stdc.errno, symname); 344 345 if (errnoStrings[idx].length) 346 { 347 writefln("%s DUPLICATE %d", symname, idx); 348 } 349 else 350 { 351 errnoStrings[idx] = symname; 352 } 353 } 354 } 355 356 writeln("static immutable string[134] errnoStrings =\n["); 357 358 foreach (immutable i, immutable e; errnoStrings) 359 { 360 if (!e.length) continue; 361 writefln(` %-3d : "%s",`, i, e); 362 } 363 364 writeln("];"); 365 --- 366 +/ 367 version(Posix) 368 static immutable string[134] errnoStrings = 369 [ 370 0 : "<unset>", 371 1 : "EPERM", 372 2 : "ENOENT", 373 3 : "ESRCH", 374 4 : "EINTR", 375 5 : "EIO", 376 6 : "ENXIO", 377 7 : "E2BIG", 378 8 : "ENOEXEC", 379 9 : "EBADF", 380 10 : "ECHILD", 381 11 : "EAGAIN", // duplicate EWOULDBLOCK 382 12 : "ENOMEM", 383 13 : "EACCES", 384 14 : "EFAULT", 385 15 : "ENOTBLK", 386 16 : "EBUSY", 387 17 : "EEXIST", 388 18 : "EXDEV", 389 19 : "ENODEV", 390 20 : "ENOTDIR", 391 21 : "EISDIR", 392 22 : "EINVAL", 393 23 : "ENFILE", 394 24 : "EMFILE", 395 25 : "ENOTTY", 396 26 : "ETXTBSY", 397 27 : "EFBIG", 398 28 : "ENOSPC", 399 29 : "ESPIPE", 400 30 : "EROFS", 401 31 : "EMLINK", 402 32 : "EPIPE", 403 33 : "EDOM", 404 34 : "ERANGE", 405 35 : "EDEADLK", // duplicate EDEADLOCK 406 36 : "ENAMETOOLONG", 407 37 : "ENOLCK", 408 38 : "ENOSYS", 409 39 : "ENOTEMPTY", 410 40 : "ELOOP", 411 42 : "ENOMSG", 412 43 : "EIDRM", 413 44 : "ECHRNG", 414 45 : "EL2NSYNC", 415 46 : "EL3HLT", 416 47 : "EL3RST", 417 48 : "ELNRNG", 418 49 : "EUNATCH", 419 50 : "ENOCSI", 420 51 : "EL2HLT", 421 52 : "EBADE", 422 53 : "EBADR", 423 54 : "EXFULL", 424 55 : "ENOANO", 425 56 : "EBADRQC", 426 57 : "EBADSLT", 427 59 : "EBFONT", 428 60 : "ENOSTR", 429 61 : "ENODATA", 430 62 : "ETIME", 431 63 : "ENOSR", 432 64 : "ENONET", 433 65 : "ENOPKG", 434 66 : "EREMOTE", 435 67 : "ENOLINK", 436 68 : "EADV", 437 69 : "ESRMNT", 438 70 : "ECOMM", 439 71 : "EPROTO", 440 72 : "EMULTIHOP", 441 73 : "EDOTDOT", 442 74 : "EBADMSG", 443 75 : "EOVERFLOW", 444 76 : "ENOTUNIQ", 445 77 : "EBADFD", 446 78 : "EREMCHG", 447 79 : "ELIBACC", 448 80 : "ELIBBAD", 449 81 : "ELIBSCN", 450 82 : "ELIBMAX", 451 83 : "ELIBEXEC", 452 84 : "EILSEQ", 453 85 : "ERESTART", 454 86 : "ESTRPIPE", 455 87 : "EUSERS", 456 88 : "ENOTSOCK", 457 89 : "EDESTADDRREQ", 458 90 : "EMSGSIZE", 459 91 : "EPROTOTYPE", 460 92 : "ENOPROTOOPT", 461 93 : "EPROTONOSUPPORT", 462 94 : "ESOCKTNOSUPPORT", 463 95 : "EOPNOTSUPP", // duplicate ENOTSUPP 464 96 : "EPFNOSUPPORT", 465 97 : "EAFNOSUPPORT", 466 98 : "EADDRINUSE", 467 99 : "EADDRNOTAVAIL", 468 100 : "ENETDOWN", 469 101 : "ENETUNREACH", 470 102 : "ENETRESET", 471 103 : "ECONNABORTED", 472 104 : "ECONNRESET", 473 105 : "ENOBUFS", 474 106 : "EISCONN", 475 107 : "ENOTCONN", 476 108 : "ESHUTDOWN", 477 109 : "ETOOMANYREFS", 478 110 : "ETIMEDOUT", 479 111 : "ECONNREFUSED", 480 112 : "EHOSTDOWN", 481 113 : "EHOSTUNREACH", 482 114 : "EALREADY", 483 115 : "EINPROGRESS", 484 116 : "ESTALE", 485 117 : "EUCLEAN", 486 118 : "ENOTNAM", 487 119 : "ENAVAIL", 488 120 : "EISNAM", 489 121 : "EREMOTEIO", 490 122 : "EDQUOT", 491 123 : "ENOMEDIUM", 492 124 : "EMEDIUMTYPE", 493 125 : "ECANCELED", 494 126 : "ENOKEY", 495 127 : "EKEYEXPIRED", 496 128 : "EKEYREVOKED", 497 129 : "EKEYREJECTED", 498 130 : "EOWNERDEAD", 499 131 : "ENOTRECOVERABLE", 500 132 : "ERFKILL", 501 133 : "EHWPOISON", 502 ]; 503 504 505 // RehashingAA 506 /++ 507 A wrapper around a native associative array that you can controllably set to 508 automatically rehash as entries are added. 509 510 Params: 511 K = Key type. 512 V = Value type. 513 +/ 514 struct RehashingAA(K, V) 515 { 516 private: 517 /++ 518 Internal associative array. 519 +/ 520 V[K] aa; 521 522 /++ 523 The number of times this instance has rehashed itself. Private value. 524 +/ 525 uint _numRehashes; 526 527 /++ 528 The number of new entries that has been added since the last rehash. Private value. 529 +/ 530 uint _newKeysSinceLastRehash; 531 532 /++ 533 The number of keys (and length of the array) when the last rehash took place. 534 Private value. 535 +/ 536 size_t _lengthAtLastRehash; 537 538 public: 539 /++ 540 The minimum number of additions needed before the first rehash takes place. 541 +/ 542 uint minimumNeededForRehash = 64; 543 544 /++ 545 The modifier by how much more entries must be added before another rehash 546 takes place, with regards to the current [RehashingAA.aa|aa] length. 547 548 A multiplier of `2.0` means the associative array will be rehashed as 549 soon as its length doubles in size. Must be more than 1. 550 +/ 551 double rehashThresholdMultiplier = 1.5; 552 553 // opIndexAssign 554 /++ 555 Assigns a value into the internal associative array. If it created a new 556 entry, then call [maybeRehash] to bump the internal counter and maybe rehash. 557 558 Params: 559 value = Value. 560 key = Key. 561 +/ 562 void opIndexAssign(V value, K key) 563 { 564 if (auto existing = key in aa) 565 { 566 *existing = value; 567 } 568 else 569 { 570 aa[key] = value; 571 maybeRehash(); 572 } 573 } 574 575 // opAssign 576 /++ 577 Inherit a native associative array into [RehashingAA.aa|aa]. 578 579 Params: 580 aa = Other associative array. 581 +/ 582 void opAssign(V[K] aa) 583 { 584 this.aa = aa; 585 this.rehash(); 586 _numRehashes = 0; 587 } 588 589 // opCast 590 /++ 591 Allows for casting this into the base associative array type. 592 593 Params: 594 T = Type to cast to, here the same as the type of [RehashingAA.aa|aa]. 595 596 Returns: 597 The internal associative array. 598 +/ 599 auto opCast(T : V[K])() inout 600 { 601 return aa; 602 } 603 604 // aaOf 605 /++ 606 Returns the internal associative array, for when the wrapper is insufficient. 607 608 Returns: 609 The internal associative array. 610 +/ 611 inout(V[K]) aaOf() inout 612 { 613 return aa; 614 } 615 616 // remove 617 /++ 618 Removes a key from the [RehashingAA.aa|aa] associative array by merely 619 invoking `.remove`. 620 621 Params: 622 key = The key to remove. 623 624 Returns: 625 Whatever `aa.remove(key)` returns. 626 +/ 627 auto remove(K key) 628 { 629 //scope(exit) maybeRehash(); 630 return aa.remove(key); 631 } 632 633 // maybeRehash 634 /++ 635 Bumps the internal counter of new keys since the last rehash, and depending 636 on the resulting value of it, maybe rehashes. 637 638 Returns: 639 `true` if the associative array was rehashed; `false` if not. 640 +/ 641 auto maybeRehash() 642 { 643 if (++_newKeysSinceLastRehash > minimumNeededForRehash) 644 { 645 if (aa.length > (_lengthAtLastRehash * rehashThresholdMultiplier)) 646 { 647 this.rehash(); 648 return true; 649 } 650 } 651 652 return false; 653 } 654 655 // clear 656 /++ 657 Clears the internal associative array and all counters. 658 +/ 659 void clear() 660 { 661 aa.clear(); 662 _newKeysSinceLastRehash = 0; 663 _lengthAtLastRehash = 0; 664 _numRehashes = 0; 665 } 666 667 // rehash 668 /++ 669 Rehashes the internal associative array, bumping the rehash counter and 670 zeroing the keys-added counter. Additionally invokes the [onRehashDg] delegate. 671 672 Returns: 673 A reference to the rehashed internal array. 674 +/ 675 ref auto rehash() 676 { 677 scope(exit) if (onRehashDg) onRehashDg(); 678 _lengthAtLastRehash = aa.length; 679 _newKeysSinceLastRehash = 0; 680 ++_numRehashes; 681 aa.rehash(); 682 return this; 683 } 684 685 // numRehashes 686 /++ 687 The number of times this instance has rehashed itself. Accessor. 688 689 Returns: 690 The number of times this instance has rehashed itself. 691 +/ 692 auto numRehashes() const 693 { 694 return _numRehashes; 695 } 696 697 // numKeysAddedSinceLastRehash 698 /++ 699 The number of new entries that has been added since the last rehash. Accessor. 700 701 Returns: 702 The number of new entries that has been added since the last rehash. 703 +/ 704 auto newKeysSinceLastRehash() const 705 { 706 return _newKeysSinceLastRehash; 707 } 708 709 // opBinaryRight 710 /++ 711 Wraps `key in aa` to the internal associative array. 712 713 Params: 714 op = Operation, here "in". 715 key = Key. 716 717 Returns: 718 A pointer to the value of the key passed, or `null` if it isn't in 719 the associative array 720 +/ 721 auto opBinaryRight(string op : "in")(K key) inout 722 { 723 return key in aa; 724 } 725 726 // length 727 /++ 728 Returns the length of the internal associative array. 729 730 Returns: 731 The length of the internal associative array. 732 +/ 733 auto length() const 734 { 735 return aa.length; 736 } 737 738 // dup 739 /++ 740 Duplicates this. Explicitly copies the internal associative array. 741 742 Returns: 743 A duplicate of this object. 744 +/ 745 auto dup() 746 { 747 auto copy = this; 748 copy.aa = copy.aa.dup; 749 return copy; 750 } 751 752 // this 753 /++ 754 Constructor. 755 756 Params: 757 aa = Associative arary to inherit. Taken by reference for now. 758 +/ 759 this(V[K] aa) 760 { 761 this.aa = aa; 762 } 763 764 // onRehashDg 765 /++ 766 Delegate called when rehashing takes place. 767 +/ 768 void delegate() onRehashDg; 769 770 /++ 771 `alias this` with regards to [RehashingAA.aa|aa]. 772 +/ 773 version(none) 774 alias aa this; 775 } 776 777 /// 778 unittest 779 { 780 import std.conv : to; 781 782 RehashingAA!(string, int) aa; 783 aa.minimumNeededForRehash = 2; 784 785 aa["abc"] = 123; 786 aa["def"] = 456; 787 assert((aa.newKeysSinceLastRehash == 2), aa.newKeysSinceLastRehash.to!string); 788 assert((aa.numRehashes == 0), aa.numRehashes.to!string); 789 aa["ghi"] = 789; 790 assert((aa.numRehashes == 1), aa.numRehashes.to!string); 791 assert((aa.newKeysSinceLastRehash == 0), aa.newKeysSinceLastRehash.to!string); 792 aa.rehash(); 793 assert((aa.numRehashes == 2), aa.numRehashes.to!string); 794 795 auto realAA = cast(int[string])aa; 796 assert("abc" in realAA); 797 assert("def" in realAA); 798 assert("ghi" in realAA); 799 assert("jkl" !in realAA); 800 801 auto aa2 = aa.dup; 802 aa2["jkl"] = 123; 803 assert("jkl" in aa2); 804 assert("jkl" !in aa); 805 } 806 807 808 version(GCStatsOnExit) version = BuildPrintGCStats; 809 else version(IncludeHeavyStuff) version = BuildPrintGCStats; 810 811 812 // printGCStats 813 /++ 814 Prints garbage collector statistics to the local terminal. 815 816 Gated behind either version `GCStatsOnExit` *or* `IncludeHeavyStuff`. 817 +/ 818 version(BuildPrintGCStats) 819 void printGCStats() 820 { 821 import core.memory : GC; 822 823 immutable stats = GC.stats(); 824 825 static if (__VERSION__ >= 2087L) 826 { 827 enum pattern = "Lifetime allocated in current thread: <l>%,d</> bytes"; 828 logger.infof(pattern, stats.allocatedInCurrentThread); 829 } 830 831 enum memoryUsedPattern = "Memory currently in use: <l>%,d</> bytes; " ~ 832 "<l>%,d</> additional bytes reserved"; 833 logger.infof(memoryUsedPattern, stats.usedSize, stats.freeSize); 834 }