1 /++ 2 The Admin plugin features bot commands which help with debugging the current 3 state, like printing the current list of users, the 4 current channels, the raw incoming strings from the server, and some other 5 things along the same line. 6 7 It also offers some less debug-y, more administrative functions, like adding 8 and removing homes on-the-fly, whitelisting or de-whitelisting account 9 names, adding/removing from the operator/staff lists, joining or leaving channels, and such. 10 11 See_Also: 12 https://github.com/zorael/kameloso/wiki/Current-plugins#admin, 13 [kameloso.plugins.common.core], 14 [kameloso.plugins.common.misc] 15 16 Copyright: [JR](https://github.com/zorael) 17 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 18 19 Authors: 20 [JR](https://github.com/zorael) 21 +/ 22 module kameloso.plugins.admin.base; 23 24 version(WithAdminPlugin): 25 26 private: 27 28 import kameloso.plugins.admin.classifiers; 29 debug import kameloso.plugins.admin.debugging; 30 31 import kameloso.plugins; 32 import kameloso.plugins.common.core; 33 import kameloso.plugins.common.awareness; 34 import kameloso.common : logger; 35 import kameloso.messaging; 36 import dialect.defs; 37 import std.concurrency : send; 38 import std.typecons : Flag, No, Yes; 39 import core.time : Duration; 40 41 42 version(OmniscientAdmin) 43 { 44 /++ 45 The [kameloso.plugins.common.core.ChannelPolicy|ChannelPolicy] to mix in 46 awareness with depending on whether version `OmniscientAdmin` is set or not. 47 +/ 48 enum omniscientChannelPolicy = ChannelPolicy.any; 49 } 50 else 51 { 52 /// Ditto 53 enum omniscientChannelPolicy = ChannelPolicy.home; 54 } 55 56 57 // AdminSettings 58 /++ 59 All Admin plugin settings, gathered in a struct. 60 +/ 61 @Settings struct AdminSettings 62 { 63 private: 64 import lu.uda : Unserialisable; 65 66 public: 67 /// Toggles whether or not the plugin should react to events at all. 68 @Enabler bool enabled = true; 69 70 @Unserialisable 71 { 72 /++ 73 Toggles whether [onAnyEvent] prints the raw strings of all incoming 74 events or not. 75 +/ 76 bool printRaw; 77 78 /++ 79 Toggles whether [onAnyEvent] prints the raw bytes of the *contents* 80 of events or not. 81 +/ 82 bool printBytes; 83 } 84 } 85 86 87 // onAnyEvent 88 /++ 89 Prints incoming events to the local terminal, in forms depending on 90 which flags have been set with bot commands. 91 92 If [AdminPlugin.printRaw] is set by way of invoking [onCommandPrintRaw], 93 prints all incoming server strings. 94 95 If [AdminPlugin.printBytes] is set by way of invoking [onCommandPrintBytes], 96 prints all incoming server strings byte by byte. 97 +/ 98 debug 99 @(IRCEventHandler() 100 .onEvent(IRCEvent.Type.ANY) 101 .channelPolicy(ChannelPolicy.any) 102 .chainable(true) 103 ) 104 void onAnyEvent(AdminPlugin plugin, const ref IRCEvent event) 105 { 106 if (plugin.state.settings.headless) return; 107 onAnyEventImpl(plugin, event); 108 } 109 110 111 // onCommandShowUser 112 /++ 113 Prints the details of one or more specific, supplied users to the local terminal. 114 115 It basically prints the matching [dialect.defs.IRCUser|IRCUsers]. 116 +/ 117 debug 118 version(IncludeHeavyStuff) 119 @(IRCEventHandler() 120 .onEvent(IRCEvent.Type.CHAN) 121 .onEvent(IRCEvent.Type.QUERY) 122 .permissionsRequired(Permissions.admin) 123 .channelPolicy(ChannelPolicy.home) 124 .addCommand( 125 IRCEventHandler.Command() 126 .word("user") 127 .policy(PrefixPolicy.nickname) 128 .description("[debug] Prints out information about one or more " ~ 129 "specific users to the local terminal.") 130 .addSyntax("$command [nickname] [nickname] ...") 131 ) 132 ) 133 void onCommandShowUser(AdminPlugin plugin, const ref IRCEvent event) 134 { 135 if (plugin.state.settings.headless) return; 136 onCommandShowUserImpl(plugin, event); 137 } 138 139 140 // onCommandWhoami 141 /++ 142 Sends what we know of the inquiring user. 143 +/ 144 @(IRCEventHandler() 145 .onEvent(IRCEvent.Type.CHAN) 146 .onEvent(IRCEvent.Type.QUERY) 147 .permissionsRequired(Permissions.anyone) 148 .channelPolicy(ChannelPolicy.home) 149 .addCommand( 150 IRCEventHandler.Command() 151 .word("whoami") 152 .policy(PrefixPolicy.prefixed) 153 .description("Replies with what we know of the inquiring user.") 154 ) 155 ) 156 void onCommandWhoami(AdminPlugin plugin, const ref IRCEvent event) 157 { 158 import lu.conv : Enum; 159 import std.format : format; 160 161 immutable account = event.sender.account.length ? event.sender.account : "*"; 162 string message; // mutable 163 164 if (event.channel.length) 165 { 166 enum pattern = "You are <h>%s<h>@<b>%s<b> (%s), class:<b>%s<b> in the scope of <b>%s<b>."; 167 message = pattern.format( 168 event.sender.nickname, 169 account, 170 event.sender.hostmask, 171 Enum!(IRCUser.Class).toString(event.sender.class_), 172 event.channel); 173 } 174 else 175 { 176 enum pattern = "You are <h>%s<h>@<b>%s<b> (%s), class:<b>%s<b> in a global scope."; 177 message = pattern.format( 178 event.sender.nickname, 179 account, 180 event.sender.hostmask, 181 Enum!(IRCUser.Class).toString(event.sender.class_)); 182 } 183 184 privmsg(plugin.state, event.channel, event.sender.nickname, message); 185 } 186 187 188 // onCommandSave 189 /++ 190 Saves current configuration to disk. 191 192 This saves all plugins' settings, not just this plugin's, effectively 193 regenerating the configuration file. 194 +/ 195 @(IRCEventHandler() 196 .onEvent(IRCEvent.Type.CHAN) 197 .onEvent(IRCEvent.Type.QUERY) 198 .permissionsRequired(Permissions.admin) 199 .channelPolicy(ChannelPolicy.home) 200 .addCommand( 201 IRCEventHandler.Command() 202 .word("save") 203 .policy(PrefixPolicy.nickname) 204 .description("Saves current configuration.") 205 ) 206 ) 207 void onCommandSave(AdminPlugin plugin, const ref IRCEvent event) 208 { 209 import kameloso.thread : ThreadMessage; 210 211 enum message = "Saving configuration to disk."; 212 privmsg(plugin.state, event.channel, event.sender.nickname, message); 213 plugin.state.mainThread.send(ThreadMessage.save()); 214 } 215 216 217 // onCommandShowUsers 218 /++ 219 Prints out the current `users` array of the [AdminPlugin]'s 220 [kameloso.plugins.common.core.IRCPluginState|IRCPluginState] to the local terminal. 221 +/ 222 debug 223 version(IncludeHeavyStuff) 224 @(IRCEventHandler() 225 .onEvent(IRCEvent.Type.CHAN) 226 .onEvent(IRCEvent.Type.QUERY) 227 .permissionsRequired(Permissions.admin) 228 .channelPolicy(ChannelPolicy.home) 229 .addCommand( 230 IRCEventHandler.Command() 231 .word("users") 232 .policy(PrefixPolicy.nickname) 233 .description("[debug] Prints out the current users array to the local terminal.") 234 ) 235 ) 236 void onCommandShowUsers(AdminPlugin plugin) 237 { 238 if (plugin.state.settings.headless) return; 239 onCommandShowUsersImpl(plugin); 240 } 241 242 243 // onCommandSudo 244 /++ 245 Sends supplied text to the server, verbatim. 246 247 You need basic knowledge of IRC server strings to use this. 248 +/ 249 debug 250 @(IRCEventHandler() 251 .onEvent(IRCEvent.Type.CHAN) 252 .onEvent(IRCEvent.Type.QUERY) 253 .permissionsRequired(Permissions.admin) 254 .channelPolicy(omniscientChannelPolicy) 255 .addCommand( 256 IRCEventHandler.Command() 257 .word("sudo") 258 .policy(PrefixPolicy.nickname) 259 .description("[debug] Sends supplied text to the server, verbatim.") 260 .addSyntax("$command [raw string]") 261 ) 262 ) 263 void onCommandSudo(AdminPlugin plugin, const ref IRCEvent event) 264 { 265 onCommandSudoImpl(plugin, event); 266 } 267 268 269 // onCommandQuit 270 /++ 271 Sends a [dialect.defs.IRCEvent.Type.QUIT|IRCEvent.Type.QUIT] event to the server. 272 273 If any extra text is following the "quit" command, it uses that as the quit 274 reason. Otherwise it falls back to what is specified in the configuration file. 275 +/ 276 277 @(IRCEventHandler() 278 .onEvent(IRCEvent.Type.CHAN) 279 .onEvent(IRCEvent.Type.QUERY) 280 .permissionsRequired(Permissions.admin) 281 .channelPolicy(ChannelPolicy.home) 282 .addCommand( 283 IRCEventHandler.Command() 284 .word("quit") 285 .policy(PrefixPolicy.nickname) 286 .description("Disconnects from the server and exits the program.") 287 .addSyntax("$command [optional quit reason]") 288 ) 289 ) 290 void onCommandQuit(AdminPlugin plugin, const ref IRCEvent event) 291 { 292 quit(plugin.state, event.content); 293 } 294 295 296 // onCommandHome 297 /++ 298 Adds or removes channels to/from the list of currently active home channels, 299 in the [kameloso.pods.IRCBot.homeChannels|IRCBot.homeChannels] array of 300 the current [AdminPlugin]'s [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]. 301 302 Merely passes on execution to [addHome] and [delHome]. 303 +/ 304 @(IRCEventHandler() 305 .onEvent(IRCEvent.Type.CHAN) 306 .onEvent(IRCEvent.Type.QUERY) 307 .permissionsRequired(Permissions.admin) 308 .channelPolicy(ChannelPolicy.home) 309 .addCommand( 310 IRCEventHandler.Command() 311 .word("home") 312 .policy(PrefixPolicy.prefixed) 313 .description("Adds or removes a channel to/from the list of home channels.") 314 .addSyntax("$command add [channel]") 315 .addSyntax("$command del [channel]") 316 .addSyntax("$command list") 317 ) 318 ) 319 void onCommandHome(AdminPlugin plugin, const ref IRCEvent event) 320 { 321 import lu.string : nom, strippedRight; 322 import std.format : format; 323 import std.typecons : Flag, No, Yes; 324 325 void sendUsage() 326 { 327 enum pattern = "Usage: <b>%s%s<b> [add|del|list] [channel]"; 328 immutable message = pattern.format(plugin.state.settings.prefix, event.aux[$-1]); 329 privmsg(plugin.state, event.channel, event.sender.nickname, message); 330 } 331 332 if (!event.content.length) 333 { 334 return sendUsage(); 335 } 336 337 string slice = event.content.strippedRight; // mutable 338 immutable verb = slice.nom!(Yes.inherit)(' '); 339 340 switch (verb) 341 { 342 case "add": 343 return addHome(plugin, event, slice); 344 345 case "del": 346 return delHome(plugin, event, slice); 347 348 case "list": 349 enum pattern = "Current home channels: %-(<b>%s<b>, %)<b>"; 350 immutable message = pattern.format(plugin.state.bot.homeChannels); 351 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 352 353 default: 354 return sendUsage(); 355 } 356 } 357 358 359 // addHome 360 /++ 361 Adds a channel to the list of currently active home channels, in the 362 [kameloso.pods.IRCBot.homeChannels|IRCBot.homeChannels] array of the 363 current [AdminPlugin]'s [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]. 364 365 Follows up with a [core.thread.fiber.Fiber|Fiber] to verify that the channel 366 was actually joined. 367 368 Params: 369 plugin = The current [AdminPlugin]. 370 event = The triggering [dialect.defs.IRCEvent|IRCEvent]. 371 rawChannel = The channel to be added, potentially in unstripped, cased form. 372 +/ 373 void addHome(AdminPlugin plugin, const /*ref*/ IRCEvent event, const string rawChannel) 374 in (rawChannel.length, "Tried to add a home but the channel string was empty") 375 { 376 import kameloso.plugins.common.delayawait : await, unawait; 377 import kameloso.constants : BufferSize; 378 import dialect.common : isValidChannel; 379 import lu.string : stripped; 380 import std.algorithm.searching : canFind, countUntil; 381 import std.uni : toLower; 382 383 immutable channelName = rawChannel.stripped.toLower; 384 385 if (!channelName.isValidChannel(plugin.state.server)) 386 { 387 enum message = "Invalid channel name."; 388 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 389 } 390 391 if (plugin.state.bot.homeChannels.canFind(channelName)) 392 { 393 enum message = "We are already in that home channel."; 394 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 395 } 396 397 // We need to add it to the homeChannels array so as to get ChannelPolicy.home 398 // ChannelAwareness to pick up the SELFJOIN. 399 plugin.state.bot.homeChannels ~= channelName; 400 plugin.state.updates |= typeof(plugin.state.updates).bot; 401 402 enum addedMessage = "Home added."; 403 privmsg(plugin.state, event.channel, event.sender.nickname, addedMessage); 404 405 immutable existingChannelIndex = plugin.state.bot.guestChannels.countUntil(channelName); 406 407 if (existingChannelIndex != -1) 408 { 409 import std.algorithm.mutation : SwapStrategy, remove; 410 411 logger.info("We're already in this channel as a guest. Cycling."); 412 413 // Make sure there are no duplicates between homes and channels. 414 plugin.state.bot.guestChannels = plugin.state.bot.guestChannels 415 .remove!(SwapStrategy.unstable)(existingChannelIndex); 416 417 return cycle(plugin, channelName); 418 } 419 420 join(plugin.state, channelName); 421 422 // We have to follow up and see if we actually managed to join the channel 423 // There are plenty ways for it to fail. 424 425 import kameloso.thread : CarryingFiber; 426 import core.thread : Fiber; 427 428 static immutable IRCEvent.Type[13] joinTypes = 429 [ 430 IRCEvent.Type.ERR_BANNEDFROMCHAN, 431 IRCEvent.Type.ERR_INVITEONLYCHAN, 432 IRCEvent.Type.ERR_BADCHANNAME, 433 IRCEvent.Type.ERR_LINKCHANNEL, 434 IRCEvent.Type.ERR_TOOMANYCHANNELS, 435 IRCEvent.Type.ERR_FORBIDDENCHANNEL, 436 IRCEvent.Type.ERR_CHANNELISFULL, 437 IRCEvent.Type.ERR_BADCHANNELKEY, 438 IRCEvent.Type.ERR_BADCHANNAME, 439 IRCEvent.Type.RPL_BADCHANPASS, 440 IRCEvent.Type.ERR_SECUREONLYCHAN, 441 IRCEvent.Type.ERR_SSLONLYCHAN, 442 IRCEvent.Type.SELFJOIN, 443 ]; 444 445 void joinHomeDg() 446 { 447 CarryingFiber!IRCEvent thisFiber; 448 449 while (true) 450 { 451 thisFiber = cast(CarryingFiber!IRCEvent)(Fiber.getThis); 452 assert(thisFiber, "Incorrectly cast Fiber: `" ~ typeof(thisFiber).stringof ~ '`'); 453 assert((thisFiber.payload != IRCEvent.init), "Uninitialised payload in carrying fiber"); 454 455 if (thisFiber.payload.channel == channelName) break; 456 457 // Different channel; yield fiber, wait for another event 458 Fiber.yield(); 459 } 460 461 const followupEvent = thisFiber.payload; 462 463 scope(exit) unawait(plugin, joinTypes[]); 464 465 with (IRCEvent.Type) 466 switch (followupEvent.type) 467 { 468 case SELFJOIN: 469 // Success! 470 // return so as to not drop down and undo the addition below. 471 return; 472 473 case ERR_LINKCHANNEL: 474 // We were redirected. Still assume we wanted to add this one? 475 logger.info("Redirected!"); 476 plugin.state.bot.homeChannels ~= followupEvent.content.toLower; // note: content 477 // Drop down and undo original addition 478 break; 479 480 default: 481 enum message = "Failed to join home channel."; 482 privmsg(plugin.state, event.channel, event.sender.nickname, message); 483 break; 484 } 485 486 // Undo original addition 487 import std.algorithm.mutation : SwapStrategy, remove; 488 import std.algorithm.searching : countUntil; 489 490 immutable homeIndex = plugin.state.bot.homeChannels.countUntil(followupEvent.channel); 491 492 if (homeIndex != -1) 493 { 494 plugin.state.bot.homeChannels = plugin.state.bot.homeChannels 495 .remove!(SwapStrategy.unstable)(homeIndex); 496 plugin.state.updates |= typeof(plugin.state.updates).bot; 497 } 498 /*else 499 { 500 logger.error("Tried to remove non-existent home channel."); 501 }*/ 502 } 503 504 Fiber fiber = new CarryingFiber!IRCEvent(&joinHomeDg, BufferSize.fiberStack); 505 await(plugin, fiber, joinTypes); 506 } 507 508 509 // delHome 510 /++ 511 Removes a channel from the list of currently active home channels, from the 512 [kameloso.pods.IRCBot.homeChannels|IRCBot.homeChannels] array of the 513 current [AdminPlugin]'s [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]. 514 +/ 515 void delHome(AdminPlugin plugin, const ref IRCEvent event, const string rawChannel) 516 in (rawChannel.length, "Tried to delete a home but the channel string was empty") 517 { 518 import lu.string : stripped; 519 import std.algorithm.mutation : SwapStrategy, remove; 520 import std.algorithm.searching : countUntil; 521 import std.uni : toLower; 522 523 immutable channelName = rawChannel.stripped.toLower; 524 immutable homeIndex = plugin.state.bot.homeChannels.countUntil(channelName); 525 526 if (homeIndex == -1) 527 { 528 import std.format : format; 529 530 enum pattern = "Channel <b>%s<b> was not listed as a home."; 531 immutable message = pattern.format(channelName); 532 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 533 } 534 535 plugin.state.bot.homeChannels = plugin.state.bot.homeChannels 536 .remove!(SwapStrategy.unstable)(homeIndex); 537 plugin.state.updates |= typeof(plugin.state.updates).bot; 538 part(plugin.state, channelName); 539 540 if (channelName != event.channel) 541 { 542 // We didn't just leave the channel, so we can report success 543 // Otherwise we get ERR_CANNOTSENDTOCHAN 544 enum message = "Home removed."; 545 privmsg(plugin.state, event.channel, event.sender.nickname, message); 546 } 547 } 548 549 550 // onCommandWhitelist 551 /++ 552 Adds a nickname to the list of users who may trigger the bot, to the current 553 [dialect.defs.IRCClient.Class.whitelist|IRCClient.Class.whitelist] of the 554 current [AdminPlugin]'s [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]. 555 556 This is on a [kameloso.plugins.common.core.Permissions.operator|Permissions.operator] level. 557 +/ 558 @(IRCEventHandler() 559 .onEvent(IRCEvent.Type.CHAN) 560 .permissionsRequired(Permissions.operator) 561 .channelPolicy(ChannelPolicy.home) 562 .addCommand( 563 IRCEventHandler.Command() 564 .word("whitelist") 565 .policy(PrefixPolicy.prefixed) 566 .description("Adds or removes an account to/from the whitelist of users " ~ 567 "(in the current channel).") 568 .addSyntax("$command add [account or nickname]") 569 .addSyntax("$command del [account or nickname]") 570 .addSyntax("$command list") 571 ) 572 ) 573 void onCommandWhitelist(AdminPlugin plugin, const ref IRCEvent event) 574 { 575 manageClassLists(plugin, event, IRCUser.Class.whitelist); 576 } 577 578 579 // onCommandElevated 580 /++ 581 Adds a nickname to the list of users who may trigger the bot, to the current 582 list of [dialect.defs.IRCClient.Class.elevated|IRCClient.Class.elevated] users of the 583 current [AdminPlugin]'s [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]. 584 585 This is on a [kameloso.plugins.common.core.Permissions.operator|Permissions.operator] level. 586 +/ 587 @(IRCEventHandler() 588 .onEvent(IRCEvent.Type.CHAN) 589 .permissionsRequired(Permissions.operator) 590 .channelPolicy(ChannelPolicy.home) 591 .addCommand( 592 IRCEventHandler.Command() 593 .word("elevated") 594 .policy(PrefixPolicy.prefixed) 595 .description("Adds or removes an account to/from the list of elevated users " ~ 596 "(in the current channel).") 597 .addSyntax("$command add [account or nickname]") 598 .addSyntax("$command del [account or nickname]") 599 .addSyntax("$command list") 600 ) 601 ) 602 void onCommandElevated(AdminPlugin plugin, const ref IRCEvent event) 603 { 604 manageClassLists(plugin, event, IRCUser.Class.elevated); 605 } 606 607 608 // onCommandOperator 609 /++ 610 Adds a nickname or account to the list of users who may trigger lower-level 611 functions of the bot, without being a full admin. 612 +/ 613 @(IRCEventHandler() 614 .onEvent(IRCEvent.Type.CHAN) 615 .permissionsRequired(Permissions.staff) 616 .channelPolicy(ChannelPolicy.home) 617 .addCommand( 618 IRCEventHandler.Command() 619 .word("operator") 620 .policy(PrefixPolicy.prefixed) 621 .description("Adds or removes an account to/from the operator list of " ~ 622 "operators/moderators (of the current channel).") 623 .addSyntax("$command add [account or nickname]") 624 .addSyntax("$command del [account or nickname]") 625 .addSyntax("$command list") 626 ) 627 ) 628 void onCommandOperator(AdminPlugin plugin, const ref IRCEvent event) 629 { 630 manageClassLists(plugin, event, IRCUser.Class.operator); 631 } 632 633 634 // onCommandStaff 635 /++ 636 Adds a nickname or account to the list of users who may trigger even lower level 637 functions of the bot, without being a full admin. This roughly corresponds to 638 channel owners. 639 +/ 640 @(IRCEventHandler() 641 .onEvent(IRCEvent.Type.CHAN) 642 .permissionsRequired(Permissions.admin) 643 .channelPolicy(ChannelPolicy.home) 644 .addCommand( 645 IRCEventHandler.Command() 646 .word("staff") 647 .policy(PrefixPolicy.prefixed) 648 .description("Adds or removes an account to/from the staff list (of the current channel).") 649 .addSyntax("$command add [account or nickname]") 650 .addSyntax("$command del [account or nickname]") 651 .addSyntax("$command list") 652 ) 653 ) 654 void onCommandStaff(AdminPlugin plugin, const ref IRCEvent event) 655 { 656 return manageClassLists(plugin, event, IRCUser.Class.staff); 657 } 658 659 660 // onCommandBlacklist 661 /++ 662 Adds a nickname to the list of users who may not trigger the bot whatsoever, 663 except on actions annotated [kameloso.plugins.common.core.Permissions.ignore|Permissions.ignore]. 664 665 This is on a [kameloso.plugins.common.core.Permissions.operator|Permissions.operator] level. 666 +/ 667 @(IRCEventHandler() 668 .onEvent(IRCEvent.Type.CHAN) 669 .permissionsRequired(Permissions.operator) 670 .channelPolicy(ChannelPolicy.home) 671 .addCommand( 672 IRCEventHandler.Command() 673 .word("blacklist") 674 .policy(PrefixPolicy.prefixed) 675 .description("Adds or removes an account to/from the blacklist of " ~ 676 "people who may explicitly not trigger the bot (in the current channel).") 677 .addSyntax("$command add [account or nickname]") 678 .addSyntax("$command del [account or nickname]") 679 .addSyntax("$command list") 680 ) 681 ) 682 void onCommandBlacklist(AdminPlugin plugin, const ref IRCEvent event) 683 { 684 manageClassLists(plugin, event, IRCUser.Class.blacklist); 685 } 686 687 688 // onCommandReload 689 /++ 690 Asks plugins to reload their resources and/or configuration as they see fit. 691 +/ 692 @(IRCEventHandler() 693 .onEvent(IRCEvent.Type.CHAN) 694 .onEvent(IRCEvent.Type.QUERY) 695 .permissionsRequired(Permissions.admin) 696 .channelPolicy(ChannelPolicy.home) 697 .addCommand( 698 IRCEventHandler.Command() 699 .word("reload") 700 .policy(PrefixPolicy.nickname) 701 .description("Asks plugins to reload their resources and/or configuration as they see fit.") 702 .addSyntax("$command [optional plugin name]") 703 ) 704 ) 705 void onCommandReload(AdminPlugin plugin, const ref IRCEvent event) 706 { 707 import kameloso.thread : ThreadMessage; 708 import std.conv : text; 709 710 immutable message = event.content.length ? 711 text("Reloading plugin \"<b>", event.content, "<b>\".") : 712 "Reloading plugins."; 713 714 privmsg(plugin.state, event.channel, event.sender.nickname, message); 715 plugin.state.mainThread.send(ThreadMessage.reload(event.content)); 716 } 717 718 719 // onCommandPrintRaw 720 /++ 721 Toggles a flag to print all incoming events *raw*. 722 723 This is for debugging purposes. 724 +/ 725 debug 726 @(IRCEventHandler() 727 .onEvent(IRCEvent.Type.CHAN) 728 .onEvent(IRCEvent.Type.QUERY) 729 .permissionsRequired(Permissions.admin) 730 .channelPolicy(ChannelPolicy.home) 731 .addCommand( 732 IRCEventHandler.Command() 733 .word("printraw") 734 .policy(PrefixPolicy.nickname) 735 .description("[debug] Toggles a flag to print all incoming events raw.") 736 ) 737 ) 738 void onCommandPrintRaw(AdminPlugin plugin, const ref IRCEvent event) 739 { 740 onCommandPrintRawImpl(plugin, event); 741 } 742 743 744 // onCommandPrintBytes 745 /++ 746 Toggles a flag to print all incoming events *as individual bytes*. 747 748 This is for debugging purposes. 749 +/ 750 debug 751 @(IRCEventHandler() 752 .onEvent(IRCEvent.Type.CHAN) 753 .onEvent(IRCEvent.Type.QUERY) 754 .permissionsRequired(Permissions.admin) 755 .channelPolicy(ChannelPolicy.home) 756 .addCommand( 757 IRCEventHandler.Command() 758 .word("printbytes") 759 .policy(PrefixPolicy.nickname) 760 .description("[debug] Toggles a flag to print all incoming events as individual bytes.") 761 ) 762 ) 763 void onCommandPrintBytes(AdminPlugin plugin, const ref IRCEvent event) 764 { 765 onCommandPrintBytesImpl(plugin, event); 766 } 767 768 769 // onCommandJoin 770 /++ 771 Joins a supplied channel. 772 +/ 773 @(IRCEventHandler() 774 .onEvent(IRCEvent.Type.CHAN) 775 .onEvent(IRCEvent.Type.QUERY) 776 .permissionsRequired(Permissions.admin) 777 .channelPolicy(ChannelPolicy.home) 778 .addCommand( 779 IRCEventHandler.Command() 780 .word("join") 781 .policy(PrefixPolicy.nickname) 782 .description("Joins a guest channel.") 783 .addSyntax("$command [channel]") 784 ) 785 ) 786 void onCommandJoin(AdminPlugin plugin, const ref IRCEvent event) 787 { 788 import lu.string : splitInto, stripped; 789 790 if (!event.content.length) 791 { 792 enum message = "No channels to join supplied..."; 793 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 794 } 795 796 string slice = event.content.stripped; // mutable 797 string channel; 798 string key; 799 800 cast(void)slice.splitInto(channel, key); 801 join(plugin.state, channel, key); 802 } 803 804 805 // onCommandPart 806 /++ 807 Parts a supplied channel. 808 +/ 809 @(IRCEventHandler() 810 .onEvent(IRCEvent.Type.CHAN) 811 .onEvent(IRCEvent.Type.QUERY) 812 .permissionsRequired(Permissions.admin) 813 .channelPolicy(ChannelPolicy.home) 814 .addCommand( 815 IRCEventHandler.Command() 816 .word("part") 817 .policy(PrefixPolicy.nickname) 818 .description("Parts a channel.") 819 .addSyntax("$command [channel]") 820 ) 821 ) 822 void onCommandPart(AdminPlugin plugin, const ref IRCEvent event) 823 { 824 import lu.string : splitInto, stripped; 825 826 if (!event.content.length) 827 { 828 enum message = "No channels to part supplied..."; 829 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 830 } 831 832 string slice = event.content.stripped; // mutable 833 string channel; 834 string reason; 835 836 cast(void)slice.splitInto(channel, reason); 837 part(plugin.state, channel, reason); 838 } 839 840 841 // onCommandSet 842 /++ 843 Sets a plugin option by variable string name. 844 +/ 845 @(IRCEventHandler() 846 .onEvent(IRCEvent.Type.CHAN) 847 .onEvent(IRCEvent.Type.QUERY) 848 .permissionsRequired(Permissions.admin) 849 .channelPolicy(ChannelPolicy.home) 850 .addCommand( 851 IRCEventHandler.Command() 852 .word("set") 853 .policy(PrefixPolicy.nickname) 854 .description("Changes a setting of a plugin.") 855 .addSyntax("$command [plugin].[setting]=[value]") 856 ) 857 ) 858 void onCommandSet(AdminPlugin plugin, const /*ref*/ IRCEvent event) 859 { 860 import kameloso.thread : CarryingFiber; 861 import kameloso.constants : BufferSize; 862 import std.typecons : Tuple; 863 import core.thread : Fiber; 864 865 alias Payload = Tuple!(bool); 866 867 void setSettingDg() 868 { 869 auto thisFiber = cast(CarryingFiber!Payload)Fiber.getThis; 870 assert(thisFiber, "Incorrectly cast Fiber: " ~ typeof(thisFiber).stringof); 871 872 immutable message = thisFiber.payload[0] ? 873 "Setting changed." : 874 "Invalid syntax or plugin/setting name."; 875 privmsg(plugin.state, event.channel, event.sender.nickname, message); 876 } 877 878 plugin.state.specialRequests ~= specialRequest!Payload(event.content, &setSettingDg); 879 } 880 881 882 // onCommandGet 883 /++ 884 Fetches a setting of a given plugin, or a list of all settings of a given plugin 885 if no setting name supplied. 886 887 Filename paths to certificate files and private keys will be visible to users 888 of this, so be careful with what permissions should be required. 889 +/ 890 @(IRCEventHandler() 891 .onEvent(IRCEvent.Type.CHAN) 892 .onEvent(IRCEvent.Type.QUERY) 893 .permissionsRequired(Permissions.admin) 894 .channelPolicy(ChannelPolicy.home) 895 .addCommand( 896 IRCEventHandler.Command() 897 .word("get") 898 .policy(PrefixPolicy.nickname) 899 .description("Fetches a setting of a given plugin, " ~ 900 "or a list of all available settings of a given plugin.") 901 .addSyntax("$command [plugin].[setting]") 902 .addSyntax("$command [plugin]") 903 ) 904 ) 905 void onCommandGet(AdminPlugin plugin, const /*ref*/ IRCEvent event) 906 { 907 import kameloso.constants : BufferSize; 908 import kameloso.thread : CarryingFiber; 909 import std.typecons : Tuple; 910 import core.thread : Fiber; 911 912 alias Payload = Tuple!(string, string, string); 913 914 void getSettingDg() 915 { 916 auto thisFiber = cast(CarryingFiber!Payload)Fiber.getThis; 917 assert(thisFiber, "Incorrectly cast Fiber: " ~ typeof(thisFiber).stringof); 918 919 immutable pluginName = thisFiber.payload[0]; 920 immutable setting = thisFiber.payload[1]; 921 immutable value = thisFiber.payload[2]; 922 923 if (!pluginName.length) 924 { 925 enum message = "Invalid plugin."; 926 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 927 } 928 else if (setting.length) 929 { 930 import lu.string : contains; 931 import std.format : format; 932 933 immutable pattern = value.contains(' ') ? 934 "%s.%s=\"%s\"" : 935 "%s.%s=%s"; 936 immutable message = pattern.format(pluginName, setting, value); 937 privmsg(plugin.state, event.channel, event.sender.nickname, message); 938 } 939 else if (value.length) 940 { 941 privmsg(plugin.state, event.channel, event.sender.nickname, value); 942 } 943 else 944 { 945 enum message = "Invalid setting."; 946 privmsg(plugin.state, event.channel, event.sender.nickname, message); 947 } 948 } 949 950 plugin.state.specialRequests ~= specialRequest!Payload(event.content, &getSettingDg); 951 } 952 953 954 // onCommandAuth 955 /++ 956 Asks the [kameloso.plugins.services.connect.ConnectService|ConnectService] to 957 (re-)authenticate to services. 958 +/ 959 version(WithConnectService) 960 @(IRCEventHandler() 961 .onEvent(IRCEvent.Type.CHAN) 962 .onEvent(IRCEvent.Type.QUERY) 963 .permissionsRequired(Permissions.admin) 964 .channelPolicy(ChannelPolicy.home) 965 .addCommand( 966 IRCEventHandler.Command() 967 .word("auth") 968 .policy(PrefixPolicy.nickname) 969 .description("(Re-)authenticates with services. Useful if the server " ~ 970 "has forcefully logged the bot out.") 971 ) 972 ) 973 void onCommandAuth(AdminPlugin plugin) 974 { 975 import kameloso.thread : ThreadMessage, boxed; 976 import std.concurrency : send; 977 978 version(TwitchSupport) 979 { 980 if (plugin.state.server.daemon == IRCServer.Daemon.twitch) return; 981 } 982 983 plugin.state.mainThread.send(ThreadMessage.busMessage("connect", boxed("auth"))); 984 } 985 986 987 // onCommandStatus 988 /++ 989 Dumps information about the current state of the bot to the local terminal. 990 991 This can be very spammy. 992 +/ 993 debug 994 version(IncludeHeavyStuff) 995 @(IRCEventHandler() 996 .onEvent(IRCEvent.Type.CHAN) 997 .onEvent(IRCEvent.Type.QUERY) 998 .permissionsRequired(Permissions.admin) 999 .channelPolicy(ChannelPolicy.home) 1000 .addCommand( 1001 IRCEventHandler.Command() 1002 .word("status") 1003 .policy(PrefixPolicy.nickname) 1004 .description("[debug] Dumps information about the current state of the bot to the local terminal.") 1005 ) 1006 ) 1007 void onCommandStatus(AdminPlugin plugin) 1008 { 1009 if (plugin.state.settings.headless) return; 1010 onCommandStatusImpl(plugin); 1011 } 1012 1013 1014 // onCommandSummary 1015 /++ 1016 Causes a connection summary to be printed to the terminal. 1017 +/ 1018 @(IRCEventHandler() 1019 .onEvent(IRCEvent.Type.CHAN) 1020 .onEvent(IRCEvent.Type.QUERY) 1021 .permissionsRequired(Permissions.admin) 1022 .channelPolicy(ChannelPolicy.home) 1023 .addCommand( 1024 IRCEventHandler.Command() 1025 .word("summary") 1026 .policy(PrefixPolicy.nickname) 1027 .description("Prints a connection summary to the local terminal.") 1028 ) 1029 ) 1030 void onCommandSummary(AdminPlugin plugin) 1031 { 1032 import kameloso.thread : ThreadMessage; 1033 1034 if (plugin.state.settings.headless) return; 1035 plugin.state.mainThread.send(ThreadMessage.wantLiveSummary()); 1036 } 1037 1038 1039 // onCommandCycle 1040 /++ 1041 Cycles (parts and immediately rejoins) a channel. 1042 +/ 1043 @(IRCEventHandler() 1044 .onEvent(IRCEvent.Type.CHAN) 1045 .onEvent(IRCEvent.Type.QUERY) 1046 .permissionsRequired(Permissions.admin) 1047 .channelPolicy(ChannelPolicy.home) 1048 .addCommand( 1049 IRCEventHandler.Command() 1050 .word("cycle") 1051 .policy(PrefixPolicy.nickname) 1052 .description("Cycles (parts and rejoins) a channel.") 1053 .addSyntax("$command [optional channel] [optional delay] [optional key(s)]") 1054 ) 1055 ) 1056 void onCommandCycle(AdminPlugin plugin, const /*ref*/ IRCEvent event) 1057 { 1058 import kameloso.time : DurationStringException, abbreviatedDuration; 1059 import lu.string : nom, stripped; 1060 import std.conv : ConvException; 1061 1062 string slice = event.content.stripped; // mutable 1063 1064 if (!slice.length) 1065 { 1066 return cycle(plugin, event.channel); 1067 } 1068 1069 immutable channelName = slice.nom!(Yes.inherit)(' '); 1070 1071 if (channelName !in plugin.state.channels) 1072 { 1073 enum message = "I am not in that channel."; 1074 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 1075 } 1076 1077 if (!slice.length) 1078 { 1079 return cycle(plugin, channelName); 1080 } 1081 1082 immutable delaystring = slice.nom!(Yes.inherit)(' '); 1083 1084 try 1085 { 1086 immutable delay = abbreviatedDuration(delaystring); 1087 cycle(plugin, channelName, delay, slice); 1088 } 1089 catch (ConvException _) 1090 { 1091 import std.format : format; 1092 1093 enum pattern = `"<b>%s<b>" is not a valid number for seconds to delay.`; 1094 immutable message = pattern.format(slice); 1095 privmsg(plugin.state, event.channel, event.sender.nickname, message); 1096 } 1097 catch (DurationStringException e) 1098 { 1099 privmsg(plugin.state, event.channel, event.sender.nickname, e.msg); 1100 } 1101 } 1102 1103 1104 // cycle 1105 /++ 1106 Implementation of cycling, called by [onCommandCycle] 1107 1108 Params: 1109 plugin = The current [AdminPlugin]. 1110 channelName = The name of the channel to cycle. 1111 delay_ = [core.time.Duration|Duration] to delay rejoining. 1112 key = The key to use when rejoining the channel. 1113 +/ 1114 void cycle( 1115 AdminPlugin plugin, 1116 const string channelName, 1117 const Duration delay_ = Duration.zero, 1118 const string key = string.init) 1119 { 1120 import kameloso.plugins.common.delayawait : await, delay, unawait; 1121 import kameloso.constants : BufferSize; 1122 import kameloso.thread : CarryingFiber; 1123 import core.thread : Fiber; 1124 1125 void cycleDg() 1126 { 1127 while (true) 1128 { 1129 auto thisFiber = cast(CarryingFiber!IRCEvent)(Fiber.getThis); 1130 assert(thisFiber, "Incorrectly cast Fiber: `" ~ typeof(thisFiber).stringof ~ '`'); 1131 assert((thisFiber.payload != IRCEvent.init), "Uninitialised payload in carrying fiber"); 1132 1133 const partEvent = thisFiber.payload; 1134 1135 if (partEvent.channel == channelName) 1136 { 1137 void joinDg() 1138 { 1139 join(plugin.state, channelName, key); 1140 } 1141 1142 unawait(plugin, Fiber.getThis, IRCEvent.Type.SELFPART); 1143 1144 return (delay_ == Duration.zero) ? 1145 joinDg() : 1146 delay(plugin, &joinDg, delay_); 1147 } 1148 1149 // Wrong channel, wait for the next SELFPART 1150 Fiber.yield(); 1151 } 1152 } 1153 1154 Fiber fiber = new CarryingFiber!IRCEvent(&cycleDg, BufferSize.fiberStack); 1155 await(plugin, fiber, IRCEvent.Type.SELFPART); 1156 part(plugin.state, channelName, "Cycling"); 1157 } 1158 1159 1160 // onCommandMask 1161 /++ 1162 Adds, removes or lists hostmasks used to identify users on servers that 1163 don't employ services. 1164 +/ 1165 @(IRCEventHandler() 1166 .onEvent(IRCEvent.Type.CHAN) 1167 .onEvent(IRCEvent.Type.QUERY) 1168 .permissionsRequired(Permissions.admin) 1169 .channelPolicy(ChannelPolicy.home) 1170 .addCommand( 1171 IRCEventHandler.Command() 1172 .word("hostmask") 1173 .policy(PrefixPolicy.prefixed) 1174 .description("Modifies a hostmask definition, for use on servers without services accounts.") 1175 .addSyntax("$command add [account] [hostmask]") 1176 .addSyntax("$command del [hostmask]") 1177 .addSyntax("$command list") 1178 ) 1179 .addCommand( 1180 IRCEventHandler.Command() 1181 .word("mask") 1182 .policy(PrefixPolicy.prefixed) 1183 .hidden(true) 1184 ) 1185 ) 1186 void onCommandMask(AdminPlugin plugin, const ref IRCEvent event) 1187 { 1188 import lu.string : SplitResults, contains, nom, splitInto, stripped; 1189 import std.format : format; 1190 1191 if (!plugin.state.settings.preferHostmasks) 1192 { 1193 enum message = "This bot is not currently configured to use hostmasks for authentication."; 1194 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 1195 } 1196 1197 void sendUsage() 1198 { 1199 enum pattern = "Usage: <b>%s%s<b> [add|del|list] [args...]"; 1200 immutable message = pattern.format(plugin.state.settings.prefix, event.aux[$-1]); 1201 privmsg(plugin.state, event.channel, event.sender.nickname, message); 1202 } 1203 1204 string slice = event.content.stripped; // mutable 1205 immutable verb = slice.nom!(Yes.inherit)(' '); 1206 1207 switch (verb) 1208 { 1209 case "add": 1210 string account; 1211 string mask; 1212 1213 immutable results = slice.splitInto(account, mask); 1214 1215 if (results != SplitResults.match) 1216 { 1217 enum pattern = "Usage: <b>%s%s add<b> [account] [hostmask]"; 1218 immutable message = pattern.format(plugin.state.settings.prefix, event.aux[$-1]); 1219 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 1220 } 1221 1222 return modifyHostmaskDefinition(plugin, Yes.add, account, mask, event); 1223 1224 case "del": 1225 case "remove": 1226 if (!slice.length || slice.contains(' ')) 1227 { 1228 enum pattern = "Usage: <b>%s%s del<b> [hostmask]"; 1229 immutable message = pattern.format(plugin.state.settings.prefix, event.aux[$-1]); 1230 return privmsg(plugin.state, event.channel, event.sender.nickname, message); 1231 } 1232 1233 return modifyHostmaskDefinition(plugin, No.add, string.init, slice, event); 1234 1235 case "list": 1236 return listHostmaskDefinitions(plugin, event); 1237 1238 default: 1239 return sendUsage(); 1240 } 1241 } 1242 1243 1244 // listHostmaskDefinitions 1245 /++ 1246 Lists existing hostmask definitions. 1247 1248 Params: 1249 plugin = The current [AdminPlugin]. 1250 event = The instigating [dialect.defs.IRCEvent|IRCEvent]. 1251 +/ 1252 void listHostmaskDefinitions(AdminPlugin plugin, const ref IRCEvent event) 1253 { 1254 import lu.json : JSONStorage, populateFromJSON; 1255 1256 if (plugin.state.settings.headless) return; 1257 1258 JSONStorage json; 1259 json.load(plugin.hostmasksFile); 1260 1261 string[string] aa; 1262 aa.populateFromJSON(json); 1263 1264 // Remove any placeholder examples 1265 enum examplePlaceholderKey = "<nickname>!<ident>@<address>"; 1266 aa.remove(examplePlaceholderKey); 1267 1268 if (aa.length) 1269 { 1270 if (event == IRCEvent.init) 1271 { 1272 import std.json : JSONValue; 1273 import std.stdio : stdout, writeln; 1274 1275 if (plugin.state.settings.headless) return; 1276 1277 logger.log("Current hostmasks:"); 1278 // json can contain the example placeholder, so make a new one out of aa 1279 writeln(JSONValue(aa).toPrettyString); 1280 if (plugin.state.settings.flush) stdout.flush(); 1281 } 1282 else 1283 { 1284 import std.format : format; 1285 1286 enum pattern = "Current hostmasks: <b>%s<b>"; 1287 immutable message = pattern.format(aa); 1288 privmsg(plugin.state, event.channel, event.sender.nickname, message); 1289 } 1290 } 1291 else 1292 { 1293 enum message = "There are presently no hostmasks defined."; 1294 1295 if (event == IRCEvent.init) 1296 { 1297 logger.info(message); 1298 } 1299 else 1300 { 1301 privmsg(plugin.state, event.channel, event.sender.nickname, message); 1302 } 1303 } 1304 } 1305 1306 1307 // onCommandReconnect 1308 /++ 1309 Disconnect from and immediately reconnects to the server. 1310 +/ 1311 @(IRCEventHandler() 1312 .onEvent(IRCEvent.Type.CHAN) 1313 .onEvent(IRCEvent.Type.QUERY) 1314 .permissionsRequired(Permissions.admin) 1315 .channelPolicy(ChannelPolicy.home) 1316 .addCommand( 1317 IRCEventHandler.Command() 1318 .word("reconnect") 1319 .policy(PrefixPolicy.nickname) 1320 .description("Disconnects from and immediately reconnects to the server.") 1321 .addSyntax("$command [optional quit message]") 1322 ) 1323 ) 1324 void onCommandReconnect(AdminPlugin plugin, const ref IRCEvent event) 1325 { 1326 import kameloso.thread : ThreadMessage, boxed; 1327 import lu.string : stripped; 1328 import std.concurrency : prioritySend; 1329 1330 logger.warning("Reconnecting upon administrator request."); 1331 plugin.state.mainThread.send(ThreadMessage.reconnect(event.content.stripped, boxed(false))); 1332 } 1333 1334 1335 // onCommandReexec 1336 /++ 1337 Re-executes the program. 1338 +/ 1339 version(Posix) 1340 @(IRCEventHandler() 1341 .onEvent(IRCEvent.Type.CHAN) 1342 .onEvent(IRCEvent.Type.QUERY) 1343 .permissionsRequired(Permissions.admin) 1344 .channelPolicy(ChannelPolicy.home) 1345 .addCommand( 1346 IRCEventHandler.Command() 1347 .word("reexec") 1348 .policy(PrefixPolicy.nickname) 1349 .description("Re-executes the program.") 1350 .addSyntax("$command [optional quit message]") 1351 ) 1352 ) 1353 void onCommandReexec(AdminPlugin plugin, const ref IRCEvent event) 1354 { 1355 import kameloso.thread : ThreadMessage, boxed; 1356 import lu.string : stripped; 1357 import std.concurrency : prioritySend; 1358 1359 plugin.state.mainThread.send(ThreadMessage.reconnect(event.content.stripped, boxed(true))); 1360 } 1361 1362 1363 // onCommandBus 1364 /++ 1365 Sends an internal bus message to other plugins, much like how such can be 1366 sent with the Pipeline plugin. 1367 +/ 1368 debug 1369 @(IRCEventHandler() 1370 .onEvent(IRCEvent.Type.CHAN) 1371 .onEvent(IRCEvent.Type.QUERY) 1372 .permissionsRequired(Permissions.admin) 1373 .channelPolicy(ChannelPolicy.home) 1374 .addCommand( 1375 IRCEventHandler.Command() 1376 .word("bus") 1377 .policy(PrefixPolicy.nickname) 1378 .description("[debug] Sends an internal bus message.") 1379 .addSyntax("$command [header] [content]") 1380 ) 1381 ) 1382 void onCommandBus(AdminPlugin plugin, const ref IRCEvent event) 1383 { 1384 onCommandBusImpl(plugin, event.content); 1385 } 1386 1387 1388 import kameloso.thread : Sendable; 1389 1390 // onBusMessage 1391 /++ 1392 Receives a passed [kameloso.thread.Boxed|Boxed] instance with the "`admin`" 1393 header, and calls functions based on the payload message. 1394 1395 This is used in the Pipeline plugin, to allow us to trigger admin verbs via 1396 the command-line pipe. 1397 1398 Params: 1399 plugin = The current [AdminPlugin]. 1400 header = String header describing the passed content payload. 1401 content = Message content. 1402 +/ 1403 void onBusMessage( 1404 AdminPlugin plugin, 1405 const string header, 1406 shared Sendable content) 1407 { 1408 import kameloso.thread : Boxed; 1409 import lu.string : contains, nom, strippedRight; 1410 1411 // Don't return if disabled, as it blocks us from re-enabling with verb set 1412 if (header != "admin") return; 1413 1414 auto message = cast(Boxed!string)content; 1415 assert(message, "Incorrectly cast message: " ~ typeof(message).stringof); 1416 1417 string slice = message.payload.strippedRight; 1418 immutable verb = slice.nom!(Yes.inherit)(' '); 1419 1420 switch (verb) 1421 { 1422 debug 1423 { 1424 version(IncludeHeavyStuff) 1425 { 1426 import kameloso.printing : printObject; 1427 import core.memory : GC; 1428 1429 case "users": 1430 return onCommandShowUsers(plugin); 1431 1432 case "status": 1433 return onCommandStatus(plugin); 1434 1435 case "user": 1436 if (const user = slice in plugin.state.users) 1437 { 1438 printObject(*user); 1439 } 1440 else 1441 { 1442 logger.error("No such user: <l>", slice); 1443 } 1444 break; 1445 1446 case "state": 1447 return printObject(plugin.state); 1448 1449 case "gc.stats": 1450 import kameloso.common : printGCStats; 1451 return printGCStats(); 1452 1453 case "gc.collect": 1454 import std.datetime.systime : Clock; 1455 1456 immutable statsPre = GC.stats(); 1457 immutable timestampPre = Clock.currTime; 1458 immutable memoryUsedPre = statsPre.usedSize; 1459 1460 GC.collect(); 1461 1462 immutable statsPost = GC.stats(); 1463 immutable timestampPost = Clock.currTime; 1464 immutable memoryUsedPost = statsPost.usedSize; 1465 immutable memoryCollected = (memoryUsedPre - memoryUsedPost); 1466 immutable duration = (timestampPost - timestampPre); 1467 1468 enum pattern = "Collected <l>%,d</> bytes of garbage in <l>%s"; 1469 return logger.infof(pattern, memoryCollected, duration); 1470 1471 case "gc.minimize": 1472 GC.minimize(); 1473 return logger.info("Memory minimised."); 1474 } 1475 1476 case "printraw": 1477 plugin.adminSettings.printRaw = !plugin.adminSettings.printRaw; 1478 return; 1479 1480 case "printbytes": 1481 plugin.adminSettings.printBytes = !plugin.adminSettings.printBytes; 1482 return; 1483 } 1484 1485 case "reexec": 1486 import kameloso.thread : ThreadMessage, boxed; 1487 import std.concurrency : prioritySend; 1488 return plugin.state.mainThread.prioritySend(ThreadMessage.reconnect(string.init, boxed(true))); 1489 1490 case "set": 1491 import kameloso.constants : BufferSize; 1492 import kameloso.thread : CarryingFiber; 1493 import std.typecons : Tuple; 1494 import core.thread : Fiber; 1495 1496 alias Payload = Tuple!(bool); 1497 1498 void setSettingBusDg() 1499 { 1500 auto thisFiber = cast(CarryingFiber!Payload)Fiber.getThis; 1501 assert(thisFiber, "Incorrectly cast Fiber: " ~ typeof(thisFiber).stringof); 1502 1503 immutable success = thisFiber.payload[0]; 1504 1505 if (success) 1506 { 1507 logger.log("Setting changed."); 1508 } 1509 else 1510 { 1511 logger.error("Invalid syntax or plugin/setting name."); 1512 } 1513 } 1514 1515 plugin.state.specialRequests ~= specialRequest!Payload(slice, &setSettingBusDg); 1516 return; 1517 1518 case "save": 1519 import kameloso.thread : ThreadMessage; 1520 1521 logger.log("Saving configuration to disk."); 1522 return plugin.state.mainThread.send(ThreadMessage.save()); 1523 1524 case "reload": 1525 import kameloso.thread : ThreadMessage; 1526 1527 if (slice.length) 1528 { 1529 enum pattern = `Reloading plugin "<i>%s</>".`; 1530 logger.logf(pattern, slice); 1531 } 1532 else 1533 { 1534 logger.log("Reloading plugins."); 1535 } 1536 1537 return plugin.state.mainThread.send(ThreadMessage.reload(slice)); 1538 1539 case "whitelist": 1540 case "elevated": 1541 case "operator": 1542 case "staff": 1543 case "blacklist": 1544 import lu.conv : Enum; 1545 import lu.string : SplitResults, splitInto; 1546 1547 string subverb; 1548 string channelName; 1549 1550 immutable results = slice.splitInto(subverb, channelName); 1551 if (results == SplitResults.underrun) 1552 { 1553 // verb_channel_nickname 1554 enum pattern = "Invalid bus message syntax; expected <l>%s</> " ~ 1555 "[verb] [channel] [nickname if add/del], got \"<l>%s</>\""; 1556 return logger.warningf(pattern, verb, message.payload.strippedRight); 1557 } 1558 1559 immutable class_ = Enum!(IRCUser.Class).fromString(verb); 1560 1561 switch (subverb) 1562 { 1563 case "add": 1564 case "del": 1565 immutable user = slice; 1566 1567 if (!user.length) 1568 { 1569 return logger.warning("Invalid bus message syntax; no user supplied, " ~ 1570 "only channel <l>", channelName); 1571 } 1572 1573 if (subverb == "add") 1574 { 1575 return lookupEnlist(plugin, user, class_, channelName); 1576 } 1577 else /*if (subverb == "del")*/ 1578 { 1579 return delist(plugin, user, class_, channelName); 1580 } 1581 1582 case "list": 1583 return listList(plugin, channelName, class_); 1584 1585 default: 1586 enum pattern = "Invalid bus message <l>%s</> subverb <l>%s"; 1587 logger.warningf(pattern, verb, subverb); 1588 break; 1589 } 1590 break; 1591 1592 case "hostmask": 1593 import lu.string : nom; 1594 1595 immutable subverb = slice.nom!(Yes.inherit)(' '); 1596 1597 switch (subverb) 1598 { 1599 case "add": 1600 import lu.string : SplitResults, splitInto; 1601 1602 string account; 1603 string mask; 1604 1605 immutable results = slice.splitInto(account, mask); 1606 if (results != SplitResults.match) 1607 { 1608 return logger.warning("Invalid bus message syntax; " ~ 1609 "expected hostmask add [account] [hostmask]"); 1610 } 1611 1612 IRCEvent lvalueEvent; 1613 return modifyHostmaskDefinition(plugin, Yes.add, account, mask, lvalueEvent); 1614 1615 case "del": 1616 case "remove": 1617 if (!slice.length) 1618 { 1619 return logger.warning("Invalid bus message syntax; " ~ 1620 "expected hostmask del [hostmask]"); 1621 } 1622 1623 IRCEvent lvalueEvent; 1624 return modifyHostmaskDefinition(plugin, No.add, string.init, slice, lvalueEvent); 1625 1626 case "list": 1627 IRCEvent lvalueEvent; 1628 return listHostmaskDefinitions(plugin, lvalueEvent); 1629 1630 default: 1631 enum pattern = "Invalid bus message <l>%s</> subverb <l>%s"; 1632 logger.warningf(pattern, verb, subverb); 1633 break; 1634 } 1635 break; 1636 1637 case "summary": 1638 return onCommandSummary(plugin); 1639 1640 default: 1641 enum pattern = "[admin] Unimplemented bus message verb: <l>%s"; 1642 logger.errorf(pattern, verb); 1643 break; 1644 } 1645 } 1646 1647 1648 mixin UserAwareness!omniscientChannelPolicy; 1649 mixin ChannelAwareness!omniscientChannelPolicy; 1650 mixin PluginRegistration!(AdminPlugin, -4.priority); 1651 1652 version(TwitchSupport) 1653 { 1654 mixin TwitchAwareness!omniscientChannelPolicy; 1655 } 1656 1657 public: 1658 1659 1660 // AdminPlugin 1661 /++ 1662 The Admin plugin is a plugin aimed for administrative use and debugging. 1663 1664 It was historically part of the [kameloso.plugins.chatbot.ChatbotPlugin|ChatbotPlugin]. 1665 +/ 1666 final class AdminPlugin : IRCPlugin 1667 { 1668 package: 1669 import kameloso.constants : KamelosoFilenames; 1670 1671 /// All Admin options gathered. 1672 AdminSettings adminSettings; 1673 1674 /// File with user definitions. Must be the same as in `persistence.d`. 1675 @Resource string userFile = KamelosoFilenames.users; 1676 1677 /// File with hostmasks definitions. Must be the same as in `persistence.d`. 1678 @Resource string hostmasksFile = KamelosoFilenames.hostmasks; 1679 1680 mixin IRCPluginImpl; 1681 }