1 /++
2     Functions related to parsing IRC events.
3 
4     IRC events come in very heterogeneous forms along the lines of:
5 
6         `:sender.address.tld TYPE [args...] :content`
7 
8         `:sender!~ident@address.tld 123 [args...] :content`
9 
10     The number and syntax of arguments for types vary wildly. As such, one
11     common parsing routine can't be used; there are simply too many exceptions.
12     The beginning `:sender.address.tld` is *almost* always the same form, but only
13     almost. It's guaranteed to be followed by the type however, which come either in
14     alphanumeric name (e.g. [dialect.defs.IRCEvent.Type.PRIVMSG|PRIVMSG],
15     [dialect.defs.IRCEvent.Type.INVITE|INVITE], [dialect.defs.IRCEvent.Type.MODE|MODE],
16     ...), or in numeric form of 001 to 999 inclusive.
17 
18     What we can do then is to parse this type, and interpret the arguments
19     following as befits it.
20 
21     This translates to large switches, which can't be helped. There are simply
22     too many variations, which switches lend themselves well to. You could make
23     it into long if...else if chains, but it would just be the same thing in a
24     different form. Likewise a nested function is not essentially different from
25     a switch case.
26 
27     ---
28     IRCParser parser;
29 
30     string fromServer = ":zorael!~NaN@address.tld MODE #channel +v nickname";
31     IRCEvent event = parser.toIRCEvent(fromServer);
32 
33     with (event)
34     {
35         assert(type == IRCEvent.Type.MODE);
36         assert(sender.nickname == "zorael");
37         assert(sender.ident == "~NaN");
38         assert(sender.address == "address.tld");
39         assert(target.nickname == "nickname");
40         assert(channel == "#channel");
41         assert(aux[0] = "+v");
42     }
43 
44     string alsoFromServer = ":cherryh.freenode.net 435 oldnick newnick #d " ~
45         ":Cannot change nickname while banned on channel";
46     IRCEvent event2 = parser.toIRCEvent(alsoFromServer);
47 
48     with (event2)
49     {
50         assert(type == IRCEvent.Type.ERR_BANONCHAN);
51         assert(sender.address == "cherryh.freenode.net");
52         assert(channel == "#d");
53         assert(target.nickname == "oldnick");
54         assert(content == "Cannot change nickname while banned on channel");
55         assert(aux[0] == "newnick");
56         assert(num == 435);
57     }
58 
59     string furtherFromServer = ":kameloso^!~ident@81-233-105-99-no80.tbcn.telia.com NICK :kameloso_";
60     IRCEvent event3 = parser.toIRCEvent(furtherFromServer);
61 
62     with (event3)
63     {
64         assert(type == IRCEvent.Type.NICK);
65         assert(sender.nickname == "kameloso^");
66         assert(sender.ident == "~ident");
67         assert(sender.address == "81-233-105-99-no80.tbcn.telia.com");
68         assert(target.nickname = "kameloso_");
69     }
70     ---
71 
72     See the `/tests` directory for more example parses.
73  +/
74 module dialect.parsing;
75 
76 private:
77 
78 import dialect.defs;
79 import dialect.common : IRCParseException;
80 import dialect.postprocessors : Postprocessor;
81 import lu.string : advancePast;
82 
83 
84 // toIRCEvent
85 /++
86     Parses an IRC string into an [dialect.defs.IRCEvent|IRCEvent].
87 
88     Parsing goes through several phases (prefix, typestring, specialcases) and
89     this is the function that calls them, in order.
90 
91     See the files in `/tests` for unittest examples.
92 
93     Params:
94         parser = Reference to the current [IRCParser].
95         raw = Raw IRC string to parse.
96 
97     Returns:
98         A finished [dialect.defs.IRCEvent|IRCEvent].
99 
100     Throws:
101         [dialect.common.IRCParseException|IRCParseException] if an empty
102         string was passed.
103  +/
104 public IRCEvent toIRCEvent(
105     ref IRCParser parser,
106     const string raw) pure @safe
107 {
108     import std.uni : toLower;
109 
110     if (!raw.length)
111     {
112         enum message = "Tried to parse an empty string";
113         throw new IRCParseException(message);
114     }
115 
116     if (raw[0] != ':')
117     {
118         if (raw[0] == '@')
119         {
120             if (raw.length < 2)
121             {
122                 enum message = "Tried to parse what was only the start of tags";
123                 throw new IRCParseException(message);
124             }
125 
126             // IRCv3 tags
127             // @badges=broadcaster/1;color=;display-name=Zorael;emote-sets=0;mod=0;subscriber=0;user-type= :tmi.twitch.tv USERSTATE #zorael
128             // @broadcaster-lang=;emote-only=0;followers-only=-1;mercury=0;r9k=0;room-id=22216721;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #zorael
129             // @badges=subscriber/3;color=;display-name=asdcassr;emotes=560489:0-6,8-14,16-22,24-30/560510:39-46;id=4d6bbafb-427d-412a-ae24-4426020a1042;mod=0;room-id=23161357;sent-ts=1510059590512;subscriber=1;tmi-sent-ts=1510059591528;turbo=0;user-id=38772474;user-type= :asdcsa!asdcss@asdcsd.tmi.twitch.tv PRIVMSG #lirik :lirikFR lirikFR lirikFR lirikFR :sled: lirikLUL
130             // @solanum.chat/ip=42.116.30.146 :Guest4!~Guest4@42.116.30.146 QUIT :Quit: Connection closed
131             // @account=AdaYuong;solanum.chat/identified :AdaYuong!AdaYuong@user/adayuong PART #libera
132             // @account=sna;solanum.chat/ip=2a01:420:17:1::ffff:536;solanum.chat/identified :sna!sna@im.vpsfree.se AWAY :I'm not here right now
133             // @solanum.chat/ip=211.51.131.179 :Guest6187!~Guest61@211-51-131-179.fiber7.init7.net NICK :carbo
134 
135             // Get rid of the prepended @
136             string newRaw = raw[1..$];  // mutable
137             immutable tags = newRaw.advancePast(' ');
138             auto event = .toIRCEvent(parser, newRaw);
139             event.tags = tags;
140             applyTags(event);
141             return event;
142         }
143         else
144         {
145             IRCEvent event;
146             event.raw = raw;
147             parser.parseBasic(event);
148             return event;
149         }
150     }
151 
152     IRCEvent event;
153     event.raw = raw;
154 
155     string slice = event.raw[1..$]; // mutable. advance past first colon
156 
157     // First pass: prefixes. This is the sender
158     parser.parsePrefix(event, slice);
159 
160     // Second pass: typestring. This is what kind of action the event is of
161     parser.parseTypestring(event, slice);
162 
163     // Third pass: specialcases. This splits up the remaining bits into
164     // useful strings, like sender, target and content
165     parser.parseSpecialcases(event, slice);
166 
167     // Final cosmetic touches
168     event.channel = event.channel.toLower;
169 
170     return event;
171 }
172 
173 ///
174 unittest
175 {
176     IRCParser parser;
177 
178     /++
179         `parser.toIRCEvent` technically calls
180         [IRCParser.toIRCEvent|IRCParser.toIRCEvent], but it in turn just passes
181         on to this `.toIRCEvent`
182      +/
183 
184     // See the files in `/tests` for more
185 
186     {
187         immutable event = parser.toIRCEvent(":adams.freenode.net 001 kameloso^ " ~
188             ":Welcome to the freenode Internet Relay Chat Network kameloso^");
189         with (IRCEvent.Type)
190         with (event)
191         {
192             assert(type == RPL_WELCOME);
193             assert(sender.address == "adams.freenode.net"),
194             assert(target.nickname == "kameloso^");
195             assert(content == "Welcome to the freenode Internet Relay Chat Network kameloso^");
196             assert(num == 1);
197         }
198     }
199     {
200         immutable event = parser.toIRCEvent(":irc.portlane.se 020 * :Please wait while we process your connection.");
201         with (IRCEvent.Type)
202         with (event)
203         {
204             assert(type == RPL_HELLO);
205             assert(sender.address == "irc.portlane.se");
206             assert(content == "Please wait while we process your connection.");
207             assert(num == 20);
208         }
209     }
210 }
211 
212 
213 // parseBasic
214 /++
215     Parses the most basic of IRC events; [dialect.defs.IRCEvent.Type.PING|PING],
216     [dialect.defs.IRCEvent.Type.ERROR|ERROR],
217     [dialect.defs.IRCEvent.Type.PONG|PONG],
218     [dialect.defs.IRCEvent.Type.NOTICE|NOTICE] (plus `NOTICE AUTH`),
219     and `AUTHENTICATE`.
220 
221     They syntactically differ from other events in that they are not prefixed
222     by their sender.
223 
224     The [dialect.defs.IRCEvent|IRCEvent] is finished at the end of this function.
225 
226     Params:
227         parser = Reference to the current [IRCParser].
228         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to start
229             working on.
230 
231     Throws:
232         [dialect.common.IRCParseException|IRCParseException] if an unknown
233         type was encountered.
234  +/
235 void parseBasic(
236     ref IRCParser parser,
237     ref IRCEvent event) pure @safe
238 {
239     import std.string : indexOf;
240 
241     string slice = event.raw;  // mutable
242 
243     immutable typestring =
244         (slice.indexOf(':') != -1) ?
245             slice.advancePast(" :") :
246             (slice.indexOf(' ') != -1) ?
247                 slice.advancePast(' ') :
248                 slice;
249 
250     with (IRCEvent.Type)
251     switch (typestring)
252     {
253     case "PING":
254         // PING :3466174537
255         // PING :weber.freenode.net
256         event.type = PING;
257 
258         if (slice.indexOf('.') != -1)
259         {
260             event.sender.address = slice;
261         }
262         else
263         {
264             event.content = slice;
265         }
266         break;
267 
268     case "ERROR":
269         // ERROR :Closing Link: 81-233-105-62-no80.tbcn.telia.com (Quit: kameloso^)
270         event.type = ERROR;
271         event.content = slice;
272         break;
273 
274     case "NOTICE AUTH":
275     case "NOTICE":
276         // QuakeNet/Undernet
277         // NOTICE AUTH :*** Couldn't look up your hostname
278         event.type = NOTICE;
279         event.content = slice;
280         break;
281 
282     case "PONG":
283         // PONG :tmi.twitch.tv
284         event.type = PONG;
285         event.sender.address = slice;
286         break;
287 
288     case "AUTHENTICATE":
289         event.type = SASL_AUTHENTICATE;
290         event.content = slice;
291         break;
292 
293     default:
294         import std.algorithm.searching : startsWith;
295 
296         if (event.raw.startsWith("NOTICE"))
297         {
298             // Probably NOTICE <client.nickname>
299             // NOTICE kameloso :*** If you are having problems connecting due to ping timeouts, please type /notice F94828E6 nospoof now.
300             goto case "NOTICE";
301         }
302         else
303         {
304             event.type = UNSET;
305             event.aux[0] = event.raw;
306             event.errors = typestring;
307 
308             /*enum message = "Unknown basic type: " ~ typestring ~ ": please report this";
309             throw new IRCParseException(message, event);*/
310         }
311     }
312 
313     // All but PING and PONG are sender-less.
314     if (!event.sender.address) event.sender.address = parser.server.address;
315 }
316 
317 ///
318 unittest
319 {
320     import lu.conv : Enum;
321 
322     IRCParser parser;
323 
324     IRCEvent e1;
325     with (e1)
326     {
327         raw = "PING :irc.server.address";
328         parser.parseBasic(e1);
329         assert((type == IRCEvent.Type.PING), Enum!(IRCEvent.Type).toString(type));
330         assert((sender.address == "irc.server.address"), sender.address);
331         assert(!sender.nickname.length, sender.nickname);
332     }
333 
334     IRCEvent e2;
335     with (e2)
336     {
337         // QuakeNet and others not having the sending server as prefix
338         raw = "NOTICE AUTH :*** Couldn't look up your hostname";
339         parser.parseBasic(e2);
340         assert((type == IRCEvent.Type.NOTICE), Enum!(IRCEvent.Type).toString(type));
341         assert(!sender.nickname.length, sender.nickname);
342         assert((content == "*** Couldn't look up your hostname"));
343     }
344 
345     IRCEvent e3;
346     with (e3)
347     {
348         raw = "ERROR :Closing Link: 81-233-105-62-no80.tbcn.telia.com (Quit: kameloso^)";
349         parser.parseBasic(e3);
350         assert((type == IRCEvent.Type.ERROR), Enum!(IRCEvent.Type).toString(type));
351         assert(!sender.nickname.length, sender.nickname);
352         assert((content == "Closing Link: 81-233-105-62-no80.tbcn.telia.com (Quit: kameloso^)"), content);
353     }
354 }
355 
356 
357 // parsePrefix
358 /++
359     Takes a slice of a raw IRC string and starts parsing it into an
360     [dialect.defs.IRCEvent|IRCEvent] struct.
361 
362     This function only focuses on the prefix; the sender, be it nickname and
363     ident or server address.
364 
365     The [dialect.defs.IRCEvent|IRCEvent] is not finished at the end of this function.
366 
367     Params:
368         parser = Reference to the current [IRCParser].
369         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to start
370             working on.
371         slice = Reference to the *slice* of the raw IRC string.
372  +/
373 void parsePrefix(
374     ref IRCParser parser,
375     ref IRCEvent event,
376     ref string slice) pure @safe
377 in (slice.length, "Tried to parse prefix on an empty slice")
378 {
379     import std.string : indexOf;
380 
381     string prefix = slice.advancePast(' ');  // mutable
382 
383     if (prefix.indexOf('!') != -1)
384     {
385         // user!~ident@address
386         event.sender.nickname = prefix.advancePast('!');
387         event.sender.ident = prefix.advancePast('@');
388         event.sender.address = prefix;
389     }
390     else if (prefix.indexOf('.') != -1)
391     {
392         // dots signify an address
393         event.sender.address = prefix;
394     }
395     else
396     {
397         // When does this happen?
398         event.sender.nickname = prefix;
399     }
400 }
401 
402 ///
403 unittest
404 {
405     import lu.conv : Enum;
406 
407     IRCParser parser;
408 
409     IRCEvent e1;
410     with (e1)
411     with (e1.sender)
412     {
413         raw = ":zorael!~NaN@some.address.org PRIVMSG kameloso :this is fake";
414         string slice1 = raw[1..$];  // mutable
415         parser.parsePrefix(e1, slice1);
416         assert((nickname == "zorael"), nickname);
417         assert((ident == "~NaN"), ident);
418         assert((address == "some.address.org"), address);
419     }
420 
421     IRCEvent e2;
422     with (e2)
423     with (e2.sender)
424     {
425         raw = ":NickServ!NickServ@services. NOTICE kameloso :This nickname is registered.";
426         string slice2 = raw[1..$];  // mutable
427         parser.parsePrefix(e2, slice2);
428         assert((nickname == "NickServ"), nickname);
429         assert((ident == "NickServ"), ident);
430         assert((address == "services."), address);
431     }
432 
433     IRCEvent e3;
434     with (e3)
435     with (e3.sender)
436     {
437         raw = ":kameloso^^!~NaN@C2802314.E23AD7D8.E9841504.IP JOIN :#flerrp";
438         string slice3 = raw[1..$];  // mutable
439         parser.parsePrefix(e3, slice3);
440         assert((nickname == "kameloso^^"), nickname);
441         assert((ident == "~NaN"), ident);
442         assert((address == "C2802314.E23AD7D8.E9841504.IP"), address);
443     }
444 
445     IRCEvent e4;
446     with (parser)
447     with (e4)
448     with (e4.sender)
449     {
450         raw = ":Q!TheQBot@CServe.quakenet.org NOTICE kameloso :You are now logged in as kameloso.";
451         string slice4 = raw[1..$];  // mutable
452         parser.parsePrefix(e4, slice4);
453         assert((nickname == "Q"), nickname);
454         assert((ident == "TheQBot"), ident);
455         assert((address == "CServe.quakenet.org"), address);
456     }
457 }
458 
459 
460 // parseTypestring
461 /++
462     Takes a slice of a raw IRC string and continues parsing it into an
463     [dialect.defs.IRCEvent|IRCEvent] struct.
464 
465     This function only focuses on the *typestring*; the part that tells what
466     kind of event happened, like [dialect.defs.IRCEvent.Type.PRIVMSG|PRIVMSG] or
467     [dialect.defs.IRCEvent.Type.MODE|MODE] or
468     [dialect.defs.IRCEvent.Type.NICK|NICK] or
469     [dialect.defs.IRCEvent.Type.KICK|KICK], etc; in string format.
470 
471     The [dialect.defs.IRCEvent|IRCEvent] is not finished at the end of this function.
472 
473     Params:
474         parser = Reference to the current [IRCParser].
475         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
476             working on.
477         slice = Reference to the slice of the raw IRC string.
478 
479     Throws:
480         [dialect.common.IRCParseException|IRCParseException] if conversion from
481         typestring to [dialect.defs.IRCEvent.Type|IRCEvent.Type] or typestring
482         to a number failed.
483  +/
484 void parseTypestring(
485     ref IRCParser parser,
486     ref IRCEvent event,
487     ref string slice) pure @safe
488 in (slice.length, "Tried to parse typestring on an empty slice")
489 {
490     import std.conv : ConvException, to;
491     import std.typecons : Flag, No, Yes;
492 
493     immutable typestring = slice.advancePast(' ', Yes.inherit);
494 
495     if ((typestring[0] >= '0') && (typestring[0] <= '9'))
496     {
497         event.num = typestring.to!uint;
498         event.type = parser.typenums[event.num];
499         if (event.type == IRCEvent.Type.UNSET) event.type = IRCEvent.Type.NUMERIC;
500     }
501     else
502     {
503         try
504         {
505             import lu.conv : Enum;
506             event.type = Enum!(IRCEvent.Type).fromString(typestring);
507         }
508         catch (ConvException e)
509         {
510             immutable message = "Unknown typestring: " ~ typestring;
511             throw new IRCParseException(message, event, e.file, e.line);
512         }
513     }
514 }
515 
516 ///
517 unittest
518 {
519     import lu.conv : Enum;
520     import std.conv : to;
521 
522     IRCParser parser;
523 
524     IRCEvent e1;
525     with (e1)
526     {
527         raw = /*":port80b.se.quakenet.org */"421 kameloso åäö :Unknown command";
528         string slice = raw;  // mutable
529         parser.parseTypestring(e1, slice);
530         assert((type == IRCEvent.Type.ERR_UNKNOWNCOMMAND), Enum!(IRCEvent.Type).toString(type));
531         assert((num == 421), num.to!string);
532     }
533 
534     IRCEvent e2;
535     with (e2)
536     {
537         raw = /*":port80b.se.quakenet.org */"353 kameloso = #garderoben :@kameloso'";
538         string slice = raw;  // mutable
539         parser.parseTypestring(e2, slice);
540         assert((type == IRCEvent.Type.RPL_NAMREPLY), Enum!(IRCEvent.Type).toString(type));
541         assert((num == 353), num.to!string);
542     }
543 
544     IRCEvent e3;
545     with (e3)
546     {
547         raw = /*":zorael!~NaN@ns3363704.ip-94-23-253.eu */"PRIVMSG kameloso^ :test test content";
548         string slice = raw;  // mutable
549         parser.parseTypestring(e3, slice);
550         assert((type == IRCEvent.Type.PRIVMSG), Enum!(IRCEvent.Type).toString(type));
551     }
552 
553     IRCEvent e4;
554     with (e4)
555     {
556         raw = /*`:zorael!~NaN@ns3363704.ip-94-23-253.eu */`PART #flerrp :"WeeChat 1.6"`;
557         string slice = raw;  // mutable
558         parser.parseTypestring(e4, slice);
559         assert((type == IRCEvent.Type.PART), Enum!(IRCEvent.Type).toString(type));
560     }
561 }
562 
563 
564 // parseSpecialcases
565 /++
566     Takes a slice of a raw IRC string and continues parsing it into an
567     [dialect.defs.IRCEvent|IRCEvent] struct.
568 
569     This function only focuses on specialcasing the remaining line, dividing it
570     into fields like `target`, `channel`, `content`, etc.
571 
572     IRC events are *riddled* with inconsistencies and specialcasings, so this
573     function is very very long, but by necessity.
574 
575     The [dialect.defs.IRCEvent|IRCEvent] is finished at the end of this function.
576 
577     Params:
578         parser = Reference to the current [IRCParser].
579         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
580             working on.
581         slice = Reference to the slice of the raw IRC string.
582 
583     Throws:
584         [dialect.common.IRCParseException|IRCParseException] if an unknown
585         to-connect-type event was encountered, or if the event was not
586         recognised at all, as neither a normal type nor a numeric.
587  +/
588 void parseSpecialcases(
589     ref IRCParser parser,
590     ref IRCEvent event,
591     ref string slice) pure @safe
592 //in (slice.length, "Tried to parse specialcases on an empty slice")
593 {
594     import lu.string : strippedRight;
595     import std.algorithm.searching : startsWith;
596     import std.conv : to;
597     import std.string : indexOf;
598     import std.typecons : Flag, No, Yes;
599 
600     with (IRCEvent.Type)
601     switch (event.type)
602     {
603     case NOTICE:
604         parser.onNotice(event, slice);
605         break;
606 
607     case JOIN:
608         // :nick!~identh@unaffiliated/nick JOIN #freenode login :realname
609         // :kameloso^!~NaN@81-233-105-62-no80.tbcn.telia.com JOIN #flerrp
610         // :kameloso^^!~NaN@C2802314.E23AD7D8.E9841504.IP JOIN :#flerrp
611         event.type = (event.sender.nickname == parser.client.nickname) ?
612             SELFJOIN :
613             JOIN;
614 
615         if (slice.indexOf(' ') != -1)
616         {
617             import lu.string : stripped;
618 
619             // :nick!user@host JOIN #channelname accountname :Real Name
620             // :nick!user@host JOIN #channelname * :Real Name
621             // :nick!~identh@unaffiliated/nick JOIN #freenode login :realname
622             // :kameloso!~NaN@2001:41d0:2:80b4:: JOIN #hirrsteff2 kameloso : kameloso!
623             event.channel = slice.advancePast(' ');
624             event.sender.account = slice.advancePast(" :");
625             if (event.sender.account == "*") event.sender.account = string.init;
626             event.sender.realName = slice.stripped;
627         }
628         else
629         {
630             event.channel = slice.startsWith(':') ?
631                 slice[1..$] :
632                 slice;
633         }
634         break;
635 
636     case PART:
637         // :zorael!~NaN@ns3363704.ip-94-23-253.eu PART #flerrp :"WeeChat 1.6"
638         // :kameloso^!~NaN@81-233-105-62-no80.tbcn.telia.com PART #flerrp
639         // :Swatas!~4--Uos3UH@9e19ee35.915b96ad.a7c9320c.IP4 PART :#cncnet-mo
640         // :gallon!~MO.11063@482c29a5.e510bf75.97653814.IP4 PART :#cncnet-yr
641         event.type = (event.sender.nickname == parser.client.nickname) ?
642             SELFPART :
643             PART;
644 
645         if (slice.indexOf(' ') != -1)
646         {
647             import lu.string : unquoted;
648             event.channel = slice.advancePast(" :");
649             event.content = slice.unquoted;
650         }
651         else
652         {
653             // Seen on GameSurge
654             if (slice.startsWith(':')) slice = slice[1..$];
655             event.channel = slice;
656         }
657         break;
658 
659     case NICK:
660         // :kameloso^!~NaN@81-233-105-62-no80.tbcn.telia.com NICK :kameloso_
661         event.target.nickname = slice[1..$];
662 
663         if (event.sender.nickname == parser.client.nickname)
664         {
665             event.type = SELFNICK;
666             parser.client.nickname = event.target.nickname;
667             version(FlagAsUpdated) parser.updates |= IRCParser.Update.client;
668         }
669         break;
670 
671     case QUIT:
672         import lu.string : unquoted;
673 
674         // :g7zon!~gertsson@178.174.245.107 QUIT :Client Quit
675         event.type = (event.sender.nickname == parser.client.nickname) ?
676             SELFQUIT :
677             QUIT;
678         event.content = slice[1..$].unquoted;
679 
680         if (event.content.startsWith("Quit: "))
681         {
682             event.content = event.content[6..$];
683         }
684         break;
685 
686     case PRIVMSG:
687     case WHISPER:  // Twitch private message
688         parser.onPRIVMSG(event, slice);
689         break;
690 
691     case MODE:
692         slice = slice.strippedRight;  // RusNet has trailing spaces
693         parser.onMode(event, slice);
694         break;
695 
696     case KICK:
697         // :zorael!~NaN@ns3363704.ip-94-23-253.eu KICK #flerrp kameloso^ :this is a reason
698         event.channel = slice.advancePast(' ');
699         event.target.nickname = slice.advancePast(" :");
700         event.content = slice;
701         event.type = (event.target.nickname == parser.client.nickname) ?
702             SELFKICK :
703             KICK;
704         break;
705 
706     case INVITE:
707         // (freenode) :zorael!~NaN@2001:41d0:2:80b4:: INVITE kameloso :#hirrsteff
708         // (quakenet) :zorael!~zorael@ns3363704.ip-94-23-253.eu INVITE kameloso #hirrsteff
709         event.target.nickname = slice.advancePast(' ');
710         event.channel = slice.startsWith(':') ? slice[1..$] : slice;
711         break;
712 
713     case AWAY:
714         // :Halcy0n!~Halcy0n@SpotChat-rauo6p.dyn.suddenlink.net AWAY :I'm busy
715         if (slice.length)
716         {
717             // :I'm busy
718             slice = slice[1..$];
719             event.content = slice;
720         }
721         else
722         {
723             event.type = BACK;
724         }
725         break;
726 
727     case ERR_NOSUCHCHANNEL: // 403
728         // <channel name> :No such channel
729         // :moon.freenode.net 403 kameloso archlinux :No such channel
730         slice.advancePast(' ');  // bot nickname
731         event.channel = slice.advancePast(" :");
732         event.content = slice;
733         break;
734 
735     case RPL_NAMREPLY: // 353
736         // <channel> :[[@|+]<nick> [[@|+]<nick> [...]]]
737         // :asimov.freenode.net 353 kameloso^ = #garderoben :kameloso^ ombudsman +kameloso @zorael @maku @klarrt
738         slice.advancePast(' ');  // bot nickname
739         slice.advancePast(' ');
740         event.channel = slice.advancePast(" :");
741         event.content = slice.strippedRight;
742         break;
743 
744     case RPL_WHOREPLY: // 352
745         import lu.string : strippedLeft;
746 
747         // "<channel> <user> <host> <server> <nick> ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] :<hopcount> <real name>"
748         // :moon.freenode.net 352 kameloso ##linux LP9NDWY7Cy gentoo/contributor/Fieldy moon.freenode.net Fieldy H :0 Ni!
749         // :moon.freenode.net 352 kameloso ##linux sid99619 gateway/web/irccloud.com/x-eviusxrezdarwcpk moon.freenode.net tjsimmons G :0 T.J. Simmons
750         // :moon.freenode.net 352 kameloso ##linux sid35606 gateway/web/irccloud.com/x-rvrdncbvklhxwjrr moon.freenode.net Whisket H :0 Whisket
751         // :moon.freenode.net 352 kameloso ##linux ~rahlff b29beb9d.rev.stofanet.dk orwell.freenode.net Axton H :0 Michael Rahlff
752         // :moon.freenode.net 352 kameloso ##linux ~wzhang sea.mrow.org card.freenode.net wzhang H :0 wzhang
753         // :irc.rizon.no 352 kameloso^^ * ~NaN C2802314.E23AD7D8.E9841504.IP * kameloso^^ H :0  kameloso!
754         // :irc.rizon.no 352 kameloso^^ * ~zorael Rizon-64330364.ip-94-23-253.eu * wob^2 H :0 zorael
755         slice.advancePast(' ');  // bot nickname
756         event.channel = slice.advancePast(' ');
757         if (event.channel == "*") event.channel = string.init;
758 
759         immutable userOrIdent = slice.advancePast(' ');
760         if (userOrIdent.startsWith('~')) event.target.ident = userOrIdent;
761 
762         event.target.address = slice.advancePast(' ');
763         slice.advancePast(' ');  // server
764         event.target.nickname = slice.advancePast(' ');
765 
766         immutable hg = slice.advancePast(' ');  // H|G
767         if (hg.length > 1)
768         {
769             // H
770             // H@
771             // H+
772             // H@+
773             event.aux[0] = hg[1..$];
774         }
775 
776         slice.advancePast(' ');  // hopcount
777         event.content = slice.strippedLeft;
778         event.sender.realName = event.content;
779         break;
780 
781     case RPL_ENDOFWHO: // 315
782         // <name> :End of /WHO list
783         // :tolkien.freenode.net 315 kameloso^ ##linux :End of /WHO list.
784         // :irc.rizon.no 315 kameloso^^ * :End of /WHO list.
785         slice.advancePast(' ');  // bot nickname
786         event.channel = slice.advancePast(" :");
787         if (event.channel == "*") event.channel = string.init;
788         event.content = slice;
789         break;
790 
791     case RPL_ISUPPORT: // 005
792         parser.onISUPPORT(event, slice);
793         break;
794 
795     case RPL_MYINFO: // 004
796         // <server_name> <version> <user_modes> <chan_modes>
797         parser.onMyInfo(event, slice);
798         break;
799 
800     case RPL_QUIETLIST: // 728, oftc/hybrid 344
801         // :niven.freenode.net 728 kameloso^ #flerrp q qqqq!*@asdf.net zorael!~NaN@2001:41d0:2:80b4:: 1514405101
802         // :irc.oftc.net 344 kameloso #garderoben harbl!snarbl@* kameloso!~NaN@194.117.188.126 1515418362
803         slice.advancePast(' ');  // bot nickname
804         event.channel = (slice.indexOf(" q ") != -1) ?
805             slice.advancePast(" q ") :
806             slice.advancePast(' ');
807         event.content = slice.advancePast(' ');
808         event.aux[0] = slice.advancePast(' ');
809         event.count[0] = slice.to!long;
810         break;
811 
812     case RPL_WHOISHOST: // 378
813         // <nickname> :is connecting from *@<address> <ip>
814         // :wilhelm.freenode.net 378 kameloso^ kameloso^ :is connecting from *@81-233-105-62-no80.tbcn.telia.com 81.233.105.62
815         // TRIED TO NOM TOO MUCH:'kameloso :is connecting from NaN@194.117.188.126 194.117.188.126' with ' :is connecting from *@'
816         slice.advancePast(' ');  // bot nickname
817         event.target.nickname = slice.advancePast(" :is connecting from ");
818         event.target.ident = slice.advancePast('@');
819         if (event.target.ident == "*") event.target.ident = string.init;
820         event.content = slice.advancePast(' ');
821         event.aux[0] = slice;
822         break;
823 
824     case ERR_UNKNOWNCOMMAND: // 421
825         // <command> :Unknown command
826         slice.advancePast(' ');  // bot nickname
827 
828         if (slice.indexOf(" :Unknown command") != -1)
829         {
830             import std.string : lastIndexOf;
831 
832             // :asimov.freenode.net 421 kameloso^ sudo :Unknown command
833             // :tmi.twitch.tv 421 kamelosobot ZORAEL!ZORAEL@TMI.TWITCH.TV PRIVMSG #ZORAEL :HELLO :Unknown command
834             immutable spaceColonPos = slice.lastIndexOf(" :");
835             event.aux[0] = slice[0..spaceColonPos];
836             event.content = slice[spaceColonPos+2..$];
837             slice = string.init;
838         }
839         else
840         {
841             // :karatkievich.freenode.net 421 kameloso^ systemd,#kde,#kubuntu,...
842             event.content = slice;
843         }
844         break;
845 
846     case RPL_WHOISIDLE: //  317
847         // <nick> <integer> :seconds idle
848         // :rajaniemi.freenode.net 317 kameloso zorael 0 1510219961 :seconds idle, signon time
849         slice.advancePast(' ');  // bot nickname
850         event.target.nickname = slice.advancePast(' ');
851         event.count[0] = slice.advancePast(' ').to!long;
852         event.count[1] = slice.advancePast(" :").to!long;
853         event.content = slice;
854         break;
855 
856     case RPL_LUSEROP: // 252
857     case RPL_LUSERUNKNOWN: // 253
858     case RPL_LUSERCHANNELS: // 254
859     case ERR_ERRONEOUSNICKNAME: // 432
860     case ERR_NEEDMOREPARAMS: // 461
861     case RPL_LOCALUSERS: // 265
862     case RPL_GLOBALUSERS: // 266
863         // <integer> :operator(s) online  // 252
864         // <integer> :unknown connection(s)  // 253
865         // <integer> :channels formed // 254
866         // <nick> :Erroneus nickname // 432
867         // <command> :Not enough parameters // 461
868         // :asimov.freenode.net 252 kameloso^ 31 :IRC Operators online
869         // :asimov.freenode.net 253 kameloso^ 13 :unknown connection(s)
870         // :asimov.freenode.net 254 kameloso^ 54541 :channels formed
871         // :asimov.freenode.net 432 kameloso^ @nickname :Erroneous Nickname
872         // :asimov.freenode.net 461 kameloso^ JOIN :Not enough parameters
873         // :asimov.freenode.net 265 kameloso^ 6500 11061 :Current local users 6500, max 11061
874         // :asimov.freenode.net 266 kameloso^ 85267 92341 :Current global users 85267, max 92341
875         // :irc.uworld.se 265 kameloso^^ :Current local users: 14552  Max: 19744
876         // :irc.uworld.se 266 kameloso^^ :Current global users: 14552  Max: 19744
877         // :weber.freenode.net 265 kameloso 3385 6820 :Current local users 3385, max 6820"
878         // :weber.freenode.net 266 kameloso 87056 93012 :Current global users 87056, max 93012
879         // :irc.rizon.no 265 kameloso^^ :Current local users: 16115  Max: 17360
880         // :irc.rizon.no 266 kameloso^^ :Current global users: 16115  Max: 17360
881         slice.advancePast(' ');  // bot nickname
882 
883         if (slice.indexOf(" :") != -1)
884         {
885             import std.uni : isNumber;
886 
887             string midfield = slice.advancePast(" :");  // mutable
888             immutable first = midfield.advancePast(' ', Yes.inherit);
889             alias second = midfield;
890             event.content = slice;
891 
892             if (first.length)
893             {
894                 if (first[0].isNumber)
895                 {
896                     import std.conv : ConvException;
897 
898                     try
899                     {
900                         event.count[0] = first.to!long;
901 
902                         if (second.length && second[0].isNumber)
903                         {
904                             event.count[1] = second.to!long;
905                         }
906                     }
907                     catch (ConvException e)
908                     {
909                         // :hitchcock.freenode.net 432 * 1234567890123456789012345 :Erroneous Nickname
910                         // Treat as though not a number
911                         event.aux[0] = first;
912                     }
913                 }
914                 else
915                 {
916                     event.aux[0] = first;
917                 }
918             }
919         }
920         else
921         {
922             event.content = slice[1..$];
923         }
924         break;
925 
926     case RPL_WHOISUSER: // 311
927         import lu.string : strippedLeft;
928 
929         // <nick> <user> <host> * :<real name>
930         // :orwell.freenode.net 311 kameloso^ kameloso ~NaN ns3363704.ip-94-23-253.eu * : kameloso
931         slice.advancePast(' ');  // bot nickname
932         event.target.nickname = slice.advancePast(' ');
933         event.target.ident = slice.advancePast(' ');
934         event.target.address = slice.advancePast(" * :");
935         event.content = slice.strippedLeft;
936         event.target.realName = event.content;
937         break;
938 
939     case RPL_WHOISSERVER: // 312
940         // <nick> <server> :<server info>
941         // :asimov.freenode.net 312 kameloso^ zorael sinisalo.freenode.net :SE
942         slice.advancePast(' ');  // bot nickname
943         event.target.nickname = slice.advancePast(' ');
944         event.content = slice.advancePast(" :");
945         event.aux[0] = slice;
946         break;
947 
948     case RPL_WHOISACCOUNT: // 330
949         // <nickname> <account> :is logged in as
950         // :asimov.freenode.net 330 kameloso^ xurael zorael :is logged in as
951         slice.advancePast(' ');  // bot nickname
952         event.target.nickname = slice.advancePast(' ');
953         event.target.account = slice.advancePast(" :");
954         event.content = event.target.account;
955         break;
956 
957     case RPL_WHOISREGNICK: // 307
958         // <nickname> :has identified for this nick
959         // :irc.x2x.cc 307 kameloso^^ py-ctcp :has identified for this nick
960         // :irc.x2x.cc 307 kameloso^^ wob^2 :has identified for this nick
961         // What is the nickname? Are they always the same?
962         slice.advancePast(' '); // bot nickname
963         event.target.account = slice.advancePast(" :");
964         event.target.nickname = event.target.account;  // uneducated guess
965         event.content = event.target.nickname;
966         break;
967 
968     case RPL_WHOISACTUALLY: // 338
969         // :kinetic.oftc.net 338 kameloso wh00nix 255.255.255.255 :actually using host
970         // :efnet.port80.se 338 kameloso kameloso 255.255.255.255 :actually using host
971         // :irc.rizon.club 338 kameloso^ kameloso^ :is actually ~kameloso@194.117.188.126 [194.117.188.126]
972         // :irc.link-net.be 338 zorael zorael is actually ~kameloso@195.196.10.12 [195.196.10.12]
973         // :Prothid.NY.US.GameSurge.net 338 zorael zorael ~kameloso@195.196.10.12 195.196.10.12 :Actual user@host, Actual IP$
974         import std.string : indexOf;
975 
976         slice.advancePast(' '); // bot nickname
977         event.target.nickname = slice.advancePast(' ');
978         immutable colonPos = slice.indexOf(':');
979 
980         if ((colonPos == -1) || (colonPos == 0))
981         {
982             // :irc.link-net.be 338 zorael zorael is actually ~kameloso@195.196.10.12 [195.196.10.12]
983             // :irc.rizon.club 338 kameloso^ kameloso^ :is actually ~kameloso@194.117.188.126 [194.117.188.126]
984             slice.advancePast("is actually ");
985             event.aux[0] = slice.advancePast(' ');
986 
987             if ((slice[0] == '[') && (slice[$-1] == ']'))
988             {
989                 event.target.address = slice[1..$-1];
990             }
991             else
992             {
993                 event.content = slice;
994             }
995         }
996         else
997         {
998             if (slice[0..colonPos].indexOf('.') != -1)
999             {
1000                 string addstring = slice.advancePast(" :");  // mutable
1001 
1002                 if (addstring.indexOf(' ') != -1)
1003                 {
1004                     // :Prothid.NY.US.GameSurge.net 338 zorael zorael ~kameloso@195.196.10.12 195.196.10.12 :Actual user@host, Actual IP$
1005                     event.aux[0] = addstring.advancePast(' ');
1006                     event.target.address = addstring;
1007                 }
1008                 else
1009                 {
1010                     event.aux[0] = addstring;
1011                     if (addstring.indexOf('@') != -1) addstring.advancePast('@');
1012                     event.target.address = addstring;
1013                 }
1014 
1015                 event.content = slice;
1016             }
1017             else
1018             {
1019                 event.content = slice[colonPos+1..$];
1020             }
1021         }
1022         break;
1023 
1024     case PONG:
1025         // PONG :<address>
1026         // :<address> PONG <address> :<what was pinged>
1027         if (slice.indexOf(" :") != -1)
1028         {
1029             event.aux[0] = slice.advancePast(" :");
1030         }
1031         event.content = slice;
1032         break;
1033 
1034     case ERR_NOTREGISTERED: // 451
1035         // :You have not registered
1036         if (slice.startsWith('*'))
1037         {
1038             // :niven.freenode.net 451 * :You have not registered
1039             slice.advancePast("* :");
1040             event.content = slice;
1041         }
1042         else
1043         {
1044             // :irc.harblwefwoi.org 451 WHOIS :You have not registered
1045             event.aux[0] = slice.advancePast(" :");
1046             event.content = slice;
1047         }
1048         break;
1049 
1050     case ERR_NEEDPONG: // 513
1051         // <nickname> :To connect type /QUOTE PONG <number>
1052         /++
1053             "Also known as ERR_NEEDPONG (Unreal/Ultimate) for use during
1054             registration, however it's not used in Unreal (and might not be used
1055             in Ultimate either)."
1056          +/
1057         // :irc.uworld.se 513 kameloso :To connect type /QUOTE PONG 3705964477
1058 
1059         if (slice.indexOf(" :To connect") != -1)
1060         {
1061             event.target.nickname = slice.advancePast(" :To connect");
1062 
1063             if (slice.startsWith(','))
1064             {
1065                 // ngircd?
1066                 /* "NOTICE %s :To connect, type /QUOTE PONG %ld",
1067                     Client_ID(Client), auth_ping)) */
1068                 // :like.so 513 kameloso :To connect, type /QUOTE PONG 3705964477
1069                 // "To connect, type /QUOTE PONG <id>"
1070                 //            ^
1071                 slice = slice[1..$];
1072             }
1073 
1074             slice.advancePast(" type /QUOTE ");
1075             event.content = slice;
1076         }
1077         else
1078         {
1079             enum message = "Unknown variant of to-connect-type?";
1080             throw new IRCParseException(message, event);
1081         }
1082         break;
1083 
1084     case RPL_TRACEEND: // 262
1085     case RPL_TRYAGAIN: // 263
1086     case RPL_STATSDEBUG: // 249
1087     case RPL_ENDOFSTATS: // 219
1088     case RPL_HELPSTART: // 704
1089     case RPL_HELPTXT: // 705
1090     case RPL_ENDOFHELP: // 706
1091     case RPL_CODEPAGE: // 222
1092         // <server_name> <version>[.<debug_level>] :<info> // 262
1093         // <command> :<info> // 263
1094         // <stats letter> :End of /STATS report // 219
1095         // <nickname> index :Help topics available to users: // 704
1096         // <nickname> index :ACCEPT\tADMIN\tAWAY\tCHALLENGE // 705
1097         // <nickname> index :End of /HELP. // 706
1098         // :irc.run.net 222 kameloso KOI8-U :is your charset now
1099         // :leguin.freenode.net 704 kameloso^ index :Help topics available to users:
1100         // :leguin.freenode.net 705 kameloso^ index :ACCEPT\tADMIN\tAWAY\tCHALLENGE
1101         // :leguin.freenode.net 706 kameloso^ index :End of /HELP.
1102         // :livingstone.freenode.net 249 kameloso p :dax (dax@freenode/staff/dax)
1103         // :livingstone.freenode.net 249 kameloso p :1 staff members
1104         // :livingstone.freenode.net 219 kameloso p :End of /STATS report
1105         // :verne.freenode.net 263 kameloso^ STATS :This command could not be completed because it has been used recently, and is rate-limited
1106         // :verne.freenode.net 262 kameloso^ verne.freenode.net :End of TRACE
1107         // :irc.rizon.no 263 kameloso^ :Server load is temporarily too heavy. Please wait a while and try again.
1108         slice.advancePast(' '); // bot nickname
1109 
1110         if (!slice.length)
1111         {
1112             // Unsure if this ever happens but check before indexing
1113             break;
1114         }
1115         else if (slice[0] == ':')
1116         {
1117             slice = slice[1..$];
1118         }
1119         else
1120         {
1121             event.aux[0] = slice.advancePast(" :");
1122         }
1123 
1124         event.content = slice;
1125         break;
1126 
1127     case RPL_STATSLINKINFO: // 211
1128         // <linkname> <sendq> <sent messages> <sent bytes> <received messages> <received bytes> <time open>
1129         // :verne.freenode.net 211 kameloso^ kameloso^[~NaN@194.117.188.126] 0 109 8 15 0 :40 0 -
1130         slice.advancePast(' '); // bot nickname
1131         event.aux[0] = slice.advancePast(' ');
1132         event.content = slice;
1133         break;
1134 
1135     case RPL_TRACEUSER: // 205
1136         // User <class> <nick>
1137         // :wolfe.freenode.net 205 kameloso^ User v6users zorael[~NaN@2001:41d0:2:80b4::] (255.255.255.255) 16 :536
1138         slice.advancePast(" User "); // bot nickname
1139         event.aux[0] = slice.advancePast(' '); // "class"
1140         event.content = slice.advancePast(" :");
1141         event.count[0] = slice.to!long; // unsure
1142         break;
1143 
1144     case RPL_LINKS: // 364
1145         // <mask> <server> :<hopcount> <server info>
1146         // :rajaniemi.freenode.net 364 kameloso^ rajaniemi.freenode.net rajaniemi.freenode.net :0 Helsinki, FI, EU
1147         slice.advancePast(' '); // bot nickname
1148         slice.advancePast(' '); // "mask"
1149         event.aux[0] = slice.advancePast(" :"); // server address
1150         event.count[0] = slice.advancePast(' ').to!long; // hop count
1151         event.content = slice; // "server info"
1152         break;
1153 
1154     case ERR_BANONCHAN: // 435
1155         // <nickname> <target nickname> <channel> :Cannot change nickname while banned on channel
1156         // :cherryh.freenode.net 435 kameloso^ kameloso^^ #d3d9 :Cannot change nickname while banned on channel
1157         event.target.nickname = slice.advancePast(' ');
1158         event.aux[0] = slice.advancePast(' ');
1159         event.channel = slice.advancePast(" :");
1160         event.content = slice;
1161         break;
1162 
1163     case CAP:
1164         import std.algorithm.iteration : splitter;
1165 
1166         if (slice.indexOf('*') != -1)
1167         {
1168             // :tmi.twitch.tv CAP * LS :twitch.tv/tags twitch.tv/commands twitch.tv/membership
1169             // More CAPs follow
1170             slice.advancePast("* ");
1171         }
1172         else
1173         {
1174             // :genesis.ks.us.irchighway.net CAP 867AAF66L LS :away-notify extended-join account-notify multi-prefix sasl tls userhost-in-names
1175             // Final CAP listing
1176             /*immutable id =*/ slice.advancePast(' ');
1177         }
1178 
1179         // Store verb in content and caps in aux
1180         event.content = slice.advancePast(" :");
1181 
1182         uint i;
1183         foreach (immutable cap; slice.splitter(' '))
1184         {
1185             if (i < event.aux.length)
1186             {
1187                 event.aux[i++] = cap;
1188             }
1189             else
1190             {
1191                 // Overflow! aux is too small.
1192             }
1193         }
1194         break;
1195 
1196     case RPL_UMODEGMSG:
1197         // :rajaniemi.freenode.net 718 kameloso Freyjaun ~FREYJAUN@41.39.229.6 :is messaging you, and you have umode +g.
1198         slice.advancePast(' '); // bot nickname
1199         event.target.nickname = slice.advancePast(' ');
1200         event.target.ident = slice.advancePast('@');
1201         event.target.address = slice.advancePast(" :");
1202         event.content = slice;
1203         break;
1204 
1205     version(TwitchSupport)
1206     {
1207         case CLEARCHAT:
1208             // :tmi.twitch.tv CLEARCHAT #zorael
1209             // :tmi.twitch.tv CLEARCHAT #<channel> :<user>
1210             if (slice.indexOf(" :") != -1)
1211             {
1212                 // Banned
1213                 event.channel = slice.advancePast(" :");
1214                 event.target.nickname = slice;
1215             }
1216             else
1217             {
1218                 event.channel = slice;
1219             }
1220             break;
1221     }
1222 
1223     case RPL_LOGGEDIN: // 900
1224         // <nickname>!<ident>@<address> <nickname> :You are now logged in as <nickname>
1225         // :weber.freenode.net 900 kameloso kameloso!NaN@194.117.188.126 kameloso :You are now logged in as kameloso.
1226         // :kornbluth.freenode.net 900 * *!unknown@194.117.188.126 kameloso :You are now logged in as kameloso.
1227         if (slice.indexOf('!') != -1)
1228         {
1229             event.target.nickname = slice.advancePast(' ');  // bot nick, or an asterisk if unknown
1230             if (event.target.nickname == "*") event.target.nickname = string.init;
1231             slice.advancePast('!');  // user
1232             /*event.target.ident =*/ slice.advancePast('@');  // Doesn't seem to be the true ~ident
1233             event.target.address = slice.advancePast(' ');
1234             event.target.account = slice.advancePast(" :");
1235         }
1236         event.content = slice;
1237         break;
1238 
1239     case AUTH_SUCCESS:  // NOTICE
1240         // //:NickServ!services@services.oftc.net NOTICE kameloso :You are successfully identified as kameloso.$
1241         event.content = slice;
1242         break;
1243 
1244     case RPL_WELCOME: // 001
1245         // :Welcome to <server name> <user>
1246         // :adams.freenode.net 001 kameloso^ :Welcome to the freenode Internet Relay Chat Network kameloso^
1247         event.target.nickname = slice.advancePast(" :");
1248         event.content = slice;
1249 
1250         if (!parser.server.resolvedAddress.length)
1251         {
1252             // No RPL_HELLO. Twitch?
1253             // Inherit the sender address as the resolved server address
1254             parser.server.resolvedAddress = event.sender.address;
1255             version(FlagAsUpdated) parser.updates |= IRCParser.Update.server;
1256         }
1257 
1258         if (parser.client.nickname != event.target.nickname)
1259         {
1260             parser.client.nickname = event.target.nickname;
1261             version(FlagAsUpdated) parser.updates |= IRCParser.Update.client;
1262         }
1263         break;
1264 
1265     case ACCOUNT:
1266         //:ski7777!~quassel@ip5b435007.dynamic.kabel-deutschland.de ACCOUNT ski7777
1267         event.sender.account = slice;
1268         event.content = slice;  // to make it visible?
1269         break;
1270 
1271     case RPL_HOSTHIDDEN: // 396
1272         // <nickname> <host> :is now your hidden host
1273         // :TAL.DE.EU.GameSurge.net 396 kameloso ~NaN@1b24f4a7.243f02a4.5cd6f3e3.IP4 :is now your hidden host
1274         slice.advancePast(' '); // bot nickname
1275         event.aux[0] = slice.advancePast(" :");
1276         event.content = slice;
1277         break;
1278 
1279     case RPL_VERSION: // 351
1280         // <version>.<debuglevel> <server> :<comments>
1281         // :irc.rizon.no 351 kameloso^^ plexus-4(hybrid-8.1.20)(20170821_0-607). irc.rizon.no :TS6ow
1282         slice.advancePast(' '); // bot nickname
1283         event.content = slice.advancePast(" :");
1284         event.aux[0] = slice;
1285         break;
1286 
1287     case RPL_YOURID: // 42
1288     case ERR_YOUREBANNEDCREEP: // 465
1289     case ERR_HELPNOTFOUND: // 524, also ERR_QUARANTINED
1290     case ERR_UNKNOWNMODE: // 472
1291         // <nickname> <id> :your unique ID // 42
1292         // :You are banned from this server // 465
1293         // <char> :is unknown mode char to me // 472
1294         // :caliburn.pa.us.irchighway.net 042 kameloso 132AAMJT5 :your unique ID
1295         // :irc.rizon.no 524 kameloso^^ 502 :Help not found
1296         // :irc.rizon.no 472 kameloso^^ X :is unknown mode char to me
1297         // :miranda.chathispano.com 465 kameloso 1511086908 :[1511000504768] G-Lined by ChatHispano Network. Para mas informacion visite http://chathispano.com/gline/?id=<id> (expires at Dom, 19/11/2017 11:21:48 +0100).
1298         // event.time was 1511000921
1299         // TRIED TO NOM TOO MUCH:':You are banned from this server- Your irc client seems broken and is flooding lots of channels. Banned for 240 min, if in error, please contact kline@freenode.net. (2017/12/1 21.08)' with ' :'
1300         string misc = slice.advancePast(" :");  // mutable
1301         event.content = slice;
1302         misc.advancePast(' ', Yes.inherit);
1303         event.aux[0] = misc;
1304         break;
1305 
1306     case RPL_UMODEIS:
1307         // <user mode string>
1308         // :lamia.ca.SpotChat.org 221 kameloso :+ix
1309         // :port80b.se.quakenet.org 221 kameloso +i
1310         // The general heuristics is good enough for this but places modes in
1311         // content rather than aux, which is inconsistent with other mode events
1312         slice.advancePast(' '); // bot nickname
1313 
1314         if (slice.startsWith(':'))
1315         {
1316             slice = slice[1..$];
1317         }
1318 
1319         event.aux[0] = slice;
1320         break;
1321 
1322     case RPL_CHANNELMODEIS: // 324
1323         // <channel> <mode> <mode params>
1324         // :niven.freenode.net 324 kameloso^ ##linux +CLPcnprtf ##linux-overflow
1325         // :kornbluth.freenode.net 324 kameloso #flerrp +ns
1326         slice.advancePast(' '); // bot nickname
1327         event.channel = slice.advancePast(' ');
1328 
1329         if (slice.indexOf(' ') != -1)
1330         {
1331             event.aux[0] = slice.advancePast(' ');
1332             //event.content = slice.advancePast(' ');
1333             event.content = slice.strippedRight;
1334         }
1335         else
1336         {
1337             event.aux[0] = slice.strippedRight;
1338         }
1339         break;
1340 
1341     case RPL_CREATIONTIME: // 329
1342         // :kornbluth.freenode.net 329 kameloso #flerrp 1512995737
1343         slice.advancePast(' ');
1344         event.channel = slice.advancePast(' ');
1345         event.count[0] = slice.to!long;
1346         break;
1347 
1348     case RPL_LIST: // 322
1349         // <channel> <# visible> :<topic>
1350         // :irc.RomaniaChat.eu 322 kameloso #GameOfThrones 1 :[+ntTGfB]
1351         // :irc.RomaniaChat.eu 322 kameloso #radioclick 63 :[+ntr]  Bun venit pe #Radioclick! Site oficial www.radioclick.ro sau servere irc.romaniachat.eu, irc.radioclick.ro
1352         // :eggbert.ca.na.irchighway.net 322 kameloso * 3 :
1353         /*
1354             (asterisk channels)
1355             milky | channel isn't public nor are you a member
1356             milky | Unreal inserts that instead of not sending the result
1357             milky | Other IRCd may do same because they are all derivatives
1358          */
1359         slice.advancePast(' '); // bot nickname
1360         event.channel = slice.advancePast(' ');
1361         event.count[0] = slice.advancePast(" :").to!long;
1362         event.content = slice;
1363         break;
1364 
1365     case RPL_LISTSTART: // 321
1366         // Channel :Users  Name
1367         // :cherryh.freenode.net 321 kameloso^ Channel :Users  Name
1368         // none of the fields are interesting...
1369         break;
1370 
1371     case RPL_ENDOFQUIETLIST: // 729, oftc/hybrid 345
1372         // :niven.freenode.net 729 kameloso^ #hirrsteff q :End of Channel Quiet List
1373         // :irc.oftc.net 345 kameloso #garderoben :End of Channel Quiet List
1374         slice.advancePast(' ');
1375         event.channel = (slice.indexOf(" q :") != -1) ?
1376             slice.advancePast(" q :") :
1377             slice.advancePast(" :");
1378         event.content = slice;
1379         break;
1380 
1381     case RPL_WHOISMODES: // 379
1382         // <nickname> :is using modes <modes>
1383         // :cadance.canternet.org 379 kameloso kameloso :is using modes +ix
1384         slice.advancePast(' '); // bot nickname
1385         event.target.nickname = slice.advancePast(" :is using modes ");
1386 
1387         if (slice.indexOf(' ') != -1)
1388         {
1389             event.aux[0] = slice.advancePast(' ');
1390             event.content = slice;
1391         }
1392         else
1393         {
1394             event.aux[0] = slice;
1395         }
1396         break;
1397 
1398     case RPL_WHOWASUSER: // 314
1399         import lu.string : stripped;
1400 
1401         // <nick> <user> <host> * :<real name>
1402         // :irc.uworld.se 314 kameloso^^ kameloso ~NaN C2802314.E23AD7D8.E9841504.IP * : kameloso!
1403         slice.advancePast(' '); // bot nickname
1404         event.target.nickname = slice.advancePast(' ');
1405         event.target.ident = slice.advancePast(' ');
1406         event.aux[0] = slice.advancePast(" * :");
1407         if (event.aux[0].length) event.target.address = event.aux[0];
1408         event.content = slice.stripped;
1409         event.target.realName = event.content;
1410         break;
1411 
1412     case CHGHOST:
1413         // :Miyabro!~Miyabro@DA8192E8:4D54930F:650EE60D:IP CHGHOST ~Miyabro Miyako.is.mai.waifu
1414         event.sender.ident = slice.advancePast(' ');
1415         event.sender.address = slice;
1416         event.content = slice;
1417         break;
1418 
1419     case RPL_HELLO: // 020
1420         // :irc.run.net 020 irc.run.net :*** You are connected to RusNet. Please wait...
1421         // :irc.portlane.se 020 * :Please wait while we process your connection.
1422         slice.advancePast(" :");
1423         event.content = slice;
1424         parser.server.resolvedAddress = event.sender.address;
1425         version(FlagAsUpdated) parser.updates |= IRCParser.Update.server;
1426         break;
1427 
1428     case SPAMFILTERLIST: // 941
1429     case RPL_BANLIST: // 367
1430         // <channel> <banid> // 367
1431         // :siren.de.SpotChat.org 941 kameloso #linuxmint-help spotify.com/album Butterfly 1513796216
1432         // ":kornbluth.freenode.net 367 kameloso #flerrp harbl!harbl@snarbl.com zorael!~NaN@2001:41d0:2:80b4:: 1513899521"
1433         // :irc.run.net 367 kameloso #politics *!*@broadband-46-242-*.ip.moscow.rt.ru
1434         slice.advancePast(' '); // bot nickname
1435         event.channel = slice.advancePast(' ');
1436 
1437         if (slice.indexOf(' ') != -1)
1438         {
1439             event.content = slice.advancePast(' ');
1440             event.aux[0] = slice.advancePast(' ');  // nickname that set the mode
1441             event.count[0] = slice.to!long;
1442         }
1443         else
1444         {
1445             event.content = slice;
1446         }
1447         break;
1448 
1449     case RPL_AWAY: // 301
1450         // <nick> :<away message>
1451         // :hitchcock.freenode.net 301 kameloso^ Morrolan :Auto away at Tue Mar  3 09:43:26 2020
1452         // Sent if you send a message (or WHOIS) a user who is away
1453         slice.advancePast(' '); // bot nickname
1454         event.sender.nickname = slice.advancePast(" :");
1455         event.sender.address = string.init;
1456         version(BotElements) event.sender.class_ = IRCUser.Class.unset;
1457         event.content = slice;
1458         break;
1459 
1460     case RPL_SASLSUCCESS: // 903
1461         // :weber.freenode.net 903 * :SASL authentication successful
1462         slice.advancePast(" :");  // asterisk, possible nickname?
1463         event.content = slice;
1464         break;
1465 
1466     case THISSERVERINSTEAD: // 010
1467         // :irc.link-net.be 010 zorael irc.link-net.be +6697 :Please use this Server/Port instead$
1468         import std.conv : ConvException, to;
1469 
1470         slice.advancePast(' '); // bot nickname
1471         event.aux[0] = slice.advancePast(' ');
1472         string portstring = slice.advancePast(" :");  // mutable
1473         event.content = slice;
1474 
1475         if (portstring.startsWith('+')) portstring = portstring[1..$];
1476 
1477         event.aux[1] = portstring;
1478 
1479         try
1480         {
1481             event.count[0] = portstring.to!int;
1482         }
1483         catch (ConvException _)
1484         {
1485             // Ignore
1486         }
1487         break;
1488 
1489     case ERR_BADCHANNAME: // 479
1490         // :helix.oftc.net 479 zorael|8 - :Illegal channel name
1491         slice.advancePast(' '); // bot nickname
1492         event.aux[0] = slice.advancePast(" :");
1493         event.content = slice;
1494         break;
1495 
1496     default:
1497         if ((event.type == NUMERIC) || (event.type == UNSET))
1498         {
1499             enum message = "Uncaught `IRCEvent.Type.NUMERIC` or `IRCEvent.Type.UNSET`";
1500             throw new IRCParseException(message, event);
1501         }
1502 
1503         return parser.parseGeneralCases(event, slice);
1504     }
1505 }
1506 
1507 
1508 // parseGeneralCases
1509 /++
1510     Takes a slice of a raw IRC string and continues parsing it into an
1511     [dialect.defs.IRCEvent|IRCEvent] struct.
1512 
1513     This function only focuses on applying general heuristics to the remaining
1514     line, dividing it into fields like `target`, `channel`, `content`, etc; not
1515     based by its type but rather by how the string looks.
1516 
1517     The [dialect.defs.IRCEvent|IRCEvent] is finished at the end of this function.
1518 
1519     Params:
1520         parser = Reference to the current [IRCParser].
1521         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
1522             working on.
1523         slice = Reference to the slice of the raw IRC string.
1524  +/
1525 void parseGeneralCases(
1526     const ref IRCParser parser,
1527     ref IRCEvent event,
1528     ref string slice) pure @safe
1529 {
1530     import std.algorithm.searching : startsWith;
1531     import std.string : indexOf;
1532 
1533     if (!slice.length)
1534     {
1535         // Do nothing
1536     }
1537     else if (slice.startsWith(':'))
1538     {
1539         // Merely nickname!ident@address.tld TYPESTRING :content
1540         event.content = slice[1..$];
1541     }
1542     else if (slice.indexOf(" :") != -1)
1543     {
1544         // Has colon-content
1545         string targets = slice.advancePast(" :");  // mutable
1546 
1547         if (!targets.length)
1548         {
1549             // This should never happen, but ward against range errors
1550             event.content = slice;
1551         }
1552         else if (targets.indexOf(' ') != -1)
1553         {
1554             // More than one target
1555             immutable firstTarget = targets.advancePast(' ');
1556 
1557             if ((firstTarget == parser.client.nickname) || (firstTarget == "*"))
1558             {
1559                 // More than one target, first is bot
1560                 // Can't use isChan here since targets may contain spaces
1561 
1562                 if (parser.server.chantypes.indexOf(targets[0]) != -1)
1563                 {
1564                     // More than one target, first is bot
1565                     // Second target is/begins with a channel
1566 
1567                     if (targets.indexOf(' ') != -1)
1568                     {
1569                         // More than one target, first is bot
1570                         // Second target is more than one, first is channel
1571                         // assume third is content
1572                         event.channel = targets.advancePast(' ');
1573                         event.content = targets;
1574                     }
1575                     else
1576                     {
1577                         // More than one target, first is bot
1578                         // Only one channel
1579                         event.channel = targets;
1580                     }
1581                 }
1582                 else
1583                 {
1584                     import std.algorithm.searching : count;
1585 
1586                     // More than one target, first is bot
1587                     // Second is not a channel
1588 
1589                     immutable numSpaces = targets.count(' ');
1590 
1591                     if (numSpaces == 1)
1592                     {
1593                         // Two extra targets; assume nickname and channel
1594                         event.target.nickname = targets.advancePast(' ');
1595                         event.channel = targets;
1596                     }
1597                     else if (numSpaces > 1)
1598                     {
1599                         // A lot of spaces; cannot say for sure what is what
1600                         event.aux[0] = targets;
1601                     }
1602                     else /*if (numSpaces == 0)*/
1603                     {
1604                         // Only one second target
1605 
1606                         if (parser.server.chantypes.indexOf(targets[0]) != -1)
1607                         {
1608                             // Second is a channel
1609                             event.channel = targets;
1610                         }
1611                         else if (targets == event.sender.address)
1612                         {
1613                             // Second is sender's address, probably server
1614                             event.aux[0] = targets;
1615                         }
1616                         else
1617                         {
1618                             // Second is not a channel
1619                             event.target.nickname = targets;
1620                         }
1621                     }
1622                 }
1623             }
1624             else
1625             {
1626                 // More than one target, first is not bot
1627 
1628                 if (parser.server.chantypes.indexOf(firstTarget[0]) != -1)
1629                 {
1630                     // First target is a channel
1631                     // Assume second is a nickname
1632                     event.channel = firstTarget;
1633                     event.target.nickname = targets;
1634                 }
1635                 else
1636                 {
1637                     // First target is not channel, assume nick
1638                     // Assume second is channel
1639                     event.target.nickname = firstTarget;
1640                     event.channel = targets;
1641                 }
1642             }
1643         }
1644         else if (parser.server.chantypes.indexOf(targets[0]) != -1)
1645         {
1646             // Only one target, it is a channel
1647             event.channel = targets;
1648         }
1649         else
1650         {
1651             // Only one target, not a channel
1652             event.target.nickname = targets;
1653         }
1654     }
1655     else
1656     {
1657         // Does not have colon-content
1658         if (slice.indexOf(' ') != -1)
1659         {
1660             // More than one target
1661             immutable target = slice.advancePast(' ');
1662 
1663             if (!target.length)
1664             {
1665                 // This should never happen, but ward against range errors
1666                 event.content = slice;
1667             }
1668             else if (parser.server.chantypes.indexOf(target[0]) != -1)
1669             {
1670                 // More than one target, first is a channel
1671                 // Assume second is content
1672                 event.channel = target;
1673                 event.content = slice;
1674             }
1675             else
1676             {
1677                 // More than one target, first is not a channel
1678                 // Assume first is nickname and second is aux
1679                 event.target.nickname = target;
1680 
1681                 if ((target == parser.client.nickname) && (slice.indexOf(' ') != -1))
1682                 {
1683                     // First target is bot, and there is more
1684                     // :asimov.freenode.net 333 kameloso^ #garderoben klarrt!~bsdrouter@h150n13-aahm-a11.ias.bredband.telia.com 1476294377
1685                     // :kornbluth.freenode.net 367 kameloso #flerrp harbl!harbl@snarbl.com zorael!~NaN@2001:41d0:2:80b4:: 1513899521
1686                     // :niven.freenode.net 346 kameloso^ #flerrp asdf!fdas@asdf.net zorael!~NaN@2001:41d0:2:80b4:: 1514405089
1687                     // :irc.run.net 367 kameloso #Help *!*@broadband-5-228-255-*.moscow.rt.ru
1688                     // :irc.atw-inter.net 344 kameloso #debian.de towo!towo@littlelamb.szaf.org
1689 
1690                     if (parser.server.chantypes.indexOf(slice[0]) != -1)
1691                     {
1692                         // Second target is channel
1693                         event.channel = slice.advancePast(' ');
1694 
1695                         if (slice.indexOf(' ') != -1)
1696                         {
1697                             // Remaining slice has at least two fields;
1698                             // separate into content and aux
1699                             event.content = slice.advancePast(' ');
1700                             event.aux[0] = slice;
1701                         }
1702                         else
1703                         {
1704                             // Remaining slice is one bit of text
1705                             event.content = slice;
1706                         }
1707                     }
1708                     else
1709                     {
1710                         // No-channel second target
1711                         // When does this happen?
1712                         event.content = slice;
1713                     }
1714                 }
1715                 else
1716                 {
1717                     // No second target
1718                     // :port80b.se.quakenet.org 221 kameloso +i
1719                     event.aux[0] = slice;
1720                 }
1721             }
1722         }
1723         else
1724         {
1725             // Only one target
1726 
1727             if (parser.server.chantypes.indexOf(slice[0]) != -1)
1728             {
1729                 // Target is a channel
1730                 event.channel = slice;
1731             }
1732             else
1733             {
1734                 // Target is a nickname
1735                 event.target.nickname = slice;
1736             }
1737         }
1738     }
1739 
1740     // If content is empty and slice hasn't already been used, assign it
1741     if (!event.content.length && (slice != event.channel) &&
1742         (slice != event.target.nickname))
1743     {
1744         import lu.string : strippedRight;
1745         event.content = slice.strippedRight;
1746     }
1747 }
1748 
1749 
1750 // postparseSanityCheck
1751 /++
1752     Checks for some specific erroneous edge cases in an [dialect.defs.IRCEvent|IRCEvent].
1753 
1754     Descriptions of the errors are stored in `event.errors`.
1755 
1756     Params:
1757         parser = Reference to the current [IRCParser].
1758         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
1759             working on.
1760  +/
1761 void postparseSanityCheck(
1762     const ref IRCParser parser,
1763     ref IRCEvent event) pure @safe
1764 {
1765     import std.algorithm.searching : startsWith;
1766     import std.array : Appender;
1767     import std.string : indexOf;
1768 
1769     Appender!(char[]) sink;
1770     // The sink will very rarely be used; treat it as an edge case and don't reserve
1771 
1772     if ((event.type == IRCEvent.Type.UNSET) && event.errors.length)
1773     {
1774         sink.put("Unknown typestring: ");
1775         sink.put(event.errors);
1776     }
1777 
1778     if ((event.target.nickname.indexOf(' ') != -1) ||
1779         (event.channel.indexOf(' ') != -1))
1780     {
1781         if (sink.data.length) sink.put(" | ");
1782         sink.put("Spaces in target nickname or channel");
1783     }
1784 
1785     if (event.target.nickname.startsWith(':'))
1786     {
1787         if (sink.data.length) sink.put(" | ");
1788         sink.put("Colon in target nickname");
1789     }
1790 
1791     if (event.target.nickname.length &&
1792         (parser.server.chantypes.indexOf(event.target.nickname[0]) != -1))
1793     {
1794         if (sink.data.length) sink.put(" | ");
1795         sink.put("Target nickname is a channel");
1796     }
1797 
1798     if (event.channel.length &&
1799         (parser.server.chantypes.indexOf(event.channel[0]) == -1) &&
1800         (event.type != IRCEvent.Type.ERR_NOSUCHCHANNEL) &&
1801         (event.type != IRCEvent.Type.RPL_ENDOFWHO) &&
1802         (event.type != IRCEvent.Type.RPL_NAMREPLY) &&
1803         (event.type != IRCEvent.Type.RPL_ENDOFNAMES) &&
1804         (event.type != IRCEvent.Type.SELFJOIN) &&  // Twitch
1805         (event.type != IRCEvent.Type.SELFPART) &&  // Twitch
1806         (event.type != IRCEvent.Type.RPL_LIST))  // Some channels can be asterisks if they aren't public
1807     {
1808         if (sink.data.length) sink.put(" | ");
1809         sink.put("Channel is not a channel");
1810     }
1811 
1812     if (sink.data.length)
1813     {
1814         event.errors = sink.data.idup;
1815     }
1816 }
1817 
1818 
1819 // onNotice
1820 /++
1821     Handle [dialect.defs.IRCEvent.Type.NOTICE|NOTICE] events.
1822 
1823     These are all(?) sent by the server and/or services. As such they often
1824     convey important special things, so parse those.
1825 
1826     Params:
1827         parser = Reference to the current [IRCParser].
1828         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
1829             working on.
1830         slice = Reference to the slice of the raw IRC string.
1831  +/
1832 void onNotice(
1833     ref IRCParser parser,
1834     ref IRCEvent event,
1835     ref string slice) pure @safe
1836 in (slice.length, "Tried to process `onNotice` on an empty slice")
1837 {
1838     import dialect.common : isAuthService;
1839     import std.algorithm.comparison : among;
1840     import std.algorithm.searching : startsWith;
1841     import std.string : indexOf;
1842     import std.typecons : Flag, No, Yes;
1843 
1844     // :ChanServ!ChanServ@services. NOTICE kameloso^ :[##linux-overflow] Make sure your nick is registered, then please try again to join ##linux.
1845     // :ChanServ!ChanServ@services. NOTICE kameloso^ :[#ubuntu] Welcome to #ubuntu! Please read the channel topic.
1846     // :tolkien.freenode.net NOTICE * :*** Checking Ident
1847 
1848     // At least Twitch sends NOTICEs to channels, maybe other daemons do too
1849     immutable channelOrNickname = slice.advancePast(" :", Yes.inherit);
1850     event.content = slice;
1851 
1852     if (channelOrNickname.length &&
1853         (parser.server.chantypes.indexOf(channelOrNickname[0]) != -1))
1854     {
1855         event.channel = channelOrNickname;
1856     }
1857 
1858     if (!event.content.length) return;
1859 
1860     if (!parser.server.resolvedAddress.length && event.content.startsWith("***"))
1861     {
1862         // This is where we catch the resolved address
1863         assert(!event.sender.nickname.length, "Unexpected nickname: " ~ event.sender.nickname);
1864         parser.server.resolvedAddress = event.sender.address;
1865         version(FlagAsUpdated) parser.updates |= IRCParser.Update.server;
1866     }
1867 
1868     if (!event.sender.isServer && event.sender.isAuthService(parser))
1869     {
1870         import std.algorithm.searching : canFind;
1871         import std.algorithm.comparison : among;
1872         import std.uni : asLowerCase;
1873 
1874         enum AuthChallenge
1875         {
1876             dalnet = "This nick is owned by someone else. Please choose another.",
1877             oftc = "This nickname is registered and protected.",
1878         }
1879 
1880         if (event.content.asLowerCase.canFind("/msg nickserv identify") ||
1881             (event.content == AuthChallenge.dalnet) ||
1882             event.content.startsWith(cast(string)AuthChallenge.oftc))
1883         {
1884             event.type = IRCEvent.Type.AUTH_CHALLENGE;
1885             return;
1886         }
1887 
1888         enum AuthSuccess
1889         {
1890             freenode = "You are now identified for",
1891             rizon = "Password accepted - you are now recognized.",  // also gimpnet
1892             quakenet = "You are now logged in as",  // also mozilla, snoonet
1893             gamesurge = "I recognize you.",
1894             dalnet = "Password accepted for",
1895             oftc = "You are successfully identified as",
1896         }
1897 
1898         alias AS = AuthSuccess;
1899 
1900         if ((event.content.startsWith(cast(string)AS.freenode)) ||
1901             (event.content.startsWith(cast(string)AS.quakenet)) || // also Freenode SASL
1902             (event.content.startsWith(cast(string)AS.dalnet)) ||
1903             (event.content.startsWith(cast(string)AS.oftc)) ||
1904             event.content.among!(cast(string)AS.rizon, cast(string)AS.gamesurge))
1905         {
1906             event.type = IRCEvent.Type.AUTH_SUCCESS;
1907 
1908             // Restart with the new type
1909             return parser.parseSpecialcases(event, slice);
1910         }
1911 
1912         enum AuthFailure
1913         {
1914             rizon = "Your nick isn't registered.",
1915             quakenet = "Username or password incorrect.",
1916             freenodeInvalid = "is not a registered nickname.",
1917             freenodeRejected = "Invalid password for",
1918             dalnetInvalid = "is not registered.",  // also OFTC
1919             dalnetRejected = "The password supplied for",
1920             unreal = "isn't registered.",
1921             gamesurgeInvalid = "Could not find your account -- did you register yet?",
1922             gamesurgeRejected = "Incorrect password; please try again.",
1923             geekshedRejected = "Password incorrect.",  // also irchighway, rizon, rusnet
1924             oftcRejected = "Identify failed as",
1925         }
1926 
1927         alias AF = AuthFailure;
1928 
1929         if (event.content.among!(AF.rizon, AF.quakenet, AF.gamesurgeInvalid,
1930                 AF.gamesurgeRejected, AF.geekshedRejected) ||
1931             (event.content.indexOf(cast(string)AF.freenodeInvalid) != -1) ||
1932             event.content.startsWith(cast(string)AF.freenodeRejected) ||
1933             (event.content.indexOf(cast(string)AF.dalnetInvalid) != -1) ||
1934             event.content.startsWith(cast(string)AF.dalnetRejected) ||
1935             (event.content.indexOf(cast(string)AF.unreal) != -1) ||
1936             event.content.startsWith(cast(string)AF.oftcRejected))
1937         {
1938             event.type = IRCEvent.Type.AUTH_FAILURE;
1939         }
1940     }
1941 
1942     // FIXME: support
1943     // *** If you are having problems connecting due to ping timeouts, please type /quote PONG j`ruV\rcn] or /raw PONG j`ruV\rcn] now.
1944 }
1945 
1946 
1947 // onPRIVMSG
1948 /++
1949     Handle [dialect.defs.IRCEvent.Type.QUERY|QUERY] and
1950     [dialect.defs.IRCEvent.Type.CHAN|CHAN] messages
1951     ([dialect.defs.IRCEvent.Type.PRIVMSG|PRIVMSG]).
1952 
1953     Whether or not it is a private query message or a channel message is only
1954     obvious by looking at the target field of it; if it starts with a `#`, it is
1955     a channel message.
1956 
1957     Also handle `ACTION` events (`/me slaps foo with a large trout`), and change
1958     the type to `CTCP_`-types if applicable.
1959 
1960     Params:
1961         parser = Reference to the current [IRCParser].
1962         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
1963             working on.
1964         slice = Reference to the slice of the raw IRC string.
1965 
1966     Throws: [dialect.common.IRCParseException|IRCParseException] on unknown CTCP types.
1967  +/
1968 void onPRIVMSG(
1969     const ref IRCParser parser,
1970     ref IRCEvent event,
1971     ref string slice) pure @safe
1972 in (slice.length, "Tried to process `IRCEvent.Type.PRIVMSG` on an empty slice")
1973 {
1974     import dialect.common : IRCControlCharacter, isValidChannel;
1975     import std.string : indexOf;
1976 
1977     string target = slice.advancePast(' ');  // mutable
1978     if (slice.length && slice[0] == ':') slice = slice[1..$];
1979     event.content = slice;
1980 
1981     /*  When a server sends a PRIVMSG/NOTICE to someone else on behalf of a
1982         client connected to it – common when multiple clients are connected to a
1983         bouncer – it is called a self-message. With the echo-message capability,
1984         they are also sent in reply to every PRIVMSG/NOTICE a client sends.
1985         These are represented by a protocol message looking like this:
1986 
1987         :yournick!~foo@example.com PRIVMSG someone_else :Hello world!
1988 
1989         They should be put in someone_else's query and displayed as though they
1990         they were sent by the connected client themselves. This page displays
1991         which clients properly parse and display this type of echo'd
1992         PRIVMSG/NOTICE.
1993 
1994         http://defs.ircdocs.horse/info/selfmessages.html
1995 
1996         (common requested cap: znc.in/self-message)
1997      */
1998 
1999     if (target.isValidChannel(parser.server))
2000     {
2001         // :zorael!~NaN@ns3363704.ip-94-23-253.eu PRIVMSG #flerrp :test test content
2002         event.type = (event.sender.nickname == parser.client.nickname) ?
2003             IRCEvent.Type.SELFCHAN :
2004             IRCEvent.Type.CHAN;
2005         event.channel = target;
2006     }
2007     else
2008     {
2009         // :zorael!~NaN@ns3363704.ip-94-23-253.eu PRIVMSG kameloso^ :test test content
2010         event.type = (event.sender.nickname == parser.client.nickname) ?
2011             IRCEvent.Type.SELFQUERY :
2012             IRCEvent.Type.QUERY;
2013         event.target.nickname = target;
2014     }
2015 
2016     if (slice.length < 3) return;
2017 
2018     if ((slice[0] == IRCControlCharacter.ctcp) && (slice[$-1] == IRCControlCharacter.ctcp))
2019     {
2020         import std.traits : EnumMembers;
2021 
2022         slice = slice[1..$-1];
2023         immutable ctcpEvent = (slice.indexOf(' ') != -1) ? slice.advancePast(' ') : slice;
2024         event.content = slice;
2025 
2026         // :zorael!~NaN@ns3363704.ip-94-23-253.eu PRIVMSG #flerrp :ACTION test test content
2027         // :zorael!~NaN@ns3363704.ip-94-23-253.eu PRIVMSG kameloso^ :ACTION test test content
2028         // :py-ctcp!ctcp@ctcp-scanner.rizon.net PRIVMSG kameloso^^ :VERSION
2029         // :wob^2!~zorael@2A78C947:4EDD8138:3CB17EDC:IP PRIVMSG kameloso^^ :TIME
2030         // :wob^2!~zorael@2A78C947:4EDD8138:3CB17EDC:IP PRIVMSG kameloso^^ :PING 1495974267 590878
2031         // :wob^2!~zorael@2A78C947:4EDD8138:3CB17EDC:IP PRIVMSG kameloso^^ :CLIENTINFO
2032         // :wob^2!~zorael@2A78C947:4EDD8138:3CB17EDC:IP PRIVMSG kameloso^^ :DCC
2033         // :wob^2!~zorael@2A78C947:4EDD8138:3CB17EDC:IP PRIVMSG kameloso^^ :SOURCE
2034         // :wob^2!~zorael@2A78C947:4EDD8138:3CB17EDC:IP PRIVMSG kameloso^^ :USERINFO
2035         // :wob^2!~zorael@2A78C947:4EDD8138:3CB17EDC:IP PRIVMSG kameloso^^ :FINGER
2036 
2037         /++
2038             This iterates through all [dialect.defs.IRCEvent.Type|IRCEvent.Type]s that
2039             begin with `CTCP_` and generates switch cases for the string of
2040             each. Inside it will assign `event.type` to the corresponding
2041             [dialect.defs.IRCEvent.Type|IRCEvent.Type].
2042 
2043             Like so, except automatically generated through compile-time
2044             introspection:
2045 
2046                 case "CTCP_PING":
2047                     event.type = CTCP_PING;
2048                     event.aux[0] = "PING";
2049                     break;
2050          +/
2051 
2052         with (IRCEvent.Type)
2053         top:
2054         switch (ctcpEvent)
2055         {
2056         case "ACTION":
2057             // We already sliced away the control characters and advanced past the
2058             // "ACTION" ctcpEvent string, so just set the type and break.
2059             event.type = (event.sender.nickname == parser.client.nickname) ?
2060                 IRCEvent.Type.SELFEMOTE :
2061                 IRCEvent.Type.EMOTE;
2062             break;
2063 
2064         foreach (immutable type; EnumMembers!(IRCEvent.Type))
2065         {
2066             import lu.conv : Enum;
2067             import std.algorithm.searching : startsWith;
2068 
2069             //enum typestring = type.to!string;
2070             enum typestring = Enum!(IRCEvent.Type).toString(type);
2071 
2072             static if (typestring.startsWith("CTCP_"))
2073             {
2074                 case typestring[5..$]:
2075                     event.type = type;
2076                     event.aux[0] = typestring[5..$];
2077                     if (event.content == event.aux[0]) event.content = string.init;
2078                     break top;
2079             }
2080         }
2081 
2082         default:
2083             immutable message = "Unknown CTCP event: `" ~ ctcpEvent ~ '`';
2084             throw new IRCParseException(message, event);
2085         }
2086     }
2087 }
2088 
2089 
2090 // onMode
2091 /++
2092     Handle [dialect.defs.IRCEvent.Type.MODE|MODE] changes.
2093 
2094     Params:
2095         parser = Reference to the current [IRCParser].
2096         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
2097             working on.
2098         slice = Reference to the slice of the raw IRC string.
2099  +/
2100 void onMode(
2101     ref IRCParser parser,
2102     ref IRCEvent event,
2103     ref string slice) pure @safe
2104 in (slice.length, "Tried to process `onMode` on an empty slice")
2105 {
2106     import dialect.common : isValidChannel;
2107     import std.string : indexOf;
2108     import std.algorithm.searching : startsWith;
2109 
2110     immutable target = slice.advancePast(' ');
2111 
2112     if (target.isValidChannel(parser.server))
2113     {
2114         event.channel = target;
2115 
2116         if (slice.indexOf(' ') != -1)
2117         {
2118             // :zorael!~NaN@ns3363704.ip-94-23-253.eu MODE #flerrp +v kameloso^
2119             event.aux[0] = slice.advancePast(' ');
2120             // save target in content; there may be more than one
2121             event.content = slice;
2122         }
2123         else
2124         {
2125             // :zorael!~NaN@ns3363704.ip-94-23-253.eu MODE #flerrp +i
2126             // :niven.freenode.net MODE #sklabjoier +ns
2127             //event.type = IRCEvent.Type.USERMODE;
2128             event.aux[0] = slice;
2129         }
2130     }
2131     else
2132     {
2133         import std.string : representation;
2134 
2135         // :kameloso^ MODE kameloso^ :+i
2136         // :<something> MODE kameloso :ix
2137         // Does not always have the plus sign. Strip it if it's there.
2138 
2139         event.type = IRCEvent.Type.SELFMODE;
2140         if (slice.startsWith(':')) slice = slice[1..$];
2141 
2142         bool subtractive;
2143         string modechange = slice;  // mutable
2144 
2145         if (!slice.length) return;  // Just to safeguard before indexing [0]
2146 
2147         switch (slice[0])
2148         {
2149         case '-':
2150             subtractive = true;
2151             goto case '+';
2152 
2153         case '+':
2154             slice = slice[1..$];
2155             break;
2156 
2157         default:
2158             // No sign, implicitly additive
2159             modechange = '+' ~ slice;
2160         }
2161 
2162         event.aux[0] = modechange;
2163 
2164         if (subtractive)
2165         {
2166             // Remove the mode from client.modes
2167             auto mutModes  = parser.client.modes.dup.representation;  // mnutable
2168 
2169             foreach (immutable modechar; slice.representation)
2170             {
2171                 import std.algorithm.mutation : SwapStrategy, remove;
2172                 mutModes = mutModes.remove!((listedModechar => listedModechar == modechar), SwapStrategy.unstable);
2173             }
2174 
2175             parser.client.modes = cast(string)mutModes.idup;
2176         }
2177         else
2178         {
2179             import std.algorithm.iteration : filter, uniq;
2180             import std.algorithm.sorting : sort;
2181             import std.array : array;
2182 
2183             // Add the new mode to client.modes
2184             auto modes = parser.client.modes.dup.representation;
2185             modes ~= slice;
2186             parser.client.modes = cast(string)modes
2187                 .sort
2188                 .uniq
2189                 .array
2190                 .idup;
2191         }
2192 
2193         version(FlagAsUpdated) parser.updates |= IRCParser.Update.client;
2194     }
2195 }
2196 
2197 ///
2198 unittest
2199 {
2200     IRCParser parser;
2201     parser.client.nickname = "kameloso^";
2202     parser.client.modes = "x";
2203 
2204     {
2205         IRCEvent event;
2206         string slice = /*":kameloso^ MODE */"kameloso^ :+i";
2207         parser.onMode(event, slice);
2208         assert((parser.client.modes == "ix"), parser.client.modes);
2209     }
2210     {
2211         IRCEvent event;
2212         string slice = /*":kameloso^ MODE */"kameloso^ :-i";
2213         parser.onMode(event, slice);
2214         assert((parser.client.modes == "x"), parser.client.modes);
2215     }
2216     {
2217         IRCEvent event;
2218         string slice = /*":kameloso^ MODE */"kameloso^ :+abc";
2219         parser.onMode(event, slice);
2220         assert((parser.client.modes == "abcx"), parser.client.modes);
2221     }
2222     {
2223         IRCEvent event;
2224         string slice = /*":kameloso^ MODE */"kameloso^ :-bx";
2225         parser.onMode(event, slice);
2226         assert((parser.client.modes == "ac"), parser.client.modes);
2227     }
2228 }
2229 
2230 
2231 // onISUPPORT
2232 /++
2233     Handles [dialect.defs.IRCEvent.Type.RPL_ISUPPORT|RPL_ISUPPORT] events.
2234 
2235     [dialect.defs.IRCEvent.Type.RPL_ISUPPORT|RPL_ISUPPORT] contains a bunch of
2236     interesting information that changes how we look at the
2237     [dialect.defs.IRCServer|IRCServer]. Notably which *network* the server is of
2238     and its max channel and nick lengths, and available modes. Then much
2239     more that we're currently ignoring.
2240 
2241     Params:
2242         parser = Reference to the current [IRCParser].
2243         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
2244             working on.
2245         slice = Reference to the slice of the raw IRC string.
2246 
2247     Throws:
2248         [dialect.common.IRCParseException|IRCParseException] if something
2249         could not be parsed or converted.
2250  +/
2251 void onISUPPORT(
2252     ref IRCParser parser,
2253     ref IRCEvent event,
2254     ref string slice) pure @safe
2255 in (slice.length, "Tried to process `IRCEvent.Type.RPL_ISUPPORT` on an empty slice")
2256 {
2257     import lu.conv : Enum;
2258     import std.algorithm.iteration : splitter;
2259     import std.conv : /*ConvException,*/ to;
2260     import std.string : indexOf;
2261 
2262     // :barjavel.freenode.net 005 kameloso^ CHARSET=ascii NICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D FNC TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 WHOX KNOCK CPRIVMSG :are supported by this server
2263     // :barjavel.freenode.net 005 kameloso^ CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz CHANLIMIT=#:120 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=freenode STATUSMSG=@+ CALLERID=g CASEMAPPING=rfc1459 :are supported by this server
2264 
2265     slice.advancePast(' ');  // bot nickname
2266     if (slice.indexOf(" :") != -1) event.content = slice.advancePast(" :");
2267 
2268     if (parser.server.supports.length)
2269     {
2270         // Not the first event, add a space first
2271         parser.server.supports ~= ' ';
2272     }
2273 
2274     parser.server.supports ~= event.content;
2275 
2276     try
2277     {
2278         uint n;
2279 
2280         foreach (value; event.content.splitter(' '))
2281         {
2282             if (n < event.aux.length)
2283             {
2284                 // Include the value-key pairs in aux, now that there's room
2285                 event.aux[n++] = value;
2286             }
2287 
2288             if (value.indexOf('=') == -1)
2289             {
2290                 // insert switch on value for things like EXCEPTS, INVEX, CPRIVMSG, etc
2291                 continue;
2292             }
2293 
2294             immutable key = value.advancePast('=');
2295 
2296             /// http://www.irc.org/tech_docs/005.html
2297 
2298             switch (key)
2299             {
2300             case "PREFIX":
2301                 // PREFIX=(Yqaohv)!~&@%+
2302                 import std.format : formattedRead;
2303 
2304                 string modechars;  // mutable
2305                 string modesigns;  // mutable
2306 
2307                 // formattedRead can throw but just let the main loop pick it up
2308                 value.formattedRead("(%s)%s", modechars, modesigns);
2309                 parser.server.prefixes = modechars;
2310 
2311                 foreach (immutable i; 0..modechars.length)
2312                 {
2313                     parser.server.prefixchars[modesigns[i]] = modechars[i];
2314                 }
2315                 break;
2316 
2317             case "CHANTYPES":
2318                 // CHANTYPES=#
2319                 // ...meaning which characters may prefix channel names.
2320                 parser.server.chantypes = value;
2321                 break;
2322 
2323             case "CHANMODES":
2324                 /++
2325                     This is a list of channel modes according to 4 types.
2326 
2327                     A = Mode that adds or removes a nick or address to a list.
2328                         Always has a parameter.
2329                     B = Mode that changes a setting and always has a parameter.
2330                     C = Mode that changes a setting and only has a parameter when
2331                         set.
2332                     D = Mode that changes a setting and never has a parameter.
2333 
2334                     Freenode: CHANMODES=eIbq,k,flj,CFLMPQScgimnprstz
2335                  +/
2336                 string modeslice = value;  // mutable
2337                 parser.server.aModes = modeslice.advancePast(',');
2338                 parser.server.bModes = modeslice.advancePast(',');
2339                 parser.server.cModes = modeslice.advancePast(',');
2340                 parser.server.dModes = modeslice;
2341                 assert((parser.server.dModes.indexOf(',') == -1),
2342                     "Bad chanmodes; dModes has comma: " ~ parser.server.dModes);
2343                 break;
2344 
2345             case "NETWORK":
2346                 import dialect.common : typenumsOf;
2347 
2348                 switch (value)
2349                 {
2350                 case "RusNet":
2351                     // RusNet servers do not advertise an easily-identifiable
2352                     // daemonstring like "1.5.24/uk_UA.KOI8-U", so fake the daemon
2353                     // here.
2354                     parser.typenums = typenumsOf(IRCServer.Daemon.rusnet);
2355                     parser.server.daemon = IRCServer.Daemon.rusnet;
2356                     break;
2357 
2358                 case "IRCnet":
2359                     // Likewise IRCnet only advertises the daemon version and not
2360                     // the daemon name. (2.11.2p3)
2361                     parser.typenums = typenumsOf(IRCServer.Daemon.ircnet);
2362                     parser.server.daemon = IRCServer.Daemon.ircnet;
2363                     break;
2364 
2365                 case "Rizon":
2366                     // Rizon reports hybrid but actually has some extras
2367                     // onMyInfo will have already melded typenums for Daemon.hybrid,
2368                     // but Daemon.rizon just applies on top of it.
2369                     parser.typenums = typenumsOf(IRCServer.Daemon.rizon);
2370                     parser.server.daemon = IRCServer.Daemon.rizon;
2371                     break;
2372 
2373                 default:
2374                     break;
2375                 }
2376 
2377                 parser.server.network = value;
2378                 break;
2379 
2380             case "NICKLEN":
2381                 parser.server.maxNickLength = value.to!uint;
2382                 break;
2383 
2384             case "CHANNELLEN":
2385                 parser.server.maxChannelLength = value.to!uint;
2386                 break;
2387 
2388             case "CASEMAPPING":
2389                 parser.server.caseMapping = Enum!(IRCServer.CaseMapping).fromString(value);
2390                 break;
2391 
2392             case "EXTBAN":
2393                 // EXTBAN=$,ajrxz
2394                 // EXTBAN=
2395                 // no character means implicitly $, I believe?
2396                 immutable prefix = value.advancePast(',');
2397                 parser.server.extbanPrefix = prefix.length ? prefix.to!char : '$';
2398                 parser.server.extbanTypes = value;
2399                 break;
2400 
2401             case "EXCEPTS":
2402                 parser.server.exceptsChar = value.length ? value.to!char : 'e';
2403                 break;
2404 
2405             case "INVEX":
2406                 parser.server.invexChar = value.length ? value.to!char : 'I';
2407                 break;
2408 
2409             default:
2410                 break;
2411             }
2412         }
2413 
2414         event.content = string.init;
2415         version(FlagAsUpdated) parser.updates |= IRCParser.Update.server;
2416     }
2417     /*catch (ConvException e)
2418     {
2419         throw new IRCParseException(e.msg, event, e.file, e.line);
2420     }*/
2421     catch (Exception e)
2422     {
2423         throw new IRCParseException(e.msg, event, e.file, e.line);
2424     }
2425 }
2426 
2427 
2428 // onMyInfo
2429 /++
2430     Handle [dialect.defs.IRCEvent.Type.RPL_MYINFO|RPL_MYINFO] events.
2431 
2432     `MYINFO` contains information about which *daemon* the server is running.
2433     We want that to be able to meld together a good `typenums` array.
2434 
2435     It fires before [dialect.defs.IRCEvent.Type.RPL_ISUPPORT|RPL_ISUPPORT].
2436 
2437     Params:
2438         parser = Reference to the current [IRCParser].
2439         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] to continue
2440             working on.
2441         slice = Reference to the slice of the raw IRC string.
2442  +/
2443 void onMyInfo(
2444     ref IRCParser parser,
2445     ref IRCEvent event,
2446     ref string slice) pure @safe
2447 in (slice.length, "Tried to process `onMyInfo` on an empty slice")
2448 {
2449     import dialect.common : typenumsOf;
2450     import std.string : indexOf;
2451     import std.uni : toLower;
2452 
2453     /*
2454     cadance.canternet.org                   InspIRCd-2.0
2455     barjavel.freenode.net                   ircd-seven-1.1.4
2456     irc.uworld.se                           plexus-4(hybrid-8.1.20)
2457     port80c.se.quakenet.org                 u2.10.12.10+snircd(1.3.4a)
2458     Ashburn.Va.Us.UnderNet.org              u2.10.12.18
2459     irc2.unrealircd.org                     UnrealIRCd-4.0.16-rc1
2460     nonstop.ix.me.dal.net                   bahamut-2.0.7
2461     TAL.DE.EU.GameSurge.net                 u2.10.12.18(gs2)
2462     efnet.port80.se                         ircd-ratbox-3.0.9
2463     conclave.il.us.SwiftIRC.net             Unreal3.2.6.SwiftIRC(10)
2464     caliburn.pa.us.irchighway.net           InspIRCd-2.0
2465     (twitch)                                -
2466     irc.RomaniaChat.eu                      Unreal3.2.10.6
2467     Defiant.GeekShed.net                    Unreal3.2.10.3-gs
2468     irc.inn.at.euirc.net                    euIRCd 1.3.4-c09c980819
2469     irc.krstarica.com                       UnrealIRCd-4.0.9
2470     XxXChatters.Com                         UnrealIRCd-4.0.3.1
2471     noctem.iZ-smart.net                     Unreal3.2.10.4-iZ
2472     fedora.globalirc.it                     InspIRCd-2.0
2473     ee.ircworld.org                         charybdis-3.5.0.IRCWorld
2474     Armida.german-elite.net                 Unreal3.2.7
2475     procrastinate.idlechat.net              Unreal3.2.10.4
2476     irc2.chattersweb.nl                     UnrealIRCd-4.0.11
2477     Heol.Immortal-Anime.Net                 Unreal3.2.10.5
2478     brlink.vircio.net                       InspIRCd-2.2
2479     MauriChat.s2.de.GigaIRC.net             UnrealIRCd-4.0.10
2480     IRC.101Systems.Com.BR                   UnrealIRCd-4.0.15
2481     IRC.Passatempo.Org                      UnrealIRCd-4.0.14
2482     irc01-green.librairc.net                InspIRCd-2.0
2483     irc.place2chat.com                      UnrealIRCd-4.0.10
2484     irc.ircportal.net                       Unreal3.2.10.1
2485     irc.de.icq-chat.com                     InspIRCd-2.0
2486     lightning.ircstorm.net                  CR1.8.03-Unreal3.2.10.1
2487     irc.chat-garden.nl                      UnrealIRCd-4.0.10
2488     alpha.noxether.net                      UnrealIRCd-4.0-Noxether
2489     CraZyPaLaCe.Be_ChatFun.Be_Webradio.VIP  CR1.8.03-Unreal3.2.8.1
2490     redhispana.org                          Unreal3.2.8+UDB-3.6.1
2491     irc.portlane.se (ircnet)                2.11.2p3
2492     */
2493 
2494     // :asimov.freenode.net 004 kameloso^ asimov.freenode.net ircd-seven-1.1.4 DOQRSZaghilopswz CFILMPQSbcefgijklmnopqrstvz bkloveqjfI
2495     // :tmi.twitch.tv 004 zorael :-
2496 
2497     /*if (parser.server.daemon != IRCServer.Daemon.init)
2498     {
2499         // Daemon remained from previous connects.
2500         // Trust that the typenums did as well.
2501         import std.stdio;
2502         debug writeln("RETURNING BECAUSE NON-INIT DAEMON: ", parser.server.daemon);
2503         return;
2504     }*/
2505 
2506     slice.advancePast(' ');  // nickname
2507 
2508     version(TwitchSupport)
2509     {
2510         import std.algorithm.searching : endsWith;
2511 
2512         if ((slice == ":-") && (parser.server.address.endsWith(".twitch.tv")))
2513         {
2514             parser.typenums = typenumsOf(IRCServer.Daemon.twitch);
2515 
2516             // Twitch doesn't seem to support any modes other than normal prefix op
2517             with (parser.server)
2518             {
2519                 daemon = IRCServer.Daemon.twitch;
2520                 daemonstring = "Twitch";
2521                 network = "Twitch";
2522                 prefixes = "o";
2523                 prefixchars = [ '@' : 'o' ];
2524                 maxNickLength = 25;
2525             }
2526 
2527             version(FlagAsUpdated) parser.updates |= IRCParser.Update.server;
2528             return;
2529         }
2530     }
2531 
2532     slice.advancePast(' ');  // server address
2533     immutable daemonstring = slice.advancePast(' ');
2534     immutable daemonstringLower = daemonstring.toLower;
2535     event.content = slice;
2536     event.aux[0] = daemonstring;
2537 
2538     // https://upload.wikimedia.org/wikipedia/commons/d/d5/IRCd_software_implementations3.svg
2539 
2540     IRCServer.Daemon daemon;
2541 
2542     with (IRCServer.Daemon)
2543     {
2544         if (daemonstringLower.indexOf("unreal") != -1)
2545         {
2546             daemon = unreal;
2547         }
2548         else if (daemonstringLower.indexOf("solanum") != -1)
2549         {
2550             daemon = solanum;
2551         }
2552         else if (daemonstringLower.indexOf("inspircd") != -1)
2553         {
2554             daemon = inspircd;
2555         }
2556         else if (daemonstringLower.indexOf("snircd") != -1)
2557         {
2558             daemon = snircd;
2559         }
2560         else if (daemonstringLower.indexOf("u2.") != -1)
2561         {
2562             daemon = u2;
2563         }
2564         else if (daemonstringLower.indexOf("bahamut") != -1)
2565         {
2566             daemon = bahamut;
2567         }
2568         else if (daemonstringLower.indexOf("hybrid") != -1)
2569         {
2570             daemon = hybrid;
2571         }
2572         else if (daemonstringLower.indexOf("ratbox") != -1)
2573         {
2574             daemon = ratbox;
2575         }
2576         else if (daemonstringLower.indexOf("charybdis") != -1)
2577         {
2578             daemon = charybdis;
2579         }
2580         else if (daemonstringLower.indexOf("ircd-seven") != -1)
2581         {
2582             daemon = ircdseven;
2583         }
2584         else if (daemonstring == "BSDUnix")
2585         {
2586             daemon = bsdunix;
2587         }
2588         else if (daemonstring.indexOf("MFVX") != -1)
2589         {
2590             daemon = mfvx;
2591         }
2592         else
2593         {
2594             daemon = unknown;
2595         }
2596     }
2597 
2598     parser.typenums = typenumsOf(daemon);
2599     parser.server.daemon = daemon;
2600     parser.server.daemonstring = daemonstring;
2601     version(FlagAsUpdated) parser.updates |= IRCParser.Update.server;
2602 }
2603 
2604 
2605 // applyTags
2606 /++
2607     This is technically a postprocessor but it's small enough that we can just
2608     keep it in here (and provide the functionality as `pure` and `@safe`).
2609 
2610     Params:
2611         event = Reference to the [dialect.defs.IRCEvent|IRCEvent] whose tags to
2612             apply to itself.
2613  +/
2614 void applyTags(ref IRCEvent event) pure @safe
2615 {
2616     import std.algorithm.iteration : splitter;
2617 
2618     foreach (tag; event.tags.splitter(";"))
2619     {
2620         import lu.string : advancePast;
2621         import std.string : indexOf;
2622 
2623         if (tag.indexOf('=') != -1)
2624         {
2625             immutable key = tag.advancePast('=');
2626             alias value = tag;
2627 
2628             switch (key)
2629             {
2630             case "account":
2631                 event.sender.account = value;
2632                 break;
2633 
2634             default:
2635                 // Unknown tag
2636                 break;
2637             }
2638         }
2639         /*else
2640         {
2641             switch (tag)
2642             {
2643             case "solanum.chat/identify-msg":
2644                 // mquin | I don't think you'd get account tags for an unverified account
2645                 // mquin | yep, account= but no solanum.chat/identified while I was mquin__
2646                 // Currently no use for
2647                 break;
2648 
2649             case "solanum.chat/ip":
2650                 // Currently no use for
2651                 break;
2652 
2653             default:
2654                 // Unknown tag
2655                 break;
2656             }
2657         }*/
2658     }
2659 }
2660 
2661 
2662 public:
2663 
2664 
2665 // IRCParser
2666 /++
2667     Parser that takes raw IRC strings and produces [dialect.defs.IRCEvent|IRCEvent]s based on them.
2668 
2669     Parsing requires state, which means that [IRCParser]s must be equipped with
2670     a [dialect.defs.IRCServer|IRCServer] and a [dialect.defs.IRCClient|IRCClient] for context when parsing.
2671     Because of this it has its postblit `@disable`d, so as not to make copies
2672     when only one instance should exist.
2673 
2674     The alternative is to make it a class, which works too.
2675 
2676     See the `/tests` directory for unit tests.
2677 
2678     Example:
2679     ---
2680     IRCClient client;
2681     client.nickname = "...";
2682 
2683     IRCServer server;
2684     server.address = "...";
2685 
2686     IRCParser parser = IRCParser(client, server);
2687 
2688     string fromServer = ":zorael!~NaN@address.tld MODE #channel +v nickname";
2689     IRCEvent event = parser.toIRCEvent(fromServer);
2690 
2691     with (event)
2692     {
2693         assert(type == IRCEvent.Type.MODE);
2694         assert(sender.nickname == "zorael");
2695         assert(sender.ident == "~NaN");
2696         assert(sender.address == "address.tld");
2697         assert(target.nickname == "nickname");
2698         assert(channel == "#channel");
2699         assert(aux[0] = "+v");
2700     }
2701 
2702     string alsoFromServer = ":cherryh.freenode.net 435 oldnick newnick #d :Cannot change nickname while banned on channel";
2703     IRCEvent event2 = parser.toIRCEvent(alsoFromServer);
2704 
2705     with (event2)
2706     {
2707         assert(type == IRCEvent.Type.ERR_BANONCHAN);
2708         assert(sender.address == "cherryh.freenode.net");
2709         assert(channel == "#d");
2710         assert(target.nickname == "oldnick");
2711         assert(content == "Cannot change nickname while banned on channel");
2712         assert(aux[0] == "newnick");
2713         assert(num == 435);
2714     }
2715 
2716     // Requires Twitch support via build configuration "twitch"
2717     string fullExample = "@badge-info=subscriber/15;badges=subscriber/12;color=;display-name=SomeoneOnTwitch;emotes=;flags=;id=d6729804-2bf3-495d-80ce-a2fe8ed00a26;login=someoneontwitch;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=1;msg-param-origin-id=49\\s9d\\s3e\\s68\\sca\\s26\\se9\\s2a\\s6e\\s44\\sd4\\s60\\s9b\\s3d\\saa\\sb9\\s4c\\sad\\s43\\s5c;msg-param-sender-count=4;msg-param-sub-plan=1000;room-id=71092938;subscriber=1;system-msg=someoneOnTwitch\\sis\\sgifting\\s1\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!\\sThey've\\sgifted\\sa\\stotal\\sof\\s4\\sin\\sthe\\schannel!;tmi-sent-ts=1569013433362;user-id=224578549;user-type= :tmi.twitch.tv USERNOTICE #xqcow"
2718     IRCEvent event4 = parser.toIRCEvent(fullExample);
2719 
2720     with (event)
2721     {
2722         assert(type == IRCEvent.Type.TWITCH_BULKGIFT);
2723         assert(sender.nickname == "someoneontwitch");
2724         assert(sender.displayName == "SomeoneOnTwitch");
2725         assert(sender.badges == "subscriber/12");
2726         assert(channel == "#xqcow");
2727         assert(content == "SomeoneOnTwitch is gifting 1 Tier 1 Subs to xQcOW's community! They've gifted a total of 4 in the channel!");
2728         assert(aux[0] == "1000");
2729         assert(count[0] == 1);
2730         assert(count[1] == 4);
2731     }
2732     ---
2733  +/
2734 struct IRCParser
2735 {
2736 private:
2737     import dialect.postprocessors : Postprocessor;
2738 
2739 public:
2740     /++
2741         The current [dialect.defs.IRCClient|IRCClient] with all the context
2742         needed for parsing.
2743      +/
2744     IRCClient client;
2745 
2746 
2747     /++
2748         The current [dialect.defs.IRCServer|IRCServer] with all the context
2749         needed for parsing.
2750      +/
2751     IRCServer server;
2752 
2753 
2754     /++
2755         An `dialect.defs.IRCEvent.Type[1024]` reverse lookup table for fast
2756         numeric lookups.
2757      +/
2758     IRCEvent.Type[1024] typenums = Typenums.base;
2759 
2760 
2761     // toIRCEvent
2762     /++
2763         Parses an IRC string into an [dialect.defs.IRCEvent|IRCEvent].
2764 
2765         The return type is kept as `auto` to infer purity. It will be `pure` if
2766         there are no postprocessors available, and merely `@safe` if there are.
2767 
2768         Proxies the call to the top-level [dialect.parsing.toIRCEvent|.toIRCEvent].
2769 
2770         Params:
2771             raw = Raw IRC string as received from a server.
2772 
2773         Returns:
2774             A complete [dialect.defs.IRCEvent|IRCEvent].
2775      +/
2776     auto toIRCEvent(const string raw) // infer @safe-ness
2777     {
2778         IRCEvent event = .toIRCEvent(this, raw);
2779 
2780         // Final pass: sanity check. This verifies some fields and gives
2781         // meaningful error messages if something doesn't look right.
2782         postparseSanityCheck(this, event);
2783 
2784         version(Postprocessors)
2785         {
2786             // Epilogue: let postprocessors alter the event
2787             foreach (postprocessor; this.postprocessors)
2788             {
2789                 postprocessor.postprocess(this, event);
2790             }
2791         }
2792 
2793         return event;
2794     }
2795 
2796 
2797     // ctor
2798     /++
2799         Create a new [IRCParser] with the passed [dialect.defs.IRCClient|IRCClient]
2800         and [dialect.defs.IRCServer|IRCServer] as base context for parsing.
2801 
2802         Initialises any [dialect.common.Postprocessor|Postprocessor]s available
2803         iff version `Postprocessors` is declared.
2804      +/
2805     auto this(
2806         IRCClient client,
2807         IRCServer server)  // infer attributes
2808     {
2809         this.client = client;
2810         this.server = server;
2811 
2812         version(Postprocessors)
2813         {
2814             initPostprocessors();
2815         }
2816     }
2817 
2818 
2819     /++
2820         Disallow copying of this struct.
2821      +/
2822     @disable this(this);
2823 
2824 
2825     version(Postprocessors)
2826     {
2827         /++
2828             Array of active [dialect.common.Postprocessor|Postprocessor]s, to be
2829             iterated through and processed after parsing is complete.
2830          +/
2831         Postprocessor[] postprocessors;
2832 
2833 
2834         // initPostprocessors
2835         /++
2836             Initialises defined postprocessors.
2837          +/
2838         void initPostprocessors() @system
2839         {
2840             import dialect.postprocessors : instantiatePostprocessors;
2841 
2842             postprocessors = instantiatePostprocessors();
2843 
2844             version(TwitchSupport)
2845             {
2846                 enum message = "No postprocessors were instantiated despite version " ~
2847                     "`TwitchSupport` declared. Make sure to `import dialect.postprocessors.twitch` " ~
2848                     "somewhere in the importing project.";
2849                 assert(postprocessors.length, message);
2850             }
2851         }
2852     }
2853 
2854 
2855     version(FlagAsUpdated)
2856     {
2857         // Update
2858         /++
2859             Bitfield enum of what member of an instance of `IRCParser` was
2860             updated (if any).
2861          +/
2862         enum Update
2863         {
2864             /++
2865                 Nothing marked as updated. Initial value.
2866              +/
2867             nothing = 0,
2868             /++
2869                 Parsing updated the internal [dialect.defs.IRCClient|IRCClient].
2870              +/
2871             client = 1 << 0,
2872 
2873             /++
2874                 Parsing updated the internal [dialect.defs.IRCServer|IRCServer].
2875              +/
2876             server = 1 << 1,
2877         }
2878 
2879 
2880         // updates
2881         /++
2882             Bitfield of in what way the parser's internal state was altered
2883             during parsing.
2884 
2885             Example:
2886             ---
2887             if (parser.updates & IRCParser.Update.client)
2888             {
2889                 // parser.client was marked as updated
2890                 parser.updates |= IRCParser.Update.server;
2891                 // parser.server now marked as updated
2892             }
2893             ---
2894          +/
2895         Update updates;
2896 
2897 
2898         // clientUpdated
2899         /++
2900             Wrapper for backwards compatibility with pre-bitfield update-signaling.
2901 
2902             Returns:
2903                 Whether or not the internal client was updated.
2904          +/
2905         pragma(inline, true)
2906         bool clientUpdated() const pure @safe @nogc nothrow
2907         {
2908             return cast(bool)(updates & Update.client);
2909         }
2910 
2911 
2912         // serverUpdated
2913         /++
2914             Wrapper for backwards compatibility with pre-bitfield update-signaling.
2915 
2916             Returns:
2917                 Whether or not the internal server was updated.
2918          +/
2919         pragma(inline, true)
2920         bool serverUpdated() const pure @safe @nogc nothrow
2921         {
2922             return cast(bool)(updates & Update.server);
2923         }
2924 
2925 
2926         // clientUpdated
2927         /++
2928             Wrapper for backwards compatibility with pre-bitfield update-signaling.
2929 
2930             Params:
2931                 updated = Whether or not the internal client should be flagged as updated.
2932          +/
2933         pragma(inline, true)
2934         void clientUpdated(const bool updated) pure @safe @nogc nothrow
2935         {
2936             if (updated) updates |= Update.client;
2937             else updates &= ~Update.client;
2938         }
2939 
2940 
2941         // serverUpdated
2942         /++
2943             Wrapper for backwards compatibility with pre-bitfield update-signaling.
2944 
2945             Params:
2946                 updated = Whether or not the internal server should be flagged as updated.
2947          +/
2948         pragma(inline, true)
2949         void serverUpdated(const bool updated) pure @safe @nogc nothrow
2950         {
2951             if (updated) updates |= Update.server;
2952             else updates &= ~Update.server;
2953         }
2954     }
2955 }
2956 
2957 unittest
2958 {
2959     import lu.meld : MeldingStrategy, meldInto;
2960 
2961     IRCParser parser;
2962 
2963     alias T = IRCEvent.Type;
2964 
2965     parser.typenums = Typenums.base;
2966 
2967     assert(parser.typenums[344] == T.init);
2968     Typenums.hybrid[].meldInto!(MeldingStrategy.aggressive)(parser.typenums);
2969     assert(parser.typenums[344] != T.init);
2970 }