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