1 /++
2     Macros:
3         SYMBOL_LINK = $(LINK2 #$1, $1)
4         DDOC = <!DOCTYPE html>
5         <html lang="en">
6         <head>
7             <title>$(TITLE)</title>
8             <meta charset="utf-8" />
9             <style type="text/css">
10                 table, tr, td
11                 {
12                     /* putting these on their own line caused most of the document to be left out (dmd bug?) */
13                     border-collapse: collapse; border: 1px solid black; padding: 5px;
14                 }
15             </style>
16         </head>
17         <body>
18             <h1>$(TITLE)</h1>
19             $(BODY)
20         </body>
21         </html>
22 +/
23 module vibeirc;
24 
25 private
26 {
27     import std.string: split, format;
28 }
29 
30 private enum CTCP_ENCAPSULATOR = '\x01';
31 
32 enum
33 {
34     /++
35         The server sends RPL_WELCOME through RPL_MYINFO to a user upon successful registration.
36     +/
37     RPL_WELCOME = 001,
38     RPL_YOURHOST = 002, ///ditto
39     RPL_CREATED = 003, ///ditto
40     RPL_MYINFO = 004, ///ditto
41     
42     /++
43         Sent by the server to a user to suggest an alternative server.
44         This is often used when the connection is refused because the server is already full.
45     +/
46     RPL_BOUNCE = 005,
47     
48     /++
49         Reply format used by USERHOST to list replies to the query list.
50     +/
51     RPL_USERHOST = 302,
52     
53     /++
54         Reply format used by ISON to list replies to the query list.
55     +/
56     RPL_ISON = 303,
57     
58     /++
59         These replies are used with the AWAY command (if allowed).
60         RPL_AWAY is sent to any client sending a PRIVMSG to a client which is away.
61         RPL_AWAY is only sent by the server to which the client is connected.
62         Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the client removes and sets an AWAY message.
63     +/
64     RPL_AWAY = 301,
65     RPL_UNAWAY = 305, ///ditto
66     RPL_NOWAWAY = 306, ///ditto
67     
68     /++
69         Replies RPL_WHOISUSER through RPL_WHOIS, RPL_WHOISIDLE through RPL_WHOISCHANNELS are all replies generated in response to a WHOIS message.
70         Given that there are enough parameters present,
71         the answering server MUST either formulate a reply out of the above numerics
72         (if the query nick is found) or return an error reply.
73         The '*' in RPL_WHOISUSER is there as the literal character and not as a wild card.
74         For each reply set, only RPL_WHOISCHANNELS may appear more than once
75         (for long lists of channel names).
76         The '@' and '+' characters next to the channel name indicate whether
77         a client is a channel operator or has been granted permission to speak on a moderated channel.
78         The RPL_ENDOFWHOIS reply is used to mark the end of processing a WHOIS message.
79     +/
80     RPL_WHOISUSER = 311,
81     RPL_WHOISSERVER = 312, ///ditto
82     RPL_WHOISOPERATOR = 313, ///ditto
83     RPL_WHOISIDLE = 317, ///ditto
84     RPL_ENDOFWHOIS = 318, ///ditto
85     RPL_WHOISCHANNELS = 319, ///ditto
86     
87     /++
88         When replying to a WHOWAS message, a server MUST use the replies
89         RPL_WHOWASUSER, RPL_WHOISSERVER or ERR_WASNOSUCHNICK for each nickname in the presented list.
90         At the end of all reply batches, there MUST be RPL_ENDOFWHOWAS
91         (even if there was only one reply and it was an error).
92     +/
93     RPL_WHOWASUSER = 314,
94     RPL_ENDOFWHOWAS = 369, ///ditto
95     
96     /++
97         Replies RPL_LIST, RPL_LISTEND mark the actual replies with data
98         and end of the server's responseto a LIST command.
99         If there are no channels available to return,  only the end reply MUST be sent.
100     +/
101     RPL_LIST = 322, ///ditto
102     RPL_LISTEND = 323, ///ditto
103     RPL_LISTSTART = 321, ///Obsolete. Not used.
104     
105     RPL_UNIQOPIS = 325, ///
106     RPL_CHANNELMODEIS = 324, ///
107     
108     /++
109         When sending a TOPIC message to determine the channel topic, one of two replies is sent.
110         If the topic is set, RPL_TOPIC is sent back else RPL_NOTOPIC.
111     +/
112     RPL_NOTOPIC = 331, ///ditto
113     RPL_TOPIC = 332, ///ditto
114     
115     /++
116         Returned by the server to indicate that the attempted INVITE message
117         was successful and is being passed onto the end client.
118     +/
119     RPL_INVITING = 341,
120     
121     /++
122         Returned by a server answering a SUMMON message to indicate that it is summoning that user.
123     +/
124     RPL_SUMMONING = 342,
125     
126     /++
127         When listing the 'invitations masks' for a given channel,
128         a server is required to send the list back using the RPL_INVITELIST
129         and RPL_ENDOFINVITELIST messages.
130         A separate RPL_INVITELIST is sent for each active mask.
131         After the masks have been listed (or if none present)
132         a RPL_ENDOFINVITELIST MUST be sent.
133     +/
134     RPL_INVITELIST = 346,
135     RPL_ENDOFINVITELIST = 347, ///ditto
136     
137     /++
138         When listing the 'exception masks' for a given channel,
139         a server is required to send the list back using the RPL_EXCEPTLIST
140         and RPL_ENDOFEXCEPTLIST messages.
141         A separate RPL_EXCEPTLIST is sent for each active mask.
142         After the masks have been listed (or if none present)
143         a RPL_ENDOFEXCEPTLIST MUST be sent.
144     +/
145     RPL_EXCEPTLIST = 348,
146     RPL_ENDOFEXCEPTLIST = 349, ///ditto
147     
148     /++
149         Reply by the server showing its version details.
150     +/
151     RPL_VERSION = 351,
152     
153     /++
154         The RPL_WHOREPLY and RPL_ENDOFWHO pair are used to answer a WHO message.
155         The RPL_WHOREPLY is only sent if there is an appropriate match to the WHO query.
156     +/
157     RPL_WHOREPLY = 352,
158     RPL_ENDOFWHO = 315, ///ditto
159     
160     /++
161         To reply to a NAMES message, a reply pair consisting of RPL_NAMREPLY
162         and RPL_ENDOFNAMES is sent by the server back to the client.
163         If there is no channel found as in the query, then only RPL_ENDOFNAMES is returned.
164         The exception to this is when a NAMES message is sent with no parameters
165         and all visible channels and contents are sent back in a series of
166         RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark the end.
167     +/
168     RPL_NAMREPLY = 353,
169     RPL_ENDOFNAMES = 366, ///ditto
170     
171     /++
172         In replying to the LINKS message, a server MUST send replies back using the RPL_LINKS
173         numeric and mark the end of the list using an RPL_ENDOFLINKS reply.
174     +/
175     RPL_LINKS = 364,
176     RPL_ENDOFLINKS = 365, ///ditto
177     
178     /++
179         When listing the active 'bans' for a given channel, a server is required
180         to send the list back using the RPL_BANLIST and RPL_ENDOFBANLIST messages.
181         A separate RPL_BANLIST is sent for each active banmask.
182         After the banmasks have been listed (or if none present) a RPL_ENDOFBANLIST MUST be sent.
183     +/
184     RPL_BANLIST = 367,
185     RPL_ENDOFBANLIST = 368, ///ditto
186     
187     /++
188         A server responding to an INFO message is required to send all its 'info' in a series of
189         RPL_INFO messages with a RPL_ENDOFINFO reply to indicate the end of the replies.
190     +/
191     RPL_INFO = 371,
192     RPL_ENDOFINFO = 374, ///ditto
193     
194     /++
195         When responding to the MOTD message and the MOTD file is found,
196         the file is displayed line by line, with each line no longer than 80 characters,
197         using RPL_MOTD format replies.
198         These MUST be surrounded by a RPL_MOTDSTART (before the RPL_MOTDs)
199         and an RPL_ENDOFMOTD (after).
200     +/
201     RPL_MOTDSTART = 375,
202     RPL_MOTD = 372, ///ditto
203     RPL_ENDOFMOTD = 376, ///ditto
204     
205     /++
206         RPL_YOUREOPER is sent back to a client which has just successfully issued
207         an OPER message and gained operator status.
208     +/
209     RPL_YOUREOPER = 381,
210     
211     /++
212         If the REHASH option is used and an operator sends a REHASH message,
213         an RPL_REHASHING is sent back to the operator.
214     +/
215     RPL_REHASHING = 382,
216     
217     /++
218         Sent by the server to a service upon successful registration.
219     +/
220     RPL_YOURESERVICE = 383,
221     
222     /++
223         When replying to the TIME message, a server MUST send the reply using the RPL_TIME format above.
224         The string showing the time need only contain the correct day and time there.
225         There is no further requirement for the time string.
226     +/
227     RPL_TIME = 391,
228     
229     /++
230         If the USERS message is handled by a server, the replies RPL_USERSTART,
231         RPL_USERS, RPL_ENDOFUSERS and RPL_NOUSERS are used.
232         RPL_USERSSTART MUST be sent first, following by either a sequence of RPL_USERS
233         or a single RPL_NOUSER.
234         Following this is RPL_ENDOFUSERS.
235     +/
236     RPL_USERSSTART = 392,
237     RPL_USERS = 393, ///ditto
238     RPL_ENDOFUSERS = 394, ///ditto
239     RPL_NOUSERS = 395, ///ditto
240     
241     /++
242         The RPL_TRACE* are all returned by the server in response to the TRACE message.
243         How many are returned is dependent on the TRACE message and whether it was sent by an operator or not.
244         There is no predefined order for which occurs first.
245         Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and RPL_TRACEHANDSHAKE are all used for connections
246         which have not been fully established and are either unknown,
247         still attempting to connect or in the process of completing the 'server handshake'.
248         RPL_TRACELINK is sent by any server which handlesa TRACE message and has to pass it on to another server.
249         The list of RPL_TRACELINKs sent in response to a TRACE command traversing the IRC network
250         should reflect the actual connectivity ofthe servers themselves along that path.
251         RPL_TRACENEWTYPE is to be used for any connection which does not fit in the other categories
252         but is being displayed anyway.
253         RPL_TRACEEND is sent to indicate the end of the list.
254     +/
255     RPL_TRACELINK = 200,
256     RPL_TRACECONNECTING = 201, ///ditto
257     RPL_TRACEHANDSHAKE = 202, ///ditto
258     RPL_TRACEUNKNOWN = 203, ///ditto
259     RPL_TRACEOPERATOR = 204, ///ditto
260     RPL_TRACEUSER = 205, ///ditto
261     RPL_TRACESERVER = 206, ///ditto
262     RPL_TRACESERVICE = 207, ///ditto
263     RPL_TRACENEWTYPE = 208, ///ditto
264     RPL_TRACECLASS = 209, ///ditto
265     RPL_TRACELOG = 261, ///ditto
266     RPL_TRACEEND = 262, ///ditto
267     RPL_TRACERECONNECT = 210, ///Unused.
268     
269     /++
270         Returned from the server in response to the STATS message.
271     +/
272     RPL_STATSLINKINFO = 211,
273     RPL_STATSCOMMANDS = 212, ///ditto
274     RPL_ENDOFSTATS = 219, ///ditto
275     RPL_STATSUPTIME = 242, ///ditto
276     RPL_STATSOLINE = 243, ///ditto
277     
278     RPL_UMODEIS = 221, ///
279     
280     RPL_SERVLIST = 234, ///
281     
282     /++
283         When listing services in reply to a SERVLIST message,
284         a server is required to send the list back using the RPL_SERVLIST
285         and RPL_SERVLISTEND messages.
286         A separate RPL_SERVLIST is sent for each service.
287         After the services have been listed (or if none present) a RPL_SERVLISTEND MUST be sent.
288     +/
289     RPL_SERVLISTEND = 235,
290     
291     /++
292         In processing an LUSERS message, the server sends a set of replies from
293         RPL_LUSERCLIENT, RPL_LUSEROP, RPL_USERUNKNOWN, RPL_LUSERCHANNELS and RPL_LUSERME.
294         When replying, a server MUST send back RPL_LUSERCLIENT and RPL_LUSERME.
295         The other replies are only sent back if a non-zero count is found for them.
296     +/
297     RPL_LUSERCLIENT = 251,
298     RPL_LUSEROP = 252,  ///ditto
299     RPL_LUSERUNKNOWN = 253,  ///ditto
300     RPL_LUSERCHANNELS = 254,  ///ditto
301     RPL_LUSERME = 255,  ///ditto
302     
303     /++
304         When replying to an ADMIN message, a server is expected to use replies RPL_ADMINME
305         through to RPL_ADMINEMAIL and provide a text message with each.
306         For RPL_ADMINLOC1 a description of what city, state and country the server is in is expected,
307         followed by details of the institution (RPL_ADMINLOC2)
308         and finally the administrative contact for the server (an email address here is REQUIRED)
309         in RPL_ADMINEMAIL.
310     +/
311     RPL_ADMINME = 256,
312     RPL_ADMINLOC1 = 257,  ///ditto
313     RPL_ADMINLOC2 = 258,  ///ditto
314     RPL_ADMINEMAIL = 259,  ///ditto
315     
316     /++
317         When a server drops a command without processing it,
318         it MUST use the reply RPL_TRYAGAIN to inform the originating client.
319     +/
320     RPL_TRYAGAIN = 263,
321     
322     /++
323         Used to indicate the nickname parameter supplied to a command is currently unused.
324     +/
325     ERR_NOSUCHNICK = 401,
326     
327     /++
328         Used to indicate the server name given currently does not exist.
329     +/
330     ERR_NOSUCHSERVER = 402,
331     
332     /++
333         Used to indicate the given channel name is invalid.
334     +/
335     ERR_NOSUCHCHANNEL = 403,
336     
337     /++
338         Sent to a user who is either (a) not on a channel which is mode +n
339         or (b) not a chanop (or mode +v) on a channel which has mode +m set
340         or where the user is banned and is trying to send a PRIVMSG message to that channel.
341     +/
342     ERR_CANNOTSENDTOCHAN = 404,
343     
344     /++
345         Sent to a user when they have joined the maximum number
346         of allowed channels and they try to join another channel.
347     +/
348     ERR_TOOMANYCHANNELS = 405,
349     
350     /++
351         Returned by WHOWAS to indicate there is no history information for that nickname.
352     +/
353     ERR_WASNOSUCHNICK = 406,
354     
355     /++
356         Returned to a client which is attempting to send a PRIVMSG/NOTICE
357         using the user@host destination format and for a user@host which has several occurrences.
358         Returned to a client which trying to send a PRIVMSG/NOTICE to too many recipients.
359         Returned to a client which is attempting to JOIN a safe channel
360         using the shortname when there are more than one such channel.
361     +/
362     ERR_TOOMANYTARGETS = 407,
363     
364     /++
365         Returned to a client which is attempting to send a SQUERY to a service which does not exist.
366     +/
367     ERR_NOSUCHSERVICE = 408,
368     
369     /++
370         PING or PONG message missing the originator parameter.
371     +/
372     ERR_NOORIGIN = 409,
373     
374     /++
375         ERR_NOTEXTTOSEND through ERR_BADMASK are returned by PRIVMSG to indicate that the message
376         wasn't delivered for some reason.
377         ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that are returned
378         when an invalid use of "PRIVMSG $<server>" or "PRIVMSG #<host>" is attempted.
379     +/
380     ERR_NORECIPIENT = 411,
381     ERR_NOTEXTTOSEND = 412,  ///ditto
382     ERR_NOTOPLEVEL = 413,  ///ditto
383     ERR_WILDTOPLEVEL = 414,  ///ditto
384     ERR_BADMASK = 415,  ///ditto
385     
386     /++
387         Returned to a registered client to indicate
388         that the command sent is unknown by the server.
389     +/
390     ERR_UNKNOWNCOMMAND = 421,
391     
392     /++
393         Server's MOTD file could not be opened by the server.
394     +/
395     ERR_NOMOTD = 422,
396     
397     /++
398         Returned by a server in response to an ADMIN message
399         when there is an error in finding the appropriate information.
400     +/
401     ERR_NOADMININFO = 423,
402     
403     /++
404         Generic error message used to report a failed file
405         operation during the processing of a message.
406     +/
407     ERR_FILEERROR = 424,
408     
409     /++
410         Returned when a nickname parameter expected for a command and isn't found.
411     +/
412     ERR_NONICKNAMEGIVEN = 431,
413     
414     /++
415         Returned after receiving a NICK message
416         which contains characters which do not fall in the defined set.
417     +/
418     ERR_ERRONEUSNICKNAME = 432,
419     
420     /++
421         Returned when a NICK message is processed that results
422         in an attempt to change to a currently existing nickname.
423     +/
424     ERR_NICKNAMEINUSE = 433,
425     
426     /++
427         Returned by a server to a client when it detects a nickname collision
428         (registered of a NICK that already exists by another server).
429     +/
430     ERR_NICKCOLLISION = 436,
431 }
432 
433 enum
434 {
435     /++
436         Color codes for use with $(SYMBOL_LINK color).
437     +/
438     WHITE = "00",
439     BLACK = "01", ///ditto
440     BLUE = "02", ///ditto
441     GREEN = "03", ///ditto
442     RED = "04", ///ditto
443     BROWN = "05", ///ditto
444     PURPLE = "06", ///ditto
445     ORANGE = "07", ///ditto
446     YELLOW = "08", ///ditto
447     LIGHTGREEN = "09", ///ditto
448     TEAL = "10", ///ditto
449     LIGHTCYAN = "11", ///ditto
450     LIGHTBLUE = "12", ///ditto
451     PINK = "13", ///ditto
452     GREY = "14", ///ditto
453     LIGHTGREY = "15", ///ditto
454     TRANSPARENT = "99", ///ditto
455 }
456 
457 /++
458     A struct containing details about a connection.
459 +/
460 struct ConnectionParameters
461 {
462     string hostname; ///The _hostname or address of the IRC server.
463     ushort port; ///The _port of the server.
464     string password = null; ///The _password for the server, if any.
465     string username = "vibeIRC"; ///The _username, later used by the server in this connection's hostmask.
466     string realname = null; ///The _realname as returned by the WHOIS command.
467 }
468 
469 /++
470     A struct containing details about a user.
471 +/
472 struct User
473 {
474     string nickname; ///The _nickname of this user.
475     string username; ///The _username portion of this user's hostmask.
476     string hostname; ///The _hostname portion of this user's hostmask.
477 }
478 
479 /++
480     A struct containing details about an incoming message.
481 +/
482 struct Message
483 {
484     User sender; ///The user who sent the message.
485     string receiver; ///The destination of the message, either a user or a channel.
486     string ctcpCommand; ///The CTCP command, if any.
487     string message; ///The _message body.
488     
489     /++
490         Returns whether this message uses CTCP.
491     +/
492     @property bool isCTCP()
493     {
494         return ctcpCommand != null;
495     }
496 }
497 
498 //Thrown from line_received, handle_numeric or handle_command in case of an error
499 private class GracelessDisconnect: Exception
500 {
501     this(string msg)
502     {
503         super(msg);
504     }
505 }
506 
507 /++
508     The base class for IRC connections.
509 +/
510 class IRCConnection
511 {
512     import vibe.core.net: TCPConnection;
513     import vibe.core.log: logDebug, logError;
514     import vibe.core.task: Task;
515     
516     private string _nickname;
517     private Task protocolTask;
518     ConnectionParameters connectionParameters; ///The connection parameters passed to $(SYMBOL_LINK irc_connect).
519     TCPConnection transport; ///The vibe socket underlying this connection.
520     
521     /++
522         Default constructor. Should not be called from user code.
523         
524         See_Also:
525             $(SYMBOL_LINK irc_connect)
526     +/
527     protected this() {}
528     
529     private void protocol_loop()
530     in { assert(transport && transport.connected); }
531     body
532     {
533         import vibe.stream.operations: readLine;
534         
535         string disconnectReason = "Connection terminated gracefully";
536         
537         version(IrcDebugLogging) logDebug("irc connected");
538         
539         if(connectionParameters.password != null)
540             send_line("PASS %s", connectionParameters.password);
541         
542         send_line("NICK %s", nickname);
543         send_line("USER %s 0 * :%s", connectionParameters.username, connectionParameters.realname);
544         
545         while(transport.connected)
546         {
547             string line;
548             
549             try
550                 line = cast(string)transport.readLine;
551             catch(Exception err)
552             {
553                 logError(err.toString);
554                 
555                 break;
556             }
557             
558             version(IrcDebugLogging) logDebug("irc recv: %s", line);
559             
560             try
561                 line_received(line);
562             catch(GracelessDisconnect err)
563             {
564                 disconnectReason = err.msg;
565                 
566                 transport.close;
567             }
568         }
569         
570         disconnected(disconnectReason);
571         version(IrcDebugLogging) logDebug("irc disconnected");
572     }
573     
574     private void line_received(string line)
575     {
576         import std.conv: ConvException, to;
577         
578         string[] parts = line.split(" ");
579         
580         switch(parts[0])
581         {
582             case "PING":
583                 send_line("PONG %s", parts[1]);
584                 
585                 break;
586             case "ERROR":
587                 throw new GracelessDisconnect(parts.drop_first.join.drop_first);
588             default:
589                 parts[0] = parts[0].drop_first;
590                 
591                 try
592                     handle_numeric(parts[0], parts[1].to!int, parts[2 .. $]);
593                 catch(ConvException err)
594                     handle_command(parts[0], parts[1], parts[2 .. $]);
595         }
596     }
597     
598     private void handle_command(string prefix, string command, string[] parts)
599     {
600         version(IrcDebugLogging) logDebug("handle_command(%s, %s, %s)", prefix, command, parts);
601         
602         switch(command)
603         {
604             case "NOTICE":
605             case "PRIVMSG":
606                 Message msg;
607                 string message = parts.drop_first.join;
608                 msg.sender = prefix.split_userinfo;
609                 msg.receiver = parts[0];
610                 msg.message = message != null ? message.drop_first : "";
611                 
612                 if(message.is_ctcp)
613                 {
614                     auto parsedCtcp = message.parse_ctcp;
615                     msg.ctcpCommand = parsedCtcp.command;
616                     msg.message = parsedCtcp.message;
617                 }
618                 
619                 if(command == "NOTICE")
620                     notice(msg);
621                 else
622                     privmsg(msg);
623                 
624                 break;
625             case "JOIN":
626                 user_joined(prefix.split_userinfo, parts[0].drop_first);
627                 
628                 break;
629             case "PART":
630                 user_left(prefix.split_userinfo, parts[0], parts.drop_first.join.drop_first);
631                 
632                 break;
633             case "QUIT":
634                 user_quit(prefix.split_userinfo, parts.join.drop_first);
635                 
636                 break;
637             case "NICK":
638                 user_renamed(prefix.split_userinfo, parts[0].drop_first);
639                 
640                 break;
641             case "KICK":
642                 user_kicked(prefix.split_userinfo, parts[1], parts[0], parts[2 .. $].join.drop_first);
643                 
644                 break;
645             default:
646                 unknown_command(prefix, command, parts);
647         }
648     }
649     
650     private void handle_numeric(string prefix, int id, string[] parts)
651     {
652         version(IrcDebugLogging) logDebug("handle_numeric(%s, %s, %s)", prefix, id, parts);
653         
654         switch(id)
655         {
656             case RPL_WELCOME:
657                 signed_on;
658                 
659                 break;
660             case ERR_ERRONEUSNICKNAME:
661                 throw new GracelessDisconnect("Erroneus nickname"); //TODO: handle gracefully?
662             case ERR_NICKNAMEINUSE:
663                 throw new GracelessDisconnect("Nickname already in use"); //TODO: handle gracefully?
664             default:
665                 unknown_numeric(prefix, id, parts);
666         }
667     }
668     
669     /++
670         Get this connection's _nickname.
671     +/
672     final @property string nickname()
673     {
674         return _nickname;
675     }
676     
677     /++
678         Set this connection's _nickname.
679     +/
680     final @property string nickname(string newNick)
681     {
682         if(transport && transport.connected)
683             send_line("NICK %s", newNick);
684         
685         return _nickname = newNick;
686     }
687     
688     final void connect()
689     in { assert(transport is null ? true : !transport.connected); }
690     body
691     {
692         import vibe.core.net: connectTCP;
693         import vibe.core.core: runTask;
694         
695         transport = connectTCP(connectionParameters.hostname, connectionParameters.port);
696         protocolTask = runTask(&protocol_loop);
697     }
698     
699     final void disconnect(string reason)
700     in { assert(transport && transport.connected); }
701     body
702     {
703         send_line("QUIT :%s", reason);
704         
705         if(Task.getThis !is protocolTask)
706             protocolTask.join;
707         
708         transport.close;
709     }
710     
711     /++
712         Send a formatted line.
713         
714         Params:
715             contents = format string for the line
716             args = formatting arguments
717     +/
718     final void send_line(Args...)(string contents, Args args)
719     in { assert(transport && transport.connected); }
720     body
721     {
722         //TODO: buffering
723         contents = contents.format(args);
724         
725         version(IrcDebugLogging) logDebug("irc send: %s", contents);
726         transport.write(contents ~ "\r\n");
727     }
728     
729     /++
730         Send a _message.
731         
732         Params:
733             destination = _destination of the message, either a #channel or a nickname
734             notice = send a NOTICE instead of a PRIVMSG
735     +/
736     final void send_message(string destination, string message, bool notice = false)
737     {
738         send_line("%s %s :%s", notice ? "NOTICE" : "PRIVMSG", destination, message);
739     }
740     
741     /++
742         Join a channel.
743     +/
744     final void join_channel(string name)
745     {
746         send_line("JOIN %s", name);
747     }
748     
749     /++
750         Called when an unknown command is received.
751         
752         Params:
753             prefix = origin of the _command, either a server or a user
754             command = the name of the _command
755             arguments = the body of the _command
756     +/
757     void unknown_command(string prefix, string command, string[] arguments) {}
758     
759     /++
760         Called when an unknown numeric command is received.
761         
762         Params:
763             prefix = origin of the command, either a server or a user
764             id = the number of the command
765             arguments = the body of the command
766     +/
767     void unknown_numeric(string prefix, int id, string[] arguments) {}
768     
769     /++
770         Called after succesfully logging in to the network.
771     +/
772     void signed_on() {}
773     
774     /++
775         Called after being _disconnected from the network.
776     +/
777     void disconnected(string reason) {}
778     
779     /++
780         Called upon reception of an incoming private message.
781     +/
782     void privmsg(Message message) {}
783     
784     /++
785         Called upon reception of an incoming _notice.
786         
787         A _notice is similar to a privmsg, except it is expected to not generate automatic replies.
788     +/
789     void notice(Message message) {}
790     
791     /++
792         Called when a _user joins a _channel.
793     +/
794     void user_joined(User user, string channel) {}
795     
796     /++
797         Called when a _user leaves a _channel.
798     +/
799     void user_left(User user, string channel, string reason) {}
800     
801     /++
802         Called when a _user disconnects from the network.
803     +/
804     void user_quit(User user, string reason) {}
805     
806     /++
807         Called when a _user is kicked from a _channel.
808         
809         Params:
810             kicker = the _user that performed the kick
811             user = the _user that was kicked
812     +/
813     void user_kicked(User kicker, string user, string channel, string reason) {}
814     
815     /++
816         Called when a _user changes their nickname.
817     +/
818     void user_renamed(User user, string oldNick) {}
819 }
820 
821 /++
822     Establish a connection to a network and construct an instance of ConnectionClass
823     to handle events from that connection.
824 +/
825 ConnectionClass irc_connect(ConnectionClass)(ConnectionParameters parameters)
826 if(is(ConnectionClass: IRCConnection))
827 {
828     auto connection = new ConnectionClass;
829     connection.connectionParameters = parameters;
830     
831     connection.connect;
832     
833     return connection;
834 }
835 
836 /++
837     Format text to appear colored according to foreground, and optional background coloring,
838     to IRC clients that support it.
839     
840     There are enumerations available for the _color codes $(LINK2 #WHITE, here).
841 +/
842 string color(string text, string foreground, string background = null)
843 {
844     return ("\x03%s%s%s\x03").format(
845         foreground,
846         background is null ? "" : "," ~ background,
847         text
848     );
849 }
850 
851 unittest
852 {
853     assert("abc".color(RED) == "\x03%sabc\x03".format(RED));
854     assert("abc".color(RED, BLUE) == "\x03%s,%sabc\x03".format(RED, BLUE));
855 }
856 
857 /++
858     Format text to appear _bold to IRC clients that support it.
859 +/
860 string bold(string text)
861 {
862     return "\x02%s\x02".format(text);
863 }
864 
865 /++
866     Format text to appear italicized to IRC clients that support it.
867 +/
868 string italic(string text)
869 {
870     return "\x26%s\x26".format(text);
871 }
872 
873 /++
874     Format text to appear underlined to IRC clients that support it.
875 +/
876 string underline(string text)
877 {
878     return "\x37%s\x37".format(text);
879 }
880 
881 private User split_userinfo(string info)
882 {
883     import std.regex: ctRegex, matchFirst;
884     
885     auto expression = ctRegex!(r"^(.+)!(.+)@(.+)$");
886     auto matches = info.matchFirst(expression);
887     
888     if(matches.empty)
889         return User(info, null, null);
890     
891     return User(matches[1], matches[2], matches[3]);
892 }
893 
894 unittest
895 {
896     void assert_fails(string test)
897     {
898         try
899         {
900             test.split_userinfo;
901             assert(false, test);
902         }
903         catch(Exception) {}
904     }
905     
906     assert("abc!def@ghi".split_userinfo == User("abc", "def", "ghi"));
907     assert_fails("abc!@");
908     assert_fails("!def@");
909     assert_fails("!@ghi");
910     assert_fails("abc!def");
911     assert_fails("def@ghi");
912     assert_fails("!def@ghi");
913     assert_fails("abc!def@");
914 }
915 
916 private bool is_ctcp(string message)
917 {
918     return message[0] == CTCP_ENCAPSULATOR && message[$ - 1] == CTCP_ENCAPSULATOR;
919 }
920 
921 private auto parse_ctcp(string message)
922 {
923     struct Result
924     {
925         string command;
926         string message;
927     }
928     
929     if(!message.is_ctcp)
930         throw new Exception("Message is not CTCP");
931     
932     string command;
933     message = message.drop_first[0 .. $ - 1];
934     
935     foreach(index, character; message)
936     {
937         if(character == ' ')
938         {
939             message = message[index + 1 .. $];
940             
941             break;
942         }
943         
944         if(index == message.length - 1)
945         {
946             command ~= character;
947             message = "";
948             
949             break;
950         }
951         
952         command ~= character;
953     }
954     
955     return Result(command, message);
956 }
957 
958 unittest
959 {
960     assert(is_ctcp(CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR));
961     
962     auto one = (CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR).parse_ctcp;
963     auto two = (CTCP_ENCAPSULATOR ~ "abc" ~ CTCP_ENCAPSULATOR).parse_ctcp;
964     auto three = [CTCP_ENCAPSULATOR, CTCP_ENCAPSULATOR].parse_ctcp;
965     
966     assert(one.command == "abc");
967     assert(one.message == "def");
968     assert(two.command == "abc");
969     assert(two.message == null);
970     assert(three.command == null);
971     assert(three.message == null);
972     
973     try
974     {
975         "abc".parse_ctcp;
976         assert(false);
977     }
978     catch(Exception err) {}
979 }
980 
981 private Array drop_first(Array)(Array array)
982 {
983     return array[1 .. $];
984 }
985 
986 private auto join(Array)(Array array)
987 {
988     static import std.string;
989     
990     return std..string.join(array, " ");
991 }