1 /** 2 * Client definition 3 */ 4 module birchwood.client.client; 5 6 import std.socket : Socket, SocketException, Address, getAddress, SocketType, ProtocolType, SocketOSException; 7 import std.socket : SocketFlags, SocketShutdown; 8 import std.conv : to; 9 import std.container.slist : SList; 10 import core.sync.mutex : Mutex; 11 import core.thread : Thread, dur; 12 import std.string; 13 import eventy : EventyEvent = Event, Engine, EventType, Signal, EventyException; 14 import birchwood.config; 15 import birchwood.client.exceptions : BirchwoodException, ErrorType; 16 import birchwood.protocol.messages : Message, encodeMessage, decodeMessage, isValidText; 17 import birchwood.protocol.constants : ReplyType; 18 19 import birchwood.client.receiver : ReceiverThread; 20 import birchwood.client.sender : SenderThread; 21 import birchwood.client.events; 22 import birchwood.logging; 23 24 // TODO: Make abstract and for unit tests make a `DefaultClient` 25 // ... which logs outputs for the `onX()` handler functions 26 27 /** 28 * IRC client 29 */ 30 public class Client : Thread 31 { 32 /** 33 * Connection information 34 */ 35 package shared ConnectionInfo connInfo; 36 37 /* TODO: We should learn some info in here (or do we put it in connInfo)? */ 38 private string serverName; //TODO: Make use of 39 40 /** 41 * Underlying connection to the server 42 */ 43 package Socket socket; 44 45 /** 46 * Receive queue meneger 47 */ 48 private ReceiverThread receiver; 49 50 /** 51 * Send queue manager 52 */ 53 private SenderThread sender; 54 55 /** 56 * Eventy event engine 57 */ 58 private Engine engine; 59 60 /** 61 * Whether the client is running or not 62 */ 63 private bool running = false; 64 65 /** 66 * Checks whether this client is running 67 * 68 * Returns: `true` if running, `false` 69 * otherwise 70 */ 71 package bool isRunning() 72 { 73 return this.running; 74 } 75 76 /** 77 * Returns the eventy engine 78 * 79 * Returns: the `Engine` 80 */ 81 package Engine getEngine() 82 { 83 return this.engine; 84 } 85 86 /** 87 * Constructs a new IRC client with the given configuration 88 * info 89 * 90 * Params: 91 * connInfo = the connection parameters 92 */ 93 this(ConnectionInfo connInfo) 94 { 95 super(&loop); 96 this.connInfo = connInfo; 97 98 /** 99 * Set defaults in db 100 */ 101 setDefaults(this.connInfo); 102 } 103 104 // TODO: ANything worth callin on destruction? 105 ~this() 106 { 107 //TODO: Do something here, tare downs 108 } 109 110 /** 111 * Retrieve the active configuration at this 112 * moment 113 * 114 * Returns: the ConnectionInfo struct 115 */ 116 public ConnectionInfo getConnInfo() 117 { 118 return connInfo; 119 } 120 121 /** 122 * Called on reception of a channel message 123 * 124 * Params: 125 * fullMessage = the channel message in its entirety 126 * channel = the channel 127 * msgBody = the body of the message 128 */ 129 public void onChannelMessage(Message fullMessage, string channel, string msgBody) 130 { 131 /* Default implementation */ 132 DEBUG("Channel("~channel~"): "~msgBody); 133 } 134 135 /** 136 * Called on reception of a direct message 137 * 138 * Params: 139 * fullMessage = the direct message in its entirety 140 * nickname = the sender 141 * msgBody = the body of the message 142 */ 143 public void onDirectMessage(Message fullMessage, string nickname, string msgBody) 144 { 145 /* Default implementation */ 146 DEBUG("DirectMessage("~nickname~"): "~msgBody); 147 } 148 149 /** 150 * Called on generic commands 151 * 152 * Params: 153 * commandReply = the generic message 154 */ 155 public void onGenericCommand(Message message) 156 { 157 /* Default implementation */ 158 DEBUG("Generic("~message.getCommand()~", "~message.getFrom()~"): "~message.getParams()); 159 } 160 161 // TODO: Hook certain ones default style with an implemenation 162 // ... for things that the client can learn from 163 /** 164 * Called on command replies 165 * 166 * Params: 167 * commandReply = the command's reply 168 */ 169 public void onCommandReply(Message commandReply) 170 { 171 // TODO: Add numeric response check here for CERTAIN ones which add to client 172 // ... state 173 174 /* Default implementation */ 175 DEBUG("Response("~to!(string)(commandReply.getReplyType())~", "~commandReply.getFrom()~"): "~commandReply.toString()); 176 177 if(commandReply.getReplyType() == ReplyType.RPL_ISUPPORT) 178 { 179 // TODO: Testing code was here 180 // DEBUG(); 181 // DEBUG("<<<>>>"); 182 183 // DEBUG("Take a look:\n\n"~commandReply.getParams()); 184 185 // DEBUG("And here is key-value pairs: ", commandReply.getKVPairs()); 186 // DEBUG("And here is array: ", commandReply.getPairs()); 187 188 // // TODO: DLog bug, this prints nothing 189 // DEBUG("And here is trailing: ", commandReply.getTrailing()); 190 191 // import std.stdio; 192 // writeln("Trailer: "~commandReply.getTrailing()); 193 194 // writeln(cast(ubyte[])commandReply.getTrailing()); 195 196 // DEBUG("<<<>>>"); 197 // DEBUG(); 198 199 import std.stdio; 200 writeln("Support stuff: ", commandReply.getKVPairs()); 201 202 /* Fetch and parse the received key-value pairs */ 203 string[string] receivedKV = commandReply.getKVPairs(); 204 foreach(string key; receivedKV.keys()) 205 { 206 /* Update the db */ 207 string value = receivedKV[key]; 208 connInfo.updateDB(key, value); 209 DEBUG("Updated key in db '"~key~"' with value '"~value~"'"); 210 } 211 212 } 213 } 214 215 /** 216 * Called when the connection to the remote host is closed 217 */ 218 public void onConnectionClosed() 219 { 220 // TODO: Add log as default behaviour? 221 DEBUG("Connection was closed, not doing anything"); 222 } 223 224 /** 225 * Requests setting of the provided nickname 226 * 227 * Params: 228 * nickname = the nickname to request 229 * Throws: 230 * `BirchwoodException` on invalid nickname 231 */ 232 public void nick(string nickname) 233 { 234 /* Ensure no illegal characters in nick name */ 235 if(textPass(nickname)) 236 { 237 // TODO: We could investigate this later if we want to be safer 238 ulong maxNickLen = connInfo.getDB!(ulong)("MAXNICKLEN"); 239 240 /* If the username's lenght is within the allowed bounds */ 241 if(nickname.length <= maxNickLen) 242 { 243 /* Set the nick */ 244 Message nickMessage = new Message("", "NICK", nickname); 245 sendMessage(nickMessage); 246 } 247 /* If not */ 248 else 249 { 250 throw new BirchwoodException(ErrorType.NICKNAME_TOO_LONG, "The nickname was over thge length of "~to!(string)(maxNickLen)~" characters"); 251 } 252 } 253 else 254 { 255 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "There are illegal characters in the nickname"); 256 } 257 } 258 259 /** 260 * Joins the requested channel 261 * 262 * Params: 263 * channel = the channel to join 264 * Throws: 265 * `BirchwoodException` on invalid channel name 266 */ 267 public void joinChannel(string channel) 268 { 269 /* Ensure no illegal characters in channel name */ 270 if(textPass(channel)) 271 { 272 /* Channel name must start with a `#` */ 273 if(channel[0] == '#') 274 { 275 /* Join the channel */ 276 Message joinMessage = new Message("", "JOIN", channel); 277 sendMessage(joinMessage); 278 } 279 else 280 { 281 throw new BirchwoodException(ErrorType.INVALID_CHANNEL_NAME, "Channel name does not start with a #"); 282 } 283 } 284 else 285 { 286 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Invalid characters in channel"); 287 } 288 } 289 290 291 /** 292 * Provided with a reference to a string 293 * this will check to see if it contains 294 * any illegal characters and then if so 295 * it will strip them if the `ChecksMode` 296 * is set to `EASY` (and return `true`) 297 * else it will return `false` if set to 298 * `HARDCORE` whilst illegal characters 299 * are present. 300 * 301 * Params: 302 * text = the ref'd `string` 303 * Returns: `true` if validated, `false` 304 * otherwise 305 */ 306 private bool textPass(ref string text) 307 { 308 /* If there are any invalid characters */ 309 if(Message.hasIllegalCharacters(text)) 310 { 311 import birchwood.config.conninfo : ChecksMode; 312 if(connInfo.getMode() == ChecksMode.EASY) 313 { 314 // Filter the text and update it in-place 315 text = Message.stripIllegalCharacters(text); 316 return true; 317 } 318 else 319 { 320 return false; 321 } 322 } 323 /* If there are no invalid characters prewsent */ 324 else 325 { 326 return true; 327 } 328 } 329 330 /** 331 * Joins the requested channels 332 * 333 * Params: 334 * channels = the channels to join 335 * Throws: 336 * `BirchwoodException` on invalid channel name or 337 * if the list is empty 338 */ 339 public void joinChannel(string[] channels) 340 { 341 /* If single channel */ 342 if(channels.length == 1) 343 { 344 /* Join the channel */ 345 joinChannel(channels[0]); 346 } 347 /* If multiple channels */ 348 else if(channels.length > 1) 349 { 350 string channelLine = channels[0]; 351 352 /* Ensure valid characters in first channel */ 353 if(textPass(channelLine)) 354 { 355 //TODO: Add check for # 356 357 /* Append on a trailing `,` */ 358 channelLine ~= ","; 359 360 for(ulong i = 1; i < channels.length; i++) 361 { 362 string currentChannel = channels[i]; 363 364 /* Ensure the character channel is valid */ 365 if(textPass(currentChannel)) 366 { 367 //TODO: Add check for # 368 369 if(i == channels.length-1) 370 { 371 channelLine~=currentChannel; 372 } 373 else 374 { 375 channelLine~=currentChannel~","; 376 } 377 } 378 else 379 { 380 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Invalid characters in the channel"); 381 } 382 } 383 384 /* Join multiple channels */ 385 Message joinMessage = new Message("", "JOIN", channelLine); 386 sendMessage(joinMessage); 387 } 388 else 389 { 390 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Invalid characters in the channel"); 391 } 392 } 393 /* If no channels provided at all (error) */ 394 else 395 { 396 throw new BirchwoodException(ErrorType.EMPTY_PARAMS, "No channels provided"); 397 } 398 } 399 400 /** 401 * Parts from a list of channel(s) in one go 402 * 403 * Params: 404 * channels = the list of channels to part from 405 * Throws: 406 * `BirchwoodException` if the channels list is empty 407 * or there are illegal characters present 408 */ 409 public void leaveChannel(string[] channels) 410 { 411 // TODO: Add check for valid and non-empty channel names 412 413 /* If single channel */ 414 if(channels.length == 1) 415 { 416 /* Leave the channel */ 417 leaveChannel(channels[0]); 418 } 419 /* If multiple channels */ 420 else if(channels.length > 1) 421 { 422 string channelLine = channels[0]; 423 424 /* Ensure valid characters in first channel */ 425 if(textPass(channelLine)) 426 { 427 //TODO: Add check for # 428 429 /* Append on a trailing `,` */ 430 channelLine ~= ","; 431 432 for(ulong i = 1; i < channels.length; i++) 433 { 434 string currentChannel = channels[i]; 435 436 /* Ensure the character channel is valid */ 437 if(textPass(currentChannel)) 438 { 439 //TODO: Add check for # 440 441 if(i == channels.length-1) 442 { 443 channelLine~=currentChannel; 444 } 445 else 446 { 447 channelLine~=currentChannel~","; 448 } 449 } 450 else 451 { 452 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Invalid characters in the channel"); 453 } 454 } 455 456 /* Leave multiple channels */ 457 Message leaveMessage = new Message("", "PART", channelLine); 458 sendMessage(leaveMessage); 459 } 460 else 461 { 462 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Invalid characters in the channel"); 463 } 464 } 465 /* If no channels provided at all (error) */ 466 else 467 { 468 throw new BirchwoodException(ErrorType.EMPTY_PARAMS, "No channels were provided"); 469 } 470 } 471 472 /** 473 * Part from a single channel 474 * 475 * Params: 476 * channel = the channel to leave 477 * Throws: 478 * `BirchwoodException` if the channel name 479 * is invalid 480 */ 481 public void leaveChannel(string channel) 482 { 483 /* Ensure the channel name contains only valid characters */ 484 if(textPass(channel)) 485 { 486 /* Leave the channel */ 487 Message leaveMessage = new Message("", "PART", channel); 488 sendMessage(leaveMessage); 489 } 490 /* If invalid characters were present */ 491 else 492 { 493 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "There are illegal characters in the channel name"); 494 } 495 } 496 497 /** 498 * Sends a direct message to the intended recipients 499 * 500 * Params: 501 * message = The message to send 502 * recipients = The receipients of the message 503 * Throws: 504 * `BirchwoodException` if the recipients list is empty 505 * or illegal characters are present 506 */ 507 public void directMessage(string message, string[] recipients) 508 { 509 /* Single recipient */ 510 if(recipients.length == 1) 511 { 512 /* Send a direct message */ 513 directMessage(message, recipients[0]); 514 } 515 /* Multiple recipients */ 516 else if(recipients.length > 1) 517 { 518 /* Ensure message is valid */ 519 if(textPass(message)) 520 { 521 string recipientLine = recipients[0]; 522 523 /* Ensure valid characters in first recipient */ 524 if(textPass(recipientLine)) 525 { 526 /* Append on a trailing `,` */ 527 recipientLine ~= ","; 528 529 for(ulong i = 1; i < recipients.length; i++) 530 { 531 string currentRecipient = recipients[i]; 532 533 /* Ensure valid characters in the current recipient */ 534 if(textPass(currentRecipient)) 535 { 536 if(i == recipients.length-1) 537 { 538 recipientLine~=currentRecipient; 539 } 540 else 541 { 542 recipientLine~=currentRecipient~","; 543 } 544 } 545 else 546 { 547 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "There are illegal characters in the recipient"); 548 } 549 } 550 551 /* Send the message */ 552 Message privMessage = new Message("", "PRIVMSG", recipientLine~" "~message); 553 sendMessage(privMessage); 554 } 555 else 556 { 557 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "There are illegal characters in the recipient"); 558 } 559 } 560 else 561 { 562 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "The message contains invalid characters"); 563 } 564 } 565 /* If no recipients provided at all (error) */ 566 else 567 { 568 throw new BirchwoodException(ErrorType.EMPTY_PARAMS, "No recipients were provided"); 569 } 570 } 571 572 /** 573 * Sends a direct message to the intended recipient 574 * 575 * Params: 576 * message = The message to send 577 * recipients = The receipient of the message 578 * Throws: 579 * `BirchwoodException` if the receipient's nickname 580 * is invalid or there are illegal characters present 581 */ 582 public void directMessage(string message, string recipient) 583 { 584 /* Ensure the message and recipient are valid text */ 585 if(textPass(message) && textPass(recipient)) 586 { 587 /* Ensure the recipient does NOT start with a # (as that is reserved for channels) */ 588 if(recipient[0] != '#') 589 { 590 /* Send the message */ 591 Message privMessage = new Message("", "PRIVMSG", recipient~" "~message); 592 sendMessage(privMessage); 593 } 594 else 595 { 596 throw new BirchwoodException(ErrorType.INVALID_NICK_NAME, "The provided nickname contains invalid characters"); 597 } 598 } 599 else 600 { 601 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "There are illegal characters in either the message of the recipient"); 602 } 603 } 604 605 /** 606 * Sends a channel message to the intended recipients 607 * 608 * Params: 609 * message = The message to send 610 * recipients = The receipients of the message 611 * Throws: 612 * `BirchwoodException` if the channels list is empty 613 */ 614 public void channelMessage(string message, string[] channels) 615 { 616 /* If single channel */ 617 if(channels.length == 1) 618 { 619 /* Send to a single channel */ 620 channelMessage(message, channels[0]); 621 } 622 /* If multiple channels */ 623 else if(channels.length > 1) 624 { 625 /* Ensure message is valid */ 626 if(textPass(message)) 627 { 628 string channelLine = channels[0]; 629 630 /* Ensure valid characters in first channel */ 631 if(textPass(channelLine)) 632 { 633 /* Append on a trailing `,` */ 634 channelLine ~= ","; 635 636 for(ulong i = 1; i < channels.length; i++) 637 { 638 string currentChannel = channels[i]; 639 640 /* Ensure valid characters in current channel */ 641 if(textPass(currentChannel)) 642 { 643 if(i == channels.length-1) 644 { 645 channelLine~=currentChannel; 646 } 647 else 648 { 649 channelLine~=currentChannel~","; 650 } 651 } 652 else 653 { 654 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "One of the channel names contains invalid characters"); 655 } 656 } 657 658 /* Send to multiple channels */ 659 Message privMessage = new Message("", "PRIVMSG", channelLine~" "~message); 660 sendMessage(privMessage); 661 } 662 else 663 { 664 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "One of the channel names contains invalid characters"); 665 } 666 } 667 else 668 { 669 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Invalid characters in the message"); 670 } 671 } 672 /* If no channels provided at all (error) */ 673 else 674 { 675 throw new BirchwoodException(ErrorType.EMPTY_PARAMS, "No channels were provided"); 676 } 677 } 678 679 /** 680 * Sends a message to a given channel 681 * 682 * Params: 683 * message = The message to send 684 * channel = The channel to send the message to 685 * Throws: 686 * `BirchwoodException` if the message or channel name 687 * contains illegal characters 688 */ 689 public void channelMessage(string message, string channel) 690 { 691 //TODO: Add check on recipient 692 //TODO: Add emptiness check 693 if(textPass(message) && textPass(channel)) 694 { 695 if(channel[0] == '#') 696 { 697 /* Send the channel message */ 698 Message privMessage = new Message("", "PRIVMSG", channel~" "~message); 699 sendMessage(privMessage); 700 } 701 else 702 { 703 throw new BirchwoodException(ErrorType.INVALID_CHANNEL_NAME, "The channel is missign a # infront of its name"); 704 } 705 } 706 else 707 { 708 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Channel name of message contains invalid characters"); 709 } 710 } 711 712 /** 713 * Issues a command to the server 714 * 715 * Params: 716 * message = the Message object containing the command to issue 717 */ 718 public void command(Message message) 719 { 720 /* Send the message */ 721 sendMessage(message); 722 } 723 724 /** 725 * Initialize the event handlers 726 * 727 * Throws: 728 * `EventyException` on error registering 729 * the signals and event types 730 */ 731 private void initEvents() 732 { 733 /* TODO: For now we just register one signal type for all messages */ 734 735 /* Register all event types */ 736 engine.addEventType(new EventType(IRCEventType.GENERIC_EVENT)); 737 engine.addEventType(new EventType(IRCEventType.PONG_EVENT)); 738 739 740 /* Base signal with IRC client in it */ 741 abstract class BaseSignal : Signal 742 { 743 /* ICR client */ 744 private Client client; 745 746 this(Client client, ulong[] eventIDs) 747 { 748 super(eventIDs); 749 this.client = client; 750 } 751 } 752 753 754 /* Handles all IRC messages besides PING */ 755 class GenericSignal : BaseSignal 756 { 757 this(Client client) 758 { 759 super(client, [IRCEventType.GENERIC_EVENT]); 760 } 761 762 public override void handler(EventyEvent e) 763 { 764 /* TODO: Insert cast here to our custoim type */ 765 IRCEvent ircEvent = cast(IRCEvent)e; 766 assert(ircEvent); //Should never fail, unless some BOZO regged multiple handles for 1 - wait idk does eventy do that even mmm 767 768 // NOTE: Enable this when debugging 769 // DEBUG("IRCEvent(message): "~ircEvent.getMessage().toString()); 770 771 /* TODO: We should use a switch statement, imagine how nice */ 772 Message ircMessage = ircEvent.getMessage(); 773 string command = ircMessage.getCommand(); 774 string params = ircMessage.getParams(); 775 776 777 if(cmp(command, "PRIVMSG") == 0) 778 { 779 // TODO: We will need a non kv pair thing as well to see (in the 780 // ... case of channel messages) the singular pair <channel> 781 // ... name. 782 // 783 // Then our message will be in `getTrailing()` 784 logger.debug_("PrivMessage parser (kv-pairs): ", ircMessage.getKVPairs()); 785 logger.debug_("PrivMessage parser (trailing): ", ircMessage.getTrailing()); 786 787 /* Split up into (channel/nick) and (message)*/ 788 long firstSpaceIdx = indexOf(params, " "); //TODO: validity check; 789 string chanNick = params[0..firstSpaceIdx]; 790 791 /* Extract the message from params */ 792 long firstColonIdx = indexOf(params, ":"); //TODO: validity check 793 string message = params[firstColonIdx+1..params.length]; 794 795 /* If it starts with `#` then channel */ 796 if(chanNick[0] == '#') 797 { 798 /* Call the channel message handler */ 799 onChannelMessage(ircMessage, chanNick, message); 800 } 801 /* Else, direct message */ 802 else 803 { 804 /* Call the direct message handler */ 805 onDirectMessage(ircMessage, chanNick, message); 806 } 807 } 808 // If the command is numeric then it is a reply of some sorts 809 else if(ircMessage.isResponseMessage()) 810 { 811 // TODO: Add numeric response check here for CERTAIN ones which add to client 812 // ... state 813 814 /* Call the command reply handler */ 815 onCommandReply(ircMessage); 816 } 817 /* Generic handler */ 818 else 819 { 820 onGenericCommand(ircMessage); 821 } 822 823 //TODO: add more commands 824 } 825 } 826 engine.addSignalHandler(new GenericSignal(this)); 827 828 /* Handles PING messages */ 829 class PongSignal : BaseSignal 830 { 831 this(Client client) 832 { 833 super(client, [IRCEventType.PONG_EVENT]); 834 } 835 836 /* Send a PONG back with the received PING id */ 837 public override void handler(EventyEvent e) 838 { 839 PongEvent pongEvent = cast(PongEvent)e; 840 assert(pongEvent); 841 842 // string messageToSend = "PONG "~pongEvent.getID(); 843 Message pongMessage = new Message("", "PONG", pongEvent.getID()); 844 client.sendMessage(pongMessage); 845 DEBUG("Ponged back with "~pongEvent.getID()); 846 } 847 } 848 engine.addSignalHandler(new PongSignal(this)); 849 } 850 851 /** 852 * Connects to the server 853 * 854 * Throws: 855 * `BirchwoodException` if there is an error connecting 856 * or something failed internally 857 */ 858 public void connect() 859 { 860 if(!running) 861 { 862 try 863 { 864 /* Attempt to connect */ 865 this.socket = new Socket(connInfo.getAddr().addressFamily(), SocketType.STREAM, ProtocolType.TCP); 866 this.socket.connect(connInfo.getAddr()); 867 868 /* Start the event engine */ 869 this.engine = new Engine(); 870 871 /* Register default handler */ 872 initEvents(); 873 874 /* Set the running status to true */ 875 running = true; 876 877 /** 878 * Setups the receiver and sender queue managers 879 */ 880 this.receiver = new ReceiverThread(this); 881 this.sender = new SenderThread(this); 882 883 /* Start the receive queue and send queue managers */ 884 this.receiver.start(); 885 this.sender.start(); 886 887 /* Start the socket read-decode loop */ 888 this.start(); 889 890 /* Do the /NICK and /USER handshake */ 891 doAuth(); 892 } 893 catch(SocketOSException e) 894 { 895 throw new BirchwoodException(ErrorType.CONNECT_ERROR); 896 } 897 catch(EventyException e) 898 { 899 // TODO: Could deallocate here 900 throw new BirchwoodException(ErrorType.INTERNAL_FAILURE, e.toString()); 901 } 902 } 903 // TODO: Do actual liveliness check here 904 else 905 { 906 throw new BirchwoodException(ErrorType.ALREADY_CONNECTED); 907 } 908 } 909 910 /** 911 * Performs the /NICK and /USER handshake. 912 * 913 * This method will set the hostname to be equal to the chosen 914 * username in the ConnectionInfo struct 915 * 916 * Params: 917 * servername = the servername to use (default: bogus.net) 918 */ 919 private void doAuth(string servername = "bogus.net") 920 { 921 Thread.sleep(dur!("seconds")(2)); 922 nick(connInfo.nickname); 923 924 Thread.sleep(dur!("seconds")(2)); 925 // TODO: Note I am making hostname the same as username always (is this okay?) 926 // TODO: Note I am making the servername always bogus.net 927 user(connInfo.username, connInfo.username, servername, connInfo.realname); 928 } 929 930 /** 931 * Performs user identification 932 * 933 * Params: 934 * username = the username to identify with 935 * hostname = the hostname to use 936 * servername = the servername to use 937 * realname = your realname 938 * Throws: 939 * `BirchwoodException` if the username, jostname, 940 * servername or realname contains illegal characters 941 */ 942 public void user(string username, string hostname, string servername, string realname) 943 { 944 // TODO: Implement me properly with all required checks 945 946 if(textPass(username) && textPass(hostname) && textPass(servername) && textPass(realname)) 947 { 948 /* User message */ 949 Message userMessage = new Message("", "USER", username~" "~hostname~" "~servername~" "~":"~realname); 950 sendMessage(userMessage); 951 } 952 else 953 { 954 throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS, "Illegal characters present in either the username, hostname, server name or real name"); 955 } 956 } 957 958 /** 959 * Adds a given message onto the receieve queue for 960 * later processing by the receieve queue worker thread 961 * 962 * Params: 963 * message = the message to enqueue to the receieve queue 964 */ 965 private void receiveQ(ubyte[] message) 966 { 967 /* Enqueue the message to the receive queue */ 968 receiver.rq(message); 969 } 970 971 /** 972 * Sends a message to the server by enqueuing it on 973 * the client-side send queue. 974 * 975 * Any invalid characters will be stripped prior 976 * to encoding IF `ChecksMode` is set to `EASY` (default) 977 * 978 * Params: 979 * message = the message to send 980 * Throws: 981 * A `BirchwoodException` is thrown if the messages 982 * final length exceeds 512 bytes of if `ChecksMode` 983 * is set to `HARDCORE` 984 */ 985 private void sendMessage(Message message) 986 { 987 // TODO: Do message splits here 988 989 /* Encode the message */ 990 ubyte[] encodedMessage = encodeMessage(message.encode(connInfo.getMode())); 991 992 /* If the message is 512 bytes or less then send */ 993 if(encodedMessage.length <= 512) 994 { 995 /* Enqueue the message to the send queue */ 996 sender.sq(encodedMessage); 997 } 998 /* If above then throw an exception */ 999 else 1000 { 1001 throw new BirchwoodException(ErrorType.COMMAND_TOO_LONG, "The final encoded length of the message is too long"); 1002 } 1003 } 1004 1005 /** 1006 * Disconnect from the IRC server gracefully 1007 */ 1008 public void quit() 1009 { 1010 /* Generate the quit command using the custom quit message */ 1011 Message quitCommand = new Message("", "QUIT", connInfo.quitMessage); 1012 sendMessage(quitCommand); 1013 1014 /* TODO: I don't know how long we should wait here */ 1015 Thread.sleep(dur!("seconds")(1)); 1016 1017 /* Tare down the client */ 1018 disconnect(); 1019 } 1020 1021 /** 1022 * Tare down the client by setting the run state 1023 * to false, closing the socket, stopping the 1024 * receieve and send handlers and the event engine 1025 */ 1026 private void disconnect() 1027 { 1028 /* Set the state of running to false */ 1029 running = false; 1030 DEBUG("disconnect() begin"); 1031 1032 /* Shutdown the socket */ 1033 1034 /** 1035 * Shutdown the socket unblocking 1036 * any reads and writes occuring 1037 * 1038 * Notably this unblocks the receiver 1039 * thread and causes it to handle 1040 * the shutdown. 1041 */ 1042 import std.socket : SocketShutdown; 1043 socket.shutdown(SocketShutdown.BOTH); 1044 DEBUG("disconnect() socket shutdown"); 1045 1046 1047 } 1048 1049 /** 1050 * Cleans up resources which would have been allocated 1051 * during the call to `connect()` and for the duration 1052 * of the open session 1053 */ 1054 private void doThreadCleanup() 1055 { 1056 /* Stop the receive queue manager and wait for it to stop */ 1057 receiver.end(); 1058 DEBUG("doThreadCleanup() recvQueue manager stopped"); 1059 receiver = null; 1060 1061 /* Stop the send queue manager and wait for it to stop */ 1062 sender.end(); 1063 DEBUG("doThreadCleanup() sendQueue manager stopped"); 1064 sender = null; 1065 1066 /* TODO: Stop eventy (FIXME: I don't know if this is implemented in Eventy yet, do this!) */ 1067 engine.shutdown(); 1068 DEBUG("doThreadCleanup() eventy stopped"); 1069 engine = null; 1070 1071 DEBUG("doThreadCleanup() end"); 1072 } 1073 1074 /** 1075 * Called by the main loop thread to process the received 1076 * and CRLF-delimited message 1077 * 1078 * Params: 1079 * message = the message to add to the receive queue 1080 */ 1081 private void processMessage(ubyte[] message) 1082 { 1083 // import std.stdio; 1084 // DEBUG("Message length: "~to!(string)(message.length)); 1085 // DEBUG("InterpAsString: "~cast(string)message); 1086 1087 receiveQ(message); 1088 } 1089 1090 /** 1091 * The main loop for the Client thread which receives data 1092 * sent from the server 1093 */ 1094 private void loop() 1095 { 1096 /* TODO: We could do below but nah for now as we know max 512 bytes */ 1097 /* TODO: Make the read bulk size a configurable parameter */ 1098 /* TODO: Make static array allocation outside, instead of a dynamic one */ 1099 // ulong bulkReadSize = 20; 1100 1101 /* Fixed allocation of `bulkReadSize` for temporary data */ 1102 ubyte[] currentData; 1103 currentData.length = connInfo.getBulkReadSize(); 1104 1105 // malloc(); 1106 1107 /* Total built message */ 1108 ubyte[] currentMessage; 1109 1110 bool hasCR = false; 1111 1112 /** 1113 * Message loop 1114 * 1115 * FIXME: We need to find a way to tare down this socket, we don't 1116 * want to block forever after running quit 1117 */ 1118 readLoop: while(running) 1119 { 1120 /* Receieve at most 512 bytes (as per RFC) */ 1121 ptrdiff_t bytesRead = socket.receive(currentData, SocketFlags.PEEK); 1122 1123 // TODO: Should not be JUST unittest builds 1124 // TODO: This sort of logic should be used by EVERY read 1125 version(unittest) 1126 { 1127 import std.stdio; 1128 writeln("(peek) bytesRead: '", bytesRead, "' (status var or count)"); 1129 writeln("(peek) currentData: '", currentData, "'"); 1130 } 1131 1132 /** 1133 * Check if the remote host closed the connection 1134 * OR some general error occurred 1135 * 1136 * TODO: See if the code is safe enough to only 1137 * have to do this ONCE 1138 */ 1139 if(bytesRead == 0 || bytesRead < 0) 1140 { 1141 version(unittest) 1142 { 1143 import std.stdio; 1144 writeln("Remote host ended connection or general error, Socket.ERROR: '", bytesRead, "'"); 1145 } 1146 1147 /* Set running state to false, then exit loop */ 1148 this.running = false; 1149 continue readLoop; 1150 } 1151 1152 1153 1154 1155 1156 /* FIXME: CHECK BYTES READ FOR SOCKET ERRORS! */ 1157 1158 /* If we had a CR previously then now we need a LF */ 1159 if(hasCR) 1160 { 1161 /* First byte following it should be LF */ 1162 if(currentData[0] == 10) 1163 { 1164 /* Add to the message */ 1165 currentMessage~=currentData[0]; 1166 1167 /* TODO: Process mesaage */ 1168 processMessage(currentMessage); 1169 1170 /* Reset state for next message */ 1171 currentMessage.length = 0; 1172 hasCR=false; 1173 1174 /* Chop off the LF */ 1175 ubyte[] scratch; 1176 scratch.length = 1; 1177 long status = this.socket.receive(scratch); 1178 1179 /** 1180 * Check if the remote host closed the connection 1181 * OR some general error occurred 1182 */ 1183 if(status == 0 || status < 0) 1184 { 1185 /* Set running state to false, then exit loop */ 1186 this.running = false; 1187 continue readLoop; 1188 } 1189 1190 continue; 1191 } 1192 else 1193 { 1194 /* TODO: This is an error */ 1195 assert(false); 1196 } 1197 } 1198 1199 ulong pos; 1200 for(pos = 0; pos < bytesRead; pos++) 1201 { 1202 /* Find first CR */ 1203 if(currentData[pos] == 13) 1204 { 1205 /* If we already have CR then that is an error */ 1206 if(hasCR) 1207 { 1208 /* TODO: Handle this */ 1209 assert(false); 1210 } 1211 1212 hasCR = true; 1213 break; 1214 } 1215 } 1216 1217 /* If we have a CR, then read up to that */ 1218 if(hasCR) 1219 { 1220 /* Read up to CR */ 1221 currentMessage~=currentData[0..pos+1]; 1222 1223 /* Dequeue this (TODO: way to dispose without copy over) */ 1224 /* Guaranteed as we peeked this lenght */ 1225 ubyte[] scratch; 1226 scratch.length = pos+1; 1227 long status = this.socket.receive(scratch); 1228 1229 /** 1230 * Check if the remote host closed the connection 1231 * OR some general error occurred 1232 */ 1233 if(status == 0 || status < 0) 1234 { 1235 /* Set running state to false, then exit loop */ 1236 this.running = false; 1237 continue readLoop; 1238 } 1239 1240 continue; 1241 } 1242 1243 /* Add whatever we have read to build-up */ 1244 currentMessage~=currentData[0..bytesRead]; 1245 1246 /* TODO: Dequeue without peek after this */ 1247 ubyte[] scratch; 1248 scratch.length = bytesRead; 1249 long status = this.socket.receive(scratch); 1250 1251 /** 1252 * Check if the remote host closed the connection 1253 * OR some general error occurred 1254 */ 1255 if(status == 0 || status < 0) 1256 { 1257 /* Set running state to false, then exit loop */ 1258 this.running = false; 1259 continue readLoop; 1260 } 1261 1262 /* TODO: Yield here and in other places before continue */ 1263 } 1264 1265 /* Shut down socket AND close it */ 1266 socket.shutdown(SocketShutdown.BOTH); 1267 socket.close(); 1268 1269 /* Shutdown sub-systems */ 1270 doThreadCleanup(); 1271 1272 // FIXME: Really invalidate everything here 1273 1274 /* Call the onDisconnect thing (TODO) */ 1275 onConnectionClosed(); 1276 } 1277 1278 1279 version(unittest) 1280 { 1281 import core.thread; 1282 } 1283 1284 unittest 1285 { 1286 // ConnectionInfo connInfo = ConnectionInfo.newConnection("irc.freenode.net", 6667, "testBirchwood"); 1287 //freenode: 149.28.246.185 1288 //snootnet: 178.62.125.123 1289 //bonobonet: fd08:8441:e254::5 1290 ConnectionInfo connInfo = ConnectionInfo.newConnection("rany.irc.bnet.eu.org", 6667, "birchwood", "doggie", "Tristan B. Kildaire"); 1291 1292 // Set the fakelag to 1 second (server kicks me for spam me thinks if not) 1293 connInfo.setFakeLag(1); 1294 1295 // Create a new Client 1296 Client client = new Client(connInfo); 1297 1298 // Authenticate 1299 client.connect(); 1300 1301 1302 // TODO: The below should all be automatic, maybe once IRCV3 is done 1303 // ... we should automate sending in NICK and USER stuff 1304 // Thread.sleep(dur!("seconds")(2)); 1305 // client.nick("birchwood"); 1306 1307 // Thread.sleep(dur!("seconds")(2)); 1308 // client.command(new Message("", "USER", "doggie doggie irc.frdeenode.net :Tristan B. Kildaire")); 1309 // client.user("doggie", "doggie", "irc.frdeenode.net", "Tristan B. Kildaire"); 1310 1311 1312 1313 1314 1315 Thread.sleep(dur!("seconds")(4)); 1316 // client.command(new Message("", "JOIN", "#birchwood")); 1317 client.joinChannel("#birchwood"); 1318 // TODO: Add a joinChannels(string[]) 1319 client.joinChannel("#birchwood2"); 1320 1321 client.joinChannel(["#birchwoodLeave1", "#birchwoodLeave2", "#birchwoodLeave3"]); 1322 // client.joinChannel("#birchwoodLeave1"); 1323 // client.joinChannel("#birchwoodLeave2"); 1324 // client.joinChannel("#birchwoodLeave3"); 1325 1326 Thread.sleep(dur!("seconds")(2)); 1327 client.command(new Message("", "NAMES", "")); // TODO: add names commdn 1328 1329 Thread.sleep(dur!("seconds")(2)); 1330 client.channelMessage("naai", "#birchwood"); 1331 1332 Thread.sleep(dur!("seconds")(2)); 1333 client.directMessage("naai", "deavmi"); 1334 1335 1336 /** 1337 * Test sending a message to a single channel (multi) 1338 */ 1339 client.channelMessage("This is a test message sent to a channel 1", ["#birchwood"]); 1340 1341 /** 1342 * Test sending a message to a single channel (singular) 1343 */ 1344 client.channelMessage("This is a test message sent to a channel 2", "#birchwood"); 1345 1346 /** 1347 * Test sending a message to multiple channels (multi) 1348 */ 1349 client.channelMessage("This is a message sent to multiple channels one-shot", ["#birchwood", "#birchwood2"]); 1350 1351 /* TODO: Add a check here to make sure the above worked I guess? */ 1352 /* TODO: Make this end */ 1353 // while(true) 1354 // { 1355 1356 // } 1357 1358 /** 1359 * Test sending a message to myself (singular) 1360 */ 1361 client.directMessage("(1) Message to myself", "birchwood"); 1362 1363 /** 1364 * Test sending a message to myself (multi) 1365 */ 1366 client.directMessage("(2) Message to myself (multi)", ["birchwood"]); 1367 1368 /** 1369 * Test sending a message to myself 2x (multi) 1370 */ 1371 client.directMessage("(3) Message to myself (multi)", ["birchwood", "birchwood"]); 1372 1373 1374 /** 1375 * Test formatting of text 1376 */ 1377 import birchwood.protocol.formatting; 1378 string formattedTextBold = bold("Hello in bold!"); 1379 string formattedTextItalics = italics("Hello in italics!"); 1380 string formattedTextUnderline = underline("Hello in underline!"); 1381 string formattedTextMonospace = monospace("Hello in monospace!"); 1382 string formattedTextStrikthrough = strikethrough("Hello in strikethrough!"); 1383 client.channelMessage(formattedTextBold, "#birchwood"); 1384 client.channelMessage(formattedTextItalics, "#birchwood"); 1385 client.channelMessage(formattedTextUnderline, "#birchwood"); 1386 client.channelMessage(formattedTextMonospace, "#birchwood"); 1387 client.channelMessage(formattedTextStrikthrough, "#birchwood"); 1388 1389 string combination = bold(italics("Italiano Boldino")); 1390 client.channelMessage(combination, "#birchwood"); 1391 1392 string foregroundRedtext = setForeground(SimpleColor.RED)~"This is red text"; 1393 client.channelMessage(foregroundRedtext, "#birchwood"); 1394 1395 string alternatePattern = setForeground(SimpleColor.RED)~"This "~setForeground(SimpleColor.WHITE)~"is "~setForeground(SimpleColor.BLUE)~"America!"; 1396 client.channelMessage(alternatePattern, "#birchwood"); 1397 1398 string backgroundText = setForegroundBackground(SimpleColor.RED, SimpleColor.CYAN)~"Birchwood"; 1399 client.channelMessage(backgroundText, "#birchwood"); 1400 1401 string combined = combination~foregroundRedtext~resetForegroundBackground()~backgroundText~resetForegroundBackground()~alternatePattern; 1402 client.channelMessage(combined, "#birchwood"); 1403 1404 1405 /** 1406 * Test leaving multiple channels (multi) 1407 */ 1408 Thread.sleep(dur!("seconds")(2)); 1409 client.leaveChannel(["#birchwood", "#birchwood2"]); 1410 1411 /** 1412 * Test leaving a single channel (singular) 1413 */ 1414 client.leaveChannel("#birchwoodLeave1"); 1415 1416 /** 1417 * Test leaving a single channel (multi) 1418 */ 1419 client.leaveChannel(["#birchwoodLeave2"]); 1420 1421 1422 /** 1423 * Definately by now we would have learnt the new MAXNICLEN 1424 * which on BonoboNET is 30, hence the below should work 1425 */ 1426 try 1427 { 1428 client.nick("birchwood123456789123456789123"); 1429 assert(true); 1430 } 1431 catch(BirchwoodException e) 1432 { 1433 assert(false); 1434 } 1435 1436 // TODO: Don't forget to re-enable this when done testing! 1437 Thread.sleep(dur!("seconds")(4)); 1438 client.quit(); 1439 1440 1441 /** 1442 * Reconnect again (to test it) 1443 */ 1444 client.connect(); 1445 1446 /** 1447 * Join #birchwood, send a message 1448 * and then quit once again 1449 */ 1450 Thread.sleep(dur!("seconds")(4)); 1451 client.joinChannel("#birchwood"); 1452 client.channelMessage("Lekker", "#birchwood"); 1453 client.quit(); 1454 } 1455 }