1 module irc.client; 2 3 import irc.exception; 4 import irc.linebuffer; 5 import irc.protocol; 6 public import irc.protocol : IrcUser; 7 8 import irc.ctcp; 9 import irc.util; 10 11 import std.socket; 12 public import std.socket : InternetAddress; 13 14 import std.exception; 15 import std.algorithm; 16 import std.array; 17 import std.range; 18 import std.regex; // TEMP: For EOL identification 19 import std..string : format, indexOf, sformat, munch; 20 import std.traits; 21 import std.typetuple; 22 import std.utf : byChar; 23 24 //debug=Dirk; 25 debug(Dirk) static import std.stdio; 26 debug(Dirk) import std.conv; 27 28 // Not using standard library because of auto-decoding issues 29 private size_t indexOfNewline(in char[] haystack) pure nothrow @safe @nogc 30 { 31 foreach(i, char c; haystack) 32 if(c == '\r' || c == '\n') 33 return i; 34 return -1; 35 } 36 37 private inout(char)[] stripNewlinesLeft(inout(char)[] haystack) 38 { 39 while(!haystack.empty && (haystack[0] == '\r' || haystack[0] == '\n')) 40 haystack = haystack[1 .. $]; 41 return haystack; 42 } 43 44 /** 45 * Thrown if the server sends an error message to the client. 46 */ 47 class IrcErrorException : Exception 48 { 49 IrcClient client; 50 51 this(IrcClient client, string message, string file = __FILE__, size_t line = __LINE__) 52 { 53 super(message, file, line); 54 this.client = client; 55 } 56 57 this(IrcClient client, string message, Exception cause, string file = __FILE__, size_t line = __LINE__) 58 { 59 super(message, file, line, cause); 60 this.client = client; 61 } 62 } 63 64 void unsubscribeHandler(T)(ref T[] event, T handler) 65 { 66 enum strategy = 67 is(ReturnType!T == void)? SwapStrategy.unstable : SwapStrategy.stable; 68 69 event = event.remove!(e => e == handler, strategy); 70 } 71 72 /** 73 * Represents an IRC client connection. 74 * 75 * Use the separate type $(DPREF tracker, IrcTracker) returned by 76 * $(DPREF tracker, track) to keep track of the channels the 77 * user for this connection is a member of, and the members of 78 * those channels. 79 */ 80 class IrcClient 81 { 82 private: 83 string m_nick = "dirkuser"; 84 string m_user = "dirk"; 85 string m_name = "dirk"; 86 Address m_address = null; 87 bool _connected = false; 88 89 char[] buffer; 90 IncomingLineBuffer lineBuffer; 91 92 // ISUPPORT data 93 // PREFIX 94 static immutable char[2][] defaultPrefixedChannelModes = [['@', 'o'], ['+', 'v']]; // RFC2812 95 const(char[2])[] prefixedChannelModes = defaultPrefixedChannelModes; // [[prefix, mode], ...] 96 97 // CHANMODES 98 string channelListModes = "b"; // Type A 99 string channelParameterizedModes = null; // Type B 100 string channelNullaryRemovableModes = null; // Type C 101 string channelSettingModes = null; // Type D 102 103 // NICKLEN 104 enum defaultMaxNickLength = 9; 105 ushort _maxNickLength = defaultMaxNickLength; 106 bool enforceMaxNickLength = false; // Only enforce max nick length when server has specified one 107 108 // NETWORK 109 string _networkName = null; 110 111 // MODES 112 enum defaultMessageModeLimit = 3; // RFC2812 113 ubyte messageModeLimit = defaultMessageModeLimit; 114 115 package: 116 Socket socket; 117 118 public: 119 /** 120 * Create a new unconnected IRC client. 121 * 122 * If $(D socket) is provided, it must be an unconnected TCP socket. 123 * Provide an instance of $(RREF ssl, socket, SslSocket) to 124 * use SSL/TLS. 125 * 126 * User information should be configured before connecting. 127 * Only the nick name can be changed after connecting. 128 * Event callbacks can be added both before and after connecting. 129 * See_Also: 130 * $(MREF IrcClient.connect) 131 */ 132 this() 133 { 134 this(new TcpSocket()); 135 } 136 137 /// Ditto 138 this(Socket socket) 139 { 140 this.socket = socket; 141 this.buffer = new char[](2048); 142 this.lineBuffer = IncomingLineBuffer(buffer, &onReceivedLine); 143 } 144 145 private void onReceivedLine(in char[] rawLine) 146 { 147 debug(Dirk) std.stdio.writefln(`>> "%s" pos: %s`, rawLine, lineBuffer.position); 148 149 IrcLine line; 150 151 auto succeeded = parse(rawLine, line); 152 assert(succeeded); 153 154 handle(line); 155 } 156 157 /** 158 * Connect this client to a server. 159 * Params: 160 * serverAddress = address of server 161 * password = server _password, or $(D null) to specify no _password 162 */ 163 void connect(Address serverAddress, in char[] password) 164 { 165 enforceEx!UnconnectedClientException(!connected, "IrcClient is already connected"); 166 167 socket.connect(serverAddress); 168 169 m_address = serverAddress; 170 _connected = true; 171 172 if(password.length) 173 writef("PASS %s", password); 174 175 writef("NICK %s", nickName); 176 writef("USER %s * * :%s", userName, realName); // TODO: Initial user-mode argument 177 } 178 179 /// Ditto 180 void connect(Address serverAddress) 181 { 182 connect(serverAddress, null); 183 } 184 185 /** 186 * Read all available data from the connection, 187 * parse all complete IRC messages and invoke registered callbacks. 188 * Returns: 189 * $(D true) if the connection was closed. 190 * See_Also: 191 * $(DPREF eventloop, IrcEventLoop.run) 192 */ 193 bool read() 194 { 195 enforceEx!UnconnectedClientException(connected, "cannot read from unconnected IrcClient"); 196 197 while(connected) 198 { 199 socket.blocking = false; // TODO: Make writes non-blocking too, so this isn't needed 200 auto received = socket.receive(buffer[lineBuffer.position .. $]); 201 if(received == Socket.ERROR) 202 { 203 if(wouldHaveBlocked()) 204 { 205 socket.blocking = true; 206 break; 207 } 208 else 209 throw new Exception("socket read operation failed: " ~ socket.getErrorText()); 210 } 211 else if(received == 0) 212 { 213 debug(Dirk) std.stdio.writeln("remote ended connection"); 214 socket.close(); 215 _connected = false; 216 return true; 217 } 218 219 socket.blocking = true; 220 lineBuffer.commit(received); 221 } 222 223 return !connected; 224 } 225 226 /** 227 * Write a raw IRC protocol message to the connection stream. 228 * 229 * If there is more than one argument, then the first argument is formatted 230 * with subsequent arguments. Arguments must not contain newlines. 231 * Messages longer than 510 characters (UTF-8 code units) will be cut off. 232 * It is the caller's responsibility to ensure a cut-off message is valid. 233 * See_Also: 234 * $(STDREF format, formattedWrite) 235 * Throws: 236 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 237 */ 238 void writef(T...)(in char[] messageFormat, T formatArgs) 239 if(T.length) 240 { 241 import std.format : formattedWrite; 242 243 enforceEx!UnconnectedClientException(connected, "cannot write to unconnected IrcClient"); 244 245 char[512] formatBuffer = void; 246 247 auto tail = formatBuffer[0 .. $ - 2]; 248 formattedWrite((const(char)[] chunk) { 249 immutable chunkSize = min(chunk.length, tail.length); 250 tail[0 .. chunkSize] = chunk[0 .. chunkSize]; 251 tail = tail[chunkSize .. $]; 252 }, messageFormat, formatArgs); 253 254 auto message = formatBuffer[0 .. $ - tail.length]; 255 message[$ - 2 .. $] = "\r\n"; 256 257 debug(Dirk) 258 { 259 auto sansNewline = message[0 .. $ - 2]; 260 std.stdio.writefln(`<< "%s" (length: %d)`, sansNewline, sansNewline.length); 261 } 262 263 socket.send(message); 264 } 265 266 /// Ditto 267 void writef(in char[] rawline) 268 { 269 enforceEx!UnconnectedClientException(connected, "cannot write to unconnected IrcClient"); 270 socket.send(rawline[0 .. min($, 510)]); 271 socket.send("\r\n"); 272 } 273 274 // TODO: attempt not to split lines in the middle of code points or graphemes 275 private void sendMessage(in char[] command, in char[] target, in char[] message) 276 { 277 auto buffer = OutgoingLineBuffer(this.socket, command, target); 278 const(char)[] messageTail = message.stripNewlinesLeft; 279 280 while(messageTail.length) 281 { 282 immutable maxChunkSize = min(messageTail.length, buffer.capacity); 283 immutable newlinePos = messageTail[0 .. maxChunkSize].indexOfNewline; 284 immutable hasNewline = newlinePos != -1; 285 immutable chunkEnd = hasNewline? newlinePos : maxChunkSize; 286 287 buffer.consume(messageTail, chunkEnd); 288 buffer.flush(); 289 290 if(hasNewline) 291 messageTail = messageTail.stripNewlinesLeft; 292 } 293 } 294 295 private void sendMessage(Range)(in char[] command, in char[] target, Range message) 296 if(isInputRange!Range && isSomeChar!(ElementType!Range)) 297 { 298 static if(!is(Unqual!(ElementType!Range) == char)) 299 { 300 import std.utf : byChar; 301 auto r = message.byChar; 302 } 303 else 304 alias r = message; 305 306 r = r.stripLeft!(c => c == '\r' || c == '\n'); 307 308 auto buffer = OutgoingLineBuffer(this.socket, command, target); 309 auto messageBuffer = buffer.messageBuffer; 310 size_t i = 0; 311 312 while(!r.empty) 313 { 314 auto c = r.front; 315 316 if(c == '\r' || c == '\n') 317 { 318 buffer.commit(i); 319 buffer.flush(); 320 i = 0; 321 r = r.stripLeft!(c => c == '\r' || c == '\n'); 322 } 323 else 324 { 325 messageBuffer[i++] = c; 326 r.popFront(); 327 if(i == messageBuffer.length) 328 { 329 buffer.commit(i); 330 buffer.flush(); 331 i = 0; 332 } 333 } 334 } 335 336 if(i != 0) 337 { 338 buffer.commit(i); 339 buffer.flush(); 340 } 341 } 342 343 private void sendMessage(T...)(in char[] command, in char[] target, in char[] messageFormat, T formatArgs) 344 if(T.length) 345 { 346 import std.format : formattedWrite; 347 348 auto buffer = OutgoingLineBuffer(this.socket, command, target); 349 350 formattedWrite((const(char)[] chunk) { 351 if(!chunk.length) 352 return; 353 354 if(!buffer.hasMessage) 355 chunk = chunk.stripNewlinesLeft; 356 357 while(chunk.length > buffer.capacity) 358 { 359 immutable newlinePos = chunk[0 .. buffer.capacity].indexOfNewline; 360 immutable hasNewline = newlinePos != -1; 361 immutable chunkEnd = hasNewline? newlinePos : buffer.capacity; 362 363 buffer.consume(chunk, chunkEnd); 364 buffer.flush(); 365 366 if(hasNewline) 367 chunk = chunk.stripNewlinesLeft; // normalize consecutive newline characters 368 } 369 370 auto newlinePos = chunk.indexOfNewline; 371 while(newlinePos != -1) 372 { 373 buffer.consume(chunk, newlinePos); 374 buffer.flush(); 375 chunk = chunk.stripNewlinesLeft; 376 newlinePos = chunk.indexOfNewline; // normalize consecutive newline characters 377 } 378 379 buffer.consume(chunk, chunk.length); 380 }, messageFormat, formatArgs); 381 382 if(buffer.hasMessage) 383 buffer.flush(); 384 } 385 386 /** 387 * Send lines of chat to a channel or user. 388 * Each line in $(D message) is sent as one _message. 389 * Lines exceeding the IRC _message length limit will be 390 * split up into multiple messages. 391 * Params: 392 * target = channel or nick name to _send to 393 * message = _message(s) to _send. Can contain multiple lines. 394 * Throws: 395 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 396 */ 397 void send(in char[] target, in char[] message) 398 { 399 sendMessage("PRIVMSG", target, message); 400 } 401 402 /// Ditto 403 void send(Range)(in char[] target, Range message) 404 if(isInputRange!Range && isSomeChar!(ElementType!Range)) 405 { 406 sendMessage("PRIVMSG", target, message); 407 } 408 409 /** 410 * Send formatted lines of chat to a channel or user. 411 * Each line in the formatted result is sent as one message. 412 * Lines exceeding the IRC message length limit will be 413 * split up into multiple messages. 414 * Params: 415 * target = channel or nick name to _send to 416 * fmt = message format 417 * fmtArgs = format arguments 418 * Throws: 419 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 420 * See_Also: 421 * $(STDREF format, formattedWrite) 422 */ 423 void sendf(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs) 424 { 425 sendMessage("PRIVMSG", target, fmt, fmtArgs); 426 } 427 428 /** 429 * Send notices to a channel or user. 430 * Each line in $(D message) is sent as one _notice. 431 * Lines exceeding the IRC _message length limit will be 432 * split up into multiple notices. 433 * Params: 434 * target = channel or nick name to _notice 435 * message = notices(s) to send. Can contain multiple lines. 436 * Throws: 437 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 438 */ 439 void notice(in char[] target, in char[] message) 440 { 441 sendMessage("NOTICE", target, message); 442 } 443 444 /// Ditto 445 void notice(Range)(in char[] target, Range message) 446 if(isInputRange!Range && isSomeChar!(ElementType!Range)) 447 { 448 sendMessage("NOTICE", target, message); 449 } 450 451 /** 452 * Send formatted notices to a channel or user. 453 * Each line in the formatted result is sent as one notice. 454 * Lines exceeding the IRC message length limit will be 455 * split up into multiple notices. 456 * Params: 457 * target = channel or nick name to _send to 458 * fmt = message format 459 * fmtArgs = format arguments 460 * Throws: 461 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 462 * See_Also: 463 * $(STDREF format, formattedWrite) 464 */ 465 void noticef(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs) 466 { 467 sendMessage("NOTICE", target, fmt, fmtArgs); 468 } 469 470 /** 471 * Send a CTCP _query to a channel or user. 472 */ 473 // TODO: reuse buffer for output 474 void ctcpQuery(in char[] target, in char[] query) 475 { 476 send(target, ctcpMessage(query)); 477 } 478 479 /// Ditto 480 void ctcpQuery(in char[] target, in char[] tag, in char[] data) 481 { 482 send(target, ctcpMessage(tag, data)); 483 } 484 485 /** 486 * Send a CTCP _reply to a user. 487 */ 488 void ctcpReply(in char[] targetNick, in char[] reply) 489 { 490 notice(targetNick, ctcpMessage(reply)); 491 } 492 493 /// Ditto 494 void ctcpReply(in char[] targetNick, in char[] tag, in char[] data) 495 { 496 notice(targetNick, ctcpMessage(tag, data)); 497 } 498 499 /** 500 * Send a CTCP _error message reply. 501 * Params: 502 * invalidData = data that caused the _error 503 * error = human-readable _error message 504 */ 505 void ctcpError(in char[] targetNick, in char[] invalidData, in char[] error) 506 { 507 notice(targetNick, ctcpMessage("ERRMSG", format("%s :%s", invalidData, error))); 508 } 509 510 /** 511 * Check if this client is _connected. 512 */ 513 bool connected() const @property 514 { 515 return _connected; 516 } 517 518 /** 519 * Address of the server this client is currently connected to, 520 * or null if this client is not connected. 521 */ 522 inout(Address) serverAddress() inout pure @property 523 { 524 return m_address; 525 } 526 527 /** 528 * Real name of the user for this client. 529 * 530 * Cannot be changed after connecting. 531 */ 532 string realName() const pure @property 533 { 534 return m_name; 535 } 536 537 /// Ditto 538 void realName(string newRealName) @property 539 { 540 enforce(!connected, "cannot change real name while connected"); 541 enforce(!newRealName.empty); 542 m_name = newRealName; 543 } 544 545 /** 546 * User name of the user for this client. 547 * 548 * Cannot be changed after connecting. 549 */ 550 string userName() const pure @property 551 { 552 return m_user; 553 } 554 555 /// Ditto 556 void userName(string newUserName) @property 557 { 558 enforce(!connected, "cannot change user-name while connected"); 559 enforce(!newUserName.empty); 560 m_user = newUserName; 561 } 562 563 /** 564 * Nick name of the user for this client. 565 * 566 * Setting this property when connected can cause the $(MREF IrcClient.onNickInUse) event to fire. 567 */ 568 string nickName() const pure @property 569 { 570 return m_nick; 571 } 572 573 /// Ditto 574 void nickName(in char[] newNick) @property 575 { 576 enforce(!newNick.empty); 577 578 if(enforceMaxNickLength) 579 enforce(newNick.length <= _maxNickLength, 580 `desired nick name "%s" (%s bytes) is too long; nick name must be within %s bytes` 581 .format(newNick, newNick.length, _maxNickLength)); 582 583 if(connected) // m_nick will be set later if the nick is accepted. 584 writef("NICK %s", newNick); 585 else 586 m_nick = newNick.idup; 587 } 588 589 /// Ditto 590 // Duplicated to show up nicer in DDoc - previously used a template and aliases 591 void nickName(string newNick) @property 592 { 593 enforce(!newNick.empty); 594 595 if(enforceMaxNickLength) 596 enforce(newNick.length <= _maxNickLength, 597 `desired nick name "%s" (%s bytes) is too long; nick name must be within %s bytes` 598 .format(newNick, newNick.length, _maxNickLength)); 599 600 if(connected) // m_nick will be set later if the nick is accepted. 601 writef("NICK %s", newNick); 602 else 603 m_nick = newNick; 604 } 605 606 /// Ditto 607 deprecated alias nick = nickName; 608 609 /** 610 * The name of the IRC network the server is part of, or $(D null) 611 * if the server has not advertised the network name. 612 */ 613 string networkName() const @property nothrow @nogc pure 614 { 615 return _networkName; 616 } 617 618 /** 619 * The maximum number of characters (bytes) allowed in this user's nick name. 620 * 621 * The limit is network-specific. 622 */ 623 ushort maxNickNameLength() const @property nothrow @nogc pure 624 { 625 return _maxNickLength; 626 } 627 628 /** 629 * Add or remove user modes to/from this user. 630 */ 631 void addUserModes(in char[] modes...) 632 { 633 writef("MODE %s +%s", m_nick, modes); 634 } 635 636 /// Ditto 637 void removeUserModes(in char[] modes...) 638 { 639 writef("MODE %s -%s", m_nick, modes); 640 } 641 642 private void editChannelModes(Modes, Args)(char editAction, in char[] channel, Modes modes, Args args) 643 if(allSatisfy!(isInputRange, Modes, Args) && isSomeChar!(ElementType!Modes) && isSomeString!(ElementType!Args)) 644 in { 645 assert(modes.length == args.length); 646 assert(modes.length <= messageModeLimit); 647 } body { 648 writef("MODE %s %c%s %-(%s%| %)", editAction, channel, modes, args); 649 } 650 651 private void editChannelList(char editAction, in char[] channel, char list, in char[][] addresses...) 652 { 653 import std.range : chunks; 654 655 enforce(channelListModes.canFind(list), 656 `specified channel mode "` ~ list ~ `" is not a list mode`); 657 658 foreach(chunk; addresses.chunks(messageModeLimit)) // TODO: split up if too long 659 { 660 if(messageModeLimit <= 16) // arbitrary number 661 { 662 char[16] modeBuffer = list; 663 editChannelModes(editAction, channel, modeBuffer[0 .. chunk.length], chunk); 664 } 665 else 666 editChannelModes(editAction, channel, list.repeat(chunk.length).byChar(), chunk); 667 } 668 } 669 670 /** 671 * Add or remove an address to/from a _channel list. 672 Examples: 673 Ban Alice and Bob from _channel #foo: 674 ------ 675 client.addToChannelList("#foo", 'b', "Alice!*@*", "Bob!*@*"); 676 ------ 677 */ 678 void addToChannelList(in char[] channel, char list, in char[][] addresses...) 679 { 680 editChannelList('+', channel, list, addresses); 681 } 682 683 /// Ditto 684 void removeFromChannelList(in char[] channel, char list, in char[][] addresses...) 685 { 686 editChannelList('-', channel, list, addresses); 687 } 688 689 /** 690 * Add or remove channel modes in the given channel. 691 * Examples: 692 Give channel operator status (+o) to Alice and voice status (+v) to Bob in channel #foo: 693 ------ 694 client.addChannelModes("#foo", ChannelMode('o', "Alice"), ChannelMode('v', "Bob")); 695 ------ 696 */ 697 struct ChannelMode 698 { 699 char mode; /// 700 const(char)[] argument; /// Ditto 701 } 702 703 /// Ditto 704 void addChannelModes(in char[] channel, ChannelMode[] modes...) 705 { 706 import std.range : chunks; 707 708 foreach(chunk; modes.chunks(messageModeLimit)) // TODO: split up if too long 709 writef("MODE %s +%s %-(%s%| %)", channel, 710 modes.map!(pair => pair.mode), 711 modes.map!(pair => pair.argument).filter!(arg => !arg.empty)); 712 } 713 714 /// Ditto 715 void removeChannelModes(in char[] channel, ChannelMode[] modes...) 716 { 717 import std.range : chunks; 718 719 foreach(chunk; modes.chunks(messageModeLimit)) // TODO: split up if too long 720 writef("MODE %s -%s %-(%s%| %)", channel, 721 modes.map!(pair => pair.mode), 722 modes.map!(pair => pair.argument)); 723 } 724 725 /** 726 * Join a _channel. 727 * Params: 728 * channel = _channel to _join 729 * Throws: 730 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 731 */ 732 void join(in char[] channel) 733 { 734 writef("JOIN %s", channel); 735 } 736 737 /** 738 * Join a passworded _channel. 739 * Params: 740 * channel = _channel to _join 741 * key = _channel password 742 * Throws: 743 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 744 */ 745 void join(in char[] channel, in char[] key) 746 { 747 writef("JOIN %s :%s", channel, key); 748 } 749 750 /** 751 * Leave a _channel. 752 * Params: 753 * channel = _channel to leave 754 * Throws: 755 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 756 */ 757 void part(in char[] channel) 758 { 759 writef("PART %s", channel); 760 //fireEvent(onMePart, channel); 761 } 762 763 /** 764 * Leave a _channel with a parting _message. 765 * Params: 766 * channel = _channel to leave 767 * message = parting _message 768 * Throws: 769 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 770 */ 771 void part(in char[] channel, in char[] message) 772 { 773 writef("PART %s :%s", channel, message); 774 } 775 776 /** 777 * Kick one or more _users from a _channel. 778 * 779 * This _user must have channel operator status in $(D channel). 780 * Params: 781 * channel = _channel to kick user(s) from 782 * users = _user(s) to kick 783 * comment = _comment to broadcast with the _kick message, 784 * which typically contains the reason the _user is being kicked 785 */ 786 void kick()(in char[] channel, in char[] user) 787 { 788 writef("KICK %s %s", channel, user); 789 } 790 791 /// Ditto 792 void kick()(in char[] channel, in char[] user, in char[] comment) 793 { 794 writef("KICK %s %s :%s", channel, user, comment); 795 } 796 797 /// Ditto 798 void kick(Range)(in char[] channel, Range users) 799 if(isInputRange!Range && isSomeString!(ElementType!Range)) 800 { 801 writef("KICK %s %(%s%|,%)", channel, users); 802 } 803 804 /// Ditto 805 void kick(Range)(in char[] channel, Range users, in char[] comment) 806 if(isInputRange!Range && isSomeString!(ElementType!Range)) 807 { 808 writef("KICK %s %(%s%|,%) :%s", channel, users, comment); 809 } 810 811 /** 812 * Kick users from channels in a single message. 813 * 814 * $(D channelUserPairs) must be a range of $(STDREF typecons, Tuple) 815 * pairs of strings, where the first string is the name of a channel 816 * and the second string is the user to kick from that channel. 817 */ 818 void kick(Range)(Range channelUserPairs) 819 if(isInputRange!Range && 820 isTuple!(ElementType!Range) && ElementType!Range.length == 2 && 821 allSatisfy!(isSomeString, ElementType!Range.Types)) 822 { 823 writef("KICK %(%s%|,%) %(%s%|,%)", 824 channelUserPairs.map!(pair => pair[0]), 825 channelUserPairs.map!(pair => pair[1])); 826 } 827 828 /// Ditto 829 void kick(Range)(Range channelUserPairs, in char[] comment) 830 if(isInputRange!Range && 831 isTuple!(ElementType!Range) && ElementType!Range.length == 2 && 832 allSatisfy!(isSomeString, ElementType!Range.Types)) 833 { 834 writef("KICK %(%s%|,%) %(%s%|,%) :%s", 835 channelUserPairs.map!(pair => pair[0]), 836 channelUserPairs.map!(pair => pair[1]), 837 comment); 838 } 839 840 /** 841 * Query the user name and host name of up to 5 users. 842 * Params: 843 * nicks = between 1 and 5 nick names to query 844 * See_Also: 845 * $(MREF IrcClient.onUserhostReply) 846 */ 847 void queryUserhost(in char[][] nicks...) 848 { 849 version(assert) 850 { 851 import core.exception; 852 if(nicks.length < 1 || nicks.length > 5) 853 throw new RangeError(); 854 } 855 writef("USERHOST %-(%s%| %)", nicks); 856 } 857 858 /** 859 * Query information about a particular user. 860 * Params: 861 * nick = target user's nick name 862 * See_Also: 863 * $(MREF IrcClient.onWhoisReply) 864 */ 865 void queryWhois(in char[] nick) 866 { 867 writef("WHOIS %s", nick); 868 } 869 870 /** 871 * Query the list of members in the given channels. 872 * See_Also: 873 * $(MREF IrcClient.onNameList) 874 */ 875 void queryNames(in char[][] channels...) 876 { 877 // TODO: support automatic splitting of messages 878 //writef("NAMES %s", channels.map!(channel => channel[]).joiner(",")); 879 880 // NOTE: one message per channel because some servers ignore subsequent channels (confirmed: IRCd-Hybrid) 881 foreach(channel; channels) 882 writef("NAMES %s", channel); 883 } 884 885 /** 886 * Leave and disconnect from the server. 887 * Params: 888 * message = comment sent in _quit notification 889 * Throws: 890 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 891 */ 892 void quit(in char[] message) 893 { 894 writef("QUIT :%s", message); 895 socket.close(); 896 _connected = false; 897 } 898 899 /// Invoked when this client has successfully connected to a server. 900 void delegate()[] onConnect; 901 902 /** 903 * Invoked when a message is picked up by the user for this client. 904 * Params: 905 * user = _user who sent the message 906 * target = message _target. This is either the nick of this client in the case of a personal 907 * message, or the name of the channel which the message was sent to. 908 */ 909 void delegate(IrcUser user, in char[] target, in char[] message)[] onMessage; 910 911 /** 912 * Invoked when a notice is picked up by the user for this client. 913 * Params: 914 * user = _user who sent the notice 915 * target = notice _target. This is either the nick of this client in the case of a personal 916 * notice, or the name of the channel which the notice was sent to. 917 */ 918 void delegate(IrcUser user, in char[] target, in char[] message)[] onNotice; 919 920 /** 921 * Invoked when a user receives a new nickname. 922 * 923 * When the user is this user, the $(MREF IrcClient.nick) property will return the old nickname 924 * until after all $(D _onNickChange) callbacks have been invoked. 925 * Params: 926 * user = user which nickname was changed; the $(D nick) field contains the old nickname. Can be this user 927 * newNick = new nickname of user 928 */ 929 void delegate(IrcUser user, in char[] newNick)[] onNickChange; 930 931 /** 932 * Invoked following a call to $(MREF IrcClient.join) when the _channel was successfully joined. 933 * Params: 934 * channel = _channel that was successfully joined 935 */ 936 void delegate(in char[] channel)[] onSuccessfulJoin; 937 938 /** 939 * Invoked when another user joins a channel that this user is a member of. 940 * Params: 941 * user = joining user 942 * channel = channel that was joined 943 */ 944 void delegate(IrcUser user, in char[] channel)[] onJoin; 945 946 /** 947 * Invoked when a user parts a channel that this user is a member of. 948 * Also invoked when this user parts a channel. 949 * Params: 950 * user = parting user 951 * channel = channel that was parted 952 */ 953 void delegate(IrcUser user, in char[] channel)[] onPart; 954 955 // TODO: public? 956 // package void delegate(in char[] channel)[] onMePart; 957 958 /** 959 * Invoked when another user disconnects from the network. 960 * Params: 961 * user = disconnecting user 962 * comment = quit message 963 */ 964 void delegate(IrcUser user, in char[] comment)[] onQuit; 965 966 /** 967 * Invoked when a user is kicked (forcefully removed) from a channel that this user is a member of. 968 * Params: 969 * kicker = user that initiated the kick 970 * channel = channel from which the user was kicked 971 * kickedNick = nickname of the user that was kicked 972 * comment = comment sent with the kick; usually describing the reason the user was kicked. Can be null 973 */ 974 void delegate(IrcUser kicker, in char[] channel, in char[] kickedNick, in char[] comment)[] onKick; 975 976 /** 977 * Invoked when a list of member nick names for a channel are received. 978 * 979 * The list is received after a successful join to a channel by this user, 980 * or when explicitly queried with $(MREF IrcClient.queryNames). 981 * 982 * The list for a single invocation is partial; 983 * the event can be invoked several times for the same channel 984 * as a response to a single trigger. The list is signaled complete 985 * when $(MREF IrcClient.onNameListEnd) is invoked. 986 * Params: 987 * channel = channel of which the users are members 988 * nickNames = list of member nicknames 989 */ 990 void delegate(in char[] channel, in char[][] nickNames)[] onNameList; 991 992 /** 993 * Invoked when the complete list of members of a _channel have been received. 994 * All invocations of $(D onNameList) between invocations of this event 995 * are part of the same member list. 996 * See_Also: 997 * $(MREF IrcClient.onNameList) 998 */ 999 void delegate(in char[] channel)[] onNameListEnd; 1000 1001 /** 1002 * Invoked when a CTCP query is received in a message. 1003 * $(MREF IrcClient.onMessage) is not invoked for the given message 1004 * when onCtcpQuery has a non-zero number of registered handlers. 1005 * Note: 1006 * This callback is only invoked when there is a CTCP message at the start 1007 * of the message, and any subsequent CTCP messages in the same message are 1008 * discarded. To handle multiple CTCP queries in one message, use 1009 * $(MREF IrcClient.onMessage) with $(DPREF ctcp, ctcpExtract). 1010 */ 1011 void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpQuery; 1012 1013 /** 1014 * Invoked when a CTCP reply is received in a notice. 1015 * $(MREF IrcClient.onNotice) is not invoked for the given notice 1016 * when onCtcpReply has a non-zero number of registered handlers. 1017 * Note: 1018 * This callback is only invoked when there is a CTCP message at the start 1019 * of the notice, and any subsequent CTCP messages in the same notice are 1020 * discarded. To handle multiple CTCP replies in one notice, use 1021 * $(MREF IrcClient.onNotice) with $(DPREF ctcp, ctcpExtract). 1022 */ 1023 void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpReply; 1024 1025 /** 1026 * Invoked when the requested nick name of the user for this client is already in use. 1027 * 1028 * Return a non-null string to provide a new nick. No further callbacks in the list 1029 * are called once a callback provides a nick. 1030 * Params: 1031 * newNick = the nick name that was requested. 1032 * Note: 1033 * The current nick name can be read from the $(MREF IrcClient.nick) property of this client. 1034 */ 1035 const(char)[] delegate(in char[] newNick)[] onNickInUse; 1036 1037 /** 1038 * Invoked when a _channel is joined, a _topic is set in a _channel or when 1039 * the current _topic was requested. 1040 * 1041 * Params: 1042 * channel 1043 * topic = _topic or new _topic for channel 1044 */ 1045 void delegate(in char[] channel, in char[] topic)[] onTopic; 1046 1047 /** 1048 * Invoked when a _channel is joined or when the current _topic was requested. 1049 * 1050 * Params: 1051 * channel 1052 * nick = _nick name of user who set the topic 1053 * time = _time the topic was set 1054 */ 1055 void delegate(in char[] channel, in char[] nick, in char[] time)[] onTopicInfo; 1056 1057 /** 1058 * Invoked with the reply of a userhost query. 1059 * See_Also: 1060 * $(MREF IrcClient.queryUserhost) 1061 */ 1062 void delegate(in IrcUser[] users)[] onUserhostReply; 1063 1064 /** 1065 * Invoked when a user invites us to a channel. 1066 * Params: 1067 * channel = _channel channel we were invited to 1068 */ 1069 void delegate(in char[] channel)[] onInvite; 1070 1071 /** 1072 * Invoked when a WHOIS reply is received. 1073 * See_Also: 1074 * $(MREF IrcClient.queryWhois) 1075 */ 1076 // TODO: document more, and maybe parse `channels` 1077 void delegate(IrcUser userInfo, in char[] realName)[] onWhoisReply; 1078 1079 /// Ditto 1080 void delegate(in char[] nick, in char[] serverHostName, in char[] serverInfo)[] onWhoisServerReply; 1081 1082 /// Ditto 1083 void delegate(in char[] nick)[] onWhoisOperatorReply; 1084 1085 /// Ditto 1086 void delegate(in char[] nick, int idleTime)[] onWhoisIdleReply; 1087 1088 /// Ditto 1089 void delegate(in char[] nick, in char[][] channels)[] onWhoisChannelsReply; 1090 1091 /// Ditto 1092 void delegate(in char[] nick, in char[] accountName)[] onWhoisAccountReply; 1093 1094 /// Ditto 1095 void delegate(in char[] nick)[] onWhoisEnd; 1096 1097 protected: 1098 IrcUser getUser(in char[] prefix) 1099 { 1100 return IrcUser.fromPrefix(prefix); 1101 } 1102 1103 private: 1104 void fireEvent(T, U...)(T[] event, U args) 1105 { 1106 foreach(cb; event) 1107 { 1108 cb(args); 1109 } 1110 } 1111 1112 bool ctcpCheck(void delegate(IrcUser, in char[], in char[], in char[])[] event, 1113 in char[] prefix, 1114 in char[] target, 1115 in char[] message) 1116 { 1117 if(event.empty || message[0] != CtcpToken.delimiter) 1118 return false; 1119 1120 auto extractor = message.ctcpExtract(); 1121 1122 if(extractor.empty) 1123 return false; 1124 1125 // TODO: re-use buffer 1126 auto ctcpMessage = cast(string)extractor.front.array(); 1127 auto tag = ctcpMessage.munch("^ "); 1128 1129 if(!ctcpMessage.empty && ctcpMessage.front == ' ') 1130 ctcpMessage.popFront(); 1131 1132 fireEvent( 1133 event, 1134 getUser(prefix), 1135 target, 1136 tag, 1137 ctcpMessage 1138 ); 1139 1140 return true; 1141 } 1142 1143 // TODO: Switch getting large, change to something more performant? 1144 void handle(ref IrcLine line) 1145 { 1146 import std.conv : to; 1147 1148 switch(line.command) 1149 { 1150 case "PING": 1151 writef("PONG :%s", line.arguments[0]); 1152 break; 1153 case "433": 1154 void failed433(Exception cause) 1155 { 1156 socket.close(); 1157 _connected = false; 1158 throw new IrcErrorException(this, `"433 Nick already in use" was unhandled`, cause); 1159 } 1160 1161 auto failedNick = line.arguments[1]; 1162 bool handled = false; 1163 1164 foreach(cb; onNickInUse) 1165 { 1166 const(char)[] nextNickToTry; 1167 1168 try nextNickToTry = cb(failedNick); 1169 catch(Exception e) 1170 failed433(e); 1171 1172 if(nextNickToTry) 1173 { 1174 writef("NICK %s", nextNickToTry); 1175 handled = true; 1176 break; 1177 } 1178 } 1179 1180 if(!handled) 1181 failed433(null); 1182 1183 break; 1184 case "PRIVMSG": 1185 auto prefix = line.prefix; 1186 auto target = line.arguments[0]; 1187 auto message = line.arguments[1]; 1188 1189 if(!ctcpCheck(onCtcpQuery, prefix, target, message)) 1190 fireEvent(onMessage, getUser(prefix), target, message); 1191 1192 break; 1193 case "NOTICE": 1194 auto prefix = line.prefix; 1195 auto target = line.arguments[0]; 1196 auto notice = line.arguments[1]; 1197 1198 if(!ctcpCheck(onCtcpReply, prefix, target, notice)) 1199 fireEvent(onNotice, getUser(prefix), target, notice); 1200 1201 break; 1202 case "NICK": 1203 auto user = getUser(line.prefix); 1204 auto newNick = line.arguments[0]; 1205 1206 scope(exit) 1207 { 1208 if(m_nick == user.nickName) 1209 m_nick = newNick.idup; 1210 } 1211 1212 fireEvent(onNickChange, user, newNick); 1213 break; 1214 case "JOIN": 1215 auto user = getUser(line.prefix); 1216 1217 if(user.nickName == m_nick) 1218 fireEvent(onSuccessfulJoin, line.arguments[0]); 1219 else 1220 fireEvent(onJoin, user, line.arguments[0]); 1221 1222 break; 1223 case "353": // TODO: operator/voice status etc. should be propagated to callbacks 1224 // line.arguments[0] == client.nick 1225 version(none) auto type = line.arguments[1]; 1226 auto channelName = line.arguments[2]; 1227 1228 auto names = line.arguments[3].split(); 1229 1230 // Strip channel modes from nick names (TODO: present them to the user in some way) 1231 foreach(ref name; names) 1232 { 1233 while(prefixedChannelModes.map!(pair => pair[0]).canFind(name[0])) 1234 name = name[1 .. $]; 1235 } 1236 1237 fireEvent(onNameList, channelName, names); 1238 break; 1239 case "366": 1240 fireEvent(onNameListEnd, line.arguments[1]); 1241 break; 1242 case "PART": 1243 fireEvent(onPart, getUser(line.prefix), line.arguments[0]); 1244 break; 1245 case "QUIT": 1246 fireEvent(onQuit, getUser(line.prefix), 1247 line.arguments.length? line.arguments[0] : null); 1248 break; 1249 case "KICK": 1250 fireEvent(onKick, 1251 getUser(line.prefix), 1252 line.arguments[0], 1253 line.arguments[1], 1254 line.arguments.length > 2? line.arguments[2] : null); 1255 break; 1256 case "302": 1257 IrcUser[5] users; 1258 auto n = IrcUser.parseUserhostReply(users, line.arguments[1]); 1259 1260 fireEvent(onUserhostReply, users[0 .. n]); 1261 break; 1262 case "332": 1263 fireEvent(onTopic, line.arguments[1], line.arguments[2]); 1264 break; 1265 case "333": 1266 fireEvent(onTopicInfo, line.arguments[1], line.arguments[2], line.arguments[3]); 1267 break; 1268 // WHOIS replies 1269 case "311": 1270 auto user = IrcUser( 1271 line.arguments[1], // Nick 1272 line.arguments[2]); // Username 1273 1274 fireEvent(onWhoisReply, user, line.arguments[5]); 1275 break; 1276 case "312": 1277 fireEvent(onWhoisServerReply, line.arguments[1], line.arguments[2], line.arguments[3]); 1278 break; 1279 case "313": 1280 fireEvent(onWhoisOperatorReply, line.arguments[1]); 1281 break; 1282 case "317": 1283 import std.conv : to; 1284 fireEvent(onWhoisIdleReply, line.arguments[1], to!int(line.arguments[2])); 1285 break; 1286 case "319": 1287 auto nickName = line.arguments[1]; 1288 auto channels = split(line.arguments[2]); 1289 1290 // Strip channel modes from channel names (TODO: present them to the user in some way) 1291 foreach(ref channel; channels) 1292 { 1293 while(prefixedChannelModes.map!(pair => pair[0]).canFind(channel[0])) 1294 channel = channel[1 .. $]; 1295 } 1296 1297 fireEvent(onWhoisChannelsReply, nickName, channels); 1298 break; 1299 case "318": 1300 fireEvent(onWhoisEnd, line.arguments[1]); 1301 break; 1302 // Non-standard WHOIS replies 1303 case "307": // UnrealIRCd? 1304 if(line.arguments[0] == m_nick) 1305 fireEvent(onWhoisAccountReply, line.arguments[1], line.arguments[1]); 1306 break; 1307 case "330": // Freenode 1308 fireEvent(onWhoisAccountReply, line.arguments[1], line.arguments[2]); 1309 break; 1310 // End of WHOIS replies 1311 case "ERROR": 1312 _connected = false; 1313 throw new IrcErrorException(this, line.arguments[0].idup); 1314 case "005": // ISUPPORT 1315 // TODO: handle "\xHH" sequences 1316 auto tokens = line.arguments[1 .. $ - 1]; // trim nick name and "are supported by this server" 1317 foreach(const token; tokens) 1318 { 1319 if(token[0] == '-') // Negation 1320 { 1321 auto parameter = token[1 .. $]; 1322 switch(parameter) 1323 { 1324 case "NICKLEN": 1325 _maxNickLength = defaultMaxNickLength; 1326 enforceMaxNickLength = false; 1327 break; 1328 default: 1329 debug(Dirk) std.stdio.writefln(`Unhandled negation of ISUPPORT parameter "%s"`, parameter); 1330 break; 1331 } 1332 } 1333 else 1334 { 1335 auto sepPos = token.indexOf('='); 1336 const(char)[] parameter, value; 1337 if(sepPos == -1) 1338 { 1339 parameter = token; 1340 value = null; 1341 } 1342 else 1343 { 1344 parameter = token[0 .. sepPos]; 1345 value = token[sepPos + 1 .. $]; // May be empty 1346 } 1347 1348 debug(Dirk) std.stdio.writefln(`ISUPPORT parameter "%s" has value "%s"`, parameter, value); 1349 1350 switch(parameter) 1351 { 1352 case "PREFIX": 1353 if(value.empty) 1354 prefixedChannelModes = defaultPrefixedChannelModes; 1355 else 1356 { 1357 assert(value[0] == '('); 1358 auto endParenPos = value.indexOf(')'); 1359 assert(endParenPos != -1 && endParenPos != value.length - 1); 1360 auto modes = value[1 .. endParenPos]; 1361 auto prefixes = value[endParenPos + 1 .. $]; 1362 assert(modes.length == prefixes.length); 1363 auto newChannelModes = new char[2][](modes.length); 1364 foreach(immutable i, ref pair; newChannelModes) 1365 pair = [prefixes[i], modes[i]]; 1366 prefixedChannelModes = newChannelModes; 1367 debug(Dirk) std.stdio.writefln("ISUPPORT PREFIX: %s", prefixedChannelModes); 1368 } 1369 break; 1370 case "CHANMODES": 1371 assert(!value.empty); 1372 const(char)[][4] modeTypes; // Types A, B, C and D 1373 1374 value.splitter(',') 1375 .takeExactly(4) 1376 .copy(modeTypes[]); 1377 1378 if(channelListModes != modeTypes[0]) 1379 channelListModes = modeTypes[0].idup; 1380 1381 if(channelParameterizedModes != modeTypes[1]) 1382 channelParameterizedModes = modeTypes[1].idup; 1383 1384 if(channelNullaryRemovableModes != modeTypes[2]) 1385 channelNullaryRemovableModes = modeTypes[2].idup; 1386 1387 if(channelSettingModes != modeTypes[3]) 1388 channelSettingModes = modeTypes[3].idup; 1389 break; 1390 case "NICKLEN": 1391 assert(!value.empty); 1392 _maxNickLength = to!(typeof(_maxNickLength))(value); 1393 enforceMaxNickLength = true; 1394 break; 1395 case "NETWORK": 1396 assert(!value.empty); 1397 _networkName = value.idup; 1398 break; 1399 default: 1400 debug(Dirk) std.stdio.writefln(`Unhandled ISUPPORT parameter "%s"`, parameter); 1401 break; 1402 } 1403 } 1404 } 1405 1406 break; 1407 case "001": 1408 m_nick = line.arguments[0].idup; 1409 fireEvent(onConnect); 1410 break; 1411 case "INVITE": 1412 fireEvent(onInvite, line.arguments[1]); 1413 break; 1414 default: 1415 debug(Dirk) std.stdio.writefln(`Unhandled command "%s"`, line.command); 1416 break; 1417 } 1418 } 1419 }