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