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