1 /// 2 module vibeirc.client; 3 4 import std.datetime; 5 import std.traits; 6 7 import vibe.core.log; 8 import vibe.core.net; 9 import vibe.core.task; 10 11 import vibeirc.constants; 12 import vibeirc.data; 13 import vibeirc.utility; 14 15 //Thrown from lineReceived, handleNumeric or handleCommand in case of an error 16 private class GracelessDisconnect: Exception 17 { 18 this(string msg) 19 { 20 super(msg); 21 } 22 } 23 24 /++ 25 Return type for the onConnect callback. 26 +/ 27 enum PerformLogin 28 { 29 /++ 30 Login procedure was not handled in user code, and needs to be done by the library. 31 +/ 32 yes, 33 34 /++ 35 Login procedure was handled in user code, and does not need to be done by the library. 36 +/ 37 no, 38 } 39 40 /++ 41 Represents a connection to an IRC server. 42 +/ 43 final class IRCClient 44 { 45 //placed here to prevent symbols leaking into user code 46 import std.string: format, split; 47 48 private Task protocolTask; //The task running protocolLoop 49 private TCPConnection transport; //The TCP socket 50 private string[] buffer; //Buffered messages 51 private uint bufferSent; //Number of messages sent this time period 52 private SysTime bufferNextTime; //The start of the next time period 53 private SysTime lastIncomingLineTime; //When we last received a line from the server 54 private bool sentPing; //Whether we sent a PING 55 private bool receivedPong; //Whether the server has answered our PING 56 57 /*======================================* 58 *======================================* 59 * Properties * 60 *======================================* 61 *======================================*/ 62 63 private string _nickname = "vibeirc"; 64 65 /++ 66 The display name this client will use. 67 68 Defaults to vibeirc. 69 +/ 70 @property string nickname() 71 { 72 return _nickname; 73 } 74 75 /++ 76 ditto 77 +/ 78 @property string nickname(string newNick) 79 { 80 if(connected) 81 sendLine("NICK %s", newNick); 82 83 return _nickname = newNick; 84 } 85 86 private string _username = "vibeirc"; 87 88 /++ 89 The username shown by the WHOIS command. 90 91 Defaults to vibeirc. 92 +/ 93 @property string username() 94 { 95 return _username; 96 } 97 98 /++ 99 ditto 100 +/ 101 @property string username(string newValue) 102 { 103 return _username = newValue; 104 } 105 106 private string _realname = "vibeirc"; 107 108 /++ 109 The real name shown by the WHOIS command. 110 111 Defaults to vibeirc. 112 +/ 113 @property string realname() 114 { 115 return _realname; 116 } 117 118 /++ 119 ditto 120 +/ 121 @property string realname(string newValue) 122 { 123 return _realname = newValue; 124 } 125 126 private Duration _sleepTimeout = dur!"msecs"(10); 127 128 /++ 129 How long the protocol loop should sleep after failing to read a line. 130 131 Defaults to 10 ms. 132 +/ 133 @property Duration sleepTimeout() 134 { 135 return _sleepTimeout; 136 } 137 138 /++ 139 ditto 140 +/ 141 @property Duration sleepTimeout(Duration newValue) 142 { 143 return _sleepTimeout = newValue; 144 } 145 146 private bool _buffering = false; 147 148 /++ 149 Whether to buffer outgoing messages. 150 151 Defaults to off (false). 152 +/ 153 @property bool buffering() 154 { 155 return _buffering; 156 } 157 158 /++ 159 ditto 160 +/ 161 @property bool buffering(bool newValue) 162 { 163 return _buffering = newValue; 164 } 165 166 private uint _bufferLimit = 20; 167 168 /++ 169 Maximum number of messages to send per time period, if buffering is enabled. 170 171 Defaults to 20. 172 +/ 173 @property uint bufferLimit() 174 { 175 return _bufferLimit; 176 } 177 178 /++ 179 ditto 180 +/ 181 @property uint bufferLimit(uint newValue) 182 { 183 return _bufferLimit = newValue; 184 } 185 186 private Duration _bufferTimeout = dur!"seconds"(30); 187 188 /++ 189 Amount of time to wait before sending each batch of messages, if buffering is enabled. 190 +/ 191 @property Duration bufferTimeout() 192 { 193 return _bufferTimeout; 194 } 195 196 /++ 197 ditto 198 +/ 199 @property Duration bufferTimeout(Duration newValue) 200 { 201 return _bufferTimeout = newValue; 202 } 203 204 /++ 205 Returns whether this connection is active. 206 +/ 207 @property bool connected() 208 { 209 return transport && transport.connected; 210 } 211 212 private bool _loggedIn; 213 214 /++ 215 Whether or not this connection has successfully logged in to the network. 216 +/ 217 @property bool loggedIn() 218 { 219 return _loggedIn; 220 } 221 222 private @property bool loggedIn(bool newValue) 223 { 224 return _loggedIn = newValue; 225 } 226 227 private string _serverHostname; 228 229 /++ 230 The hostname of the server this client is connected to. 231 +/ 232 @property string serverHostname() 233 { 234 return _serverHostname; 235 } 236 237 private @property string serverHostname(string newValue) 238 { 239 return _serverHostname = newValue; 240 } 241 242 /*======================================* 243 *======================================* 244 * Callbacks * 245 *======================================* 246 *======================================*/ 247 248 private void delegate(string prefix, string command, string[] arguments) _onUnknownCommand; 249 250 /++ 251 Called when an unknown command is received. 252 253 Params: 254 a = origin of the command, either a server or a user 255 b = the name of the command 256 c = the body of the command 257 +/ 258 @property typeof(_onUnknownCommand) onUnknownCommand() 259 { 260 return _onUnknownCommand; 261 } 262 263 /++ 264 ditto 265 +/ 266 @property typeof(_onUnknownCommand) onUnknownCommand(typeof(_onUnknownCommand) newValue) 267 { 268 return _onUnknownCommand = newValue; 269 } 270 271 private void delegate(string prefix, int id, string[] arguments) _onUnknownNumeric; 272 273 /++ 274 Called when an unknown numeric command is received. 275 276 Params: 277 a = origin of the command, either a server or a user 278 b = the number of the command 279 c = the body of the command 280 +/ 281 @property typeof(_onUnknownNumeric) onUnknownNumeric() 282 { 283 return _onUnknownNumeric; 284 } 285 286 /++ 287 ditto 288 +/ 289 @property typeof(_onUnknownNumeric) onUnknownNumeric(typeof(_onUnknownNumeric) newValue) 290 { 291 return _onUnknownNumeric = newValue; 292 } 293 294 private PerformLogin delegate() _onConnect; 295 296 /++ 297 Called after the connection is established, before logging in to the network. 298 299 Returns: 300 whether login procedure (sending of PASSWORD, NICK and USER commands) 301 was handled in the callback 302 +/ 303 @property typeof(_onConnect) onConnect() 304 { 305 return _onConnect; 306 } 307 308 /++ 309 ditto 310 +/ 311 @property typeof(_onConnect) onConnect(typeof(_onConnect) newValue) 312 { 313 return _onConnect = newValue; 314 } 315 316 private void delegate(string reason) _onDisconnect; 317 318 /++ 319 Called after being disconnected from the network. 320 321 Params: 322 a = the reason for quitting 323 +/ 324 @property typeof(_onDisconnect) onDisconnect() 325 { 326 return _onDisconnect; 327 } 328 329 /++ 330 ditto 331 +/ 332 @property typeof(_onDisconnect) onDisconnect(typeof(_onDisconnect) newValue) 333 { 334 return _onDisconnect = newValue; 335 } 336 337 private void delegate() _onLogin; 338 339 /++ 340 Called after succesfully logging in to the network. 341 +/ 342 @property typeof(_onLogin) onLogin() 343 { 344 return _onLogin; 345 } 346 347 /++ 348 ditto 349 +/ 350 @property typeof(_onLogin) onLogin(typeof(_onLogin) newValue) 351 { 352 return _onLogin = newValue; 353 } 354 355 private void delegate(Message message) _onMessage; 356 357 /++ 358 Called upon reception of an incoming message. 359 360 Params: 361 a = information on the incoming message 362 +/ 363 @property typeof(_onMessage) onMessage() 364 { 365 return _onMessage; 366 } 367 368 /++ 369 ditto 370 +/ 371 @property typeof(_onMessage) onMessage(typeof(_onMessage) newValue) 372 { 373 return _onMessage = newValue; 374 } 375 376 private void delegate(Message message) _onNotice; 377 378 /++ 379 Called upon reception of an incoming notice. 380 381 A notice is similar to a privmsg, except it is expected to not generate automatic replies. 382 383 Params: 384 a = information on the incoming message 385 +/ 386 @property typeof(_onNotice) onNotice() 387 { 388 return _onNotice; 389 } 390 391 /++ 392 ditto 393 +/ 394 @property typeof(_onNotice) onNotice(typeof(_onNotice) newValue) 395 { 396 return _onNotice = newValue; 397 } 398 399 private void delegate(User user, string channel) _onUserJoin; 400 401 /++ 402 Called when a user joins a channel. 403 404 Params: 405 a = the user that joined 406 b = the channel they joined 407 +/ 408 @property typeof(_onUserJoin) onUserJoin() 409 { 410 return _onUserJoin; 411 } 412 413 /++ 414 ditto 415 +/ 416 @property typeof(_onUserJoin) onUserJoin(typeof(_onUserJoin) newValue) 417 { 418 return _onUserJoin = newValue; 419 } 420 421 private void delegate(User user, string channel, string reason) _onUserPart; 422 423 /++ 424 Called when a user leaves a channel. 425 426 Params: 427 a = the user that left 428 b = the channel they left 429 c = the reason they left, if any 430 +/ 431 @property typeof(_onUserPart) onUserPart() 432 { 433 return _onUserPart; 434 } 435 436 /++ 437 ditto 438 +/ 439 @property typeof(_onUserPart) onUserPart(typeof(_onUserPart) newValue) 440 { 441 return _onUserPart = newValue; 442 } 443 444 private void delegate(User user, string reason) _onUserQuit; 445 446 /++ 447 Called when a user disconnects from the network. 448 449 Params: 450 a = the user that quit 451 b = the reason they quit, if any 452 +/ 453 @property typeof(_onUserQuit) onUserQuit() 454 { 455 return _onUserQuit; 456 } 457 458 /++ 459 ditto 460 +/ 461 @property typeof(_onUserQuit) onUserQuit(typeof(_onUserQuit) newValue) 462 { 463 return _onUserQuit = newValue; 464 } 465 466 private void delegate(User kicker, string user, string channel, string reason) _onUserKick; 467 468 /++ 469 Called when a user is kicked from a channel. 470 471 Params: 472 a = the user that performed the kick 473 b = the user that was kicked 474 c = the channel they were kicked from 475 d = the reason they were kicked 476 +/ 477 @property typeof(_onUserKick) onUserKick() 478 { 479 return _onUserKick; 480 } 481 482 /++ 483 ditto 484 +/ 485 @property typeof(_onUserKick) onUserKick(typeof(_onUserKick) newValue) 486 { 487 return _onUserKick = newValue; 488 } 489 490 private void delegate(User user, string newNick) _onUserRename; 491 492 /++ 493 Called when a user changes their nickname. 494 495 Params: 496 a = the user that changed their name 497 b = the user's new name 498 +/ 499 @property typeof(_onUserRename) onUserRename() 500 { 501 return _onUserRename; 502 } 503 504 /++ 505 ditto 506 +/ 507 @property typeof(_onUserRename) onUserRename(typeof(_onUserRename) newValue) 508 { 509 return _onUserRename = newValue; 510 } 511 512 /*======================================* 513 *======================================* 514 * Private Methods * 515 *======================================* 516 *======================================*/ 517 518 private void protocolLoop(string password) 519 in { assert(connected); } 520 body 521 { 522 import vibe.core.log: logError; 523 import vibe.core.core: sleep; 524 525 version(IrcDebugLogging) logDebug("irc connected"); 526 527 if(runCallback(onConnect) == PerformLogin.yes) 528 { 529 if(password != null) 530 sendLine("PASS %s", password); 531 532 sendLine("NICK %s", nickname); 533 sendLine("USER %s 0 * :%s", username, realname); 534 } 535 536 while(true) 537 { 538 string line; 539 540 if(buffering) 541 flushMessageBuffer; 542 543 checkPingTime; 544 transport.waitForData; 545 546 try 547 line = transport.tryReadLine; 548 catch(Exception err) 549 { 550 logError(err.toString); 551 552 break; 553 } 554 555 if(line == null) 556 { 557 if(!connected) //reading final lines 558 break; 559 560 sleep(sleepTimeout); 561 562 continue; 563 } 564 565 version(IrcDebugLogging) logDebug("irc recv: %s", line); 566 lineReceived(line); 567 } 568 569 loggedIn = false; 570 571 version(IrcDebugLogging) logDebug("irc disconnected"); 572 } 573 574 private void lineReceived(string line) 575 { 576 import std.conv: ConvException, to; 577 578 lastIncomingLineTime = Clock.currTime; 579 string[] parts = line.split(" "); 580 581 //commands of the form `CMD :data` are handled here 582 //handleCommand and handleNumeric are for `:origin CMD :data` 583 switch(parts[0]) 584 { 585 case "PING": 586 sendLine("PONG %s", parts[1]); 587 588 break; 589 case "ERROR": 590 throw new GracelessDisconnect(parts.dropFirst.join.dropFirst); 591 default: 592 parts[0] = parts[0].dropFirst; 593 594 try 595 handleNumeric(parts[0], parts[1].to!int, parts[2 .. $]); 596 catch(ConvException err) 597 handleCommand(parts[0], parts[1], parts[2 .. $]); 598 } 599 } 600 601 private void handleCommand(string prefix, string command, string[] parts) 602 { 603 version(IrcDebugLogging) logDebug("handleCommand(%s, %s, %s)", prefix, command, parts); 604 605 switch(command) 606 { 607 case "NOTICE": 608 case "PRIVMSG": 609 Message msg; 610 string message = parts.dropFirst.join.dropFirst; 611 msg.sender = prefix.splitUserinfo; 612 msg.target = parts[0]; 613 msg.message = message; 614 615 if(message.isCTCP) 616 { 617 auto parsedCtcp = message.parseCTCP; 618 msg.ctcpCommand = parsedCtcp.command; 619 msg.message = parsedCtcp.message; 620 } 621 622 if(command == "NOTICE") 623 runCallback(onNotice, msg); 624 else 625 runCallback(onMessage, msg); 626 627 break; 628 case "JOIN": 629 runCallback(onUserJoin, prefix.splitUserinfo, parts[0].dropFirst); 630 631 break; 632 case "PART": 633 runCallback(onUserPart, prefix.splitUserinfo, parts[0], parts.dropFirst.join.dropFirst); 634 635 break; 636 case "QUIT": 637 runCallback(onUserQuit, prefix.splitUserinfo, parts.join.dropFirst); 638 639 break; 640 case "NICK": 641 runCallback(onUserRename, prefix.splitUserinfo, parts[0].dropFirst); 642 643 break; 644 case "KICK": 645 runCallback(onUserKick, prefix.splitUserinfo, parts[1], parts[0], parts[2 .. $].join.dropFirst); 646 647 break; 648 case "PONG": 649 receivedPong = true; 650 651 break; 652 default: 653 runCallback(onUnknownCommand, prefix, command, parts); 654 } 655 } 656 657 private void handleNumeric(string prefix, int id, string[] parts) 658 { 659 version(IrcDebugLogging) logDebug("handleNumeric(%s, %s, %s)", prefix, id, parts); 660 661 switch(id) 662 { 663 case Numeric.RPL_WELCOME: 664 version(IrcDebugLogging) logDebug("irc logged in"); 665 666 loggedIn = true; 667 serverHostname = prefix; 668 669 runCallback(onLogin); 670 671 break; 672 case Numeric.ERR_ERRONEUSNICKNAME: 673 if(!loggedIn) 674 throw new GracelessDisconnect("Erroneus nickname"); 675 676 break; 677 case Numeric.ERR_NICKNAMEINUSE: 678 if(!loggedIn) 679 throw new GracelessDisconnect("Nickname already in use"); 680 681 break; 682 default: 683 runCallback(onUnknownNumeric, prefix, id, parts); 684 } 685 } 686 687 private void flushMessageBuffer() 688 { 689 import std.datetime: Clock; 690 691 version(IrcDebugLogging) uint currentSend = 0; 692 693 void updateTime() 694 { 695 bufferNextTime = Clock.currTime + bufferTimeout + dur!"seconds"(1); //add a second just to be safe 696 } 697 698 if(buffer.length == 0) 699 return; 700 701 if(Clock.currTime > bufferNextTime) 702 { 703 bufferSent = 0; 704 705 updateTime; 706 } 707 708 if(bufferSent >= bufferLimit) 709 return; 710 711 version(IrcDebugLogging) logDebug("irc flushMessageBuffer: about to send, %s so far this period", bufferSent); 712 713 while(true) 714 { 715 if(buffer.length == 0) 716 { 717 version(IrcDebugLogging) logDebug("irc flushMessageBuffer: ran out of messages"); 718 719 break; 720 } 721 722 if(bufferSent >= bufferLimit) 723 { 724 version(IrcDebugLogging) logDebug("irc flushMessageBuffer: hit buffering limit"); 725 726 break; 727 } 728 729 string line = buffer[0]; 730 buffer = buffer[1 .. $]; 731 bufferSent++; 732 version(IrcDebugLogging) currentSend++; 733 734 version(IrcDebugLogging) logDebug("irc send: %s", line); 735 transport.write(line ~ "\r\n"); 736 } 737 738 updateTime; 739 740 version(IrcDebugLogging) logDebug("irc flushMessageBuffer: sent %s this loop", currentSend); 741 } 742 743 private auto runCallback(CallbackType, Args...)(CallbackType callback, Args args) 744 { 745 static assert( 746 isCallable!callback, 747 "runCallback passed a non-delegate: " ~ CallbackType.stringof 748 ); 749 static assert( 750 __traits(compiles, callback(args)), 751 "Cannot call " ~ CallbackType.stringof ~ " with types " ~ Args.stringof 752 ); 753 754 if(callback !is null) 755 return callback(args); 756 else 757 { 758 static if(is(ReturnType!callback == void)) 759 return; 760 else 761 return ReturnType!callback.init; 762 } 763 } 764 765 private void checkPingTime() 766 { 767 //how long to wait before sending a PING 768 static const timeUntilSendPing = 1.minutes; 769 //how long to wait after sending PING before considering the connection closed 770 static const timeUntilErroring = 15.seconds; 771 auto now = Clock.currTime; 772 auto nextPing = lastIncomingLineTime + timeUntilSendPing; 773 774 if(receivedPong) 775 { 776 receivedPong = false; 777 sentPing = false; 778 779 return; 780 } 781 782 if(now < nextPing) 783 return; 784 785 if(sentPing) 786 { 787 if(now < nextPing + timeUntilErroring) 788 return; 789 790 throw new GracelessDisconnect("Connection timed out"); 791 } 792 793 sendLine("PING :%s", serverHostname); 794 795 sentPing = true; 796 } 797 798 private void resetFields() 799 { 800 protocolTask = Task.init; 801 transport = TCPConnection.init; 802 buffer.length = 0; 803 bufferSent = 0; 804 bufferNextTime = Clock.currTime; 805 lastIncomingLineTime = Clock.currTime; 806 sentPing = false; 807 receivedPong = false; 808 } 809 810 /*======================================* 811 *======================================* 812 * Public Methods * 813 *======================================* 814 *======================================*/ 815 816 /++ 817 Connect to the IRC network and start the protocol loop. 818 819 Params: 820 host = hostname/address to connect to 821 port = port to connect on 822 password = password to use when logging in to the network (optional) 823 +/ 824 void connect(string host, ushort port, string password = null) 825 { 826 import vibe.core.net: connectTCP; 827 import vibe.core.core: runTask; 828 829 if(connected) 830 throw new Exception("Already connected!"); 831 832 resetFields; 833 834 string disconnectReason = "Connection terminated gracefully"; 835 protocolTask = runTask( 836 { 837 version(IrcDebugLogging) logDebug("Starting protocol loop"); 838 839 try 840 { 841 transport = connectTCP(host, port); 842 843 protocolLoop(password); 844 } 845 catch(Exception err) 846 { 847 if(cast(GracelessDisconnect)err) 848 disconnectReason = err.msg; 849 else 850 disconnectReason = "%s: %s".format(typeid(err).name, err.msg); 851 852 if(connected) 853 transport.close; 854 } 855 856 runCallback(onDisconnect, disconnectReason); 857 } 858 ); 859 } 860 861 /++ 862 Disconnect from the network, giving reason as the quit message. 863 +/ 864 void quit(string reason) 865 in { assert(connected); } 866 body 867 { 868 sendLine("QUIT :%s", reason); 869 870 if(Task.getThis !is protocolTask) 871 protocolTask.join; 872 873 transport.close; 874 } 875 876 /++ 877 Send a raw IRC command. 878 879 Params: 880 contents = format string for the line 881 args = formatting arguments 882 +/ 883 void sendLine(Args...)(string contents, Args args) 884 in { assert(connected); } 885 body 886 { 887 contents = contents.format(args); 888 889 if(buffering) 890 buffer ~= contents; 891 else 892 { 893 version(IrcDebugLogging) logDebug("irc send: %s", contents); 894 transport.write(contents ~ "\r\n"); 895 } 896 } 897 898 /++ 899 Send a message. 900 901 Params: 902 destination = destination of the message, either a #channel or a nickname 903 message = the body of the message 904 notice = send a NOTICE instead of a PRIVMSG 905 +/ 906 void send(string destination, string message, bool notice = false) 907 { 908 foreach(line; message.split("\n")) 909 sendLine("%s %s :%s", notice ? "NOTICE" : "PRIVMSG", destination, line); 910 } 911 912 /++ 913 Join a channel. 914 +/ 915 void join(string name) 916 { 917 sendLine("JOIN %s", name); 918 } 919 }