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(&regainDg, 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 }