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 }