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, sformat, munch; 20 import std.traits; 21 22 //debug=Dirk; 23 debug(Dirk) static import std.stdio; 24 debug(Dirk) import std.conv; 25 26 enum IRC_MAX_LEN = 510; 27 28 /** 29 * Thrown if the server sends an error message to the client. 30 */ 31 class IrcErrorException : Exception 32 { 33 IrcClient client; 34 35 this(IrcClient client, string message, string file = __FILE__, size_t line = __LINE__) 36 { 37 super(message, file, line); 38 this.client = client; 39 } 40 41 this(IrcClient client, string message, Exception cause, string file = __FILE__, size_t line = __LINE__) 42 { 43 super(message, file, line, cause); 44 this.client = client; 45 } 46 } 47 48 void unsubscribeHandler(T)(ref T[] event, T handler) 49 { 50 enum strategy = 51 is(ReturnType!T == void)? SwapStrategy.unstable : SwapStrategy.stable; 52 53 event = event.remove!(e => e == handler, strategy); 54 } 55 56 /** 57 * Represents an IRC client connection. 58 * 59 * Use the separate type $(DPREF tracker, IrcTracker) returned by 60 * $(DPREF tracker, track) to keep track of the channels the 61 * user for this connection is a member of, and the members of 62 * those channels. 63 */ 64 class IrcClient 65 { 66 private: 67 string m_nick = "dirkuser"; 68 string m_user = "dirk"; 69 string m_name = "dirk"; 70 Address m_address = null; 71 bool _connected = false; 72 73 char[] buffer; 74 LineBuffer lineBuffer; 75 76 package: 77 Socket socket; 78 79 public: 80 /** 81 * Create a new unconnected IRC client. 82 * 83 * If $(D socket) is provided, it must be an unconnected TCP socket. 84 * Provide an instance of $(RREF ssl, socket, SslSocket) to 85 * use SSL/TLS. 86 * 87 * User information should be configured before connecting. 88 * Only the nick name can be changed after connecting. 89 * Event callbacks can be added both before and after connecting. 90 * See_Also: 91 * $(MREF IrcClient.connect) 92 */ 93 this() 94 { 95 this(new TcpSocket()); 96 } 97 98 /// Ditto 99 this(Socket socket) 100 { 101 this.socket = socket; 102 this.buffer = new char[](2048); 103 104 void onReceivedLine(in char[] rawLine) 105 { 106 debug(Dirk) std.stdio.writefln(`>> "%s" pos: %s`, rawLine, lineBuffer.position); 107 108 IrcLine line; 109 110 auto succeeded = parse(rawLine, line); 111 assert(succeeded); 112 113 handle(line); 114 } 115 116 this.lineBuffer = LineBuffer(buffer, &onReceivedLine); 117 } 118 119 /** 120 * Connect this client to a server. 121 * Params: 122 * serverAddress = address of server 123 */ 124 void connect(Address serverAddress) 125 { 126 enforceEx!UnconnectedClientException(!connected, "IrcClient is already connected"); 127 128 socket.connect(serverAddress); 129 130 m_address = serverAddress; 131 _connected = true; 132 133 // TODO: Implement `PASS` 134 135 writef("NICK %s", nickName); 136 writef("USER %s * * :%s", userName, realName); // TODO: Initial user-mode argument 137 } 138 139 /** 140 * Read all available data from the connection, 141 * parse all complete IRC messages and invoke registered callbacks. 142 * Returns: 143 * $(D true) if the connection was closed. 144 * See_Also: 145 * $(DPREF eventloop, IrcEventLoop.run) 146 */ 147 bool read() 148 { 149 enforceEx!UnconnectedClientException(connected, "cannot read from unconnected IrcClient"); 150 151 while(connected) 152 { 153 socket.blocking = false; // TODO: Make writes non-blocking too, so this isn't needed 154 auto received = socket.receive(buffer[lineBuffer.position .. $]); 155 if(received == Socket.ERROR) 156 { 157 if(wouldHaveBlocked()) 158 { 159 socket.blocking = true; 160 break; 161 } 162 else 163 throw new Exception("socket read operation failed: " ~ socket.getErrorText()); 164 } 165 else if(received == 0) 166 { 167 debug(Dirk) std.stdio.writeln("remote ended connection"); 168 socket.close(); 169 _connected = false; 170 return true; 171 } 172 173 socket.blocking = true; 174 lineBuffer.commit(received); 175 } 176 177 return !connected; 178 } 179 180 static char[1540] formatBuffer; 181 182 /** 183 * Write a raw message to the connection stream. 184 * 185 * If there is more than one argument, then the first argument is formatted with the subsequent ones. 186 * Arguments must not contain newlines. 187 * Messages longer than 510 characters (UTF-8 code units) will be cut off. 188 * Params: 189 * rawline = line to send 190 * fmtArgs = format arguments for the first argument 191 * See_Also: 192 * $(STDREF format, formattedWrite) 193 * Throws: 194 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 195 */ 196 void writef(T...)(const(char)[] rawline, T fmtArgs) 197 { 198 enforceEx!UnconnectedClientException(connected, "cannot write to unconnected IrcClient"); 199 200 static if(fmtArgs.length > 0) 201 { 202 rawline = sformat(formatBuffer, rawline, fmtArgs); 203 } 204 205 debug(Dirk) std.stdio.writefln(`<< "%s" (length: %d)`, rawline, rawline.length); 206 207 socket.send(rawline); 208 socket.send("\r\n"); // TODO: should be in one call to send? 209 } 210 211 // Takes care of splitting 'message' into multiple messages when necessary 212 private void sendMessage(string method)(in char[] target, in char[] message) 213 { 214 static linePattern = ctRegex!(`[^\r\n]+`, "g"); 215 216 immutable maxMsgLength = IRC_MAX_LEN - method.length - 1 - target.length - 2; 217 static immutable lineHead = method ~ " %s :%s"; 218 219 foreach(m; match(message, linePattern)) 220 { 221 auto line = cast(const ubyte[])m.hit; 222 foreach(chunk; line.chunks(maxMsgLength)) 223 writef(lineHead, target, cast(const char[])chunk); 224 } 225 } 226 227 /** 228 * Send lines of chat to a channel or user. 229 * Each line in $(D message) is sent as one _message. 230 * Lines exceeding the IRC _message length limit will be 231 * split up into multiple messages. 232 * Params: 233 * target = channel or nick name to _send to 234 * message = _message(s) to _send. Can contain multiple lines. 235 * Throws: 236 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 237 */ 238 void send(in char[] target, in char[] message) 239 { 240 sendMessage!"PRIVMSG"(target, message); 241 } 242 243 /** 244 * Send formatted lines of chat to a channel or user. 245 * Each line in the formatted result is sent as one message. 246 * Lines exceeding the IRC message length limit will be 247 * split up into multiple messages. 248 * Params: 249 * target = channel or nick name to _send to 250 * fmt = message format 251 * fmtArgs = format arguments 252 * Throws: 253 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 254 * See_Also: 255 * $(STDREF format, formattedWrite) 256 */ 257 void sendf(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs) 258 { 259 // TODO: use a custom format writer that doesn't necessarily allocate 260 send(target, format(fmt, fmtArgs)); 261 } 262 263 /** 264 * Send notices to a channel or user. 265 * Each line in $(D message) is sent as one _notice. 266 * Lines exceeding the IRC _message length limit will be 267 * split up into multiple notices. 268 * Params: 269 * target = channel or nick name to _notice 270 * message = notices(s) to send. Can contain multiple lines. 271 * Throws: 272 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 273 */ 274 void notice(in char[] target, in char[] message) 275 { 276 sendMessage!"NOTICE"(target, message); 277 } 278 279 /** 280 * Send formatted notices to a channel or user. 281 * Each line in the formatted result is sent as one notice. 282 * Lines exceeding the IRC message length limit will be 283 * split up into multiple notices. 284 * Params: 285 * target = channel or nick name to _send to 286 * fmt = message format 287 * fmtArgs = format arguments 288 * Throws: 289 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 290 * See_Also: 291 * $(STDREF format, formattedWrite) 292 */ 293 void noticef(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs) 294 { 295 // TODO: use a custom format writer that doesn't necessarily allocate 296 notice(target, format(fmt, fmtArgs)); 297 } 298 299 /** 300 * Send a CTCP _query to a channel or user. 301 */ 302 // TODO: reuse buffer for output 303 void ctcpQuery(in char[] target, in char[] query) 304 { 305 send(target, ctcpMessage(query).array()); 306 } 307 308 /// Ditto 309 void ctcpQuery(in char[] target, in char[] tag, in char[] data) 310 { 311 send(target, ctcpMessage(tag, data).array()); 312 } 313 314 /** 315 * Send a CTCP _reply to a user. 316 */ 317 void ctcpReply(in char[] targetNick, in char[] reply) 318 { 319 notice(targetNick, ctcpMessage(reply).array()); 320 } 321 322 /// Ditto 323 void ctcpReply(in char[] targetNick, in char[] tag, in char[] data) 324 { 325 notice(targetNick, ctcpMessage(tag, data).array()); 326 } 327 328 /** 329 * Send a CTCP _error message reply. 330 * Params: 331 * invalidData = data that caused the _error 332 * error = human-readable _error message 333 */ 334 void ctcpError(in char[] targetNick, in char[] invalidData, in char[] error) 335 { 336 notice(targetNick, ctcpMessage("ERRMSG", format("%s :%s", invalidData, error)).array()); 337 } 338 339 /** 340 * Check if this client is _connected. 341 */ 342 bool connected() const @property 343 { 344 return _connected; 345 } 346 347 /** 348 * Address of the server this client is currently connected to, 349 * or null if this client is not connected. 350 */ 351 inout(Address) serverAddress() inout pure @property 352 { 353 return m_address; 354 } 355 356 /** 357 * Real name of the user for this client. 358 * 359 * Cannot be changed after connecting. 360 */ 361 string realName() const pure @property 362 { 363 return m_name; 364 } 365 366 /// Ditto 367 void realName(string newRealName) @property 368 { 369 enforce(!connected, "cannot change real name while connected"); 370 enforce(!newRealName.empty); 371 m_name = newRealName; 372 } 373 374 /** 375 * User name of the user for this client. 376 * 377 * Cannot be changed after connecting. 378 */ 379 string userName() const pure @property 380 { 381 return m_user; 382 } 383 384 /// Ditto 385 void userName(string newUserName) @property 386 { 387 enforce(!connected, "cannot change user-name while connected"); 388 enforce(!newUserName.empty); 389 m_user = newUserName; 390 } 391 392 /** 393 * Nick name of the user for this client. 394 * 395 * Setting this property when connected can cause the $(MREF IrcClient.onNickInUse) event to fire. 396 */ 397 string nickName() const pure @property 398 { 399 return m_nick; 400 } 401 402 /// Ditto 403 void nickName(in char[] newNick) @property 404 { 405 enforce(!newNick.empty); 406 if(connected) // m_nick will be set later if the nick is accepted. 407 writef("NICK %s", newNick); 408 else 409 m_nick = newNick.idup; 410 } 411 412 /// Ditto 413 // Duplicated to show up nicer in DDoc - previously used a template and aliases 414 void nickName(string newNick) @property 415 { 416 enforce(!newNick.empty); 417 if(connected) // m_nick will be set later if the nick is accepted. 418 writef("NICK %s", newNick); 419 else 420 m_nick = newNick; 421 } 422 423 deprecated alias nick = nickName; 424 425 /** 426 * Join a _channel. 427 * Params: 428 * channel = _channel to _join 429 * Throws: 430 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 431 */ 432 void join(in char[] channel) 433 { 434 writef("JOIN %s", channel); 435 } 436 437 /** 438 * Join a passworded _channel. 439 * Params: 440 * channel = _channel to _join 441 * key = _channel password 442 * Throws: 443 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 444 */ 445 void join(in char[] channel, in char[] key) 446 { 447 writef("JOIN %s :%s", channel, key); 448 } 449 450 /** 451 * Leave a _channel. 452 * Params: 453 * channel = _channel to leave 454 * Throws: 455 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 456 */ 457 void part(in char[] channel) 458 { 459 writef("PART %s", channel); 460 //fireEvent(onMePart, channel); 461 } 462 463 /** 464 * Leave a _channel with a parting _message. 465 * Params: 466 * channel = _channel to leave 467 * message = parting _message 468 * Throws: 469 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 470 */ 471 void part(in char[] channel, in char[] message) 472 { 473 writef("PART %s :%s", channel, message); 474 } 475 476 /** 477 * Kick one or more _users from a _channel. 478 * 479 * This _user must have channel operator status in $(D channel). 480 * Params: 481 * channel = _channel to kick user(s) from 482 * users = _user(s) to kick 483 * comment = _comment to broadcast with the _kick message, 484 * which typically contains the reason the _user is being kicked 485 */ 486 void kick()(in char[] channel, in char[] user) 487 { 488 writef("KICK %s %s", channel, user); 489 } 490 491 /// Ditto 492 void kick()(in char[] channel, in char[] user, in char[] comment) 493 { 494 writef("KICK %s %s :%s", channel, user, comment); 495 } 496 497 /// Ditto 498 void kick(Range)(in char[] channel, Range users) 499 if(isInputRange!Range && isSomeString!(ElementType!Range)) 500 { 501 writef("KICK %s %(%s%|,%)", channel, users); 502 } 503 504 /// Ditto 505 void kick(Range)(in char[] channel, Range users, in char[] comment) 506 if(isInputRange!Range && isSomeString!(ElementType!Range)) 507 { 508 writef("KICK %s %(%s%|,%) :%s", channel, users, comment); 509 } 510 511 /** 512 * Kick users from channels in a single message. 513 * 514 * $(D channelUserPairs) must be a range of $(STDREF typecons, Tuple) 515 * pairs of strings, where the first string is the name of a channel 516 * and the second string is the user to kick from that channel. 517 */ 518 void kick(Range)(Range channelUserPairs) 519 if(isInputRange!Range && 520 isTuple!(ElementType!Range) && ElementType!Range.length == 2 && 521 allSatisfy!(isSomeString, ElementType!Range.Types)) 522 { 523 writef("KICK %(%s%|,%) %(%s%|,%)", 524 channelUserPairs.map!(pair => pair[0]), 525 channelUserPairs.map!(pair => pair[1])); 526 } 527 528 /// Ditto 529 void kick(Range)(Range channelUserPairs, in char[] comment) 530 if(isInputRange!Range && 531 isTuple!(ElementType!Range) && ElementType!Range.length == 2 && 532 allSatisfy!(isSomeString, ElementType!Range.Types)) 533 { 534 writef("KICK %(%s%|,%) %(%s%|,%) :%s", 535 channelUserPairs.map!(pair => pair[0]), 536 channelUserPairs.map!(pair => pair[1]), 537 comment); 538 } 539 540 /** 541 * Query the user name and host name of up to 5 users. 542 * Params: 543 * nicks = between 1 and 5 nick names to query 544 * See_Also: 545 * $(MREF IrcClient.onUserhostReply) 546 */ 547 void queryUserhost(in char[][] nicks...) 548 { 549 version(assert) 550 { 551 import core.exception; 552 if(nicks.length < 1 || nicks.length > 5) 553 throw new RangeError(); 554 } 555 writef("USERHOST %s", nicks.map!(nick => nick[]).joiner(" ")); 556 } 557 558 /** 559 * Query information about a particular user. 560 * Params: 561 * nick = target user's nick name 562 * See_Also: 563 * $(MREF IrcClient.onWhoisReply) 564 */ 565 void queryWhois(in char[] nick) 566 { 567 writef("WHOIS %s", nick); 568 } 569 570 /** 571 * Query the list of members in the given channels. 572 * See_Also: 573 * $(MREF IrcClient.onNameList) 574 */ 575 void queryNames(in char[][] channels...) 576 { 577 // TODO: support automatic splitting of messages 578 writef("NAMES %s", channels.map!(channel => channel[]).joiner(",")); 579 } 580 581 /** 582 * Leave and disconnect from the server. 583 * Params: 584 * message = comment sent in _quit notification 585 * Throws: 586 * $(DPREF exception, UnconnectedClientException) if this client is not connected. 587 */ 588 void quit(in char[] message) 589 { 590 writef("QUIT :%s", message); 591 socket.close(); 592 _connected = false; 593 } 594 595 /// Invoked when this client has successfully connected to a server. 596 void delegate()[] onConnect; 597 598 /** 599 * Invoked when a message is picked up by the user for this client. 600 * Params: 601 * user = _user who sent the message 602 * target = message _target. This is either the nick of this client in the case of a personal 603 * message, or the name of the channel which the message was sent to. 604 */ 605 void delegate(IrcUser user, in char[] target, in char[] message)[] onMessage; 606 607 /** 608 * Invoked when a notice is picked up by the user for this client. 609 * Params: 610 * user = _user who sent the notice 611 * target = notice _target. This is either the nick of this client in the case of a personal 612 * notice, or the name of the channel which the notice was sent to. 613 */ 614 void delegate(IrcUser user, in char[] target, in char[] message)[] onNotice; 615 616 /** 617 * Invoked when a user receives a new nickname. 618 * 619 * When the user is this user, the $(MREF IrcClient.nick) property will return the old nickname 620 * until after all $(D _onNickChange) callbacks have been invoked. 621 * Params: 622 * user = user which nickname was changed; the $(D nick) field contains the old nickname. Can be this user 623 * newNick = new nickname of user 624 */ 625 void delegate(IrcUser user, in char[] newNick)[] onNickChange; 626 627 /** 628 * Invoked following a call to $(MREF IrcClient.join) when the _channel was successfully joined. 629 * Params: 630 * channel = _channel that was successfully joined 631 */ 632 void delegate(in char[] channel)[] onSuccessfulJoin; 633 634 /** 635 * Invoked when another user joins a channel that this user is a member of. 636 * Params: 637 * user = joining user 638 * channel = channel that was joined 639 */ 640 void delegate(IrcUser user, in char[] channel)[] onJoin; 641 642 /** 643 * Invoked when a user parts a channel that this user is a member of. 644 * Also invoked when this user parts a channel. 645 * Params: 646 * user = parting user 647 * channel = channel that was parted 648 */ 649 void delegate(IrcUser user, in char[] channel)[] onPart; 650 651 // TODO: public? 652 // package void delegate(in char[] channel)[] onMePart; 653 654 /** 655 * Invoked when another user disconnects from the network. 656 * Params: 657 * user = disconnecting user 658 * comment = quit message 659 */ 660 void delegate(IrcUser user, in char[] comment)[] onQuit; 661 662 /** 663 * Invoked when a user is kicked (forcefully removed) from a channel that this user is a member of. 664 * Params: 665 * kicker = user that initiated the kick 666 * channel = channel from which the user was kicked 667 * kickedNick = nickname of the user that was kicked 668 * comment = comment sent with the kick; usually describing the reason the user was kicked. Can be null 669 */ 670 void delegate(IrcUser kicker, in char[] channel, in char[] kickedNick, in char[] comment)[] onKick; 671 672 /** 673 * Invoked when a list of member nick names for a channel are received. 674 * 675 * The list is received after a successful join to a channel by this user, 676 * or when explicitly queried with $(MREF IrcClient.queryNames). 677 * 678 * The list for a single invocation is partial; 679 * the event can be invoked several times for the same channel 680 * as a response to a single trigger. The list is signaled complete 681 * when $(MREF IrcClient.onNameListEnd) is invoked. 682 * Params: 683 * channel = channel of which the users are members 684 * nickNames = list of member nicknames 685 */ 686 void delegate(in char[] channel, in char[][] nickNames)[] onNameList; 687 688 /** 689 * Invoked when the complete list of members of a _channel have been received. 690 * All invocations of $(D onNameList) between invocations of this event 691 * are part of the same member list. 692 * See_Also: 693 * $(MREF IrcClient.onNameList) 694 */ 695 void delegate(in char[] channel)[] onNameListEnd; 696 697 /** 698 * Invoked when a CTCP query is received in a message. 699 * $(MREF IrcClient.onMessage) is not invoked for the given message 700 * when onCtcpQuery has a non-zero number of registered handlers. 701 * Note: 702 * This callback is only invoked when there is a CTCP message at the start 703 * of the message, and any subsequent CTCP messages in the same message are 704 * discarded. To handle multiple CTCP queries in one message, use 705 * $(MREF IrcClient.onMessage) with $(DPREF ctcp, ctcpExtract). 706 */ 707 void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpQuery; 708 709 /** 710 * Invoked when a CTCP reply is received in a notice. 711 * $(MREF IrcClient.onNotice) is not invoked for the given notice 712 * when onCtcpReply has a non-zero number of registered handlers. 713 * Note: 714 * This callback is only invoked when there is a CTCP message at the start 715 * of the notice, and any subsequent CTCP messages in the same notice are 716 * discarded. To handle multiple CTCP replies in one notice, use 717 * $(MREF IrcClient.onNotice) with $(DPREF ctcp, ctcpExtract). 718 */ 719 void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpReply; 720 721 /** 722 * Invoked when the requested nick name of the user for this client is already in use. 723 * 724 * Return a non-null string to provide a new nick. No further callbacks in the list 725 * are called once a callback provides a nick. 726 * Params: 727 * newNick = the nick name that was requested. 728 * Note: 729 * The current nick name can be read from the $(MREF IrcClient.nick) property of this client. 730 */ 731 const(char)[] delegate(in char[] newNick)[] onNickInUse; 732 733 /** 734 * Invoked when a _channel is joined, a _topic is set in a _channel or when 735 * the current _topic was requested. 736 * 737 * Params: 738 * channel 739 * topic = _topic or new _topic for channel 740 */ 741 void delegate(in char[] channel, in char[] topic)[] onTopic; 742 743 /** 744 * Invoked when a _channel is joined or when the current _topic was requested. 745 * 746 * Params: 747 * channel 748 * nick = _nick name of user who set the topic 749 * time = _time the topic was set 750 */ 751 void delegate(in char[] channel, in char[] nick, in char[] time)[] onTopicInfo; 752 753 /** 754 * Invoked with the reply of a userhost query. 755 * See_Also: 756 * $(MREF IrcClient.queryUserhost) 757 */ 758 void delegate(in IrcUser[] users)[] onUserhostReply; 759 760 /** 761 * Invoked when a WHOIS reply is received. 762 * See_Also: 763 * $(MREF IrcClient.queryWhois) 764 */ 765 // TODO: document more, and maybe parse `channels` 766 void delegate(IrcUser userInfo, in char[] realName)[] onWhoisReply; 767 768 /// Ditto 769 void delegate(in char[] nick, in char[] serverHostName, in char[] serverInfo)[] onWhoisServerReply; 770 771 /// Ditto 772 void delegate(in char[] nick)[] onWhoisOperatorReply; 773 774 /// Ditto 775 void delegate(in char[] nick, int idleTime)[] onWhoisIdleReply; 776 777 /// Ditto 778 void delegate(in char[] nick, in char[][] channels)[] onWhoisChannelsReply; 779 780 /// Ditto 781 void delegate(in char[] nick, in char[] accountName)[] onWhoisAccountReply; 782 783 /// Ditto 784 void delegate(in char[] nick)[] onWhoisEnd; 785 786 protected: 787 IrcUser getUser(in char[] prefix) 788 { 789 return IrcUser.fromPrefix(prefix); 790 } 791 792 private: 793 void fireEvent(T, U...)(T[] event, U args) 794 { 795 foreach(cb; event) 796 { 797 cb(args); 798 } 799 } 800 801 bool ctcpCheck(void delegate(IrcUser, in char[], in char[], in char[])[] event, 802 in char[] prefix, 803 in char[] target, 804 in char[] message) 805 { 806 if(event.empty || message[0] != CtcpToken.delimiter) 807 return false; 808 809 auto extractor = message.ctcpExtract(); 810 811 if(extractor.empty) 812 return false; 813 814 // TODO: re-use buffer 815 auto ctcpMessage = cast(string)extractor.front.array(); 816 auto tag = ctcpMessage.munch("^ "); 817 818 if(!ctcpMessage.empty && ctcpMessage.front == ' ') 819 ctcpMessage.popFront(); 820 821 fireEvent( 822 event, 823 getUser(prefix), 824 target, 825 tag, 826 ctcpMessage 827 ); 828 829 return true; 830 } 831 832 // TODO: Switch getting large, change to something more performant? 833 void handle(ref IrcLine line) 834 { 835 switch(line.command) 836 { 837 case "PING": 838 writef("PONG :%s", line.arguments[0]); 839 break; 840 case "433": 841 void failed433(Exception cause) 842 { 843 socket.close(); 844 _connected = false; 845 throw new IrcErrorException(this, `"433 Nick already in use" was unhandled`, cause); 846 } 847 848 auto failedNick = line.arguments[0]; 849 bool handled = false; 850 851 foreach(cb; onNickInUse) 852 { 853 const(char)[] newNick; 854 855 try newNick = cb(failedNick); 856 catch(Exception e) 857 failed433(e); 858 859 if(newNick) 860 { 861 writef("NICK %s", newNick); 862 handled = true; 863 break; 864 } 865 } 866 867 if(!handled) 868 failed433(null); 869 870 break; 871 case "PRIVMSG": 872 auto prefix = line.prefix; 873 auto target = line.arguments[0]; 874 auto message = line.arguments[1]; 875 876 if(!ctcpCheck(onCtcpQuery, prefix, target, message)) 877 fireEvent(onMessage, getUser(prefix), target, message); 878 879 break; 880 case "NOTICE": 881 auto prefix = line.prefix; 882 auto target = line.arguments[0]; 883 auto notice = line.arguments[1]; 884 885 if(!ctcpCheck(onCtcpReply, prefix, target, notice)) 886 fireEvent(onNotice, getUser(prefix), target, notice); 887 888 break; 889 case "NICK": 890 auto user = getUser(line.prefix); 891 auto newNick = line.arguments[0]; 892 893 scope(exit) 894 { 895 if(m_nick == user.nickName) 896 m_nick = newNick.idup; 897 } 898 899 fireEvent(onNickChange, user, newNick); 900 break; 901 case "JOIN": 902 auto user = getUser(line.prefix); 903 904 if(user.nickName == m_nick) 905 fireEvent(onSuccessfulJoin, line.arguments[0]); 906 else 907 fireEvent(onJoin, user, line.arguments[0]); 908 909 break; 910 case "353": // TODO: operator/voice status etc. should be propagated to callbacks 911 // line.arguments[0] == client.nick 912 version(none) auto type = line.arguments[1]; 913 auto channelName = line.arguments[2]; 914 915 auto names = line.arguments[3].split(); 916 foreach(ref name; names) 917 { 918 auto prefix = name[0]; 919 if(prefix == '@' || prefix == '+') // TODO: smarter handling that allows for non-standard stuff 920 name = name[1 .. $]; 921 } 922 923 fireEvent(onNameList, channelName, names); 924 break; 925 case "366": 926 fireEvent(onNameListEnd, line.arguments[0]); 927 break; 928 case "PART": 929 fireEvent(onPart, getUser(line.prefix), line.arguments[0]); 930 break; 931 case "QUIT": 932 fireEvent(onQuit, getUser(line.prefix), line.arguments[0]); 933 break; 934 case "KICK": 935 fireEvent(onKick, 936 getUser(line.prefix), 937 line.arguments[0], 938 line.arguments[1], 939 line.arguments.length > 2? line.arguments[2] : null); 940 break; 941 case "302": 942 IrcUser[5] users; 943 auto n = IrcUser.parseUserhostReply(users, line.arguments[1]); 944 945 fireEvent(onUserhostReply, users[0 .. n]); 946 break; 947 case "332": 948 fireEvent(onTopic, line.arguments[1], line.arguments[2]); 949 break; 950 case "333": 951 fireEvent(onTopicInfo, line.arguments[1], line.arguments[2], line.arguments[3]); 952 break; 953 // WHOIS replies 954 case "311": 955 auto user = IrcUser( 956 line.arguments[1], // Nick 957 line.arguments[2]); // Username 958 959 fireEvent(onWhoisReply, user, line.arguments[5]); 960 break; 961 case "312": 962 fireEvent(onWhoisServerReply, line.arguments[1], line.arguments[2], line.arguments[3]); 963 break; 964 case "313": 965 fireEvent(onWhoisOperatorReply, line.arguments[0]); 966 break; 967 case "317": 968 import std.conv : to; 969 fireEvent(onWhoisIdleReply, line.arguments[1], to!int(line.arguments[2])); 970 break; 971 case "319": 972 fireEvent(onWhoisChannelsReply, line.arguments[1], split(line.arguments[2])); 973 break; 974 case "318": 975 fireEvent(onWhoisEnd, line.arguments[1]); 976 break; 977 // Non-standard WHOIS replies 978 //case "307": // UnrealIRCd? 979 // fireEvent(onWhoisAccountReply, line.arguments[0], line.arguments[1]); 980 // break; 981 case "330": // Freenode 982 fireEvent(onWhoisAccountReply, line.arguments[1], line.arguments[2]); 983 break; 984 // End of WHOIS replies 985 case "ERROR": 986 _connected = false; 987 throw new IrcErrorException(this, line.arguments[0].idup); 988 case "001": 989 m_nick = line.arguments[0].idup; 990 fireEvent(onConnect); 991 break; 992 default: 993 debug(Dirk) std.stdio.writefln(`Unhandled command "%s"`, line.command); 994 break; 995 } 996 } 997 }