1 /++ 2 Helper functions needed to parse raw IRC event strings into 3 [dialect.defs.IRCEvent|IRCEvent]s. 4 5 Also things that don't belong anywhere else. 6 +/ 7 module dialect.common; 8 9 private: 10 11 import dialect.defs; 12 import dialect.parsing; 13 import lu.string : advancePast; 14 15 public: 16 17 18 // typenumsOf 19 /++ 20 Returns the `typenums` mapping for a given 21 [dialect.defs.IRCServer.Daemon|IRCServer.Daemon]. 22 23 Example: 24 --- 25 IRCParser parser; 26 IRCServer.Daemon daemon = IRCServer.Daemon.unreal; 27 string daemonstring = "unreal"; 28 29 parser.typenums = getTypenums(daemon); 30 parser.client.daemon = daemon; 31 parser.client.daemonstring = daemonstring; 32 --- 33 34 Params: 35 daemon = The [dialect.defs.IRCServer.Daemon|IRCServer.Daemon] to get 36 the typenums for. 37 38 Returns: 39 A `typenums` array of [dialect.defs.IRCEvent|IRCEvent]s mapped to numerics. 40 +/ 41 auto typenumsOf(const IRCServer.Daemon daemon) pure @safe nothrow @nogc 42 { 43 import lu.meld : MeldingStrategy, meldInto; 44 45 /// https://upload.wikimedia.org/wikipedia/commons/d/d5/IRCd_software_implementations3.svg 46 47 IRCEvent.Type[1024] typenums = Typenums.base; 48 alias strategy = MeldingStrategy.aggressive; 49 50 with (IRCServer.Daemon) 51 final switch (daemon) 52 { 53 case unreal: 54 case mfvx: 55 Typenums.unreal[].meldInto!strategy(typenums); 56 break; 57 58 case solanum: 59 Typenums.solanum[].meldInto!strategy(typenums); 60 break; 61 62 case inspircd: 63 Typenums.inspIRCd[].meldInto!strategy(typenums); 64 break; 65 66 case bahamut: 67 Typenums.bahamut[].meldInto!strategy(typenums); 68 break; 69 70 case ratbox: 71 Typenums.ratBox[].meldInto!strategy(typenums); 72 break; 73 74 case u2: 75 // unknown! 76 break; 77 78 case rizon: 79 // Rizon is hybrid but has some own extras 80 Typenums.hybrid[].meldInto!strategy(typenums); 81 Typenums.rizon[].meldInto!strategy(typenums); 82 break; 83 84 case hybrid: 85 Typenums.hybrid[].meldInto!strategy(typenums); 86 break; 87 88 case ircu: 89 Typenums.ircu[].meldInto!strategy(typenums); 90 break; 91 92 case aircd: 93 Typenums.aircd[].meldInto!strategy(typenums); 94 break; 95 96 case rfc1459: 97 Typenums.rfc1459[].meldInto!strategy(typenums); 98 break; 99 100 case rfc2812: 101 Typenums.rfc2812[].meldInto!strategy(typenums); 102 break; 103 104 case snircd: 105 // snircd is based on ircu 106 Typenums.ircu[].meldInto!strategy(typenums); 107 Typenums.snircd[].meldInto!strategy(typenums); 108 break; 109 110 case nefarious: 111 // Both nefarious and nefarious2 are based on ircu 112 Typenums.ircu[].meldInto!strategy(typenums); 113 Typenums.nefarious[].meldInto!strategy(typenums); 114 break; 115 116 case rusnet: 117 Typenums.rusnet[].meldInto!strategy(typenums); 118 break; 119 120 case austhex: 121 Typenums.austHex[].meldInto!strategy(typenums); 122 break; 123 124 case ircnet: 125 Typenums.ircNet[].meldInto!strategy(typenums); 126 break; 127 128 case ptlink: 129 Typenums.ptlink[].meldInto!strategy(typenums); 130 break; 131 132 case ultimate: 133 Typenums.ultimate[].meldInto!strategy(typenums); 134 break; 135 136 case charybdis: 137 Typenums.charybdis[].meldInto!strategy(typenums); 138 break; 139 140 case ircdseven: 141 // Nei | freenode is based in charybdis which is based on ratbox iirc 142 Typenums.hybrid[].meldInto!strategy(typenums); 143 Typenums.ratBox[].meldInto!strategy(typenums); 144 Typenums.charybdis[].meldInto!strategy(typenums); 145 break; 146 147 case undernet: 148 Typenums.undernet[].meldInto!strategy(typenums); 149 break; 150 151 case anothernet: 152 //Typenums.anothernet[].meldInto!strategy(typenums); 153 break; 154 155 case sorircd: 156 Typenums.charybdis[].meldInto!strategy(typenums); 157 Typenums.sorircd[].meldInto!strategy(typenums); 158 break; 159 160 case bdqircd: 161 //Typenums.bdqIrcD[].meldInto!strategy(typenums); 162 break; 163 164 case chatircd: 165 //Typenums.chatIRCd[].meldInto!strategy(typenums); 166 break; 167 168 case irch: 169 //Typenums.irch[].meldInto!strategy(typenums); 170 break; 171 172 case ithildin: 173 //Typenums.ithildin[].meldInto!strategy(typenums); 174 break; 175 176 case twitch: 177 // do nothing, their events aren't numerical? 178 break; 179 180 case bsdunix: 181 // unsure. 182 break; 183 184 case unknown: 185 case unset: 186 // do nothing... 187 break; 188 } 189 190 return typenums; 191 } 192 193 194 // decodeIRCv3String 195 /++ 196 Decodes an IRCv3 tag string, replacing some characters. 197 198 IRCv3 tags need to be free of spaces, so by necessity they're encoded into 199 `\s`. Likewise; since tags are separated by semicolons, semicolons in tag 200 string are encoded into `\:`, and literal backslashes `\\`. 201 202 Example: 203 --- 204 string encoded = `This\sline\sis\sencoded\:\swith\s\\s`; 205 string decoded = decodeIRCv3String(encoded); 206 assert(decoded == r"This line is encoded; with \s"); 207 --- 208 209 Params: 210 line = Original line to decode. 211 212 Returns: 213 A decoded string without `\s` in it. 214 +/ 215 auto decodeIRCv3String(const string line) pure @safe nothrow 216 { 217 import std.array : Appender; 218 import std.string : representation; 219 220 /++ 221 - http://ircv3.net/specs/core/message-tags-3.2.html 222 223 If a lone \ exists at the end of an escaped value (with no escape 224 character following it), then there SHOULD be no output character. 225 For example, the escaped value test\ should unescape to test. 226 +/ 227 228 if (!line.length) return string.init; 229 230 Appender!(char[]) sink; 231 232 bool escaping; 233 bool dirty; 234 235 foreach (immutable i, immutable c; line.representation) 236 { 237 if (escaping) 238 { 239 if (!dirty) 240 { 241 sink.reserve(line.length); 242 sink.put(line[0..i-1]); 243 dirty = true; 244 } 245 246 switch (c) 247 { 248 case '\\': 249 sink.put('\\'); 250 break; 251 252 case ':': 253 sink.put(';'); 254 break; 255 256 case 's': 257 sink.put(' '); 258 break; 259 260 case 'n': 261 sink.put('\n'); 262 break; 263 264 case 't': 265 sink.put('\t'); 266 break; 267 268 case 'r': 269 sink.put('\r'); 270 break; 271 272 case '0': 273 sink.put('\0'); 274 break; 275 276 default: 277 // Unknown escape 278 sink.put(c); 279 } 280 281 escaping = false; 282 } 283 else 284 { 285 switch (c) 286 { 287 case '\\': 288 escaping = true; 289 break; 290 291 default: 292 if (dirty) sink.put(c); 293 break; 294 } 295 } 296 } 297 298 return dirty ? sink.data : line; 299 } 300 301 /// 302 @safe unittest 303 { 304 immutable s1 = decodeIRCv3String(`kameloso\sjust\ssubscribed\swith\sa\s` ~ 305 `$4.99\ssub.\skameloso\ssubscribed\sfor\s40\smonths\sin\sa\srow!`); 306 assert((s1 == "kameloso just subscribed with a $4.99 sub. " ~ 307 "kameloso subscribed for 40 months in a row!"), s1); 308 309 immutable s2 = decodeIRCv3String(`stop\sspamming\scaps,\sautomated\sby\sNightbot`); 310 assert((s2 == "stop spamming caps, automated by Nightbot"), s2); 311 312 immutable s3 = decodeIRCv3String(`\:__\:`); 313 assert((s3 == ";__;"), s3); 314 315 immutable s4 = decodeIRCv3String(`\\o/ \\o\\ /o/ ~o~`); 316 assert((s4 == `\o/ \o\ /o/ ~o~`), s4); 317 318 immutable s5 = decodeIRCv3String(`This\sis\sa\stest\`); 319 assert((s5 == "This is a test"), s5); 320 321 immutable s6 = decodeIRCv3String(`9\sraiders\sfrom\sVHSGlitch\shave\sjoined\n!`); 322 assert((s6 == "9 raiders from VHSGlitch have joined\n!"), s6); 323 324 { 325 enum before = `\s\s\s`; 326 immutable after = decodeIRCv3String(before); 327 assert((after == " "), after); 328 assert(before !is after); 329 } 330 { 331 enum before = `foo bar`; 332 immutable after = decodeIRCv3String(before); 333 assert((after == "foo bar"), after); 334 assert(before is after); 335 } 336 { 337 enum before = `This\sline\sis\sencoded\:\swith\s\\s`; 338 immutable after = decodeIRCv3String(before); 339 assert((after == r"This line is encoded; with \s"), after); 340 } 341 } 342 343 344 // isAuthService 345 /++ 346 Inspects an [dialect.defs.IRCUser|IRCUser] and judges whether or not it is 347 authentication services. 348 349 This is very ad-hoc. 350 351 Example: 352 --- 353 IRCUser user; 354 355 if (user.isAuthService(parser)) 356 { 357 // ... 358 } 359 --- 360 361 Params: 362 sender = [dialect.defs.IRCUser|IRCUser] to examine. 363 parser = Reference to the current [dialect.parsing.IRCParser|IRCParser]. 364 365 Returns: 366 `true` if the `sender` is judged to be from nickname services, `false` if not. 367 +/ 368 auto isAuthService( 369 const IRCUser sender, 370 const ref IRCParser parser) pure @safe 371 { 372 import lu.common : sharedDomains; 373 374 version(TwitchSupport) 375 { 376 if (parser.server.daemon == IRCServer.Daemon.twitch) return false; 377 } 378 379 top: 380 switch (sender.nickname) 381 { 382 case "NickServ": 383 case "SaslServ": 384 switch (sender.ident) 385 { 386 case "NickServ": 387 case "SaslServ": 388 if (sender.address == "services.") return true; // freenode 389 else if (sender.address == "services") return true; // snoonet 390 // Unknown address, drop to after switch 391 break top; 392 393 case "services": 394 switch (sender.address) 395 { 396 case "services.host": // SwiftIRC 397 case "geekshed.net": 398 case "services.irchighway.net": 399 case "services.oftc.net": 400 case "gimpnet-services.gimp.org": 401 return true; 402 403 default: 404 // Unknown address, drop to after switch 405 break top; 406 } 407 408 case "service": 409 switch (sender.address) 410 { 411 case "RusNet": 412 case "dal.net": 413 case "rizon.net": 414 return true; 415 416 default: 417 // Unknown address, drop to after switch 418 break top; 419 } 420 421 default: 422 // Unknown ident, drop to after switch 423 break top; 424 } 425 426 case "Q": 427 // :Q!TheQBot@CServe.quakenet.org NOTICE kameloso :You are now logged in as kameloso. 428 // :Q!services@swiftirc.net NOTICE kameloso^ :[#rot] 4Reign of Terror 429 if ((sender.ident == "TheQBot") && (sender.address == "CServe.quakenet.org")) return true; 430 //else if ((sender.ident == "services") && (sender.address == "swiftirc.net")) return true; 431 break; 432 433 case "AuthServ": 434 case "authserv": 435 // :AuthServ!AuthServ@Services.GameSurge.net NOTICE kameloso :Could not find your account 436 if ((sender.ident == "AuthServ") && (sender.address == "Services.GameSurge.net")) return true; 437 // Unknown ident/address, drop to after switch 438 break; 439 440 case string.init: 441 if (sender.address == "services.") return true; 442 goto default; 443 444 default: 445 // Unknown nickname 446 return false; 447 } 448 449 // We're here if nick nickserv/sasl/etc and unknown ident, or server mismatch 450 // As such, no need to be as strict as isSpecial is 451 452 return 453 (sharedDomains(sender.address, parser.server.address) >= 2) || 454 (sharedDomains(sender.address, parser.server.resolvedAddress) >= 2); 455 } 456 457 /// 458 /*@safe*/ unittest 459 { 460 IRCParser parser; 461 462 { 463 immutable event = parser.toIRCEvent(":Q!TheQBot@CServe.quakenet.org " ~ 464 "NOTICE kameloso :You are now logged in as kameloso."); 465 assert(event.sender.isAuthService(parser)); 466 } 467 { 468 immutable event = parser.toIRCEvent(":NickServ!NickServ@services. " ~ 469 "NOTICE kameloso :This nickname is registered."); 470 assert(event.sender.isAuthService(parser)); 471 } 472 473 parser.server.address = "irc.rizon.net"; 474 parser.server.resolvedAddress = "irc.uworld.se"; 475 476 { 477 immutable event = parser.toIRCEvent(":NickServ!service@rizon.net " ~ 478 "NOTICE kameloso^^ :nick, type /msg NickServ IDENTIFY password. Otherwise,"); 479 assert(event.sender.isAuthService(parser)); 480 } 481 } 482 483 484 // isValidChannel 485 /++ 486 Examines a string and judges whether or not it *looks* like a channel. 487 488 It needs to be passed an [dialect.defs.IRCServer|IRCServer] to know the max 489 channel name length. An alternative would be to change the 490 [dialect.defs.IRCServer|IRCServer] parameter to be an `uint`. 491 492 Example: 493 --- 494 IRCServer server; 495 assert("#channel".isValidChannel(server)); 496 assert("##channel".isValidChannel(server)); 497 assert(!"!channel".isValidChannel(server)); 498 assert(!"#ch#annel".isValidChannel(server)); 499 --- 500 501 Params: 502 channelName = String of a potential channel name. 503 server = The current [dialect.defs.IRCServer|IRCServer] with all its settings. 504 505 Returns: 506 `true` if the string content is judged to be a channel, `false` if not. 507 +/ 508 auto isValidChannel( 509 const string channelName, 510 const IRCServer server) pure @safe 511 { 512 import std.string : indexOf, representation; 513 514 /+ 515 Channels names are strings (beginning with a '&' or '#' character) of 516 length up to 200 characters. Apart from the requirement that the 517 first character being either '&' or '#'; the only restriction on a 518 channel name is that it may not contain any spaces (' '), a control G 519 (^G or ASCII 7), or a comma (',' which is used as a list item 520 separator by the protocol). 521 522 - https://tools.ietf.org/html/rfc1459.html 523 +/ 524 if ((channelName.length < 2) || (channelName.length > server.maxChannelLength)) 525 { 526 // Too short or too long a word 527 return false; 528 } 529 530 if (server.chantypes.indexOf(channelName[0]) == -1) return false; 531 532 if ((channelName.indexOf(' ') != -1) || 533 (channelName.indexOf(',') != -1) || 534 (channelName.indexOf(7) != -1)) 535 { 536 // Contains spaces, commas or byte 7 537 return false; 538 } 539 540 if (channelName.length == 2) return (server.chantypes.indexOf(channelName[1]) == -1); 541 else if (channelName.length == 3) return (server.chantypes.indexOf(channelName[2]) == -1); 542 else if (channelName.length > 3) 543 { 544 // Allow for two ##s (or &&s) in the name but no more 545 foreach (immutable chansign; server.chantypes.representation) 546 { 547 if (channelName[2..$].indexOf(chansign) != -1) return false; 548 } 549 550 version(TwitchSupport) 551 { 552 if (server.daemon == IRCServer.Daemon.twitch) 553 { 554 return channelName[1..$].isValidNickname(server); 555 } 556 } 557 return true; 558 } 559 else 560 { 561 return false; 562 } 563 } 564 565 /// 566 @safe unittest 567 { 568 IRCServer s; 569 s.chantypes = "#&"; 570 571 assert("#channelName".isValidChannel(s)); 572 assert("&otherChannel".isValidChannel(s)); 573 assert("##freenode".isValidChannel(s)); 574 assert(!"###froonode".isValidChannel(s)); 575 assert(!"#not a channel".isValidChannel(s)); 576 assert(!"notAChannelEither".isValidChannel(s)); 577 assert(!"#".isValidChannel(s)); 578 //assert(!"".isValidChannel(s)); 579 assert(!"##".isValidChannel(s)); 580 assert(!"&&".isValidChannel(s)); 581 assert("#d".isValidChannel(s)); 582 assert("#uk".isValidChannel(s)); 583 assert(!"###".isValidChannel(s)); 584 assert(!"#a#".isValidChannel(s)); 585 assert(!"a".isValidChannel(s)); 586 assert(!" ".isValidChannel(s)); 587 //assert(!"".isValidChannel(s)); 588 589 version(TwitchSupport) 590 { 591 s.daemon = IRCServer.Daemon.twitch; 592 s.chantypes = "#"; 593 s.maxNickLength = 25; 594 595 assert("#1oz".isValidChannel(s)); 596 assert(!"#åäö".isValidChannel(s)); 597 assert("#arunero9029".isValidChannel(s)); 598 } 599 } 600 601 602 // isValidNickname 603 /++ 604 Examines a string and judges whether or not it *looks* like a nickname. 605 606 It only looks for invalid characters in the name as well as it length. 607 608 Example: 609 --- 610 assert("kameloso".isValidNickname(server)); 611 assert("kameloso^".isValidNickname(server)); 612 assert("kamelåså".isValidNickname(server)); 613 assert(!"#kameloso".isValidNickname(server)); 614 assert(!"k&&me##so".isValidNickname(server)); 615 --- 616 617 Params: 618 nickname = String nickname. 619 server = The current [dialect.defs.IRCServer|IRCServer] with all its settings. 620 621 Returns: 622 `true` if the nickname string is judged to be a nickname, `false` if not. 623 +/ 624 auto isValidNickname( 625 const string nickname, 626 const IRCServer server) pure @safe nothrow @nogc 627 { 628 import std.string : representation; 629 630 if (!nickname.length || (nickname.length > server.maxNickLength)) 631 { 632 return false; 633 } 634 635 version(TwitchSupport) 636 { 637 immutable firstCharacterMayBeNumber = (server.daemon == IRCServer.Daemon.twitch); 638 } 639 else 640 { 641 enum firstCharacterMayBeNumber = false; 642 } 643 644 immutable rep = nickname.representation; 645 646 if (!firstCharacterMayBeNumber && (rep[0] >= '0') && (rep[0] <= '9')) 647 { 648 return false; 649 } 650 else if (rep[0] == '-') 651 { 652 return false; 653 } 654 655 foreach (immutable c; rep) 656 { 657 if (!c.isValidNicknameCharacter) return false; 658 } 659 660 // All seem okay 661 return true; 662 } 663 664 /// 665 @safe unittest 666 { 667 import std.range : repeat; 668 import std.conv : to; 669 670 IRCServer s; 671 672 immutable validNicknames = 673 [ 674 "kameloso", 675 "kameloso^", 676 "zorael-", 677 "hirr{}", 678 "asdf`", 679 "[afk]me", 680 "a-zA-Z0-9", 681 `\`, 682 ]; 683 684 immutable invalidNicknames = 685 [ 686 //"", 687 'X'.repeat(s.maxNickLength+1).to!string, 688 "åäöÅÄÖ", 689 "\n", 690 "¨", 691 "@pelle", 692 "+calvin", 693 "&hobbes", 694 "#channel", 695 "$deity", 696 "0kameloso", 697 "-kameloso", 698 "1oz", 699 ]; 700 701 foreach (immutable nickname; validNicknames) 702 { 703 assert(nickname.isValidNickname(s), nickname); 704 } 705 706 foreach (immutable nickname; invalidNicknames) 707 { 708 assert(!nickname.isValidNickname(s), nickname); 709 } 710 711 version(TwitchSupport) 712 { 713 // Twitch supports numbers as first character 714 s.daemon = IRCServer.Daemon.twitch; 715 assert("1oz".isValidNickname(s)); 716 } 717 } 718 719 720 // isValidNicknameCharacter 721 /++ 722 Returns whether or not a passed `char` can be part of a nickname. 723 724 The IRC standard describes nicknames as being a string of any of the 725 following characters: 726 727 `[a-z] [A-Z] [0-9] _-\\[]{}^\`|` 728 729 Example: 730 --- 731 assert('a'.isValidNicknameCharacter); 732 assert('9'.isValidNicknameCharacter); 733 assert('`'.isValidNicknameCharacter); 734 assert(!(' '.isValidNicknameCharacter)); 735 --- 736 737 Params: 738 c = Character to compare with the list of accepted characters in a nickname. 739 740 Returns: 741 `true` if the character is in the list of valid characters for 742 nicknames, `false` if not. 743 +/ 744 auto isValidNicknameCharacter(const ubyte c) pure @safe nothrow @nogc 745 { 746 switch (c) 747 { 748 case 'a': 749 .. 750 case 'z': 751 case 'A': 752 .. 753 case 'Z': 754 case '0': 755 .. 756 case '9': 757 case '_': 758 case '-': 759 case '\\': 760 case '[': 761 case ']': 762 case '{': 763 case '}': 764 case '^': 765 case '`': 766 case '|': 767 return true; 768 769 default: 770 return false; 771 } 772 } 773 774 /// 775 @safe unittest 776 { 777 import std.string : representation; 778 779 { 780 enum line = "abcDEFghi0{}29304_[]`\\^|---"; 781 foreach (immutable char c; line.representation) 782 { 783 assert(c.isValidNicknameCharacter, c ~ ""); 784 } 785 } 786 { 787 enum line = "åÄö高所恐怖症123なにぬねの "; 788 foreach (immutable char c; line.representation) 789 { 790 assert(!c.isValidNicknameCharacter, c ~ ""); 791 } 792 } 793 } 794 795 796 // stripModesign 797 /++ 798 Takes a nickname and strips it of any prepended mode signs, like the `@` in 799 `@nickname`. Saves the stripped signs in the ref string `modesigns`. 800 801 Example: 802 --- 803 IRCServer server; 804 immutable signed = "@+kameloso"; 805 string signs; 806 immutable nickname = server.stripModeSign(signed, signs); 807 assert((nickname == "kameloso"), nickname); 808 assert((signs == "@+"), signs); 809 --- 810 811 Params: 812 nickname = String with a signed nickname. 813 server = [dialect.defs.IRCServer|IRCServer], with all its settings. 814 modesigns = Reference string to write the stripped modesigns to. 815 816 Returns: 817 The nickname without any prepended prefix signs. 818 +/ 819 auto stripModesign( 820 const string nickname, 821 const IRCServer server, 822 out string modesigns) pure @safe nothrow @nogc 823 in (nickname.length, "Tried to strip modesigns off an empty nickname") 824 { 825 size_t i; 826 827 for (i = 0; i<nickname.length; ++i) 828 { 829 if (nickname[i] !in server.prefixchars) 830 { 831 break; 832 } 833 } 834 835 modesigns = nickname[0..i]; 836 return nickname[i..$]; 837 } 838 839 /// 840 unittest 841 { 842 IRCServer server; 843 server.prefixchars = 844 [ 845 '@' : 'o', 846 '+' : 'v', 847 '%' : 'h', 848 ]; 849 850 { 851 immutable signed = "@kameloso"; 852 string signs; 853 immutable nickname = signed.stripModesign(server, signs); 854 assert((nickname == "kameloso"), nickname); 855 assert((signs == "@"), signs); 856 } 857 858 { 859 immutable signed = "kameloso"; 860 string signs; 861 immutable nickname = signed.stripModesign(server, signs); 862 assert((nickname == "kameloso"), nickname); 863 assert(!signs.length, signs); 864 } 865 866 { 867 immutable signed = "@+kameloso"; 868 string signs; 869 immutable nickname = signed.stripModesign(server, signs); 870 assert((nickname == "kameloso"), nickname); 871 assert((signs == "@+"), signs); 872 } 873 } 874 875 876 // stripModesign 877 /++ 878 Convenience function to [stripModesign] that doesn't take an out string 879 parameter to store the stripped modesign characters in. 880 881 Example: 882 --- 883 IRCServer server; 884 immutable signed = "@+kameloso"; 885 immutable nickname = signed.stripModeSign(server); 886 assert((nickname == "kameloso"), nickname); 887 assert((signs == "@+"), signs); 888 --- 889 890 Params: 891 nickname = The (potentially) signed nickname to strip the prefix off. 892 server = The [dialect.defs.IRCServer|IRCServer] whose prefix characters to strip. 893 894 Returns: 895 The raw nickname, unsigned. 896 +/ 897 auto stripModesign( 898 const string nickname, 899 const IRCServer server) pure @safe nothrow @nogc 900 { 901 string _; 902 return stripModesign(nickname, server, _); 903 } 904 905 /// 906 @safe unittest 907 { 908 IRCServer server; 909 server.prefixchars = 910 [ 911 '@' : 'o', 912 '+' : 'v', 913 '%' : 'h', 914 ]; 915 916 { 917 immutable signed = "@+kameloso"; 918 immutable nickname = signed.stripModesign(server); 919 assert((nickname == "kameloso"), nickname); 920 } 921 } 922 923 924 // setMode 925 /++ 926 Sets a new or removes a [dialect.defs.IRCChannel.Mode|IRCChannel.Mode]. 927 928 [dialect.defs.IRCChannel.Mode|IRCChannel.Mode]s that are merely a character 929 in `modechars` are simply removed if the *sign* of the mode change is negative, 930 whereas a more elaborate [dialect.defs.IRCChannel.Mode|IRCChannel.Mode] 931 in the `modes` array are only replaced or removed if they match a comparison test. 932 933 Several modes can be specified at once, including modes that take a 934 `data` argument, assuming they are in the proper order (where the 935 `data`-taking modes are at the end of the string). 936 937 Care has to be taken not to have trailing spaces in the arguments. 938 939 Example: 940 --- 941 IRCServer server; 942 IRCChannel channel; 943 944 channel.setMode("+oo", "zorael!NaN@* kameloso!*@*", server); 945 assert(channel.modes.length == 2); 946 channel.setMode("-o", "kameloso!*@*", server); 947 assert(channel.modes.length == 1); 948 channel.setMode("-o" "*!*@*", server); 949 assert(!channel.modes.length); 950 --- 951 952 Params: 953 channel = [dialect.defs.IRCChannel] whose modes are being set. 954 signedModestring = String of the raw mode command, including the 955 prefixing sign (+ or -). 956 data = Appendix to the signed modestring; arguments to the modes that 957 are being set. 958 server = The current [dialect.defs.IRCServer|IRCServer] with all its settings. 959 +/ 960 void setMode( 961 ref IRCChannel channel, 962 const string signedModestring, 963 const string data, 964 const IRCServer server) pure @safe 965 { 966 import std.algorithm.iteration : splitter; 967 import std.algorithm.searching : startsWith; 968 import std.array : array; 969 import std.range : StoppingPolicy, retro, zip; 970 import std.string : indexOf, representation; 971 972 if (!signedModestring.length) return; 973 974 struct SignedModechar 975 { 976 char sign; 977 char modechar; 978 } 979 980 char nextSign = '+'; 981 SignedModechar[] modecharArray; 982 983 foreach (immutable c; signedModestring.representation) 984 { 985 if ((c == '+') || (c == '-')) 986 { 987 nextSign = c; 988 } 989 else if (((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z'))) 990 { 991 modecharArray ~= SignedModechar(nextSign, c); 992 } 993 } 994 995 if (!modecharArray.length) return; 996 997 auto datalines = data.splitter(" ").array.retro; 998 auto moderange = modecharArray.retro; 999 auto ziprange = zip(StoppingPolicy.longest, moderange, datalines); 1000 1001 IRCUser[] carriedExceptions; 1002 1003 ziploop: 1004 foreach (immutable signedModechar, immutable datastring; ziprange) 1005 { 1006 immutable modechar = signedModechar.modechar; 1007 immutable sign = signedModechar.sign; 1008 1009 if ((sign != '+') && (sign != '-')) 1010 { 1011 // Ward against stack corruption 1012 // immutable(SignedModechar)('ÿ', 'ÿ') 1013 break; 1014 } 1015 1016 IRCChannel.Mode newMode; 1017 newMode.modechar = modechar; 1018 1019 if ((modechar == server.exceptsChar) || (modechar == server.invexChar)) 1020 { 1021 // Exception, carry it to the next aMode 1022 carriedExceptions ~= IRCUser(datastring); 1023 continue; 1024 } 1025 1026 if (!datastring.startsWith(server.extbanPrefix) && 1027 (datastring.indexOf('!') != -1) && (datastring.indexOf('@') != -1)) 1028 { 1029 // Looks like a user and not an extban 1030 newMode.user = IRCUser(datastring); 1031 } 1032 else if (datastring.startsWith(server.extbanPrefix)) 1033 { 1034 // extban; https://freenode.net/kb/answer/extbans 1035 // https://defs.ircdocs.horse/defs/extbans.html 1036 // Does not support a mix of normal and second form bans 1037 // e.g. *!*@*$#channel 1038 1039 /+ extban format: 1040 "$a:dannylee$##arguments" 1041 "$a:shr000ms" 1042 "$a:deadfrogs" 1043 "$a:b4b" 1044 "$a:terabits$##arguments" 1045 // "$x:*0x71*" 1046 "$a:DikshitNijjer" 1047 "$a:NETGEAR_WNDR3300" 1048 "$~a:eir"+/ 1049 string slice = datastring[1..$]; // mutable 1050 1051 if (slice[0] == '~') 1052 { 1053 // Negated extban 1054 newMode.negated = true; 1055 slice = slice[1..$]; 1056 } 1057 1058 switch (slice[0]) 1059 { 1060 case 'a': 1061 case 'R': 1062 // Match account 1063 if (slice.indexOf(':') != -1) 1064 { 1065 // More than one field 1066 slice.advancePast(':'); 1067 1068 if (slice.indexOf('$') != -1) 1069 { 1070 // More than one field, first is account 1071 newMode.user.account = slice.advancePast('$'); 1072 newMode.data = slice; 1073 } 1074 else 1075 { 1076 // Whole slice is an account 1077 newMode.user.account = slice; 1078 } 1079 } 1080 else 1081 { 1082 // "$~a" 1083 // "$R" 1084 // FIXME: Figure out how to express this. 1085 newMode.data = slice.length ? 1086 slice : 1087 datastring; 1088 } 1089 break; 1090 1091 case 'j': 1092 //case 'c': // Conflicts with colour ban 1093 // Match channel 1094 slice.advancePast(':'); 1095 newMode.channel = slice; 1096 break; 1097 1098 /*case 'r': 1099 // GECOS/Real name, which we aren't saving currently. 1100 // Can be done if there's a use-case for it. 1101 break;*/ 1102 1103 /*case 's': 1104 // Which server the user(s) the mode refers to are connected to 1105 // which we aren't saving either. Can also be fixed. 1106 break;*/ 1107 1108 default: 1109 // Unhandled extban mode 1110 newMode.data = datastring; 1111 break; 1112 } 1113 } 1114 else 1115 { 1116 // Normal, non-user non-extban mode 1117 newMode.data = datastring; 1118 } 1119 1120 if (sign == '+') 1121 { 1122 if (server.prefixes.indexOf(modechar) != -1) 1123 { 1124 import std.algorithm.searching : canFind; 1125 1126 // Register users with prefix modes (op, halfop, voice, ...) 1127 auto prefixedUsers = newMode.modechar in channel.mods; 1128 if (prefixedUsers && (newMode.data in *prefixedUsers)) 1129 { 1130 continue; 1131 } 1132 1133 channel.mods[newMode.modechar][newMode.data] = true; 1134 continue; 1135 } 1136 else if (server.aModes.indexOf(modechar) != -1) 1137 { 1138 /++ 1139 A = Mode that adds or removes a nick or address to a 1140 list. Always has a parameter. 1141 +/ 1142 1143 // STACKS. 1144 // If an identical Mode exists, add exceptions and skip 1145 foreach (ref listedMode; channel.modes) 1146 { 1147 if (listedMode == newMode) 1148 { 1149 listedMode.exceptions ~= carriedExceptions; 1150 carriedExceptions.length = 0; 1151 continue ziploop; 1152 } 1153 } 1154 1155 newMode.exceptions ~= carriedExceptions; 1156 carriedExceptions.length = 0; 1157 } 1158 else if ( 1159 (server.bModes.indexOf(modechar) != -1) || 1160 (server.cModes.indexOf(modechar) != -1)) 1161 { 1162 /++ 1163 B = Mode that changes a setting and always has a 1164 parameter. 1165 1166 C = Mode that changes a setting and only has a 1167 parameter when set. 1168 +/ 1169 1170 // DOES NOT STACK. 1171 // If an identical Mode exists, overwrite 1172 foreach (ref listedMode; channel.modes) 1173 { 1174 if (listedMode.modechar == modechar) 1175 { 1176 listedMode = newMode; 1177 continue ziploop; 1178 } 1179 } 1180 } 1181 else /*if (server.dModes.indexOf(modechar) != -1)*/ 1182 { 1183 // Some clients assume that any mode not listed is of type D 1184 if (channel.modechars.indexOf(modechar) == -1) channel.modechars ~= modechar; 1185 continue; 1186 } 1187 1188 channel.modes ~= newMode; 1189 } 1190 else if (sign == '-') 1191 { 1192 import std.algorithm.mutation : SwapStrategy, remove; 1193 1194 if (server.prefixes.indexOf(modechar) != -1) 1195 { 1196 import std.algorithm.searching : countUntil; 1197 1198 // Remove users with prefix modes (op, halfop, voice, ...) 1199 auto prefixedUsers = newMode.modechar in channel.mods; 1200 if (!prefixedUsers) continue; 1201 1202 (*prefixedUsers).remove(newMode.data); 1203 } 1204 else if (server.aModes.indexOf(modechar) != -1) 1205 { 1206 /++ 1207 A = Mode that adds or removes a nick or address to a 1208 a list. Always has a parameter. 1209 +/ 1210 1211 // If a comparison matches, remove 1212 channel.modes = channel.modes 1213 .remove!((listed => listed == newMode), SwapStrategy.unstable); 1214 } 1215 else if ( 1216 (server.bModes.indexOf(modechar) != -1) || 1217 (server.cModes.indexOf(modechar) != -1)) 1218 { 1219 /++ 1220 B = Mode that changes a setting and always has a 1221 parameter. 1222 1223 C = Mode that changes a setting and only has a 1224 parameter when set. 1225 +/ 1226 1227 // If the modechar matches, remove 1228 channel.modes = channel.modes.remove!((listed => 1229 listed.modechar == newMode.modechar), SwapStrategy.unstable); 1230 } 1231 else /*if (server.dModes.indexOf(modechar) != -1)*/ 1232 { 1233 // Some clients assume that any mode not listed is of type D 1234 import std.string : indexOf; 1235 1236 immutable modecharIndex = channel.modechars.indexOf(modechar); 1237 if (modecharIndex != -1) 1238 { 1239 import std.string : representation; 1240 1241 // Remove the char from the modechar string 1242 channel.modechars = cast(string)channel.modechars 1243 .dup 1244 .representation 1245 .remove!(SwapStrategy.unstable)(modecharIndex) 1246 .idup; 1247 } 1248 } 1249 } 1250 else 1251 { 1252 assert(0, "Invalid mode sign: " ~ sign); 1253 } 1254 } 1255 } 1256 1257 /// 1258 @safe unittest 1259 { 1260 import std.conv; 1261 import std.stdio; 1262 1263 IRCServer server; 1264 // Freenode: CHANMODES=eIbq,k,flj,CFLMPQScgimnprstz 1265 with (server) 1266 { 1267 aModes = "eIbq"; 1268 bModes = "k"; 1269 cModes = "flj"; 1270 dModes = "CFLMPQScgimnprstz"; 1271 1272 // SpotChat: PREFIX=(Yqaohv)!~&@%+ 1273 prefixes = "Yaohv"; 1274 prefixchars = 1275 [ 1276 '!' : 'Y', 1277 '&' : 'a', 1278 '@' : 'o', 1279 '%' : 'h', 1280 '+' : 'v', 1281 ]; 1282 1283 extbanPrefix = '$'; 1284 exceptsChar = 'e'; 1285 invexChar = 'I'; 1286 } 1287 1288 { 1289 IRCChannel chan; 1290 1291 chan.topic = "Huerbla"; 1292 1293 chan.setMode("+b", "kameloso!~NaN@aasdf.freenode.org", server); 1294 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1295 //writeln("-------------------------------------"); 1296 assert((chan.modes.length == 1), chan.modes.length.to!string); 1297 1298 chan.setMode("+bbe", "hirrsteff!*@* harblsnarf!ident@* NICK!~IDENT@ADDRESS", server); 1299 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1300 //writeln("-------------------------------------"); 1301 assert((chan.modes.length == 3), chan.modes.length.to!string); 1302 1303 chan.setMode("-b", "*!*@*", server); 1304 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1305 //writeln("-------------------------------------"); 1306 assert((chan.modes.length == 3), chan.modes.length.to!string); 1307 1308 chan.setMode("+i", string.init, server); 1309 assert(chan.modechars == "i", chan.modechars); 1310 1311 chan.setMode("+v", "harbl", server); 1312 assert(chan.modechars == "i", chan.modechars); 1313 1314 chan.setMode("-i", string.init, server); 1315 assert(!chan.modechars.length, chan.modechars); 1316 1317 chan.setMode("+l", "200", server); 1318 IRCChannel.Mode lMode; 1319 lMode.modechar = 'l'; 1320 lMode.data = "200"; 1321 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1322 //writeln("-------------------------------------"); 1323 assert((chan.modes[3] == lMode), chan.modes[3].to!string); 1324 1325 chan.setMode("+l", "100", server); 1326 lMode.modechar = 'l'; 1327 lMode.data = "100"; 1328 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1329 //writeln("-------------------------------------"); 1330 assert((chan.modes[3] == lMode), chan.modes[3].to!string); 1331 } 1332 1333 { 1334 IRCChannel chan; 1335 1336 chan.setMode("+CLPcnprtf", "##linux-overflow", server); 1337 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1338 //writeln("-------------------------------------"); 1339 assert(chan.modes[0].data == "##linux-overflow"); 1340 assert(chan.modes.length == 1); 1341 assert(chan.modechars.length == 8); 1342 1343 chan.setMode("+bee", "mynick!myident@myaddress abc!def@ghi jkl!*@*", server); 1344 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1345 //writeln("-------------------------------------"); 1346 assert(chan.modes.length == 2); 1347 assert(chan.modes[1].exceptions.length == 2); 1348 } 1349 1350 { 1351 IRCChannel chan; 1352 1353 chan.setMode("+ns", string.init, server); 1354 foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1355 assert(chan.modes.length == 0); 1356 assert(chan.modechars == "sn", chan.modechars); 1357 1358 chan.setMode("-sn", string.init, server); 1359 foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1360 assert(chan.modes.length == 0); 1361 assert(chan.modechars.length == 0); 1362 } 1363 1364 { 1365 IRCChannel chan; 1366 chan.setMode("+oo", "kameloso zorael", server); 1367 assert(chan.mods['o'].length == 2); 1368 chan.setMode("-o", "kameloso", server); 1369 assert(chan.mods['o'].length == 1); 1370 chan.setMode("-o", "zorael", server); 1371 assert(!chan.mods['o'].length); 1372 } 1373 1374 { 1375 IRCChannel chan; 1376 server.extbanPrefix = '$'; 1377 1378 chan.setMode("+b", "$a:hirrsteff", server); 1379 assert(chan.modes.length); 1380 with (chan.modes[0]) 1381 { 1382 assert((modechar == 'b'), modechar.text); 1383 assert((user.account == "hirrsteff"), user.account); 1384 } 1385 1386 chan.setMode("+q", "$~a:blarf", server); 1387 assert((chan.modes.length == 2), chan.modes.length.text); 1388 with (chan.modes[1]) 1389 { 1390 assert((modechar == 'q'), modechar.text); 1391 assert((user.account == "blarf"), user.account); 1392 assert(negated); 1393 IRCUser blarf; 1394 blarf.nickname = "blarf"; 1395 blarf.account = "blarf"; 1396 assert(blarf.matchesByMask(user)); 1397 } 1398 } 1399 1400 { 1401 IRCChannel chan; 1402 1403 chan.setMode("+t", string.init, server); 1404 assert(!chan.modes.length, chan.modes.length.text); 1405 assert((chan.modechars == "t"), chan.modechars); 1406 1407 chan.setMode("-t+nlk", "42 chankey", server); 1408 assert((chan.modes.length == 2), chan.modes.length.text); 1409 with (chan.modes[0]) 1410 { 1411 assert((modechar == 'k'), modechar.text); 1412 assert((data == "chankey"), data); 1413 } 1414 with (chan.modes[1]) 1415 { 1416 assert((modechar == 'l'), modechar.text); 1417 assert((data == "42"), data); 1418 } 1419 1420 assert((chan.modechars == "n"), chan.modechars); 1421 1422 chan.setMode("-kl", string.init, server); 1423 assert(!chan.modes.length, chan.modes.length.text); 1424 } 1425 } 1426 1427 1428 // IRCParseException 1429 /++ 1430 IRC Parsing Exception, thrown when there were errors parsing. 1431 1432 It is a normal [object.Exception] but with an attached 1433 [dialect.defs.IRCEvent|IRCEvent]. 1434 +/ 1435 final class IRCParseException : Exception 1436 { 1437 /// Bundled [dialect.defs.IRCEvent|IRCEvent], parsing which threw this exception. 1438 IRCEvent event; 1439 1440 /++ 1441 Create a new [IRCParseException], without attaching an 1442 [dialect.defs.IRCEvent|IRCEvent]. 1443 +/ 1444 this(const string message, 1445 const string file = __FILE__, 1446 const size_t line = __LINE__, 1447 Throwable nextInChain = null) pure nothrow @nogc @safe 1448 { 1449 super(message, file, line, nextInChain); 1450 } 1451 1452 /++ 1453 Create a new [IRCParseException], attaching an 1454 [dialect.defs.IRCEvent|IRCEvent] to it. 1455 +/ 1456 this(const string message, 1457 const IRCEvent event, 1458 const string file = __FILE__, 1459 const size_t line = __LINE__, 1460 Throwable nextInChain = null) pure nothrow @nogc @safe 1461 { 1462 this.event = event; 1463 super(message, file, line, nextInChain); 1464 } 1465 } 1466 1467 /// 1468 @safe unittest 1469 { 1470 import std.exception : assertThrown; 1471 1472 IRCEvent event; 1473 1474 assertThrown!IRCParseException((){ throw new IRCParseException("adf"); }()); 1475 1476 assertThrown!IRCParseException(() 1477 { 1478 throw new IRCParseException("adf", event); 1479 }()); 1480 1481 assertThrown!IRCParseException(() 1482 { 1483 throw new IRCParseException("adf", event, "somefile.d"); 1484 }()); 1485 1486 assertThrown!IRCParseException(() 1487 { 1488 throw new IRCParseException("adf", event, "somefile.d", 9999U); 1489 }()); 1490 } 1491 1492 1493 // IRCControlCharacter 1494 /++ 1495 Certain characters that signal specific meaning in an IRC context. 1496 +/ 1497 enum IRCControlCharacter 1498 { 1499 ctcp = 1, /// Client-to-client Protocol marker. 1500 bold = 2, /// Bold text. 1501 colour = 3, /// Colour marker. 1502 reset = 15, /// Colour/formatting reset marker. 1503 invert = 22, /// Inverse text marker. 1504 italics = 29, /// Italics marker. 1505 underlined = 31, /// Underscore marker. 1506 } 1507 1508 1509 // matchesByMask 1510 /++ 1511 Compares this [dialect.defs.IRCUser|IRCUser] with a second one, treating 1512 fields with asterisks as glob wildcards, mimicking `*!*@*` mask matching. 1513 1514 Example: 1515 --- 1516 IRCServer server; 1517 1518 IRCUser u1; 1519 with (u1) 1520 { 1521 nickname = "foo"; 1522 ident = "NaN"; 1523 address = "asdf.asdf.com"; 1524 } 1525 1526 IRCUser u2; 1527 with (u2) 1528 { 1529 nickname = "*"; 1530 ident = "NaN"; 1531 address = "*"; 1532 } 1533 1534 assert(u1.matchesByMask(u2, server.caseMapping)); 1535 assert(u1.matchesByMask(IRCUser("f*!NaN@*.com"), server.caseMapping)); 1536 --- 1537 1538 Params: 1539 this_ = [dialect.defs.IRCUser|IRCUser] to compare. 1540 that = [dialect.defs.IRCUser|IRCUser] to compare `this_` with. 1541 caseMapping = [dialect.defs.IRCServer.CaseMapping|IRCServer.CaseMapping] 1542 with which to translate the nicknames in the relevant masks to lowercase. 1543 1544 Returns: 1545 `true` if the [dialect.defs.IRCUser|IRCUser]s are deemed to match, `false` if not. 1546 +/ 1547 auto matchesByMask( 1548 const IRCUser this_, 1549 const IRCUser that, 1550 const IRCServer.CaseMapping caseMapping = IRCServer.CaseMapping.rfc1459) pure @safe nothrow 1551 { 1552 // unpatternedGlobMatch 1553 /++ 1554 Performs a glob match without taking special consideration of 1555 bracketed patterns (with [, ], { and }). 1556 1557 Params: 1558 first = First string. 1559 second = Second expression string to glob match with the first. 1560 1561 Returns: 1562 True if `first` matches the `second` glob mask, false if not. 1563 +/ 1564 static bool unpatternedGlobMatch( 1565 const string first, 1566 const string second) 1567 { 1568 import std.array : replace; 1569 import std.path : CaseSensitive, globMatch; 1570 1571 enum caseSetting = CaseSensitive.no; 1572 1573 enum openBracketSubstitution = "\1"; 1574 enum closedBracketSubstitution = "\2"; 1575 enum openCurlySubstitution = "\3"; 1576 enum closedCurlySubstitution = "\4"; 1577 1578 immutable firstReplaced = first 1579 .replace("[", openBracketSubstitution) 1580 .replace("]", closedBracketSubstitution) 1581 .replace("{", openCurlySubstitution) 1582 .replace("}", closedCurlySubstitution); 1583 1584 immutable secondReplaced = second 1585 .replace("[", openBracketSubstitution) 1586 .replace("]", closedBracketSubstitution) 1587 .replace("{", openCurlySubstitution) 1588 .replace("}", closedCurlySubstitution); 1589 1590 return firstReplaced.globMatch!caseSetting(secondReplaced); 1591 } 1592 1593 // Only ever compare nicknames case-insensitive 1594 immutable ourLower = this_.nickname.toLowerCase(caseMapping); 1595 immutable theirLower = that.nickname.toLowerCase(caseMapping); 1596 1597 // (unpatterned) globMatch in both directions 1598 // If no match and either is empty, that means they're * 1599 1600 immutable matchNick = ( 1601 (ourLower == theirLower) || 1602 !this_.nickname.length || 1603 !that.nickname.length || 1604 unpatternedGlobMatch(ourLower, theirLower) || 1605 unpatternedGlobMatch(theirLower, ourLower)); 1606 if (!matchNick) return false; 1607 1608 immutable matchIdent = ( 1609 (this_.ident == that.ident) || 1610 !this_.ident.length || 1611 !that.ident.length || 1612 unpatternedGlobMatch(this_.ident, that.ident) || 1613 unpatternedGlobMatch(that.ident, this_.ident)); 1614 if (!matchIdent) return false; 1615 1616 immutable matchAddress = ( 1617 (this_.address == that.address) || 1618 !this_.address.length || 1619 !that.address.length || 1620 unpatternedGlobMatch(this_.address, that.address) || 1621 unpatternedGlobMatch(that.address, this_.address)); 1622 if (!matchAddress) return false; 1623 1624 return true; 1625 } 1626 1627 /// 1628 @safe unittest 1629 { 1630 IRCUser first = IRCUser("kameloso!NaN@wopkfoewopk.com"); 1631 1632 IRCUser second = IRCUser("*!*@*"); 1633 assert(first.matchesByMask(second)); 1634 1635 IRCUser third = IRCUser("kame*!*@*.com"); 1636 assert(first.matchesByMask(third)); 1637 1638 IRCUser fourth = IRCUser("*loso!*@wop*"); 1639 assert(first.matchesByMask(fourth)); 1640 1641 assert(second.matchesByMask(first)); 1642 assert(third.matchesByMask(first)); 1643 assert(fourth.matchesByMask(first)); 1644 1645 IRCUser fifth = IRCUser("kameloso!*@*"); 1646 IRCUser sixth = IRCUser("KAMELOSO!ident@address.com"); 1647 assert(fifth.matchesByMask(sixth)); 1648 1649 IRCUser seventh = IRCUser("^[0V0]^!ID@ADD"); 1650 IRCUser eight = IRCUser("~{0v0}~!id@add"); 1651 assert(seventh.matchesByMask(eight, IRCServer.CaseMapping.rfc1459)); 1652 assert(!seventh.matchesByMask(eight, IRCServer.CaseMapping.strict_rfc1459)); 1653 1654 IRCUser ninth = IRCUser("*!*@170.233.40.144]"); // Accidental trailing ] 1655 IRCUser tenth = IRCUser("Joe!Shmoe@*"); 1656 assert(ninth.matchesByMask(tenth, IRCServer.CaseMapping.rfc1459)); 1657 1658 IRCUser eleventh = IRCUser("abc]!*@*"); 1659 IRCUser twelfth = IRCUser("abc}!abc}@abc}"); 1660 assert(eleventh.matchesByMask(twelfth, IRCServer.CaseMapping.rfc1459)); 1661 } 1662 1663 1664 // isUpper 1665 /++ 1666 Checks whether the passed `char` is in uppercase as per the supplied case mappings. 1667 1668 Params: 1669 c = Character to examine. 1670 caseMapping = Server case mapping; maps uppercase to lowercase characters. 1671 1672 Returns: 1673 `true` if the passed `c` is in uppercase, `false` if not. 1674 +/ 1675 auto isUpper( 1676 const char c, 1677 const IRCServer.CaseMapping caseMapping) pure @safe nothrow @nogc 1678 { 1679 import std.ascii : isUpper; 1680 1681 if ((caseMapping == IRCServer.CaseMapping.rfc1459) || 1682 (caseMapping == IRCServer.CaseMapping.strict_rfc1459)) 1683 { 1684 switch (c) 1685 { 1686 case '[': 1687 case ']': 1688 case '\\': 1689 return true; 1690 case '^': 1691 return (caseMapping == IRCServer.CaseMapping.rfc1459); 1692 1693 default: 1694 break; 1695 } 1696 } 1697 1698 return c.isUpper; 1699 } 1700 1701 1702 // toLower 1703 /++ 1704 Produces the passed `char` in lowercase as per the supplied case mappings. 1705 1706 Params: 1707 c = Character to translate into lowercase. 1708 caseMapping = Server case mapping; maps uppercase to lowercase characters. 1709 1710 Returns: 1711 The passed `c` in lowercase as per the case mappings. 1712 +/ 1713 auto toLower( 1714 const char c, 1715 const IRCServer.CaseMapping caseMapping) pure @safe nothrow @nogc 1716 { 1717 import std.ascii : toLower; 1718 1719 if ((caseMapping == IRCServer.CaseMapping.rfc1459) || 1720 (caseMapping == IRCServer.CaseMapping.strict_rfc1459)) 1721 { 1722 switch (c) 1723 { 1724 case '[': 1725 return '{'; 1726 case ']': 1727 return '}'; 1728 case '\\': 1729 return '|'; 1730 case '^': 1731 if (caseMapping == IRCServer.CaseMapping.rfc1459) 1732 { 1733 return '~'; 1734 } 1735 break; 1736 1737 default: 1738 break; 1739 } 1740 } 1741 1742 return c.toLower; 1743 } 1744 1745 1746 // toLowerCase 1747 /++ 1748 Produces the passed string in lowercase as per the supplied case mappings. 1749 1750 This function is `@trusted` to be able to cast the internal `output` char 1751 array to string. [std.array.Appender|Appender] does this with its 1752 [std.array.Appender.data|data]/[std.array.Appender.opSlice|opSlice] methods. 1753 1754 --- 1755 @property inout(ElementEncodingType!A)[] opSlice() inout @trusted pure nothrow 1756 { 1757 /* @trusted operation: 1758 * casting Unqual!T[] to inout(T)[] 1759 */ 1760 return cast(typeof(return))(_data ? _data.arr : null); 1761 } 1762 --- 1763 1764 So just do the same. 1765 1766 Params: 1767 name = String to parse into lowercase. 1768 caseMapping = Server case mapping; maps uppercase to lowercase characters. 1769 1770 Returns: 1771 The passed `name` string with uppercase characters replaced as per 1772 the case mappings. 1773 +/ 1774 auto toLowerCase( 1775 const string name, 1776 const IRCServer.CaseMapping caseMapping) pure nothrow @trusted 1777 { 1778 import std.string : representation; 1779 1780 char[] output; // mutable 1781 bool dirty; 1782 1783 foreach (immutable i, immutable c; name.representation) 1784 { 1785 if (c.isUpper(caseMapping)) 1786 { 1787 if (!dirty) 1788 { 1789 output.length = name.length; 1790 output[0..i] = name[0..i]; 1791 dirty = true; 1792 } 1793 1794 output[i] = name[i].toLower(caseMapping); 1795 } 1796 else if (dirty) 1797 { 1798 output[i] = name[i]; 1799 } 1800 } 1801 1802 return dirty ? cast(string)output : name; 1803 } 1804 1805 /// 1806 @safe unittest 1807 { 1808 IRCServer.CaseMapping m = IRCServer.CaseMapping.rfc1459; 1809 1810 { 1811 immutable before = "ABCDEF"; 1812 immutable lowercase = toLowerCase(before, m); 1813 assert((lowercase == "abcdef"), lowercase); 1814 } 1815 { 1816 immutable before = "123"; 1817 immutable lowercase = toLowerCase(before, m); 1818 assert((lowercase == "123"), lowercase); 1819 assert(before is lowercase); 1820 assert(before.ptr == lowercase.ptr); 1821 } 1822 { 1823 immutable lowercase = toLowerCase("^[0v0]^", m); 1824 assert((lowercase == "~{0v0}~"), lowercase); 1825 } 1826 { 1827 immutable lowercase = toLowerCase(`A|\|`, m); 1828 assert((lowercase == "a|||"), lowercase); 1829 } 1830 1831 m = IRCServer.caseMapping.ascii; 1832 1833 { 1834 immutable before = "^[0v0]^"; 1835 immutable lowercase = toLowerCase(before, m); 1836 assert((lowercase == "^[0v0]^"), lowercase); 1837 assert(before is lowercase); 1838 assert(before.ptr == lowercase.ptr); 1839 } 1840 { 1841 immutable lowercase = toLowerCase(`A|\|`, m); 1842 assert((lowercase == `a|\|`), lowercase); 1843 } 1844 1845 m = IRCServer.CaseMapping.strict_rfc1459; 1846 1847 { 1848 immutable lowercase = toLowerCase("^[0v0]^", m); 1849 assert((lowercase == "^{0v0}^"), lowercase); 1850 } 1851 } 1852 1853 1854 // opEqualsCaseInsensitive 1855 /++ 1856 Compares two strings to see if they match if case is ignored. 1857 1858 Only works with ASCII. 1859 1860 Params: 1861 lhs = Left-hand side of the comparison. 1862 rhs = Right-hand side of the comparison. 1863 mapping = The server case mapping to apply. 1864 1865 Returns: 1866 `true` if `lhs` and `rhs` are deemed to be case-insensitively equal; 1867 `false` if not. 1868 +/ 1869 auto opEqualsCaseInsensitive( 1870 const string lhs, 1871 const string rhs, 1872 const IRCServer.CaseMapping mapping) pure @safe nothrow @nogc 1873 { 1874 if (lhs.length != rhs.length) return false; 1875 if (lhs is rhs) return true; 1876 1877 foreach (immutable i; 0..lhs.length) 1878 { 1879 immutable c = lhs[i]; 1880 immutable rc = rhs[i]; 1881 1882 if (c == rc) continue; 1883 1884 with (IRCServer.CaseMapping) 1885 switch (c) 1886 { 1887 case 'A': 1888 .. 1889 case 'Z': 1890 if (rc == c+32) continue; 1891 return false; 1892 1893 case 'a': 1894 .. 1895 case 'z': 1896 if (rc == c-32) continue; 1897 return false; 1898 1899 case '[': 1900 if (((mapping == rfc1459) || (mapping == strict_rfc1459)) && 1901 (rc == '{')) 1902 { 1903 continue; 1904 } 1905 return false; 1906 1907 case ']': 1908 if (((mapping == rfc1459) || (mapping == strict_rfc1459)) && 1909 (rc == '}')) 1910 { 1911 continue; 1912 } 1913 return false; 1914 1915 case '\\': 1916 if (((mapping == rfc1459) || (mapping == strict_rfc1459)) && 1917 (rc == '|')) 1918 { 1919 continue; 1920 } 1921 return false; 1922 1923 case '^': 1924 if ((mapping == rfc1459) && (rc == '~')) continue; 1925 return false; 1926 1927 default: 1928 return false; 1929 } 1930 } 1931 1932 return true; 1933 } 1934 1935 /// 1936 @safe unittest 1937 { 1938 immutable c = IRCServer.CaseMapping.rfc1459; 1939 1940 assert("joe".opEqualsCaseInsensitive("JOE", c)); 1941 assert("joe".opEqualsCaseInsensitive("joe", c)); 1942 assert(!"joe".opEqualsCaseInsensitive("Bengt", c)); 1943 assert(!"joe".opEqualsCaseInsensitive("", c)); 1944 assert("^o^".opEqualsCaseInsensitive("~o~", c)); 1945 assert("[derp]FACE".opEqualsCaseInsensitive("{DERP]face", c)); 1946 assert("C:\\".opEqualsCaseInsensitive("c:|", c)); 1947 } 1948 1949 1950 // isValidHostmask 1951 /++ 1952 Makes a cursory verification of a hostmask, ensuring that it doesn't contain 1953 invalid characters. May very well have false positives. 1954 1955 Params: 1956 hostmask = Hostmask string to examine. 1957 server = The current [dialect.defs.IRCServer|IRCServer] with its 1958 [dialect.defs.IRCServer.CaseMapping|IRCServer.CaseMapping]. 1959 1960 Returns: 1961 `true` if the hostmask seems to be valid, `false` if it obviously is not. 1962 +/ 1963 auto isValidHostmask( 1964 const string hostmask, 1965 const IRCServer server) pure @safe nothrow @nogc 1966 { 1967 import std.string : indexOf, representation; 1968 import std.typecons : Flag, No, Yes; 1969 1970 string slice = hostmask; // mutable 1971 1972 static bool isValidIdentOrAddressCharacter(const char c, const Flag!"address" address) 1973 { 1974 switch (c) 1975 { 1976 case 'A': 1977 .. 1978 case 'Z': 1979 case 'a': 1980 .. 1981 case 'z': 1982 case '0': 1983 .. 1984 case '9': 1985 case '-': 1986 case '_': 1987 case '*': 1988 break; 1989 1990 case ':': 1991 case '.': 1992 if (address) 1993 { 1994 break; 1995 } 1996 else 1997 { 1998 goto default; 1999 } 2000 2001 default: 2002 return false; 2003 } 2004 2005 return true; 2006 } 2007 2008 static bool isValidIdent(const string ident) 2009 { 2010 import std.string : representation; 2011 2012 if (!ident.length) return false; 2013 2014 foreach (immutable c; ident.representation) 2015 { 2016 if (!isValidIdentOrAddressCharacter(c, No.address)) return false; 2017 } 2018 2019 return true; 2020 } 2021 2022 static bool isValidAddress(const string address) 2023 { 2024 import std.string : representation; 2025 2026 if (!address.length) return false; 2027 2028 foreach (immutable c; address.representation) 2029 { 2030 if (!isValidIdentOrAddressCharacter(c, Yes.address)) return false; 2031 } 2032 2033 return true; 2034 } 2035 2036 static bool isValidNicknameGlob(const string nickname, const IRCServer server) 2037 { 2038 import std.string : representation; 2039 2040 if (nickname.length > server.maxNickLength) 2041 { 2042 return false; 2043 } 2044 2045 foreach (immutable c; nickname.representation) 2046 { 2047 if (!c.isValidNicknameCharacter && (c != '*')) return false; 2048 } 2049 2050 return true; 2051 } 2052 2053 immutable bangPos = slice.indexOf('!'); 2054 if (bangPos == -1) return false; 2055 immutable nickname = slice[0..bangPos]; 2056 if (!isValidNicknameGlob(nickname, server)) return false; 2057 slice = slice[bangPos+1..$]; 2058 if (!slice.length) return false; 2059 2060 if (slice[0] == '~') slice = slice[1..$]; 2061 immutable atPos = slice.indexOf('@'); 2062 if (atPos == -1) return false; 2063 immutable ident = slice[0..atPos]; 2064 if ((ident != "*") && !isValidIdent(ident)) return false; 2065 slice = slice[atPos+1..$]; 2066 2067 immutable address = slice; 2068 if (!address.length) return false; 2069 return (address == "*") || isValidAddress(address); 2070 } 2071 2072 /// 2073 @safe unittest 2074 { 2075 IRCServer server; 2076 2077 { 2078 immutable hostmask = "*!*@*"; 2079 assert(hostmask.isValidHostmask(server)); 2080 } 2081 { 2082 immutable hostmask = "nick123`!*@*"; 2083 assert(hostmask.isValidHostmask(server)); 2084 } 2085 { 2086 immutable hostmask = "*!~ident0-9_@*"; 2087 assert(hostmask.isValidHostmask(server)); 2088 } 2089 { 2090 immutable hostmask = "*!ident0-9_@*"; 2091 assert(hostmask.isValidHostmask(server)); 2092 } 2093 { 2094 immutable hostmask = "*!~~ident0-9_@*"; 2095 assert(!hostmask.isValidHostmask(server)); 2096 } 2097 { 2098 immutable hostmask = "*!*@address.tld.net"; 2099 assert(hostmask.isValidHostmask(server)); 2100 } 2101 { 2102 immutable hostmask = "*!*@~address.tld.net"; 2103 assert(!hostmask.isValidHostmask(server)); 2104 } 2105 { 2106 immutable hostmask = "*!*@2001::ff:09:ff"; 2107 assert(hostmask.isValidHostmask(server)); 2108 } 2109 { 2110 immutable hostmask = "kameloso!~kameloso@2001*"; 2111 assert(hostmask.isValidHostmask(server)); 2112 } 2113 { 2114 immutable hostmask = "harbl*!~dolmen@*"; 2115 assert(hostmask.isValidHostmask(server)); 2116 } 2117 }