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 }