1 /++ 2 The Connect service handles logging onto IRC servers after having connected, 3 as well as managing authentication to services. It also manages responding 4 to [dialect.defs.IRCEvent.Type.PING|PING] requests, and capability negotiations. 5 6 The actual connection logic is in the [kameloso.net] module. 7 8 See_Also: 9 [kameloso.net], 10 [kameloso.plugins.common.core], 11 [kameloso.plugins.common.misc] 12 13 Copyright: [JR](https://github.com/zorael) 14 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 15 16 Authors: 17 [JR](https://github.com/zorael) 18 +/ 19 module kameloso.plugins.services.connect; 20 21 version(WithConnectService): 22 23 private: 24 25 import kameloso.plugins; 26 import kameloso.plugins.common.core; 27 import kameloso.common : logger; 28 import kameloso.messaging; 29 import dialect.defs; 30 import std.typecons : Flag, No, Yes; 31 32 33 // ConnectSettings 34 /++ 35 Settings for a [ConnectService]. 36 +/ 37 @Settings struct ConnectSettings 38 { 39 private: 40 import lu.uda : CannotContainComments, /*Separator,*/ Unserialisable; 41 42 /++ 43 What to use as delimiter to separate [sendAfterConnect] into different 44 lines to send to the server. 45 46 This is to compensate for not being able to use [lu.uda.Separator] and a 47 `string[]` (because it doesn't work well with getopt). 48 +/ 49 enum sendAfterConnectSeparator = ";;"; 50 51 public: 52 /++ 53 Whether or not to try to regain nickname if there was a collision and 54 we had to rename ourselves, when registering. 55 +/ 56 bool regainNickname = true; 57 58 /// Whether or not to join channels upon being invited to them. 59 bool joinOnInvite = false; 60 61 /// Whether to use SASL authentication or not. 62 @Unserialisable bool sasl = true; 63 64 /// Whether or not to abort and exit if SASL authentication fails. 65 bool exitOnSASLFailure = false; 66 67 /// Lines to send after successfully connecting and registering. 68 //@Separator(";;") 69 @CannotContainComments string sendAfterConnect; 70 71 /++ 72 How much time to allow between incoming PINGs before suspecting something is wrong. 73 +/ 74 @Unserialisable int maxPingPeriodAllowed = 660; 75 } 76 77 78 /// Progress of a process. 79 enum Progress 80 { 81 notStarted, /// Process not yet started, init state. 82 inProgress, /// Process started but has yet to finish. 83 finished, /// Process finished. 84 } 85 86 87 // onSelfpart 88 /++ 89 Removes a channel from the list of joined channels. 90 91 Fires when the bot leaves a channel, one way or another. 92 +/ 93 @(IRCEventHandler() 94 .onEvent(IRCEvent.Type.SELFPART) 95 .onEvent(IRCEvent.Type.SELFKICK) 96 .channelPolicy(ChannelPolicy.any) 97 ) 98 void onSelfpart(ConnectService service, const ref IRCEvent event) 99 { 100 import std.algorithm.searching : canFind; 101 102 version(TwitchSupport) 103 { 104 if (service.state.server.daemon == IRCServer.Daemon.twitch) 105 { 106 service.currentActualChannels.remove(event.channel); 107 } 108 } 109 110 if (service.state.bot.homeChannels.canFind(event.channel)) 111 { 112 logger.warning("Leaving a home..."); 113 } 114 } 115 116 117 // joinChannels 118 /++ 119 Joins all channels listed as home channels *and* guest channels in the arrays in 120 [kameloso.pods.IRCBot|IRCBot] of the current [ConnectService]'s 121 [kameloso.plugins.common.core.IRCPluginState|IRCPluginState]. 122 123 Params: 124 service = The current [ConnectService]. 125 +/ 126 void joinChannels(ConnectService service) 127 { 128 scope(exit) service.joinedChannels = true; 129 130 if (!service.state.bot.homeChannels.length && !service.state.bot.guestChannels.length) 131 { 132 logger.warning("No channels, no purpose..."); 133 return; 134 } 135 136 import kameloso.messaging : Message; 137 import lu.string : plurality; 138 import std.algorithm.iteration : filter, uniq; 139 import std.algorithm.sorting : sort; 140 import std.array : array, join; 141 import std.range : walkLength; 142 static import kameloso.messaging; 143 144 auto homelist = service.state.bot.homeChannels 145 .filter!(channelName => (channelName != "-")) 146 .array 147 .sort 148 .uniq; 149 150 auto guestlist = service.state.bot.guestChannels 151 .filter!(channelName => (channelName != "-")) 152 .array 153 .sort 154 .uniq; 155 156 immutable numChans = homelist.walkLength() + guestlist.walkLength(); 157 158 enum pattern = "Joining <i>%d</> %s..."; 159 logger.logf(pattern, numChans, numChans.plurality("channel", "channels")); 160 161 // Join in two steps so home channels don't get shoved away by guest channels 162 if (service.state.bot.homeChannels.length) 163 { 164 enum properties = Message.Property.quiet; 165 immutable channelString = homelist.join(','); 166 kameloso.messaging.join(service.state, channelString, string.init, properties); 167 } 168 169 if (service.state.bot.guestChannels.length) 170 { 171 enum properties = Message.Property.quiet; 172 immutable channelString = guestlist.join(','); 173 kameloso.messaging.join(service.state, channelString, string.init, properties); 174 } 175 176 version(TwitchSupport) 177 { 178 import kameloso.plugins.common.delayawait : delay; 179 180 /+ 181 If, on Twitch, an invalid channel was supplied as a home or a guest 182 channel, it will just silently not join it but leave us thinking it has 183 (since the entry in `homeChannels`/`guestChannels` will still be there). 184 Check whether we actually joined them all, after a short delay, and 185 if not, sync the arrays. 186 +/ 187 188 // Early return if we're not on Twitch to spare us a level of indentation 189 if (service.state.server.daemon != IRCServer.Daemon.twitch) return; 190 191 void delayedChannelCheckDg() 192 { 193 import std.range : chain; 194 195 // See if we actually managed to join all channels 196 auto allChannels = chain(service.state.bot.homeChannels, service.state.bot.guestChannels); 197 string[] missingChannels; 198 199 foreach (immutable channel; allChannels) 200 { 201 if (channel !in service.currentActualChannels) 202 { 203 // We failed to join a channel for some reason. No such user? 204 missingChannels ~= channel; 205 } 206 } 207 208 if (missingChannels.length) 209 { 210 enum pattern = "Timed out waiting to join channels: %-(<l>%s</>, %)"; 211 logger.warningf(pattern, missingChannels); 212 } 213 } 214 215 delay(service, &delayedChannelCheckDg, service.channelCheckDelay); 216 } 217 } 218 219 220 // onSelfjoin 221 /++ 222 Records us as having joined a channel, when we join one. This is to allow 223 us to notice when we silently fail to join something, on Twitch. As it's 224 limited to there, gate it behind version `TwitchSupport`. 225 +/ 226 version(TwitchSupport) 227 @(IRCEventHandler() 228 .onEvent(IRCEvent.Type.SELFJOIN) 229 .channelPolicy(ChannelPolicy.any) 230 ) 231 void onSelfjoin(ConnectService service, const ref IRCEvent event) 232 { 233 if (service.state.server.daemon == IRCServer.Daemon.twitch) 234 { 235 service.currentActualChannels[event.channel] = true; 236 } 237 } 238 239 240 // onToConnectType 241 /++ 242 Responds to [dialect.defs.IRCEvent.Type.ERR_NEEDPONG|ERR_NEEDPONG] events by sending 243 the text supplied as content in the [dialect.defs.IRCEvent|IRCEvent] to the server. 244 245 "Also known as [dialect.defs.IRCEvent.Type.ERR_NEEDPONG|ERR_NEEDPONG] (Unreal/Ultimate) 246 for use during registration, however it's not used in Unreal (and might not 247 be used in Ultimate either)." 248 249 Encountered at least once, on a private server. 250 +/ 251 @(IRCEventHandler() 252 .onEvent(IRCEvent.Type.ERR_NEEDPONG) 253 ) 254 void onToConnectType(ConnectService service, const ref IRCEvent event) 255 { 256 enum properties = Message.Property.quiet; 257 immediate(service.state, event.content, properties); 258 } 259 260 261 // onPing 262 /++ 263 Pongs the server upon [dialect.defs.IRCEvent.Type.PING|PING]. 264 265 Ping with the sender as target, and not the necessarily 266 the server as saved in the [dialect.defs.IRCServer|IRCServer] struct. For 267 example, [dialect.defs.IRCEvent.Type.ERR_NEEDPONG|ERR_NEEDPONG] generally 268 wants you to ping a random number or string. 269 +/ 270 @(IRCEventHandler() 271 .onEvent(IRCEvent.Type.PING) 272 ) 273 void onPing(ConnectService service, const ref IRCEvent event) 274 { 275 import kameloso.thread : ThreadMessage; 276 import std.concurrency : prioritySend; 277 278 immutable target = event.content.length ? event.content : event.sender.address; 279 service.state.mainThread.prioritySend(ThreadMessage.pong(target)); 280 } 281 282 283 // tryAuth 284 /++ 285 Tries to authenticate with services. 286 287 The command to send vary greatly between server daemons (and networks), so 288 use some heuristics and try the best guess. 289 290 Params: 291 service = The current [ConnectService]. 292 +/ 293 void tryAuth(ConnectService service) 294 { 295 string serviceNick = "NickServ"; 296 string verb = "IDENTIFY"; 297 298 import lu.string : beginsWith, decode64; 299 immutable password = service.state.bot.password.beginsWith("base64:") ? 300 decode64(service.state.bot.password[7..$]) : service.state.bot.password; 301 302 // Specialcase networks 303 switch (service.state.server.network) 304 { 305 case "DALnet": 306 serviceNick = "NickServ@services.dal.net"; 307 break; 308 309 case "GameSurge": 310 serviceNick = "AuthServ@Services.GameSurge.net"; 311 break; 312 313 case "EFNet": 314 case "WNet1": 315 // No registration available 316 service.authentication = Progress.finished; 317 return; 318 319 case "QuakeNet": 320 serviceNick = "Q@CServe.quakenet.org"; 321 verb = "AUTH"; 322 break; 323 324 default: 325 break; 326 } 327 328 service.authentication = Progress.inProgress; 329 330 with (IRCServer.Daemon) 331 switch (service.state.server.daemon) 332 { 333 case rizon: 334 case unreal: 335 case hybrid: 336 case bahamut: 337 import std.conv : text; 338 339 // Only accepts password, no auth nickname 340 if (service.state.client.nickname != service.state.client.origNickname) 341 { 342 enum pattern = "Cannot auth when you have changed your nickname. " ~ 343 "(<l>%s</> != <l>%s</>)"; 344 logger.warningf( 345 pattern, 346 service.state.client.nickname, 347 service.state.client.origNickname); 348 349 service.authentication = Progress.finished; 350 return; 351 } 352 353 enum properties = Message.Property.quiet; 354 immutable message = text(verb, ' ', password); 355 query(service.state, serviceNick, message, properties); 356 357 if (!service.state.settings.hideOutgoing && !service.state.settings.trace) 358 { 359 enum pattern = "--> PRIVMSG %s :%s hunter2"; 360 logger.tracef(pattern, serviceNick, verb); 361 } 362 break; 363 364 case snircd: 365 case ircdseven: 366 case u2: 367 case solanum: 368 import std.conv : text; 369 370 // Accepts auth login 371 // GameSurge is AuthServ 372 string account = service.state.bot.account; 373 374 if (!service.state.bot.account.length) 375 { 376 enum pattern = "No account specified! Trying <i>%s</>..."; 377 logger.logf(pattern, service.state.client.origNickname); 378 account = service.state.client.origNickname; 379 } 380 381 enum properties = Message.Property.quiet; 382 immutable message = text(verb, ' ', account, ' ', password); 383 query(service.state, serviceNick, message, properties); 384 385 if (!service.state.settings.hideOutgoing && !service.state.settings.trace) 386 { 387 enum pattern = "--> PRIVMSG %s :%s %s hunter2"; 388 logger.tracef(pattern, serviceNick, verb, account); 389 } 390 break; 391 392 case rusnet: 393 /+ 394 This fails to compile on <2.097 compilers. 395 "Error: switch skips declaration of variable kameloso.plugins.services.connect.tryAuth.message" 396 Worrisome, but work around the issue for now by adding braces. 397 +/ 398 { 399 // Doesn't want a PRIVMSG 400 enum properties = Message.Property.quiet; 401 immutable message = "NICKSERV IDENTIFY " ~ password; 402 raw(service.state, message, properties); 403 404 if (!service.state.settings.hideOutgoing && !service.state.settings.trace) 405 { 406 logger.trace("--> NICKSERV IDENTIFY hunter2"); 407 } 408 } 409 break; 410 411 version(TwitchSupport) 412 { 413 case twitch: 414 // No registration available 415 service.authentication = Progress.finished; 416 return; 417 } 418 419 default: 420 logger.warning("Unsure of what AUTH approach to use."); 421 logger.info("Please report information about what approach succeeded!"); 422 423 if (service.state.bot.account.length) 424 { 425 goto case ircdseven; 426 } 427 else 428 { 429 goto case bahamut; 430 } 431 } 432 433 import kameloso.plugins.common.delayawait : delay; 434 435 void delayedJoinDg() 436 { 437 // If we're still authenticating after n seconds, abort and join channels. 438 439 if (service.authentication == Progress.inProgress) 440 { 441 logger.warning("Authentication timed out."); 442 service.authentication = Progress.finished; 443 } 444 445 if (!service.joinedChannels) 446 { 447 joinChannels(service); 448 } 449 } 450 451 delay(service, &delayedJoinDg, service.authenticationGracePeriod); 452 } 453 454 455 // onAuthEnd 456 /++ 457 Flags authentication as finished and join channels. 458 459 Fires when an authentication service sends a message with a known success, 460 invalid or rejected auth text, signifying completed login. 461 +/ 462 @(IRCEventHandler() 463 .onEvent(IRCEvent.Type.AUTH_SUCCESS) 464 .onEvent(IRCEvent.Type.AUTH_FAILURE) 465 ) 466 void onAuthEnd(ConnectService service, const ref IRCEvent event) 467 { 468 service.authentication = Progress.finished; 469 470 if (service.registration == Progress.finished) 471 { 472 if (!service.joinedChannels) 473 { 474 joinChannels(service); 475 } 476 } 477 } 478 479 480 // onTwitchAuthFailure 481 /++ 482 On Twitch, if the OAuth pass is wrong or malformed, abort and exit the program. 483 Only deal with it if we're currently registering. 484 485 If the bot was compiled without Twitch support, mention this and quit. 486 +/ 487 @(IRCEventHandler() 488 .onEvent(IRCEvent.Type.NOTICE) 489 ) 490 void onTwitchAuthFailure(ConnectService service, const ref IRCEvent event) 491 { 492 import std.algorithm.searching : endsWith; 493 import std.typecons : Flag, No, Yes; 494 495 if ((service.state.server.daemon != IRCServer.Daemon.unset) || 496 !service.state.server.address.endsWith(".twitch.tv")) 497 { 498 // Not early Twitch registration 499 return; 500 } 501 502 // We're registering on Twitch and we got a NOTICE, probably an error 503 504 version(TwitchSupport) 505 { 506 switch (event.content) 507 { 508 case "Improperly formatted auth": 509 if (!service.state.bot.pass.length) 510 { 511 logger.error("Missing Twitch authentication token."); 512 } 513 else 514 { 515 logger.error("Twitch authentication token is malformed. " ~ 516 "Make sure it is entered correctly."); 517 } 518 break; // drop down 519 520 case "Login authentication failed": 521 logger.error("Twitch authentication token is invalid or has expired."); 522 break; // drop down 523 524 case "Login unsuccessful": 525 logger.error("Twitch authentication token probably has insufficient privileges."); 526 break; // drop down 527 528 default: 529 // Just some notice; return 530 return; 531 } 532 533 // Do this here since it should be output in all cases except for the 534 // default, which just returns anyway and skips this. 535 enum message = "Run the program with <i>--set twitch.keygen</> to generate a new one."; 536 logger.log(message); 537 538 // Exit and let the user tend to it. 539 enum properties = Message.Property.priority; 540 quit(service.state, event.content, properties); 541 } 542 else 543 { 544 switch (event.content) 545 { 546 case "Improperly formatted auth": 547 case "Login authentication failed": 548 case "Login unsuccessful": 549 logger.error("The bot was not compiled with Twitch support enabled."); 550 enum properties = Message.Property.priority; 551 enum message = "Missing Twitch support"; 552 return quit(service.state, message, properties); 553 554 default: 555 return; 556 } 557 } 558 } 559 560 561 // onNickInUse 562 /++ 563 Modifies the nickname by appending characters to the end of it. 564 565 Don't modify [IRCPluginState.client.nickname] as the nickname only changes 566 when the [dialect.defs.IRCEvent.Type.RPL_LOGGEDIN|RPL_LOGGEDIN] event actually occurs. 567 +/ 568 @(IRCEventHandler() 569 .onEvent(IRCEvent.Type.ERR_NICKNAMEINUSE) 570 .onEvent(IRCEvent.Type.ERR_NICKCOLLISION) 571 ) 572 void onNickInUse(ConnectService service) 573 { 574 import std.conv : to; 575 import std.random : uniform; 576 577 if (service.registration == Progress.inProgress) 578 { 579 if (!service.renameDuringRegistration.length) 580 { 581 import kameloso.constants : KamelosoDefaults; 582 service.renameDuringRegistration = service.state.client.nickname ~ 583 KamelosoDefaults.altNickSeparator; 584 } 585 586 service.renameDuringRegistration ~= uniform(0, 10).to!string; 587 immutable message = "NICK " ~ service.renameDuringRegistration; 588 immediate(service.state, message); 589 } 590 } 591 592 593 // onBadNick 594 /++ 595 Aborts a registration attempt and quits if the requested nickname is too 596 long or contains invalid characters. 597 +/ 598 @(IRCEventHandler() 599 .onEvent(IRCEvent.Type.ERR_ERRONEOUSNICKNAME) 600 ) 601 void onBadNick(ConnectService service) 602 { 603 if (service.registration == Progress.inProgress) 604 { 605 // Mid-registration and invalid nickname; abort 606 607 if (service.renameDuringRegistration.length) 608 { 609 logger.error("Your nickname was taken and an alternative nickname " ~ 610 "could not be successfully generated."); 611 } 612 else 613 { 614 logger.error("Your nickname is invalid: it is reserved, too long, or contains invalid characters."); 615 } 616 617 enum message = "Invalid nickname"; 618 quit(service.state, message); 619 } 620 } 621 622 623 // onBanned 624 /++ 625 Quits the program if we're banned. 626 627 There's no point in reconnecting. 628 +/ 629 @(IRCEventHandler() 630 .onEvent(IRCEvent.Type.ERR_YOUREBANNEDCREEP) 631 ) 632 void onBanned(ConnectService service) 633 { 634 logger.error("You are banned!"); 635 enum message = "Banned"; 636 quit(service.state, message); 637 } 638 639 640 // onPassMismatch 641 /++ 642 Quits the program if we supplied a bad [kameloso.pods.IRCBot.pass|IRCBot.pass]. 643 644 There's no point in reconnecting. 645 +/ 646 @(IRCEventHandler() 647 .onEvent(IRCEvent.Type.ERR_PASSWDMISMATCH) 648 ) 649 void onPassMismatch(ConnectService service) 650 { 651 if (service.registration != Progress.inProgress) 652 { 653 // Unsure if this ever happens, but don't quit if we're actually registered 654 return; 655 } 656 657 logger.error("Pass mismatch!"); 658 enum message = "Incorrect pass"; 659 quit(service.state, message); 660 } 661 662 663 // onInvite 664 /++ 665 Upon being invited to a channel, joins it if the settings say we should. 666 +/ 667 @(IRCEventHandler() 668 .onEvent(IRCEvent.Type.INVITE) 669 .channelPolicy(ChannelPolicy.any) 670 ) 671 void onInvite(ConnectService service, const ref IRCEvent event) 672 { 673 if (!service.connectSettings.joinOnInvite) 674 { 675 enum message = "Invited, but <i>joinOnInvite</> is set to false."; 676 logger.log(message); 677 return; 678 } 679 680 join(service.state, event.channel); 681 } 682 683 684 // onCapabilityNegotiation 685 /++ 686 Handles server capability exchange. 687 688 This is a necessary step to register with some IRC server; the capabilities 689 have to be requested (`CAP LS`), and the negotiations need to be ended 690 (`CAP END`). 691 +/ 692 @(IRCEventHandler() 693 .onEvent(IRCEvent.Type.CAP) 694 ) 695 void onCapabilityNegotiation(ConnectService service, const ref IRCEvent event) 696 { 697 // http://ircv3.net/irc 698 // https://blog.irccloud.com/ircv3 699 700 if (service.registration == Progress.finished) 701 { 702 // It's possible to call CAP LS after registration, and that would start 703 // this whole process anew. So stop if we have registered. 704 return; 705 } 706 707 service.capabilityNegotiation = Progress.inProgress; 708 709 switch (event.content) 710 { 711 case "LS": 712 import std.algorithm.iteration : splitter; 713 import std.array : Appender; 714 715 Appender!(string[]) capsToReq; 716 capsToReq.reserve(8); // guesstimate 717 718 foreach (immutable rawCap; event.aux[]) 719 { 720 import lu.string : beginsWith, contains, nom; 721 722 if (!rawCap.length) continue; 723 724 string slice = rawCap; // mutable 725 immutable cap = slice.nom!(Yes.inherit)('='); 726 immutable sub = slice; 727 728 switch (cap) 729 { 730 case "sasl": 731 // Error: `switch` skips declaration of variable acceptsExternal 732 // https://issues.dlang.org/show_bug.cgi?id=21427 733 // feep[work] | the quick workaround is to wrap the switch body in a {} 734 { 735 immutable acceptsExternal = !sub.length || sub.contains("EXTERNAL"); 736 immutable acceptsPlain = !sub.length || sub.contains("PLAIN"); 737 immutable hasKey = (service.state.connSettings.privateKeyFile.length || 738 service.state.connSettings.certFile.length); 739 740 if (service.state.connSettings.ssl && acceptsExternal && hasKey) 741 { 742 // Proceed 743 } 744 else if (service.connectSettings.sasl && acceptsPlain && 745 service.state.bot.password.length) 746 { 747 // Likewise 748 } 749 else 750 { 751 // Abort 752 continue; 753 } 754 } 755 goto case; 756 757 version(TwitchSupport) 758 { 759 case "twitch.tv/membership": 760 case "twitch.tv/tags": 761 case "twitch.tv/commands": 762 // Twitch-specific capabilities 763 // Drop down 764 goto case; 765 } 766 767 case "account-tag": // @account=blahblahj; 768 //case "echo-message": // Outgoing messages are received as incoming 769 //case "solanum.chat/identify-msg": // Tag just saying "identified" 770 //case "solanum.chat/realhost": // Includes user's real host/ip 771 772 case "account-notify": 773 case "extended-join": 774 //case "identify-msg": 775 case "multi-prefix": 776 // Freenode 777 case "away-notify": 778 case "chghost": 779 case "invite-notify": 780 //case "multi-prefix": // dup 781 case "userhost-in-names": 782 // Rizon 783 //case "unrealircd.org/plaintext-policy": 784 //case "unrealircd.org/link-security": 785 //case "sts": 786 //case "extended-join": // dup 787 //case "chghost": // dup 788 //case "cap-notify": // Implicitly enabled by CAP LS 302 789 //case "userhost-in-names": // dup 790 //case "multi-prefix": // dup 791 //case "away-notify": // dup 792 //case "account-notify": // dup 793 //case "tls": 794 // UnrealIRCd 795 case "znc.in/self-message": 796 // znc SELFCHAN/SELFQUERY events 797 798 capsToReq ~= cap; 799 ++service.requestedCapabilitiesRemaining; 800 break; 801 802 default: 803 //logger.warning("Unhandled capability: ", cap); 804 break; 805 } 806 } 807 808 if (capsToReq.data.length) 809 { 810 import std.algorithm.iteration : joiner; 811 import std.conv : text; 812 813 enum properties = Message.Property.quiet; 814 immutable message = text("CAP REQ :", capsToReq.data.joiner(" ")); 815 immediate(service.state, message, properties); 816 } 817 break; 818 819 case "ACK": 820 import std.algorithm.iteration : splitter; 821 822 foreach (cap; event.aux[]) 823 { 824 if (!cap.length) continue; 825 826 switch (cap) 827 { 828 case "sasl": 829 enum properties = Message.Property.quiet; 830 immutable hasKey = (service.state.connSettings.privateKeyFile.length || 831 service.state.connSettings.certFile.length); 832 immutable mechanism = (service.state.connSettings.ssl && hasKey) ? 833 "AUTHENTICATE EXTERNAL" : 834 "AUTHENTICATE PLAIN"; 835 immediate(service.state, mechanism, properties); 836 break; 837 838 default: 839 //logger.warning("Unhandled capability ACK: ", cap); 840 --service.requestedCapabilitiesRemaining; 841 break; 842 } 843 } 844 break; 845 846 case "NAK": 847 import std.algorithm.iteration : splitter; 848 849 foreach (cap; event.aux[]) 850 { 851 if (!cap.length) continue; 852 853 switch (cap) 854 { 855 case "sasl": 856 if (service.connectSettings.exitOnSASLFailure) 857 { 858 enum message = "SASL Negotiation Failure"; 859 return quit(service.state, message); 860 } 861 break; 862 863 default: 864 //logger.warning("Unhandled capability NAK: ", cap); 865 --service.requestedCapabilitiesRemaining; 866 break; 867 } 868 } 869 break; 870 871 default: 872 //logger.warning("Unhandled capability type: ", event.content); 873 break; 874 } 875 876 if (!service.requestedCapabilitiesRemaining && 877 (service.capabilityNegotiation == Progress.inProgress)) 878 { 879 service.capabilityNegotiation = Progress.finished; 880 enum properties = Message.Property.quiet; 881 enum message = "CAP END"; 882 immediate(service.state, message, properties); 883 884 if (!service.issuedNICK) 885 { 886 negotiateNick(service); 887 } 888 } 889 } 890 891 892 // onSASLAuthenticate 893 /++ 894 Attempts to authenticate via SASL, with the EXTERNAL mechanism if a private 895 key and/or certificate is set in the configuration file, and by PLAIN otherwise. 896 +/ 897 @(IRCEventHandler() 898 .onEvent(IRCEvent.Type.SASL_AUTHENTICATE) 899 ) 900 void onSASLAuthenticate(ConnectService service) 901 { 902 service.authentication = Progress.inProgress; 903 904 immutable hasKey = (service.state.connSettings.privateKeyFile.length || 905 service.state.connSettings.certFile.length); 906 907 if (service.state.connSettings.ssl && hasKey && 908 (service.saslExternal == Progress.notStarted)) 909 { 910 service.saslExternal = Progress.inProgress; 911 enum message = "AUTHENTICATE +"; 912 immediate(service.state, message); 913 return; 914 } 915 916 immutable plainSuccess = trySASLPlain(service); 917 918 if (!plainSuccess) 919 { 920 onSASLFailure(service); 921 } 922 } 923 924 925 // trySASLPlain 926 /++ 927 Constructs a SASL plain authentication token from the bot's 928 [kameloso.pods.IRCBot.account|IRCBot.account] and 929 [kameloso.pods.IRCBot.password|IRCBot.password], 930 then sends it to the server, during registration. 931 932 A SASL plain authentication token is composed like so: 933 934 `base64(account \0 account \0 password)` 935 936 ...where [kameloso.pods.IRCBot.account|IRCBot.account] is the services 937 account name and [kameloso.pods.IRCBot.password|IRCBot.password] is the 938 account password. 939 940 Params: 941 service = The current [ConnectService]. 942 +/ 943 auto trySASLPlain(ConnectService service) 944 { 945 import lu.string : beginsWith, decode64, encode64; 946 import std.base64 : Base64Exception; 947 import std.conv : text; 948 949 try 950 { 951 immutable account_ = service.state.bot.account.length ? 952 service.state.bot.account : 953 service.state.client.origNickname; 954 955 immutable password_ = service.state.bot.password.beginsWith("base64:") ? 956 decode64(service.state.bot.password[7..$]) : 957 service.state.bot.password; 958 959 immutable authToken = text(account_, '\0', account_, '\0', password_); 960 immutable encoded = encode64(authToken); 961 immutable message = "AUTHENTICATE " ~ encoded; 962 963 enum properties = Message.Property.quiet; 964 immediate(service.state, message, properties); 965 966 if (!service.state.settings.hideOutgoing && !service.state.settings.trace) 967 { 968 logger.trace("--> AUTHENTICATE hunter2"); 969 } 970 return true; 971 } 972 catch (Base64Exception e) 973 { 974 enum pattern = "Could not authenticate: malformed password (<l>%s</>)"; 975 logger.errorf(pattern, e.msg); 976 version(PrintStacktraces) logger.trace(e.info); 977 return false; 978 } 979 } 980 981 982 // onSASLSuccess 983 /++ 984 On SASL authentication success, calls a `CAP END` to finish the 985 [dialect.defs.IRCEvent.Type.CAP|CAP] negotiations. 986 987 Flags the client as having finished registering and authing, allowing the 988 main loop to pick it up and propagate it to all other plugins. 989 +/ 990 @(IRCEventHandler() 991 .onEvent(IRCEvent.Type.RPL_SASLSUCCESS) 992 ) 993 void onSASLSuccess(ConnectService service) 994 { 995 service.authentication = Progress.finished; 996 997 /++ 998 The END subcommand signals to the server that capability negotiation 999 is complete and requests that the server continue with client 1000 registration. If the client is already registered, this command 1001 MUST be ignored by the server. 1002 1003 Clients that support capabilities but do not wish to enter negotiation 1004 SHOULD send CAP END upon connection to the server. 1005 1006 - http://ircv3.net/specs/core/capability-negotiation-3.1.html 1007 1008 Notes: Some servers don't ignore post-registration CAP. 1009 +/ 1010 1011 if (!--service.requestedCapabilitiesRemaining && 1012 (service.capabilityNegotiation == Progress.inProgress)) 1013 { 1014 service.capabilityNegotiation = Progress.finished; 1015 enum properties = Message.Property.quiet; 1016 enum message = "CAP END"; 1017 immediate(service.state, message, properties); 1018 1019 if ((service.registration == Progress.inProgress) && !service.issuedNICK) 1020 { 1021 negotiateNick(service); 1022 } 1023 } 1024 } 1025 1026 1027 // onSASLFailure 1028 /++ 1029 On SASL authentication failure, calls a `CAP END` to finish the 1030 [dialect.defs.IRCEvent.Type.CAP|CAP] negotiations and finish registration. 1031 1032 Flags the client as having finished registering, allowing the main loop to 1033 pick it up and propagate it to all other plugins. 1034 +/ 1035 @(IRCEventHandler() 1036 .onEvent(IRCEvent.Type.ERR_SASLFAIL) 1037 ) 1038 void onSASLFailure(ConnectService service) 1039 { 1040 if ((service.saslExternal == Progress.inProgress) && service.state.bot.password.length) 1041 { 1042 // Fall back to PLAIN 1043 service.saslExternal = Progress.finished; 1044 enum properties = Message.Property.quiet; 1045 enum message = "AUTHENTICATE PLAIN"; 1046 immediate(service.state, message, properties); 1047 return; 1048 } 1049 1050 if (service.connectSettings.exitOnSASLFailure) 1051 { 1052 enum message = "SASL Negotiation Failure"; 1053 return quit(service.state, message); 1054 } 1055 1056 // Auth failed and will fail even if we try NickServ, so flag as 1057 // finished auth and invoke `CAP END` 1058 service.authentication = Progress.finished; 1059 1060 if (!--service.requestedCapabilitiesRemaining && 1061 (service.capabilityNegotiation == Progress.inProgress)) 1062 { 1063 service.capabilityNegotiation = Progress.finished; 1064 enum properties = Message.Property.quiet; 1065 enum message = "CAP END"; 1066 immediate(service.state, message, properties); 1067 1068 if ((service.registration == Progress.inProgress) && !service.issuedNICK) 1069 { 1070 negotiateNick(service); 1071 } 1072 } 1073 } 1074 1075 1076 // onWelcome 1077 /++ 1078 Marks registration as completed upon [dialect.defs.IRCEvent.Type.RPL_WELCOME|RPL_WELCOME] 1079 (numeric `001`). 1080 1081 Additionally performs post-connect routines (authenticates if not already done, 1082 and send-after-connect). 1083 +/ 1084 @(IRCEventHandler() 1085 .onEvent(IRCEvent.Type.RPL_WELCOME) 1086 ) 1087 void onWelcome(ConnectService service) 1088 { 1089 import std.algorithm.iteration : splitter; 1090 import std.algorithm.searching : endsWith; 1091 1092 service.registration = Progress.finished; 1093 service.renameDuringRegistration = string.init; 1094 1095 version(WithPingMonitor) startPingMonitorFiber(service); 1096 1097 alias separator = ConnectSettings.sendAfterConnectSeparator; 1098 auto toSendRange = service.connectSettings.sendAfterConnect.splitter(separator); 1099 1100 foreach (immutable unstripped; toSendRange) 1101 { 1102 import lu.string : strippedLeft; 1103 import std.array : replace; 1104 1105 immutable line = unstripped.strippedLeft; 1106 if (!line.length) continue; 1107 1108 immutable processed = line 1109 .replace("$nickname", service.state.client.nickname) 1110 .replace("$origserver", service.state.server.address) 1111 .replace("$server", service.state.server.resolvedAddress); 1112 1113 raw(service.state, processed); 1114 } 1115 1116 if (service.state.server.address.endsWith(".twitch.tv")) 1117 { 1118 import kameloso.plugins.common.delayawait : await, unawait; 1119 1120 if (service.state.settings.preferHostmasks && 1121 !service.state.settings.force) 1122 { 1123 // We already infer account by username on Twitch; 1124 // hostmasks mode makes no sense there. So disable it. 1125 service.state.settings.preferHostmasks = false; 1126 service.state.updates |= typeof(service.state.updates).settings; 1127 } 1128 1129 static immutable IRCEvent.Type[2] endOfMotdEventTypes = 1130 [ 1131 IRCEvent.Type.RPL_ENDOFMOTD, 1132 IRCEvent.Type.ERR_NOMOTD, 1133 ]; 1134 1135 void twitchWarningDg(IRCEvent) 1136 { 1137 scope(exit) unawait(service, &twitchWarningDg, endOfMotdEventTypes[]); 1138 1139 version(TwitchSupport) 1140 { 1141 import lu.string : beginsWith; 1142 1143 /+ 1144 Upon having connected, registered and logged onto the Twitch servers, 1145 disable outgoing colours and warn about having a `.` or `/` prefix. 1146 1147 Twitch chat doesn't do colours, so ours would only show up like `00kameloso`. 1148 Furthermore, Twitch's own commands are prefixed with a dot `.` and/or a slash `/`, 1149 so we can't use that ourselves. 1150 +/ 1151 1152 if (service.state.server.daemon != IRCServer.Daemon.twitch) return; 1153 1154 service.state.settings.colouredOutgoing = false; 1155 service.state.updates |= typeof(service.state.updates).settings; 1156 1157 if (service.state.settings.prefix.beginsWith(".") || 1158 service.state.settings.prefix.beginsWith("/")) 1159 { 1160 enum pattern = `WARNING: A prefix of "<l>%s</>" will *not* work on Twitch servers, ` ~ 1161 "as <l>.</> and <l>/</> are reserved for Twitch's own commands."; 1162 logger.warningf(pattern, service.state.settings.prefix); 1163 } 1164 } 1165 else 1166 { 1167 // No Twitch support built in 1168 if (service.state.server.address.endsWith(".twitch.tv")) 1169 { 1170 logger.warning("This bot was not built with Twitch support enabled. " ~ 1171 "Expect errors and general uselessness."); 1172 } 1173 } 1174 } 1175 1176 await(service, &twitchWarningDg, endOfMotdEventTypes[]); 1177 } 1178 else 1179 { 1180 // Not on Twitch 1181 if (service.connectSettings.regainNickname && !service.state.bot.hasGuestNickname && 1182 (service.state.client.nickname != service.state.client.origNickname)) 1183 { 1184 import kameloso.plugins.common.delayawait : delay; 1185 import kameloso.constants : BufferSize; 1186 import core.thread : Fiber; 1187 1188 void regainDg() 1189 { 1190 // Concatenate the verb once 1191 immutable squelchVerb = "squelch " ~ service.state.client.origNickname; 1192 1193 while (service.state.client.nickname != service.state.client.origNickname) 1194 { 1195 import kameloso.messaging : raw; 1196 1197 version(WithPrinterPlugin) 1198 { 1199 import kameloso.thread : ThreadMessage, boxed; 1200 import std.concurrency : send; 1201 service.state.mainThread.send( 1202 ThreadMessage.busMessage("printer", boxed(squelchVerb))); 1203 } 1204 1205 enum properties = (Message.Property.quiet | Message.Property.background); 1206 immutable message = "NICK " ~ service.state.client.origNickname; 1207 raw(service.state, message, properties); 1208 delay(service, service.nickRegainPeriodicity, Yes.yield); 1209 } 1210 } 1211 1212 auto regainFiber = new Fiber(®ainDg, BufferSize.fiberStack); 1213 delay(service, regainFiber, service.nickRegainPeriodicity); 1214 } 1215 } 1216 } 1217 1218 1219 // onSelfnickSuccessOrFailure 1220 /++ 1221 Resets [kameloso.plugins.printer.base.PrinterPlugin|PrinterPlugin] squelching upon a 1222 successful or failed nick change. This so as to be squelching as little as possible. 1223 +/ 1224 version(WithPrinterPlugin) 1225 @(IRCEventHandler() 1226 .onEvent(IRCEvent.Type.SELFNICK) 1227 .onEvent(IRCEvent.Type.ERR_NICKNAMEINUSE) 1228 ) 1229 void onSelfnickSuccessOrFailure(ConnectService service) 1230 { 1231 import kameloso.thread : ThreadMessage, boxed; 1232 import std.concurrency : send; 1233 service.state.mainThread.send( 1234 ThreadMessage.busMessage("printer", boxed("unsquelch " ~ service.state.client.origNickname))); 1235 } 1236 1237 1238 // onQuit 1239 /++ 1240 Regains nickname if the holder of the one we wanted during registration quit. 1241 +/ 1242 @(IRCEventHandler() 1243 .onEvent(IRCEvent.Type.QUIT) 1244 ) 1245 void onQuit(ConnectService service, const ref IRCEvent event) 1246 { 1247 if ((service.state.server.daemon != IRCServer.Daemon.twitch) && 1248 service.connectSettings.regainNickname && 1249 (event.sender.nickname == service.state.client.origNickname)) 1250 { 1251 // The regain Fiber will end itself when it is next triggered 1252 enum pattern = "Attempting to regain nickname <l>%s</>..."; 1253 logger.infof(pattern, service.state.client.origNickname); 1254 immutable message = "NICK " ~ service.state.client.origNickname; 1255 raw(service.state, message); 1256 } 1257 } 1258 1259 1260 // onEndOfMotd 1261 /++ 1262 Joins channels and prints some Twitch warnings on end of MOTD. 1263 1264 Do this then instead of on [dialect.defs.IRCEvent.Type.RPL_WELCOME|RPL_WELCOME] 1265 for better timing, and to avoid having the message drown in MOTD. 1266 +/ 1267 @(IRCEventHandler() 1268 .onEvent(IRCEvent.Type.RPL_ENDOFMOTD) 1269 .onEvent(IRCEvent.Type.ERR_NOMOTD) 1270 ) 1271 void onEndOfMotd(ConnectService service) 1272 { 1273 // Gather information about ourselves 1274 if ((service.state.server.daemon != IRCServer.Daemon.twitch) && 1275 !service.state.client.ident.length) 1276 { 1277 enum properties = 1278 Message.Property.forced | 1279 Message.Property.quiet | 1280 Message.Property.priority; 1281 whois(service.state, service.state.client.nickname, properties); 1282 } 1283 1284 version(TwitchSupport) 1285 { 1286 if (service.state.server.daemon == IRCServer.Daemon.twitch) 1287 { 1288 service.serverSupportsWHOIS = false; 1289 } 1290 } 1291 1292 if (service.state.server.network.length && 1293 service.state.bot.password.length && 1294 (service.authentication == Progress.notStarted) && 1295 (service.state.server.daemon != IRCServer.Daemon.twitch)) 1296 { 1297 tryAuth(service); 1298 } 1299 else if (((service.authentication == Progress.finished) || 1300 !service.state.bot.password.length || 1301 (service.state.server.daemon == IRCServer.Daemon.twitch)) && 1302 !service.joinedChannels) 1303 { 1304 // tryAuth finished early with an unsuccessful login, else 1305 // `service.authentication` would be set much later. 1306 // Twitch servers can't auth so join immediately 1307 // but don't do anything if we already joined channels. 1308 joinChannels(service); 1309 } 1310 } 1311 1312 1313 // onWHOISUser 1314 /++ 1315 Catch information about ourselves (notably our `IDENT`) from `WHOIS` results. 1316 +/ 1317 @(IRCEventHandler() 1318 .onEvent(IRCEvent.Type.RPL_WHOISUSER) 1319 ) 1320 void onWHOISUser(ConnectService service, const ref IRCEvent event) 1321 { 1322 if (event.target.nickname != service.state.client.nickname) return; 1323 1324 if (service.state.client.ident != event.target.ident) 1325 { 1326 service.state.client.ident = event.target.ident; 1327 service.state.updates |= typeof(service.state.updates).client; 1328 } 1329 } 1330 1331 1332 // onISUPPORT 1333 /++ 1334 Requests a UTF-8 codepage if it seems that the server supports changing such. 1335 1336 Currently only RusNet is known to support codepages. 1337 +/ 1338 @(IRCEventHandler() 1339 .onEvent(IRCEvent.Type.RPL_ISUPPORT) 1340 ) 1341 void onISUPPORT(ConnectService service, const ref IRCEvent event) 1342 { 1343 import std.algorithm.searching : canFind; 1344 1345 if (event.aux[].canFind("CODEPAGES")) 1346 { 1347 enum properties = Message.Property.quiet; 1348 enum message = "CODEPAGE UTF-8"; 1349 raw(service.state, message, properties); 1350 } 1351 } 1352 1353 1354 // onReconnect 1355 /++ 1356 Disconnects and reconnects to the server. 1357 1358 This is a "benign" disconnect. We need to reconnect preemptively instead of 1359 waiting for the server to disconnect us, as it would otherwise constitute an error. 1360 +/ 1361 version(TwitchSupport) 1362 @(IRCEventHandler() 1363 .onEvent(IRCEvent.Type.RECONNECT) 1364 ) 1365 void onReconnect(ConnectService service) 1366 { 1367 import kameloso.thread : ThreadMessage; 1368 import std.concurrency : send; 1369 1370 logger.info("Reconnecting upon server request."); 1371 service.state.mainThread.send(ThreadMessage.reconnect()); 1372 } 1373 1374 1375 // onUnknownCommand 1376 /++ 1377 Warns the user if the server does not seem to support WHOIS queries, suggesting 1378 that they enable hostmasks mode instead. 1379 +/ 1380 @(IRCEventHandler() 1381 .onEvent(IRCEvent.Type.ERR_UNKNOWNCOMMAND) 1382 ) 1383 void onUnknownCommand(ConnectService service, const ref IRCEvent event) 1384 { 1385 if (service.serverSupportsWHOIS && !service.state.settings.preferHostmasks && (event.aux[0] == "WHOIS")) 1386 { 1387 logger.error("Error: This server does not seem to support user accounts."); 1388 enum message = "Consider enabling <l>Core</>.<l>preferHostmasks</>."; 1389 logger.error(message); 1390 logger.error("As it is, functionality will be greatly limited."); 1391 service.serverSupportsWHOIS = false; 1392 } 1393 } 1394 1395 1396 // startPingMonitorFiber 1397 /++ 1398 Starts a monitor Fiber that sends a [dialect.defs.IRCEvent.Type.PING|PING] 1399 if we haven't received one from the server for a while. This is to ensure 1400 that dead connections are properly detected. 1401 1402 Params: 1403 service = The current [ConnectService]. 1404 +/ 1405 void startPingMonitorFiber(ConnectService service) 1406 { 1407 import kameloso.plugins.common.delayawait : await, delay, removeDelayedFiber; 1408 import kameloso.constants : BufferSize; 1409 import kameloso.thread : CarryingFiber; 1410 import core.thread : Fiber; 1411 import core.time : seconds; 1412 1413 if (service.connectSettings.maxPingPeriodAllowed <= 0) return; 1414 1415 immutable pingMonitorPeriodicity = service.connectSettings.maxPingPeriodAllowed.seconds; 1416 1417 void pingMonitorDg() 1418 { 1419 static immutable timeToAllowForPingResponse = 30.seconds; 1420 static immutable briefWait = 1.seconds; 1421 long lastPongTimestamp; 1422 uint strikes; 1423 1424 enum StrikeBreakpoints 1425 { 1426 wait = 2, 1427 ping = 3, 1428 } 1429 1430 while (true) 1431 { 1432 auto thisFiber = cast(CarryingFiber!IRCEvent)(Fiber.getThis); 1433 assert(thisFiber, "Incorrectly cast Fiber: " ~ typeof(thisFiber).stringof); 1434 immutable thisEvent = thisFiber.payload; 1435 1436 with (IRCEvent.Type) 1437 switch (thisEvent.type) 1438 { 1439 case UNSET: 1440 import std.datetime.systime : Clock; 1441 1442 // Triggered by timer 1443 immutable nowInUnix = Clock.currTime.toUnixTime; 1444 1445 if ((nowInUnix - lastPongTimestamp) >= service.connectSettings.maxPingPeriodAllowed) 1446 { 1447 import kameloso.thread : ThreadMessage; 1448 import std.concurrency : prioritySend; 1449 1450 /+ 1451 Skip first two strikes; helps when resuming from suspend and similar, 1452 then allow for a PING with `timeToAllowForPingResponse` as timeout. 1453 Finally, if all else failed, reconnect. 1454 +/ 1455 ++strikes; 1456 1457 if (strikes <= StrikeBreakpoints.wait) 1458 { 1459 if (service.state.settings.trace && (strikes > 1)) 1460 { 1461 logger.warning("Server is suspiciously quiet."); 1462 } 1463 delay(service, briefWait, Yes.yield); 1464 continue; 1465 } 1466 else if (strikes == StrikeBreakpoints.ping) 1467 { 1468 // Timeout. Send a preemptive ping 1469 service.state.mainThread.prioritySend(ThreadMessage.ping(service.state.server.resolvedAddress)); 1470 delay(service, timeToAllowForPingResponse, Yes.yield); 1471 continue; 1472 } 1473 else /*if (strikes > StrikeBreakpoints.ping)*/ 1474 { 1475 // All failed, reconnect 1476 logger.warning("No response from server. Reconnecting."); 1477 service.state.mainThread.prioritySend(ThreadMessage.reconnect); 1478 return; 1479 } 1480 } 1481 else 1482 { 1483 // Early trigger, either interleaved with a PONG or due to preemptive PING 1484 // Remove current delay and re-delay at when the next PING check should be 1485 removeDelayedFiber(service); 1486 immutable elapsed = (nowInUnix - lastPongTimestamp); 1487 immutable remaining = (service.connectSettings.maxPingPeriodAllowed - elapsed); 1488 delay(service, remaining.seconds, Yes.yield); 1489 } 1490 continue; 1491 1492 case PING: 1493 case PONG: 1494 // Triggered by PING *or* PONG response from our preemptive PING 1495 // Update and remove delay, so we can drop down and re-delay it 1496 lastPongTimestamp = thisEvent.time; 1497 strikes = 0; 1498 removeDelayedFiber(service); 1499 break; 1500 1501 default: 1502 assert(0, "Impossible case hit in pingMonitorDg"); 1503 } 1504 1505 delay(service, pingMonitorPeriodicity, Yes.yield); 1506 } 1507 } 1508 1509 static immutable IRCEvent.Type[2] pingPongTypes = 1510 [ 1511 IRCEvent.Type.PING, 1512 IRCEvent.Type.PONG, 1513 ]; 1514 1515 Fiber pingMonitorFiber = new CarryingFiber!IRCEvent(&pingMonitorDg, BufferSize.fiberStack); 1516 await(service, pingMonitorFiber, pingPongTypes[]); 1517 delay(service, pingMonitorFiber, pingMonitorPeriodicity); 1518 } 1519 1520 1521 // register 1522 /++ 1523 Registers with/logs onto an IRC server. 1524 1525 Params: 1526 service = The current [ConnectService]. 1527 +/ 1528 void register(ConnectService service) 1529 { 1530 import lu.string : beginsWith; 1531 import std.algorithm.searching : canFind, endsWith; 1532 import std.uni : toLower; 1533 1534 service.registration = Progress.inProgress; 1535 1536 // Server networks we know to support capabilities 1537 static immutable capabilityServerWhitelistPrefix = 1538 [ 1539 "efnet.", 1540 ]; 1541 1542 // Ditto 1543 static immutable capabilityServerWhitelistSuffix = 1544 [ 1545 ".libera.chat", 1546 ".freenode.net", 1547 ".twitch.tv", 1548 ".acc.umu.se", 1549 ".irchighway.net", 1550 ".oftc.net", 1551 ".rizon.net", 1552 ".snoonet.org", 1553 ".spotchat.org", 1554 ".swiftirc.net", 1555 ".efnet.org", 1556 ".netbsd.se", 1557 ".geekshed.net", 1558 ".moep.net", 1559 ".esper.net", 1560 ".europnet.org", 1561 ]; 1562 1563 // Server networks we know to not support capabilities 1564 static immutable capabilityServerBlacklistSuffix = 1565 [ 1566 ".quakenet.org", 1567 ".dal.net", 1568 ".gamesurge.net", 1569 ".geveze.org", 1570 ".ircnet.net", 1571 ".undernet.org", 1572 ".team17.com", 1573 ".link-net.be", 1574 ]; 1575 1576 immutable serverToLower = service.state.server.address.toLower; 1577 immutable serverWhitelisted = capabilityServerWhitelistSuffix 1578 .canFind!((a,b) => b.endsWith(a))(serverToLower) || 1579 capabilityServerWhitelistPrefix 1580 .canFind!((a,b) => b.beginsWith(a))(serverToLower); 1581 immutable serverBlacklisted = !serverWhitelisted && 1582 capabilityServerBlacklistSuffix 1583 .canFind!((a,b) => b.endsWith(a))(serverToLower); 1584 1585 if (!serverBlacklisted || service.state.settings.force) 1586 { 1587 enum properties = Message.Property.quiet; 1588 enum message = "CAP LS 302"; 1589 immediate(service.state, message, properties); 1590 } 1591 1592 version(TwitchSupport) 1593 { 1594 import std.algorithm.searching : endsWith; 1595 immutable serverIsTwitch = service.state.server.address.endsWith(".twitch.tv"); 1596 } 1597 1598 if (service.state.bot.pass.length) 1599 { 1600 static string decodeIfPrefixedBase64(const string encoded) 1601 { 1602 import lu.string : beginsWith, decode64; 1603 import std.base64 : Base64Exception; 1604 1605 if (encoded.beginsWith("base64:")) 1606 { 1607 try 1608 { 1609 return decode64(encoded[7..$]); 1610 } 1611 catch (Base64Exception _) 1612 { 1613 // says "base64:" but can't be decoded 1614 // Something's wrong but be conservative about it. 1615 return encoded; 1616 } 1617 } 1618 else 1619 { 1620 return encoded; 1621 } 1622 } 1623 1624 immutable decoded = decodeIfPrefixedBase64(service.state.bot.pass); 1625 1626 version(TwitchSupport) 1627 { 1628 if (serverIsTwitch) 1629 { 1630 import lu.string : beginsWith; 1631 service.state.bot.pass = decoded.beginsWith("oauth:") ? decoded : ("oauth:" ~ decoded); 1632 } 1633 } 1634 1635 if (!service.state.bot.pass.length) service.state.bot.pass = decoded; 1636 service.state.updates |= typeof(service.state.updates).bot; 1637 1638 enum properties = Message.Property.quiet; 1639 immutable message = "PASS " ~ service.state.bot.pass; 1640 immediate(service.state, message, properties); 1641 1642 if (!service.state.settings.hideOutgoing && !service.state.settings.trace) 1643 { 1644 version(TwitchSupport) 1645 { 1646 if (!serverIsTwitch) 1647 { 1648 // fake it 1649 logger.trace("--> PASS hunter2"); 1650 } 1651 } 1652 else 1653 { 1654 // Ditto 1655 logger.trace("--> PASS hunter2"); 1656 } 1657 } 1658 } 1659 1660 version(TwitchSupport) 1661 { 1662 if (serverIsTwitch) 1663 { 1664 import std.uni : toLower; 1665 1666 // Make sure nickname is lowercase so we can rely on it as account name 1667 service.state.client.nickname = service.state.client.nickname.toLower; 1668 service.state.updates |= typeof(service.state.updates).client; 1669 } 1670 } 1671 1672 if (serverWhitelisted) 1673 { 1674 // CAP should work, nick will be negotiated after CAP END 1675 } 1676 else if (serverBlacklisted && !service.state.settings.force) 1677 { 1678 // No CAP, do NICK right away 1679 negotiateNick(service); 1680 } 1681 else 1682 { 1683 import kameloso.plugins.common.delayawait : delay; 1684 1685 // Unsure, so monitor CAP progress 1686 void capMonitorDg() 1687 { 1688 if (service.capabilityNegotiation == Progress.notStarted) 1689 { 1690 logger.warning("CAP timeout. Does the server not support capabilities?"); 1691 negotiateNick(service); 1692 } 1693 } 1694 1695 delay(service, &capMonitorDg, service.capLSTimeout); 1696 } 1697 } 1698 1699 1700 // negotiateNick 1701 /++ 1702 Negotiate nickname and user with the server, during registration. 1703 +/ 1704 void negotiateNick(ConnectService service) 1705 { 1706 import std.algorithm.searching : endsWith; 1707 1708 immutable serverIsTwitch = service.state.server.address.endsWith(".twitch.tv"); 1709 1710 if (!serverIsTwitch) 1711 { 1712 import kameloso.string : replaceTokens; 1713 import std.format : format; 1714 1715 // Twitch doesn't require USER, only PASS and NICK 1716 /+ 1717 Command: USER 1718 Parameters: <user> <mode> <unused> <realname> 1719 1720 The <mode> parameter should be a numeric, and can be used to 1721 automatically set user modes when registering with the server. This 1722 parameter is a bitmask, with only 2 bits having any signification: if 1723 the bit 2 is set, the user mode 'w' will be set and if the bit 3 is 1724 set, the user mode 'i' will be set. 1725 1726 https://tools.ietf.org/html/rfc2812#section-3.1.3 1727 1728 The available modes are as follows: 1729 a - user is flagged as away; 1730 i - marks a users as invisible; 1731 w - user receives wallops; 1732 r - restricted user connection; 1733 o - operator flag; 1734 O - local operator flag; 1735 s - marks a user for receipt of server notices. 1736 +/ 1737 enum properties = Message.Property.quiet; 1738 enum pattern = "USER %s 8 * :%s"; 1739 immutable message = pattern.format( 1740 service.state.client.user, 1741 service.state.client.realName.replaceTokens(service.state.client)); 1742 immediate(service.state, message, properties); 1743 } 1744 1745 immutable properties = serverIsTwitch ? 1746 Message.Property.quiet : 1747 Message.Property.none; 1748 immutable message = "NICK " ~ service.state.client.nickname; 1749 immediate(service.state, message, properties); 1750 service.issuedNICK = true; 1751 } 1752 1753 1754 // start 1755 /++ 1756 Registers with the server. 1757 1758 This initialisation event fires immediately after a successful connect, and 1759 so instead of waiting for something from the server to trigger our 1760 registration procedure (notably [dialect.defs.IRCEvent.Type.NOTICE]s 1761 about our `IDENT` and hostname), we preemptively register. 1762 1763 It seems to work. 1764 +/ 1765 void start(ConnectService service) 1766 { 1767 register(service); 1768 } 1769 1770 1771 import kameloso.thread : Boxed, Sendable; 1772 1773 // onBusMessage 1774 /++ 1775 Receives a passed [kameloso.thread.Boxed|Boxed] instance with the "`connect`" header, 1776 and calls functions based on the payload message. 1777 1778 This is used to let other plugins trigger re-authentication with services. 1779 1780 Params: 1781 service = The current [ConnectService]. 1782 header = String header describing the passed content payload. 1783 content = Message content. 1784 +/ 1785 void onBusMessage(ConnectService service, const string header, shared Sendable content) 1786 { 1787 if (header != "connect") return; 1788 1789 auto message = cast(Boxed!string)content; 1790 assert(message, "Incorrectly cast message: " ~ typeof(message).stringof); 1791 1792 if (message.payload == "auth") 1793 { 1794 tryAuth(service); 1795 } 1796 else 1797 { 1798 logger.error("[connect] Unimplemented bus message verb: ", message.payload); 1799 } 1800 } 1801 1802 1803 mixin PluginRegistration!(ConnectService, -30.priority); 1804 1805 public: 1806 1807 1808 // ConnectService 1809 /++ 1810 The Connect service is a collection of functions and state needed to connect 1811 and stay connected to an IRC server, as well as authenticate with services. 1812 1813 This is mostly a matter of sending `USER` and `NICK` during registration, 1814 but also incorporates logic to authenticate with services, and capability 1815 negotiations. 1816 +/ 1817 final class ConnectService : IRCPlugin 1818 { 1819 private: 1820 import core.time : seconds; 1821 1822 /// All Connect service settings gathered. 1823 ConnectSettings connectSettings; 1824 1825 /++ 1826 How many seconds we should wait before we tire of waiting for authentication 1827 responses and just start joining channels. 1828 +/ 1829 static immutable authenticationGracePeriod = 15.seconds; 1830 1831 /++ 1832 How many seconds to wait for a response to the request for the list of 1833 capabilities the server has. After these many seconds, it will just 1834 normally negotiate nickname and log in. 1835 +/ 1836 static immutable capLSTimeout = 15.seconds; 1837 1838 /++ 1839 How often to attempt to regain nickname, in seconds, if there was a collision 1840 and we had to rename ourselves during registration. 1841 +/ 1842 static immutable nickRegainPeriodicity = 600.seconds; 1843 1844 /++ 1845 After how much time we should check whether or not we managed to join all channels. 1846 +/ 1847 static immutable channelCheckDelay = 15.seconds; 1848 1849 /// At what step we're currently at with regards to authentication. 1850 Progress authentication; 1851 1852 /// At what step we're currently at with regards to SASL EXTERNAL authentication. 1853 Progress saslExternal; 1854 1855 /// At what step we're currently at with regards to registration. 1856 Progress registration; 1857 1858 /// At what step we're currently at with regards to capabilities. 1859 Progress capabilityNegotiation; 1860 1861 /// Whether or not we have issued a NICK command during registration. 1862 bool issuedNICK; 1863 1864 /++ 1865 Temporary: the nickname that we had to rename to, to successfully 1866 register on the server. 1867 1868 This is to avoid modifying [dialect.defs.IRCClient.nickname|IRCClient.nickname] 1869 before the nickname is actually changed, yet still carry information about the 1870 incremental rename throughout calls of [onNickInUse]. 1871 +/ 1872 string renameDuringRegistration; 1873 1874 /// Whether or not the bot has joined its channels at least once. 1875 bool joinedChannels; 1876 1877 version(TwitchSupport) 1878 { 1879 /++ 1880 Which channels we are actually in. In most cases this will be the union 1881 of our home and our guest channels, except when it isn't. 1882 +/ 1883 bool[string] currentActualChannels; 1884 } 1885 1886 /// Whether or not the server seems to be supporting WHOIS queries. 1887 bool serverSupportsWHOIS = true; 1888 1889 /// Number of capabilities requested but still not awarded. 1890 uint requestedCapabilitiesRemaining; 1891 1892 mixin IRCPluginImpl; 1893 }