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 }