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 }