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 }