1 module irc.client;
2 
3 import irc.exception;
4 import irc.linebuffer;
5 import irc.protocol;
6 public import irc.protocol : IrcUser;
7 
8 import irc.ctcp;
9 import irc.util;
10 
11 import std.socket;
12 public import std.socket : InternetAddress;
13 
14 import std.exception;
15 import std.algorithm;
16 import std.array;
17 import std.range;
18 import std.regex; // TEMP: For EOL identification
19 import std.string : format, sformat, munch;
20 import std.traits;
21 
22 //debug=Dirk;
23 debug(Dirk) static import std.stdio;
24 debug(Dirk) import std.conv;
25 
26 enum IRC_MAX_LEN = 510;
27 
28 /**
29  * Thrown if the server sends an error message to the client.
30  */
31 class IrcErrorException : Exception
32 {
33 	IrcClient client;
34 
35 	this(IrcClient client, string message, string file = __FILE__, size_t line = __LINE__)
36 	{
37 		super(message, file, line);
38 		this.client = client;
39 	}
40 
41 	this(IrcClient client, string message, Exception cause, string file = __FILE__, size_t line = __LINE__)
42 	{
43 		super(message, file, line, cause);
44 		this.client = client;
45 	}
46 }
47 
48 void unsubscribeHandler(T)(ref T[] event, T handler)
49 {
50 	enum strategy =
51 	    is(ReturnType!T == void)? SwapStrategy.unstable : SwapStrategy.stable;
52 
53 	event = event.remove!(e => e == handler, strategy);
54 }
55 
56 /**
57  * Represents an IRC client connection.
58  *
59  * Use the separate type $(DPREF tracker, IrcTracker) returned by
60  * $(DPREF tracker, track) to keep track of the channels the
61  * user for this connection is a member of, and the members of
62  * those channels.
63  */
64 class IrcClient
65 {
66 	private:
67 	string m_nick = "dirkuser";
68 	string m_user = "dirk";
69 	string m_name = "dirk";
70 	Address m_address = null;
71 	bool _connected = false;
72 
73 	char[] buffer;
74 	LineBuffer lineBuffer;
75 
76 	package:
77 	Socket socket;
78 
79 	public:
80 	/**
81 	 * Create a new unconnected IRC client.
82 	 *
83 	 * If $(D socket) is provided, it must be an unconnected TCP socket.
84 	 * Provide an instance of $(RREF ssl, socket, SslSocket) to
85 	 * use SSL/TLS.
86 	 *
87 	 * User information should be configured before connecting.
88 	 * Only the nick name can be changed after connecting.
89 	 * Event callbacks can be added both before and after connecting.
90 	 * See_Also:
91 	 *   $(MREF IrcClient.connect)
92 	 */
93 	this()
94 	{
95 		this(new TcpSocket());
96 	}
97 
98 	/// Ditto
99 	this(Socket socket)
100 	{
101 		this.socket = socket;
102 		this.buffer = new char[](2048);
103 
104 		void onReceivedLine(in char[] rawLine)
105 		{
106 			debug(Dirk) std.stdio.writefln(`>> "%s" pos: %s`, rawLine, lineBuffer.position);
107 
108 			IrcLine line;
109 
110 			auto succeeded = parse(rawLine, line);
111 			assert(succeeded);
112 
113 			handle(line);
114 		}
115 
116 		this.lineBuffer = LineBuffer(buffer, &onReceivedLine);
117 	}
118 
119 	/**
120 	 * Connect this client to a server.
121 	 * Params:
122 	 *   serverAddress = address of server
123 	 */
124 	void connect(Address serverAddress)
125 	{
126 		enforceEx!UnconnectedClientException(!connected, "IrcClient is already connected");
127 
128 		socket.connect(serverAddress);
129 
130 		m_address = serverAddress;
131 		_connected = true;
132 
133 		// TODO: Implement `PASS`
134 
135 		writef("NICK %s", nickName);
136 		writef("USER %s * * :%s", userName, realName); // TODO: Initial user-mode argument
137 	}
138 
139 	/**
140 	 * Read all available data from the connection,
141 	 * parse all complete IRC messages and invoke registered callbacks.
142 	 * Returns:
143 	 * $(D true) if the connection was closed.
144 	 * See_Also:
145 	 *   $(DPREF eventloop, IrcEventLoop.run)
146 	 */
147 	bool read()
148 	{
149 		enforceEx!UnconnectedClientException(connected, "cannot read from unconnected IrcClient");
150 
151 		while(connected)
152 		{
153 			socket.blocking = false; // TODO: Make writes non-blocking too, so this isn't needed
154 			auto received = socket.receive(buffer[lineBuffer.position .. $]);
155 			if(received == Socket.ERROR)
156 			{
157 				if(wouldHaveBlocked())
158 				{
159 					socket.blocking = true;
160 					break;
161 				}
162 				else
163 					throw new Exception("socket read operation failed: " ~ socket.getErrorText());
164 			}
165 			else if(received == 0)
166 			{
167 				debug(Dirk) std.stdio.writeln("remote ended connection");
168 				socket.close();
169 				_connected = false;
170 				return true;
171 			}
172 
173 			socket.blocking = true;
174 			lineBuffer.commit(received);
175 		}
176 
177 		return !connected;
178 	}
179 
180 	static char[1540] formatBuffer;
181 
182 	/**
183 	 * Write a raw message to the connection stream.
184 	 *
185 	 * If there is more than one argument, then the first argument is formatted with the subsequent ones.
186 	 * Arguments must not contain newlines.
187 	 * Messages longer than 510 characters (UTF-8 code units) will be cut off.
188 	 * Params:
189 	 *   rawline = line to send
190 	 *   fmtArgs = format arguments for the first argument
191 	 * See_Also:
192 	 *   $(STDREF format, formattedWrite)
193 	 * Throws:
194 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
195 	 */
196 	void writef(T...)(const(char)[] rawline, T fmtArgs)
197 	{
198 		enforceEx!UnconnectedClientException(connected, "cannot write to unconnected IrcClient");
199 
200 		static if(fmtArgs.length > 0)
201 		{
202 			rawline = sformat(formatBuffer, rawline, fmtArgs);
203 		}
204 
205 		debug(Dirk) std.stdio.writefln(`<< "%s" (length: %d)`, rawline, rawline.length);
206 
207 		socket.send(rawline);
208 		socket.send("\r\n"); // TODO: should be in one call to send?
209 	}
210 
211 	// Takes care of splitting 'message' into multiple messages when necessary
212 	private void sendMessage(string method)(in char[] target, in char[] message)
213 	{
214 		static linePattern = ctRegex!(`[^\r\n]+`, "g");
215 
216 		immutable maxMsgLength = IRC_MAX_LEN - method.length - 1 - target.length - 2;
217 		static immutable lineHead = method ~ " %s :%s";
218 
219 		foreach(m; match(message, linePattern))
220 		{
221 			auto line = cast(const ubyte[])m.hit;
222 			foreach(chunk; line.chunks(maxMsgLength))
223 				writef(lineHead, target, cast(const char[])chunk);
224 		}
225 	}
226 
227 	/**
228 	 * Send lines of chat to a channel or user.
229 	 * Each line in $(D message) is sent as one _message.
230 	 * Lines exceeding the IRC _message length limit will be
231 	 * split up into multiple messages.
232 	 * Params:
233 	 *   target = channel or nick name to _send to
234 	 *   message = _message(s) to _send. Can contain multiple lines.
235 	 * Throws:
236 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
237 	 */
238 	void send(in char[] target, in char[] message)
239 	{
240 		sendMessage!"PRIVMSG"(target, message);
241 	}
242 
243 	/**
244 	* Send formatted lines of chat to a channel or user.
245 	* Each line in the formatted result is sent as one message.
246 	* Lines exceeding the IRC message length limit will be
247 	* split up into multiple messages.
248 	* Params:
249 	*   target = channel or nick name to _send to
250 	*   fmt = message format
251 	*   fmtArgs = format arguments
252 	* Throws:
253 	*   $(DPREF exception, UnconnectedClientException) if this client is not connected.
254 	* See_Also:
255 	*   $(STDREF format, formattedWrite)
256 	*/
257 	void sendf(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs)
258 	{
259 		// TODO: use a custom format writer that doesn't necessarily allocate
260 		send(target, format(fmt, fmtArgs));
261 	}
262 
263 	/**
264 	 * Send notices to a channel or user.
265 	 * Each line in $(D message) is sent as one _notice.
266 	 * Lines exceeding the IRC _message length limit will be
267 	 * split up into multiple notices.
268 	 * Params:
269 	 *   target = channel or nick name to _notice
270 	 *   message = notices(s) to send. Can contain multiple lines.
271 	 * Throws:
272 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
273 	 */
274 	void notice(in char[] target, in char[] message)
275 	{
276 		sendMessage!"NOTICE"(target, message);
277 	}
278 
279 	/**
280 	 * Send formatted notices to a channel or user.
281 	 * Each line in the formatted result is sent as one notice.
282 	 * Lines exceeding the IRC message length limit will be
283 	 * split up into multiple notices.
284 	 * Params:
285 	 *   target = channel or nick name to _send to
286 	 *   fmt = message format
287 	 *   fmtArgs = format arguments
288 	 * Throws:
289 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
290 	 * See_Also:
291 	 *   $(STDREF format, formattedWrite)
292 	 */
293 	void noticef(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs)
294 	{
295 		// TODO: use a custom format writer that doesn't necessarily allocate
296 		notice(target, format(fmt, fmtArgs));
297 	}
298 
299 	/**
300 	 * Send a CTCP _query to a channel or user.
301 	 */
302 	// TODO: reuse buffer for output
303 	void ctcpQuery(in char[] target, in char[] query)
304 	{
305 		send(target, ctcpMessage(query).array());
306 	}
307 
308 	/// Ditto
309 	void ctcpQuery(in char[] target, in char[] tag, in char[] data)
310 	{
311 		send(target, ctcpMessage(tag, data).array());
312 	}
313 
314 	/**
315 	 * Send a CTCP _reply to a user.
316 	 */
317 	void ctcpReply(in char[] targetNick, in char[] reply)
318 	{
319 		notice(targetNick, ctcpMessage(reply).array());
320 	}
321 
322 	/// Ditto
323 	void ctcpReply(in char[] targetNick, in char[] tag, in char[] data)
324 	{
325 		notice(targetNick, ctcpMessage(tag, data).array());
326 	}
327 
328 	/**
329 	 * Send a CTCP _error message reply.
330 	 * Params:
331 	 *   invalidData = data that caused the _error
332 	 *   error = human-readable _error message
333 	 */
334 	void ctcpError(in char[] targetNick, in char[] invalidData, in char[] error)
335 	{
336 		notice(targetNick, ctcpMessage("ERRMSG", format("%s :%s", invalidData, error)).array());
337 	}
338 
339 	/**
340 	 * Check if this client is _connected.
341 	 */
342 	bool connected() const @property
343 	{
344 		return _connected;
345 	}
346 
347 	/**
348 	 * Address of the server this client is currently connected to,
349 	 * or null if this client is not connected.
350 	 */
351 	inout(Address) serverAddress() inout pure @property
352 	{
353 		return m_address;
354 	}
355 
356 	/**
357 	 * Real name of the user for this client.
358 	 *
359 	 * Cannot be changed after connecting.
360 	 */
361 	string realName() const pure @property
362 	{
363 		return m_name;
364 	}
365 
366 	/// Ditto
367 	void realName(string newRealName) @property
368 	{
369 		enforce(!connected, "cannot change real name while connected");
370 		enforce(!newRealName.empty);
371 		m_name = newRealName;
372 	}
373 
374 	/**
375 	 * User name of the user for this client.
376 	 *
377 	 * Cannot be changed after connecting.
378 	 */
379 	string userName() const pure @property
380 	{
381 		return m_user;
382 	}
383 
384 	/// Ditto
385 	void userName(string newUserName) @property
386 	{
387 		enforce(!connected, "cannot change user-name while connected");
388 		enforce(!newUserName.empty);
389 		m_user = newUserName;
390 	}
391 
392 	/**
393 	 * Nick name of the user for this client.
394 	 *
395 	 * Setting this property when connected can cause the $(MREF IrcClient.onNickInUse) event to fire.
396 	 */
397 	string nickName() const pure @property
398 	{
399 		return m_nick;
400 	}
401 
402 	/// Ditto
403 	void nickName(in char[] newNick) @property
404 	{
405 		enforce(!newNick.empty);
406 		if(connected) // m_nick will be set later if the nick is accepted.
407 			writef("NICK %s", newNick);
408 		else
409 			m_nick = newNick.idup;
410 	}
411 
412 	/// Ditto
413 	// Duplicated to show up nicer in DDoc - previously used a template and aliases
414 	void nickName(string newNick) @property
415 	{
416 		enforce(!newNick.empty);
417 		if(connected) // m_nick will be set later if the nick is accepted.
418 			writef("NICK %s", newNick);
419 		else
420 			m_nick = newNick;
421 	}
422 
423 	deprecated alias nick = nickName;
424 
425 	/**
426 	 * Join a _channel.
427 	 * Params:
428 	 *   channel = _channel to _join
429 	 * Throws:
430 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
431 	 */
432 	void join(in char[] channel)
433 	{
434 		writef("JOIN %s", channel);
435 	}
436 
437 	/**
438 	 * Join a passworded _channel.
439 	 * Params:
440 	 *   channel = _channel to _join
441 	 *   key = _channel password
442 	 * Throws:
443 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
444 	 */
445 	void join(in char[] channel, in char[] key)
446 	{
447 		writef("JOIN %s :%s", channel, key);
448 	}
449 
450 	/**
451 	 * Leave a _channel.
452 	 * Params:
453 	 *   channel = _channel to leave
454 	 * Throws:
455 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
456 	 */
457 	void part(in char[] channel)
458 	{
459 		writef("PART %s", channel);
460 		//fireEvent(onMePart, channel);
461 	}
462 
463 	/**
464 	 * Leave a _channel with a parting _message.
465 	 * Params:
466 	 *   channel = _channel to leave
467 	 *   message = parting _message
468 	 * Throws:
469 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
470 	 */
471 	void part(in char[] channel, in char[] message)
472 	{
473 		writef("PART %s :%s", channel, message);
474 	}
475 
476 	/**
477 	 * Kick one or more _users from a _channel.
478 	 *
479 	 * This _user must have channel operator status in $(D channel).
480 	 * Params:
481 	 *   channel = _channel to kick user(s) from
482 	 *   users = _user(s) to kick
483 	 *   comment = _comment to broadcast with the _kick message,
484 	 *      which typically contains the reason the _user is being kicked
485 	 */
486 	void kick()(in char[] channel, in char[] user)
487 	{
488 		writef("KICK %s %s", channel, user);
489 	}
490 
491 	/// Ditto
492 	void kick()(in char[] channel, in char[] user, in char[] comment)
493 	{
494 		writef("KICK %s %s :%s", channel, user, comment);
495 	}
496 
497 	/// Ditto
498 	void kick(Range)(in char[] channel, Range users)
499 		if(isInputRange!Range && isSomeString!(ElementType!Range))
500 	{
501 		writef("KICK %s %(%s%|,%)", channel, users);
502 	}
503 
504 	/// Ditto
505 	void kick(Range)(in char[] channel, Range users, in char[] comment)
506 		if(isInputRange!Range && isSomeString!(ElementType!Range))
507 	{
508 		writef("KICK %s %(%s%|,%) :%s", channel, users, comment);
509 	}
510 
511 	/**
512 	 * Kick users from channels in a single message.
513 	 *
514 	 * $(D channelUserPairs) must be a range of $(STDREF typecons, Tuple)
515 	 * pairs of strings, where the first string is the name of a channel
516 	 * and the second string is the user to kick from that channel.
517 	 */
518 	void kick(Range)(Range channelUserPairs)
519 		if(isInputRange!Range &&
520 		   isTuple!(ElementType!Range) && ElementType!Range.length == 2 &&
521 		   allSatisfy!(isSomeString, ElementType!Range.Types))
522 	{
523 		writef("KICK %(%s%|,%) %(%s%|,%)",
524 			   channelUserPairs.map!(pair => pair[0]),
525 			   channelUserPairs.map!(pair => pair[1]));
526 	}
527 
528 	/// Ditto
529 	void kick(Range)(Range channelUserPairs, in char[] comment)
530 		if(isInputRange!Range &&
531 		   isTuple!(ElementType!Range) && ElementType!Range.length == 2 &&
532 		   allSatisfy!(isSomeString, ElementType!Range.Types))
533 	{
534 		writef("KICK %(%s%|,%) %(%s%|,%) :%s",
535 			   channelUserPairs.map!(pair => pair[0]),
536 			   channelUserPairs.map!(pair => pair[1]),
537 			   comment);
538 	}
539 
540 	/**
541 	 * Query the user name and host name of up to 5 users.
542 	 * Params:
543 	 *   nicks = between 1 and 5 nick names to query
544 	 * See_Also:
545 	 *   $(MREF IrcClient.onUserhostReply)
546 	 */
547 	void queryUserhost(in char[][] nicks...)
548 	{
549 		version(assert)
550 		{
551 			import core.exception;
552 			if(nicks.length < 1 || nicks.length > 5)
553 				throw new RangeError();
554 		}
555 		writef("USERHOST %s", nicks.map!(nick => nick[]).joiner(" "));
556 	}
557 
558 	/**
559 	 * Query information about a particular user.
560 	 * Params:
561 	 *   nick = target user's nick name
562 	 * See_Also:
563 	 *   $(MREF IrcClient.onWhoisReply)
564 	 */
565 	void queryWhois(in char[] nick)
566 	{
567 		writef("WHOIS %s", nick);
568 	}
569 
570 	/**
571 	 * Query the list of members in the given channels.
572 	 * See_Also:
573 	 *   $(MREF IrcClient.onNameList)
574 	 */
575 	void queryNames(in char[][] channels...)
576 	{
577 		// TODO: support automatic splitting of messages
578 		writef("NAMES %s", channels.map!(channel => channel[]).joiner(","));
579 	}
580 
581 	/**
582 	 * Leave and disconnect from the server.
583 	 * Params:
584 	 *   message = comment sent in _quit notification
585 	 * Throws:
586 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
587 	 */
588 	void quit(in char[] message)
589 	{
590 		writef("QUIT :%s", message);
591 		socket.close();
592 		_connected = false;
593 	}
594 
595 	/// Invoked when this client has successfully connected to a server.
596 	void delegate()[] onConnect;
597 
598 	/**
599 	 * Invoked when a message is picked up by the user for this client.
600 	 * Params:
601 	 *   user = _user who sent the message
602 	 *   target = message _target. This is either the nick of this client in the case of a personal
603 	 *   message, or the name of the channel which the message was sent to.
604 	 */
605 	void delegate(IrcUser user, in char[] target, in char[] message)[] onMessage;
606 
607 	/**
608 	 * Invoked when a notice is picked up by the user for this client.
609 	 * Params:
610 	 *   user = _user who sent the notice
611 	 *   target = notice _target. This is either the nick of this client in the case of a personal
612 	 *   notice, or the name of the channel which the notice was sent to.
613 	 */
614 	void delegate(IrcUser user, in char[] target, in char[] message)[] onNotice;
615 
616 	/**
617 	 * Invoked when a user receives a new nickname.
618 	 *
619 	 * When the user is this user, the $(MREF IrcClient.nick) property will return the old nickname
620 	 * until after all $(D _onNickChange) callbacks have been invoked.
621 	 * Params:
622 	 *   user = user which nickname was changed; the $(D nick) field contains the old nickname. Can be this user
623 	 *   newNick = new nickname of user
624 	 */
625 	void delegate(IrcUser user, in char[] newNick)[] onNickChange;
626 
627 	/**
628 	 * Invoked following a call to $(MREF IrcClient.join) when the _channel was successfully joined.
629 	 * Params:
630 	 *   channel = _channel that was successfully joined
631 	 */
632 	void delegate(in char[] channel)[] onSuccessfulJoin;
633 
634 	/**
635 	 * Invoked when another user joins a channel that this user is a member of.
636 	 * Params:
637 	 *   user = joining user
638 	 *   channel = channel that was joined
639 	 */
640 	void delegate(IrcUser user, in char[] channel)[] onJoin;
641 
642 	/**
643 	 * Invoked when a user parts a channel that this user is a member of.
644 	 * Also invoked when this user parts a channel.
645 	 * Params:
646 	 *   user = parting user
647 	 *   channel = channel that was parted
648 	 */
649 	void delegate(IrcUser user, in char[] channel)[] onPart;
650 
651 	// TODO: public?
652 	// package void delegate(in char[] channel)[] onMePart;
653 
654 	/**
655 	* Invoked when another user disconnects from the network.
656 	* Params:
657 	*   user = disconnecting user
658 	*   comment = quit message
659 	*/
660 	void delegate(IrcUser user, in char[] comment)[] onQuit;
661 
662 	/**
663 	 * Invoked when a user is kicked (forcefully removed) from a channel that this user is a member of.
664 	 * Params:
665 	 *   kicker = user that initiated the kick
666 	 *   channel = channel from which the user was kicked
667 	 *   kickedNick = nickname of the user that was kicked
668 	 *   comment = comment sent with the kick; usually describing the reason the user was kicked. Can be null
669 	 */
670 	void delegate(IrcUser kicker, in char[] channel, in char[] kickedNick, in char[] comment)[] onKick;
671 
672 	/**
673 	 * Invoked when a list of member nick names for a channel are received.
674 	 *
675 	 * The list is received after a successful join to a channel by this user,
676 	 * or when explicitly queried with $(MREF IrcClient.queryNames).
677 	 *
678 	 * The list for a single invocation is partial;
679 	 * the event can be invoked several times for the same channel
680 	 * as a response to a single trigger. The list is signaled complete
681 	 * when $(MREF IrcClient.onNameListEnd) is invoked.
682 	 * Params:
683 	 *    channel = channel of which the users are members
684 	 *    nickNames = list of member nicknames
685 	 */
686 	void delegate(in char[] channel, in char[][] nickNames)[] onNameList;
687 
688 	/**
689 	 * Invoked when the complete list of members of a _channel have been received.
690 	 * All invocations of $(D onNameList) between invocations of this event
691 	 * are part of the same member list.
692 	 * See_Also:
693 	 *    $(MREF IrcClient.onNameList)
694 	 */
695 	void delegate(in char[] channel)[] onNameListEnd;
696 
697 	/**
698 	 * Invoked when a CTCP query is received in a message.
699 	 * $(MREF IrcClient.onMessage) is not invoked for the given message
700 	 * when onCtcpQuery has a non-zero number of registered handlers.
701 	 * Note:
702 	 *   This callback is only invoked when there is a CTCP message at the start
703 	 *   of the message, and any subsequent CTCP messages in the same message are
704 	 *   discarded. To handle multiple CTCP queries in one message, use
705 	 *   $(MREF IrcClient.onMessage) with $(DPREF ctcp, ctcpExtract).
706 	 */
707 	void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpQuery;
708 
709 	/**
710 	 * Invoked when a CTCP reply is received in a notice.
711 	 * $(MREF IrcClient.onNotice) is not invoked for the given notice
712 	 * when onCtcpReply has a non-zero number of registered handlers.
713 	 * Note:
714 	 *   This callback is only invoked when there is a CTCP message at the start
715 	 *   of the notice, and any subsequent CTCP messages in the same notice are
716 	 *   discarded. To handle multiple CTCP replies in one notice, use
717 	 *   $(MREF IrcClient.onNotice) with $(DPREF ctcp, ctcpExtract).
718 	 */
719 	void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpReply;
720 
721 	/**
722 	 * Invoked when the requested nick name of the user for this client is already in use.
723 	 *
724 	 * Return a non-null string to provide a new nick. No further callbacks in the list
725 	 * are called once a callback provides a nick.
726 	 * Params:
727 	 *   newNick = the nick name that was requested.
728 	 * Note:
729 	 *   The current nick name can be read from the $(MREF IrcClient.nick) property of this client.
730 	 */
731 	const(char)[] delegate(in char[] newNick)[] onNickInUse;
732 
733 	/**
734 	 * Invoked when a _channel is joined, a _topic is set in a _channel or when
735 	 * the current _topic was requested.
736 	 *
737 	 * Params:
738 	 *   channel
739 	 *   topic = _topic or new _topic for channel
740 	 */
741 	void delegate(in char[] channel, in char[] topic)[] onTopic;
742 
743 	/**
744 	 * Invoked when a _channel is joined or when the current _topic was requested.
745 	 *
746 	 * Params:
747 	 *   channel
748 	 *   nick = _nick name of user who set the topic
749 	 *   time = _time the topic was set
750 	 */
751 	void delegate(in char[] channel, in char[] nick, in char[] time)[] onTopicInfo;
752 
753 	/**
754 	 * Invoked with the reply of a userhost query.
755 	 * See_Also:
756 	 *   $(MREF IrcClient.queryUserhost)
757 	 */
758 	void delegate(in IrcUser[] users)[] onUserhostReply;
759 
760 	/**
761 	 * Invoked when a WHOIS reply is received.
762 	 * See_Also:
763 	 *   $(MREF IrcClient.queryWhois)
764 	 */
765 	// TODO: document more, and maybe parse `channels`
766 	void delegate(IrcUser userInfo, in char[] realName)[] onWhoisReply;
767 
768 	/// Ditto
769 	void delegate(in char[] nick, in char[] serverHostName, in char[] serverInfo)[] onWhoisServerReply;
770 
771 	/// Ditto
772 	void delegate(in char[] nick)[] onWhoisOperatorReply;
773 
774 	/// Ditto
775 	void delegate(in char[] nick, int idleTime)[] onWhoisIdleReply;
776 
777 	/// Ditto
778 	void delegate(in char[] nick, in char[][] channels)[] onWhoisChannelsReply;
779 
780 	/// Ditto
781 	void delegate(in char[] nick, in char[] accountName)[] onWhoisAccountReply;
782 
783 	/// Ditto
784 	void delegate(in char[] nick)[] onWhoisEnd;
785 
786 	protected:
787 	IrcUser getUser(in char[] prefix)
788 	{
789 		return IrcUser.fromPrefix(prefix);
790 	}
791 
792 	private:
793 	void fireEvent(T, U...)(T[] event, U args)
794 	{
795 		foreach(cb; event)
796 		{
797 			cb(args);
798 		}
799 	}
800 
801 	bool ctcpCheck(void delegate(IrcUser, in char[], in char[], in char[])[] event,
802 	               in char[] prefix,
803 	               in char[] target,
804 	               in char[] message)
805 	{
806 		if(event.empty || message[0] != CtcpToken.delimiter)
807 			return false;
808 
809 		auto extractor = message.ctcpExtract();
810 
811 		if(extractor.empty)
812 			return false;
813 
814 		// TODO: re-use buffer
815 		auto ctcpMessage = cast(string)extractor.front.array();
816 		auto tag = ctcpMessage.munch("^ ");
817 
818 		if(!ctcpMessage.empty && ctcpMessage.front == ' ')
819 			ctcpMessage.popFront();
820 
821 		fireEvent(
822 		    event,
823 		    getUser(prefix),
824 		    target,
825 		    tag,
826 		    ctcpMessage
827 		);
828 
829 		return true;
830 	}
831 
832 	// TODO: Switch getting large, change to something more performant?
833 	void handle(ref IrcLine line)
834 	{
835 		switch(line.command)
836 		{
837 			case "PING":
838 				writef("PONG :%s", line.arguments[0]);
839 				break;
840 			case "433":
841 				void failed433(Exception cause)
842 				{
843 					socket.close();
844 					_connected = false;
845 					throw new IrcErrorException(this, `"433 Nick already in use" was unhandled`, cause);
846 				}
847 
848 				auto failedNick = line.arguments[0];
849 				bool handled = false;
850 
851 				foreach(cb; onNickInUse)
852 				{
853 					const(char)[] newNick;
854 
855 					try newNick = cb(failedNick);
856 					catch(Exception e)
857 						failed433(e);
858 
859 					if(newNick)
860 					{
861 						writef("NICK %s", newNick);
862 						handled = true;
863 						break;
864 					}
865 				}
866 
867 				if(!handled)
868 					failed433(null);
869 
870 				break;
871 			case "PRIVMSG":
872 				auto prefix = line.prefix;
873 				auto target = line.arguments[0];
874 				auto message = line.arguments[1];
875 
876 				if(!ctcpCheck(onCtcpQuery, prefix, target, message))
877 					fireEvent(onMessage, getUser(prefix), target, message);
878 
879 				break;
880 			case "NOTICE":
881 				auto prefix = line.prefix;
882 				auto target = line.arguments[0];
883 				auto notice = line.arguments[1];
884 
885 				if(!ctcpCheck(onCtcpReply, prefix, target, notice))
886 					fireEvent(onNotice, getUser(prefix), target, notice);
887 
888 				break;
889 			case "NICK":
890 				auto user = getUser(line.prefix);
891 				auto newNick = line.arguments[0];
892 
893 				scope(exit)
894 				{
895 					if(m_nick == user.nickName)
896 						m_nick = newNick.idup;
897 				}
898 
899 				fireEvent(onNickChange, user, newNick);
900 				break;
901 			case "JOIN":
902 				auto user = getUser(line.prefix);
903 
904 				if(user.nickName == m_nick)
905 					fireEvent(onSuccessfulJoin, line.arguments[0]);
906 				else
907 					fireEvent(onJoin, user, line.arguments[0]);
908 
909 				break;
910 			case "353": // TODO: operator/voice status etc. should be propagated to callbacks
911 				// line.arguments[0] == client.nick
912 				version(none) auto type = line.arguments[1];
913 				auto channelName = line.arguments[2];
914 
915 				auto names = line.arguments[3].split();
916 				foreach(ref name; names)
917 				{
918 					auto prefix = name[0];
919 					if(prefix == '@' || prefix == '+') // TODO: smarter handling that allows for non-standard stuff
920 						name = name[1 .. $];
921 				}
922 
923 				fireEvent(onNameList, channelName, names);
924 				break;
925 			case "366":
926 				fireEvent(onNameListEnd, line.arguments[0]);
927 				break;
928 			case "PART":
929 				fireEvent(onPart, getUser(line.prefix), line.arguments[0]);
930 				break;
931 			case "QUIT":
932 				fireEvent(onQuit, getUser(line.prefix), line.arguments[0]);
933 				break;
934 			case "KICK":
935 				fireEvent(onKick,
936 					getUser(line.prefix),
937 					line.arguments[0],
938 					line.arguments[1],
939 					line.arguments.length > 2? line.arguments[2] : null);
940 				break;
941 			case "302":
942 				IrcUser[5] users;
943 				auto n = IrcUser.parseUserhostReply(users, line.arguments[1]);
944 
945 				fireEvent(onUserhostReply, users[0 .. n]);
946 				break;
947 			case "332":
948 				fireEvent(onTopic, line.arguments[1], line.arguments[2]);
949 				break;
950 			case "333":
951 				fireEvent(onTopicInfo, line.arguments[1], line.arguments[2], line.arguments[3]);
952 				break;
953 			// WHOIS replies
954 			case "311":
955 				auto user = IrcUser(
956 					line.arguments[1], // Nick
957 					line.arguments[2]); // Username
958 
959 				fireEvent(onWhoisReply, user, line.arguments[5]);
960 				break;
961 			case "312":
962 				fireEvent(onWhoisServerReply, line.arguments[1], line.arguments[2], line.arguments[3]);
963 				break;
964 			case "313":
965 				fireEvent(onWhoisOperatorReply, line.arguments[0]);
966 				break;
967 			case "317":
968 				import std.conv : to;
969 				fireEvent(onWhoisIdleReply, line.arguments[1], to!int(line.arguments[2]));
970 				break;
971 			case "319":
972 				fireEvent(onWhoisChannelsReply, line.arguments[1], split(line.arguments[2]));
973 				break;
974 			case "318":
975 				fireEvent(onWhoisEnd, line.arguments[1]);
976 				break;
977 			// Non-standard WHOIS replies
978 			//case "307": // UnrealIRCd?
979 			//	fireEvent(onWhoisAccountReply, line.arguments[0], line.arguments[1]);
980 			//	break;
981 			case "330": // Freenode
982 				fireEvent(onWhoisAccountReply, line.arguments[1], line.arguments[2]);
983 				break;
984 			// End of WHOIS replies
985 			case "ERROR":
986 				_connected = false;
987 				throw new IrcErrorException(this, line.arguments[0].idup);
988 			case "001":
989 				m_nick = line.arguments[0].idup;
990 				fireEvent(onConnect);
991 				break;
992 			default:
993 				debug(Dirk) std.stdio.writefln(`Unhandled command "%s"`, line.command);
994 				break;
995 		}
996 	}
997 }