1 /++
2     The Printer plugin takes incoming [dialect.defs.IRCEvent|IRCEvent]s, formats them
3     into something easily readable and prints them to the screen, optionally with colours.
4     It also supports logging to disk.
5 
6     It has no commands; all [dialect.defs.IRCEvent|IRCEvent]s will be parsed and
7     printed, excluding certain types that were deemed too spammy. Print them as
8     well by disabling `filterMost`, in the configuration file under the header `[Printer]`.
9 
10     It is not technically necessary, but it is the main form of feedback you
11     get from the plugin, so you will only want to disable it if you want a
12     really "headless" environment.
13 
14     See_Also:
15         https://github.com/zorael/kameloso/wiki/Current-plugins#printer,
16         [kameloso.plugins.printer.formatting],
17         [kameloso.plugins.printer.logging],
18         [kameloso.plugins.common.core],
19         [kameloso.plugins.common.misc]
20 
21     Copyright: [JR](https://github.com/zorael)
22     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
23 
24     Authors:
25         [JR](https://github.com/zorael)
26  +/
27 module kameloso.plugins.printer.base;
28 
29 version(WithPrinterPlugin):
30 
31 private:
32 
33 import kameloso.plugins.printer.formatting;
34 import kameloso.plugins.printer.logging;
35 
36 import kameloso.plugins;
37 import kameloso.plugins.common.core;
38 import kameloso.plugins.common.awareness : ChannelAwareness, UserAwareness;
39 import dialect.defs;
40 import std.typecons : Flag, No, Yes;
41 
42 version(Colours) import kameloso.terminal.colours.defs : TerminalForeground;
43 
44 
45 // PrinterSettings
46 /++
47     All Printer plugin options gathered in a struct.
48  +/
49 @Settings struct PrinterSettings
50 {
51 private:
52     import lu.uda : Unserialisable;
53 
54 public:
55     /// Toggles whether or not the plugin should react to events at all.
56     @Enabler bool enabled = true;
57 
58     /// Toggles whether or not the plugin should print to screen (as opposed to just log).
59     bool monitor = true;
60 
61     version(Colours)
62     {
63         /// Whether or not to display nicks in a colour based on their nickname hash.
64         bool colourfulNicknames = true;
65 
66         @Unserialisable
67         {
68             /// Whether or not two users on the same account should be coloured identically.
69             bool colourByAccount = true;
70         }
71     }
72 
73     version(TwitchSupport)
74     {
75         @Unserialisable
76         {
77             /// Whether or not to display Twitch badges next to sender/target names.
78             bool twitchBadges = true;
79 
80             /++
81                 Whether or not to display advanced colours in `RRGGBB` rather
82                 than simple ANSI codes, where available and appropriate.
83              +/
84             bool truecolour = true;
85 
86             /// Whether or not to normalise truecolours; make dark brighter and bright darker.
87             bool normaliseTruecolour = true;
88 
89             /// Whether or not emotes should be highlit in colours.
90             bool colourfulEmotes = true;
91         }
92     }
93 
94     /++
95         Whether or not to show Message of the Day upon connecting.
96 
97         Warning! MOTD generally lists server rules, which might be good to read.
98      +/
99     bool motd = false;
100 
101     /// Whether or not to filter away most uninteresting events.
102     bool filterMost = true;
103 
104     /// Whether or not to filter WHOIS queries.
105     bool filterWhois = true;
106 
107     /// Whether or not to hide events from blacklisted users.
108     bool hideBlacklistedUsers = false;
109 
110     /// Whether or not to log events.
111     bool logs = false;
112 
113     /// Whether or not to log non-home channels.
114     bool logGuestChannels = false;
115 
116     /// Whether or not to log private messages.
117     bool logPrivateMessages = true;
118 
119     @Unserialisable
120     {
121         /// Whether or not to send a terminal bell signal when the bot is mentioned in chat.
122         bool bellOnMention = false;
123 
124         /// Whether or not to bell on parsing errors.
125         bool bellOnError = false;
126 
127         /// Whether or not to log server messages.
128         bool logServer = false;
129 
130         /// Whether or not to log errors.
131         bool logErrors = true;
132 
133         /// Whether or not to log raw events.
134         bool logRaw = false;
135 
136         /// Whether or not to have the type names be in capital letters.
137         bool uppercaseTypes = false;
138 
139         /// Whether or not to print a banner to the terminal at midnights, when day changes.
140         bool daybreaks = true;
141 
142         /// Whether or not to buffer writes.
143         bool bufferedWrites = true;
144     }
145 }
146 
147 
148 // onPrintableEvent
149 /++
150     Prints an event to the local terminal.
151 
152     Buffer output in an [std.array.Appender|Appender].
153 
154     Mutable [dialect.defs.IRCEvent|IRCEvent] parameter so as to make fewer internal copies
155     (as this is a hotspot).
156  +/
157 @(IRCEventHandler()
158     .onEvent(IRCEvent.Type.ANY)
159     .channelPolicy(ChannelPolicy.any)
160     .chainable(true)
161 )
162 void onPrintableEvent(PrinterPlugin plugin, /*const*/ IRCEvent event)
163 {
164     if (!plugin.printerSettings.monitor || plugin.state.settings.headless) return;
165 
166     if (plugin.printerSettings.hideBlacklistedUsers && (event.sender.class_ == IRCUser.Class.blacklist)) return;
167 
168     // For many types there's no need to display the target nickname when it's the bot's
169     // Clear event.target.nickname for those types.
170     event.clearTargetNicknameIfUs(plugin.state);
171 
172     /++
173         Return whether or not the current event should be squelched based on
174         if the passed channel, sender or target nickname has a squelchstamp
175         that demands it. Additionally updates the squelchstamp if so.
176      +/
177     static bool updateSquelchstamp(
178         PrinterPlugin plugin,
179         const long time,
180         const string channel,
181         const string sender,
182         const string target)
183     in ((channel.length || sender.length || target.length),
184         "Tried to update squelchstamp but with no channel or user information passed")
185     {
186         /*import std.algorithm.comparison : either;
187         immutable key = either!(s => s.length)(channel, sender, target);*/
188 
189         immutable key =
190             channel.length ? channel :
191             sender.length ? sender :
192             /*target.length ?*/ target;
193 
194         // already in in-contract
195         /*assert(key.length, "Logic error; tried to update squelchstamp but " ~
196             "no `channel`, no `sender`, no `target`");*/
197 
198         auto squelchstamp = key in plugin.squelches;
199 
200         if (!squelchstamp)
201         {
202             plugin.hasSquelches = (plugin.squelches.length > 0);
203             return false;
204         }
205         else if ((time - *squelchstamp) <= plugin.squelchTimeout)
206         {
207             *squelchstamp = time;
208             return true;
209         }
210         else
211         {
212             plugin.squelches.remove(key);
213             plugin.hasSquelches = (plugin.squelches.length > 0);
214             return false;
215         }
216     }
217 
218     with (IRCEvent.Type)
219     switch (event.type)
220     {
221     case RPL_MOTDSTART:
222     case RPL_MOTD:
223     case RPL_ENDOFMOTD:
224     case ERR_NOMOTD:
225         // Only show these if we're configured to
226         if (plugin.printerSettings.motd) goto default;
227         break;
228 
229     case RPL_WHOISACCOUNT:
230     case RPL_WHOISACCOUNTONLY:
231     case RPL_WHOISADMIN:
232     case RPL_WHOISBOT:
233     case RPL_WHOISCERTFP:
234     case RPL_WHOISCHANNELS:
235     case RPL_WHOISCHANOP:
236     case RPL_WHOISHELPER:
237     case RPL_WHOISHELPOP:
238     case RPL_WHOISHOST:
239     case RPL_WHOISIDLE:
240     case RPL_ENDOFWHOIS:
241     case RPL_TARGUMODEG:
242     case RPL_WHOISREGNICK:
243     case RPL_WHOISKEYVALUE:
244     case RPL_WHOISKILL:
245     case RPL_WHOISLANGUAGE:
246     case RPL_WHOISMARKS:
247     case RPL_WHOISMODES:
248     case RPL_WHOISOPERATOR:
249     case RPL_WHOISPRIVDEAF:
250     case RPL_WHOISREALIP:
251     case RPL_WHOISSECURE:
252     case RPL_WHOISSPECIAL:
253     case RPL_WHOISSSLFP:
254     case RPL_WHOISSTAFF:
255     case RPL_WHOISSVCMSG:
256     case RPL_WHOISTEXT:
257     case RPL_WHOISUSER:
258     case RPL_WHOISVIRT:
259     case RPL_WHOISWEBIRC:
260     case RPL_WHOISYOURID:
261     case RPL_WHOIS_HIDDEN:
262     case RPL_WHOISACTUALLY:
263     case RPL_WHOWASDETAILS:
264     case RPL_WHOWASHOST:
265     case RPL_WHOWASIP:
266     case RPL_WHOWASREAL:
267     case RPL_WHOWASUSER:
268     case RPL_WHOWAS_TIME:
269     case RPL_ENDOFWHOWAS:
270     case RPL_WHOISSERVER:
271     case RPL_CHARSET:
272     case RPL_STATSRLINE:
273         immutable shouldSquelch = plugin.hasSquelches &&
274             updateSquelchstamp(
275                 plugin,
276                 event.time,
277                 event.channel,
278                 event.sender.nickname,
279                 event.target.nickname);
280 
281         if (!shouldSquelch && !plugin.printerSettings.filterWhois)
282         {
283             goto default;
284         }
285         else
286         {
287             break;
288         }
289 
290     case RPL_NAMREPLY:
291     case RPL_ENDOFNAMES:
292     case RPL_YOURHOST:
293     case RPL_ISUPPORT:
294     case RPL_LUSERCLIENT:
295     case RPL_LUSEROP:
296     case RPL_LUSERCHANNELS:
297     case RPL_LUSERME:
298     case RPL_LUSERUNKNOWN:
299     case RPL_GLOBALUSERS:
300     case RPL_LOCALUSERS:
301     case RPL_STATSCONN:
302     case RPL_MYINFO:
303     case RPL_CREATED:
304     case CAP:
305     case GLOBALUSERSTATE:
306     //case USERSTATE:
307     case ROOMSTATE:
308     case SASL_AUTHENTICATE:
309     case CTCP_AVATAR:
310     case CTCP_CLIENTINFO:
311     case CTCP_DCC:
312     case CTCP_FINGER:
313     case CTCP_LAG:
314     case CTCP_PING:
315     case CTCP_SLOTS:
316     case CTCP_SOURCE:
317     case CTCP_TIME:
318     case CTCP_USERINFO:
319     case CTCP_VERSION:
320     case SELFMODE:
321         // These event types are spammy and/or have low signal-to-noise ratio;
322         // ignore if we're configured to
323         if (plugin.printerSettings.filterMost) break;
324         goto default;
325 
326     case JOIN:
327     case PART:
328         version(TwitchSupport)
329         {
330             if (plugin.state.server.daemon == IRCServer.Daemon.twitch)
331             {
332                 // Filter overly verbose JOINs and PARTs on Twitch if we're filtering
333                 if (plugin.printerSettings.filterMost) break;
334             }
335             goto default;
336         }
337         else
338         {
339             goto default;
340         }
341 
342     version(WithConnectService)
343     {
344         case ERR_NICKNAMEINUSE:  // When failing to regain nickname
345             goto case;
346     }
347 
348     case RPL_WHOREPLY:
349     case RPL_ENDOFWHO:
350     case RPL_TOPICWHOTIME:
351     case RPL_CHANNELMODEIS:
352     case RPL_CREATIONTIME:
353     case RPL_BANLIST:
354     case RPL_QUIETLIST:
355     case RPL_INVITELIST:
356     case RPL_EXCEPTLIST:
357     case RPL_REOPLIST:
358     case RPL_ENDOFREOPLIST:
359     case SPAMFILTERLIST:
360     case RPL_ENDOFBANLIST:
361     case RPL_ENDOFQUIETLIST:
362     case RPL_ENDOFINVITELIST:
363     case RPL_ENDOFEXCEPTLIST:
364     case ENDOFEXEMPTOPSLIST:
365     case ENDOFSPAMFILTERLIST:
366     case ERR_CHANOPRIVSNEEDED:
367     case RPL_AWAY:
368     case ENDOFCHANNELACCLIST:
369     case MODELIST:
370     case ENDOFMODELIST:
371     case RPL_ENDOFQLIST:
372     case RPL_ENDOFALIST:
373     case RPL_TOPIC:
374     case RPL_NOTOPIC:
375     case ERR_NOSUCHNICK:
376     case ERR_NOSUCHCHANNEL:
377         // Error: switch skips declaration of variable shouldSquelch
378         {
379             immutable shouldSquelch = plugin.hasSquelches &&
380                 updateSquelchstamp(
381                     plugin,
382                     event.time,
383                     event.channel,
384                     event.sender.nickname,
385                     event.target.nickname);
386 
387             if (!shouldSquelch && !plugin.printerSettings.filterMost)
388             {
389                 goto default;
390             }
391             else
392             {
393                 break;
394             }
395         }
396 
397     version(TwitchSupport)
398     {
399         case USERSTATE: // Once per channel join? Once per message sent?
400             break;
401     }
402 
403     case PONG:
404         break;
405 
406     case PING:
407         import lu.string : contains;
408 
409         // Show the on-connect-ping-this type of events if !filterMost
410         // Assume those containing dots are real pings for the server address
411         if (!plugin.printerSettings.filterMost && event.content.length) goto default;
412         break;
413 
414     default:
415         import kameloso.terminal : TerminalToken;
416         import lu.string : strippedRight;
417         import std.array : replace;
418         import std.stdio : stdout, writeln;
419 
420         // Strip bells so we don't get phantom noise
421         // Strip right to get rid of trailing whitespace
422         // Do it in this order in case bells hide whitespace.
423         event.content = event.content
424             .replace(cast(ubyte)TerminalToken.bell, string.init)
425             .strippedRight;
426 
427         bool put;
428 
429         alias BellOnMention = Flag!"bellOnMention";
430         alias BellOnError = Flag!"bellOnError";
431 
432         scope(exit) plugin.linebuffer.clear();
433 
434         version(Colours)
435         {
436             if (!plugin.state.settings.monochrome)
437             {
438                 formatMessageColoured(
439                     plugin,
440                     plugin.linebuffer,
441                     event,
442                     cast(BellOnMention)plugin.printerSettings.bellOnMention,
443                     cast(BellOnError)plugin.printerSettings.bellOnError);
444                 put = true;
445             }
446         }
447 
448         if (!put)
449         {
450             formatMessageMonochrome(
451                 plugin,
452                 plugin.linebuffer,
453                 event,
454                 cast(BellOnMention)plugin.printerSettings.bellOnMention,
455                 cast(BellOnError)plugin.printerSettings.bellOnError);
456         }
457 
458         writeln(plugin.linebuffer.data);
459         if (plugin.state.settings.flush) stdout.flush();
460         break;
461     }
462 }
463 
464 
465 // onLoggableEvent
466 /++
467     Logs an event to disk.
468 
469     It is set to [kameloso.plugins.common.core.ChannelPolicy.any|ChannelPolicy.any],
470     and configuration dictates whether or not non-home events should be logged.
471     Likewise whether or not raw events should be logged.
472 
473     Lines will either be saved immediately to disk, opening a [std.stdio.File|File]
474     with appending privileges for each event as they occur, or buffered by
475     populating arrays of lines to be written in bulk, once in a while.
476 
477     See_Also:
478         [commitAllLogs]
479  +/
480 @(IRCEventHandler()
481     .onEvent(IRCEvent.Type.ANY)
482     .channelPolicy(ChannelPolicy.any)
483     .chainable(true)
484 )
485 void onLoggableEvent(PrinterPlugin plugin, const ref IRCEvent event)
486 {
487     onLoggableEventImpl(plugin, event);
488 }
489 
490 
491 // commitAllLogs
492 /++
493     Writes all buffered log lines to disk.
494 
495     Merely wraps [commitAllLogsImpl] by iterating over all buffers and invoking it.
496 
497     Params:
498         plugin = The current [PrinterPlugin].
499 
500     See_Also:
501         [kameloso.plugins.printer.logging.commitAllLogsImpl|printer.logging.commitAllLogsImpl]
502  +/
503 @(IRCEventHandler()
504     .onEvent(IRCEvent.Type.PING)
505 )
506 void commitAllLogs(PrinterPlugin plugin)
507 {
508     commitAllLogsImpl(plugin);
509 }
510 
511 
512 // onISUPPORT
513 /++
514     Prints information about the current server as we gain details of it from an
515     [dialect.defs.IRCEvent.Type.RPL_ISUPPORT|RPL_ISUPPORT] event.
516 
517     Set a flag so we only print this information once;
518     ([dialect.defs.IRCEvent.Type.RPL_ISUPPORT|RPL_ISUPPORT] can/do stretch
519     across several events.)
520  +/
521 @(IRCEventHandler()
522     .onEvent(IRCEvent.Type.RPL_ISUPPORT)
523 )
524 void onISUPPORT(PrinterPlugin plugin)
525 {
526     import kameloso.common : logger;
527 
528     static uint idWhenPrintedISUPPORT;
529 
530     if ((idWhenPrintedISUPPORT == plugin.state.connectionID) ||
531         !plugin.state.server.network.length)
532     {
533         // We already printed this information, or we haven't yet seen NETWORK
534         return;
535     }
536 
537     idWhenPrintedISUPPORT = plugin.state.connectionID;
538 
539     enum pattern = "Detected <i>%s</> running daemon <i>%s</> (<t>%s</>)";
540     logger.logf(
541         pattern,
542         plugin.state.server.network,
543         plugin.state.server.daemon,
544         plugin.state.server.daemonstring);
545 }
546 
547 
548 // datestamp
549 /++
550     Returns a string with the current date.
551 
552     Example:
553     ---
554     writeln("Current date ", datestamp);
555     ---
556 
557     Returns:
558         A string with the current date.
559  +/
560 package auto datestamp()
561 {
562     import std.datetime.systime : Clock;
563     import std.format : format;
564 
565     immutable now = Clock.currTime;
566     enum pattern = "-- [%d-%02d-%02d]";
567     return format(pattern, now.year, cast(int)now.month, now.day);
568 }
569 
570 
571 // initialise
572 /++
573     Initialises the Printer plugin by allocating a slice of memory for the linebuffer.
574  +/
575 void initialise(PrinterPlugin plugin)
576 {
577     import kameloso.terminal : isTerminal;
578 
579     plugin.linebuffer.reserve(PrinterPlugin.linebufferInitialSize);
580 
581     if (!isTerminal)
582     {
583         // Not a TTY so replace our bell string with an empty one
584         plugin.bell = string.init;
585     }
586 }
587 
588 
589 // start
590 /++
591     Sets up a Fiber to print the date in `YYYY-MM-DD` format to the screen and
592     to any active log files upon day change.
593  +/
594 void start(PrinterPlugin plugin)
595 {
596     import kameloso.plugins.common.delayawait : delay;
597     import kameloso.constants : BufferSize;
598     import core.thread : Fiber;
599     import core.time : Duration;
600 
601     static Duration untilNextMidnight()
602     {
603         import kameloso.time : nextMidnight;
604         import std.datetime.systime : Clock;
605 
606         immutable now = Clock.currTime;
607         return (now.nextMidnight - now);
608     }
609 
610     void daybreakDg()
611     {
612         while (true)
613         {
614             if (plugin.isEnabled)
615             {
616                 if (plugin.printerSettings.monitor && plugin.printerSettings.daybreaks)
617                 {
618                     import kameloso.common : logger;
619                     logger.info(datestamp);
620                 }
621 
622                 if (plugin.printerSettings.logs)
623                 {
624                     commitAllLogs(plugin);
625                     plugin.buffers.clear();  // Uncommitted lines will be LOST. Not trivial to work around.
626                 }
627             }
628 
629             delay(plugin, untilNextMidnight, Yes.yield);
630         }
631     }
632 
633     Fiber daybreakFiber = new Fiber(&daybreakDg, BufferSize.fiberStack);
634     delay(plugin, daybreakFiber, untilNextMidnight);
635 }
636 
637 
638 // initResources
639 /++
640     Ensures that there is a log directory.
641  +/
642 void initResources(PrinterPlugin plugin)
643 {
644     if (!plugin.printerSettings.logs) return;
645 
646     if (!establishLogLocation(plugin.logDirectory, plugin.state.connectionID))
647     {
648         import kameloso.plugins.common.misc : IRCPluginInitialisationException;
649 
650         throw new IRCPluginInitialisationException(
651             "Could not create log directory",
652             plugin.name,
653             string.init,
654             __FILE__,
655             __LINE__);
656     }
657 }
658 
659 
660 // teardown
661 /++
662     De-initialises the plugin.
663 
664     If we're buffering writes, commit all queued lines to disk.
665  +/
666 void teardown(PrinterPlugin plugin)
667 {
668     if (plugin.printerSettings.bufferedWrites)
669     {
670         // Commit all logs before exiting
671         commitAllLogs(plugin);
672     }
673 }
674 
675 
676 import kameloso.thread : Sendable;
677 
678 // onBusMessage
679 /++
680     Receives a passed [kameloso.thread.Boxed|Boxed] instance with the "`printer`" header,
681     listening for cues to ignore the next events caused by the
682     [kameloso.plugins.services.chanqueries.ChanQueriesService|ChanQueriesService]
683     querying current channel for information on the channels and their users.
684 
685     Params:
686         plugin = The current [PrinterPlugin].
687         header = String header describing the passed content payload.
688         content = Message content.
689  +/
690 void onBusMessage(PrinterPlugin plugin, const string header, shared Sendable content)
691 {
692     import kameloso.thread : Boxed;
693     import lu.string : nom;
694     import std.typecons : Flag, No, Yes;
695 
696     if (header != "printer") return;
697 
698     auto message = cast(Boxed!string)content;
699     assert(message, "Incorrectly cast message: " ~ typeof(message).stringof);
700 
701     string slice = message.payload;
702     immutable verb = slice.nom!(Yes.inherit)(' ');
703     immutable target = slice;
704 
705     switch (verb)
706     {
707     case "squelch":
708         import std.datetime.systime : Clock;
709         plugin.squelches[target] = Clock.currTime.toUnixTime;
710         plugin.hasSquelches = true;
711         break;
712 
713     case "unsquelch":
714         plugin.squelches.remove(target);
715         plugin.hasSquelches = (plugin.squelches.length > 0);
716         break;
717 
718     default:
719         import kameloso.common : logger;
720         logger.error("[printer] Unimplemented bus message verb: ", verb);
721         break;
722     }
723 }
724 
725 
726 // clearTargetNicknameIfUs
727 /++
728     Clears the target nickname if it matches the passed string.
729 
730     Example:
731     ---
732     event.clearTargetNicknameIfUs(plugin.state);
733     ---
734  +/
735 void clearTargetNicknameIfUs(ref IRCEvent event, const IRCPluginState state)
736 {
737     if (event.target.nickname == state.client.nickname)
738     {
739         with (IRCEvent.Type)
740         switch (event.type)
741         {
742         case MODE:
743         case QUERY:
744         case SELFNICK:
745         case RPL_WHOREPLY:
746         case RPL_WHOISUSER:
747         case RPL_WHOISCHANNELS:
748         case RPL_WHOISSERVER:
749         case RPL_WHOISHOST:
750         case RPL_WHOISIDLE:
751         case RPL_LOGGEDIN:
752         case RPL_WHOISACCOUNT:
753         case RPL_WHOISREGNICK:
754         case RPL_ENDOFWHOIS:
755         case RPL_WELCOME:
756         case RPL_WHOISSECURE:
757         case RPL_WHOISCERTFP:
758         case RPL_WHOISSSLFP:
759         case RPL_WHOISSPECIAL:
760         case RPL_WHOISSTAFF:
761         case RPL_WHOISYOURID:
762         case RPL_WHOISVIRT:
763         case RPL_WHOISSVCMSG:
764         case RPL_WHOISTEXT:
765         case RPL_WHOISWEBIRC:
766         case RPL_WHOISACTUALLY:
767         case RPL_WHOISMODES:
768         case RPL_WHOWASIP:
769         case RPL_STATSRLINE:
770             // Keep bot's nickname as target for these event types.
771             break;
772 
773         version(TwitchSupport)
774         {
775             case CLEARCHAT:
776             case CLEARMSG:
777             case TWITCH_BAN:
778             case TWITCH_GIFTCHAIN:
779             case TWITCH_GIFTRECEIVED:
780             case TWITCH_SUBGIFT:
781             case TWITCH_TIMEOUT:
782             case CHAN:
783             case EMOTE:
784                 // Likewise
785                 break;
786         }
787 
788         default:
789             event.target.nickname = string.init;
790             return;
791         }
792     }
793     else if (event.target.nickname == "*")
794     {
795         /++
796             Some events have an asterisk in what we consider the target nickname field. Sometimes.
797             [loggedin] wolfe.freenode.net (*): "You are now logged in as kameloso." (#900)
798             Clear it if so, since it conveys no information we care about.
799          +/
800         event.target.nickname = string.init;
801     }
802 }
803 
804 ///
805 unittest
806 {
807     enum us = "kameloso";
808     enum notUs = "hirrsteff";
809 
810     IRCPluginState state;
811     state.client.nickname = us;
812 
813     {
814         IRCEvent event;
815         event.type = IRCEvent.Type.MODE;
816         event.target.nickname = us;
817         event.clearTargetNicknameIfUs(state);
818         assert((event.target.nickname == us), event.target.nickname);
819     }
820     {
821         IRCEvent event;
822         event.type = IRCEvent.Type.MODE;
823         event.target.nickname = notUs;
824         event.clearTargetNicknameIfUs(state);
825         assert((event.target.nickname == notUs), event.target.nickname);
826     }
827 }
828 
829 
830 mixin UserAwareness!(ChannelPolicy.any);
831 mixin ChannelAwareness!(ChannelPolicy.any);
832 mixin PluginRegistration!(PrinterPlugin, -40.priority);
833 
834 public:
835 
836 
837 // PrinterPlugin
838 /++
839     The Printer plugin takes all [dialect.defs.IRCEvent|IRCEvent]s and prints them to
840     the local terminal, formatted and optionally in colour. Alternatively to disk as logs.
841 
842     This used to be part of the core program, but with UDAs it's easy to split
843     off into its own plugin.
844  +/
845 final class PrinterPlugin : IRCPlugin
846 {
847 private:
848     import kameloso.terminal : TerminalToken;
849     import std.array : Appender;
850 
851 package:
852     /// All Printer plugin options gathered.
853     PrinterSettings printerSettings;
854 
855     /// How many seconds before a request to squelch list events times out.
856     enum squelchTimeout = 5;  // seconds
857 
858     /// How many bytes to preallocate for the [linebuffer].
859     enum linebufferInitialSize = 2048;
860 
861     /++
862         Nicknames or channels, to or from which select events should be squelched.
863         UNIX timestamp value.
864      +/
865     long[string] squelches;
866 
867     /// Whether or not at least one squelch is active; whether [squelches] is non-empty.
868     bool hasSquelches;
869 
870     /// Buffers, to clump log file writes together.
871     LogLineBuffer[string] buffers;
872 
873     /// Buffer to fill with the line to print to screen.
874     Appender!(char[]) linebuffer;
875 
876     /// Where to save logs.
877     @Resource string logDirectory = "logs";
878 
879     /// [kameloso.terminal.TerminalToken.bell|TerminalToken.bell] as string, for use as bell.
880     private enum bellString = "" ~ cast(char)(TerminalToken.bell);
881 
882     /// Effective bell after [kameloso.terminal.isTerminal] checks.
883     string bell = bellString;
884 
885     mixin IRCPluginImpl;
886 }