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