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, indexOf, sformat, munch;
20 import std.traits;
21 import std.typetuple;
22 import std.utf : byChar;
23 
24 //debug=Dirk;
25 debug(Dirk) static import std.stdio;
26 debug(Dirk) import std.conv;
27 
28 // Not using standard library because of auto-decoding issues
29 private size_t indexOfNewline(in char[] haystack) pure nothrow @safe @nogc
30 {
31 	foreach(i, char c; haystack)
32 		if(c == '\r' || c == '\n')
33 			return i;
34 	return -1;
35 }
36 
37 private inout(char)[] stripNewlinesLeft(inout(char)[] haystack)
38 {
39 	while(!haystack.empty && (haystack[0] == '\r' || haystack[0] == '\n'))
40 		haystack = haystack[1 .. $];
41 	return haystack;
42 }
43 
44 /**
45  * Thrown if the server sends an error message to the client.
46  */
47 class IrcErrorException : Exception
48 {
49 	IrcClient client;
50 
51 	this(IrcClient client, string message, string file = __FILE__, size_t line = __LINE__)
52 	{
53 		super(message, file, line);
54 		this.client = client;
55 	}
56 
57 	this(IrcClient client, string message, Exception cause, string file = __FILE__, size_t line = __LINE__)
58 	{
59 		super(message, file, line, cause);
60 		this.client = client;
61 	}
62 }
63 
64 void unsubscribeHandler(T)(ref T[] event, T handler)
65 {
66 	enum strategy =
67 		is(ReturnType!T == void)? SwapStrategy.unstable : SwapStrategy.stable;
68 
69 	event = event.remove!(e => e == handler, strategy);
70 }
71 
72 /**
73  * Represents an IRC client connection.
74  *
75  * Use the separate type $(DPREF tracker, IrcTracker) returned by
76  * $(DPREF tracker, track) to keep track of the channels the
77  * user for this connection is a member of, and the members of
78  * those channels.
79  */
80 class IrcClient
81 {
82 	private:
83 	string m_nick = "dirkuser";
84 	string m_user = "dirk";
85 	string m_name = "dirk";
86 	Address m_address = null;
87 	bool _connected = false;
88 
89 	char[] buffer;
90 	IncomingLineBuffer lineBuffer;
91 
92 	// ISUPPORT data
93 	// PREFIX
94 	static immutable char[2][] defaultPrefixedChannelModes = [['@', 'o'], ['+', 'v']]; // RFC2812
95 	const(char[2])[] prefixedChannelModes = defaultPrefixedChannelModes; // [[prefix, mode], ...]
96 
97 	// CHANMODES
98 	string channelListModes = "b"; // Type A
99 	string channelParameterizedModes = null; // Type B
100 	string channelNullaryRemovableModes = null; // Type C
101 	string channelSettingModes = null; // Type D
102 
103 	// NICKLEN
104 	enum defaultMaxNickLength = 9;
105 	ushort _maxNickLength = defaultMaxNickLength;
106 	bool enforceMaxNickLength = false; // Only enforce max nick length when server has specified one
107 
108 	// NETWORK
109 	string _networkName = null;
110 
111 	// MODES
112 	enum defaultMessageModeLimit = 3; // RFC2812
113 	ubyte messageModeLimit = defaultMessageModeLimit;
114 
115 	package:
116 	Socket socket;
117 
118 	public:
119 	/**
120 	 * Create a new unconnected IRC client.
121 	 *
122 	 * If $(D socket) is provided, it must be an unconnected TCP socket.
123 	 * Provide an instance of $(RREF ssl, socket, SslSocket) to
124 	 * use SSL/TLS.
125 	 *
126 	 * User information should be configured before connecting.
127 	 * Only the nick name can be changed after connecting.
128 	 * Event callbacks can be added both before and after connecting.
129 	 * See_Also:
130 	 *   $(MREF IrcClient.connect)
131 	 */
132 	this()
133 	{
134 		this(new TcpSocket());
135 	}
136 
137 	/// Ditto
138 	this(Socket socket)
139 	{
140 		this.socket = socket;
141 		this.buffer = new char[](2048);
142 		this.lineBuffer = IncomingLineBuffer(buffer, &onReceivedLine);
143 	}
144 
145 	private void onReceivedLine(in char[] rawLine)
146 	{
147 		debug(Dirk) std.stdio.writefln(`>> "%s" pos: %s`, rawLine, lineBuffer.position);
148 
149 		IrcLine line;
150 
151 		auto succeeded = parse(rawLine, line);
152 		assert(succeeded);
153 
154 		handle(line);
155 	}
156 
157 	/**
158 	 * Connect this client to a server.
159 	 * Params:
160 	 *   serverAddress = address of server
161 	 *   password = server _password, or $(D null) to specify no _password
162 	 */
163 	void connect(Address serverAddress, in char[] password)
164 	{
165 		enforceEx!UnconnectedClientException(!connected, "IrcClient is already connected");
166 
167 		socket.connect(serverAddress);
168 
169 		m_address = serverAddress;
170 		_connected = true;
171 
172 		if(password.length)
173 			writef("PASS %s", password);
174 
175 		writef("NICK %s", nickName);
176 		writef("USER %s * * :%s", userName, realName); // TODO: Initial user-mode argument
177 	}
178 
179 	/// Ditto
180 	void connect(Address serverAddress)
181 	{
182 		connect(serverAddress, null);
183 	}
184 
185 	/**
186 	 * Read all available data from the connection,
187 	 * parse all complete IRC messages and invoke registered callbacks.
188 	 * Returns:
189 	 * $(D true) if the connection was closed.
190 	 * See_Also:
191 	 *   $(DPREF eventloop, IrcEventLoop.run)
192 	 */
193 	bool read()
194 	{
195 		enforceEx!UnconnectedClientException(connected, "cannot read from unconnected IrcClient");
196 
197 		while(connected)
198 		{
199 			socket.blocking = false; // TODO: Make writes non-blocking too, so this isn't needed
200 			auto received = socket.receive(buffer[lineBuffer.position .. $]);
201 			if(received == Socket.ERROR)
202 			{
203 				if(wouldHaveBlocked())
204 				{
205 					socket.blocking = true;
206 					break;
207 				}
208 				else
209 					throw new Exception("socket read operation failed: " ~ socket.getErrorText());
210 			}
211 			else if(received == 0)
212 			{
213 				debug(Dirk) std.stdio.writeln("remote ended connection");
214 				socket.close();
215 				_connected = false;
216 				return true;
217 			}
218 
219 			socket.blocking = true;
220 			lineBuffer.commit(received);
221 		}
222 
223 		return !connected;
224 	}
225 
226 	/**
227 	 * Write a raw IRC protocol message to the connection stream.
228 	 *
229 	 * If there is more than one argument, then the first argument is formatted
230 	 * with subsequent arguments. Arguments must not contain newlines.
231 	 * Messages longer than 510 characters (UTF-8 code units) will be cut off.
232 	 * It is the caller's responsibility to ensure a cut-off message is valid.
233 	 * See_Also:
234 	 *   $(STDREF format, formattedWrite)
235 	 * Throws:
236 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
237 	 */
238 	void writef(T...)(in char[] messageFormat, T formatArgs)
239 		if(T.length)
240 	{
241 		import std.format : formattedWrite;
242 
243 		enforceEx!UnconnectedClientException(connected, "cannot write to unconnected IrcClient");
244 
245 		char[512] formatBuffer = void;
246 
247 		auto tail = formatBuffer[0 .. $ - 2];
248 		formattedWrite((const(char)[] chunk) {
249 			immutable chunkSize = min(chunk.length, tail.length);
250 			tail[0 .. chunkSize] = chunk[0 .. chunkSize];
251 			tail = tail[chunkSize .. $];
252 		}, messageFormat, formatArgs);
253 
254 		auto message = formatBuffer[0 .. $ - tail.length];
255 		message[$ - 2 .. $] = "\r\n";
256 
257 		debug(Dirk)
258 		{
259 			auto sansNewline = message[0 .. $ - 2];
260 			std.stdio.writefln(`<< "%s" (length: %d)`, sansNewline, sansNewline.length);
261 		}
262 
263 		socket.send(message);
264 	}
265 
266 	/// Ditto
267 	void writef(in char[] rawline)
268 	{
269 		enforceEx!UnconnectedClientException(connected, "cannot write to unconnected IrcClient");
270 		socket.send(rawline[0 .. min($, 510)]);
271 		socket.send("\r\n");
272 	}
273 
274 	// TODO: attempt not to split lines in the middle of code points or graphemes
275 	private void sendMessage(in char[] command, in char[] target, in char[] message)
276 	{
277 		auto buffer = OutgoingLineBuffer(this.socket, command, target);
278 		const(char)[] messageTail = message.stripNewlinesLeft;
279 
280 		while(messageTail.length)
281 		{
282 			immutable maxChunkSize = min(messageTail.length, buffer.capacity);
283 			immutable newlinePos = messageTail[0 .. maxChunkSize].indexOfNewline;
284 			immutable hasNewline = newlinePos != -1;
285 			immutable chunkEnd = hasNewline? newlinePos : maxChunkSize;
286 
287 			buffer.consume(messageTail, chunkEnd);
288 			buffer.flush();
289 
290 			if(hasNewline)
291 				messageTail = messageTail.stripNewlinesLeft;
292 		}
293 	}
294 
295 	private void sendMessage(Range)(in char[] command, in char[] target, Range message)
296 		if(isInputRange!Range && isSomeChar!(ElementType!Range))
297 	{
298 		static if(!is(Unqual!(ElementType!Range) == char))
299 		{
300 			import std.utf : byChar;
301 			auto r = message.byChar;
302 		}
303 		else
304 			alias r = message;
305 
306 		r = r.stripLeft!(c => c == '\r' || c == '\n');
307 
308 		auto buffer = OutgoingLineBuffer(this.socket, command, target);
309 		auto messageBuffer = buffer.messageBuffer;
310 		size_t i = 0;
311 
312 		while(!r.empty)
313 		{
314 			auto c = r.front;
315 
316 			if(c == '\r' || c == '\n')
317 			{
318 				buffer.commit(i);
319 				buffer.flush();
320 				i = 0;
321 				r = r.stripLeft!(c => c == '\r' || c == '\n');
322 			}
323 			else
324 			{
325 				messageBuffer[i++] = c;
326 				r.popFront();
327 				if(i == messageBuffer.length)
328 				{
329 					buffer.commit(i);
330 					buffer.flush();
331 					i = 0;
332 				}
333 			}
334 		}
335 
336 		if(i != 0)
337 		{
338 			buffer.commit(i);
339 			buffer.flush();
340 		}
341 	}
342 
343 	private void sendMessage(T...)(in char[] command, in char[] target, in char[] messageFormat, T formatArgs)
344 		if(T.length)
345 	{
346 		import std.format : formattedWrite;
347 
348 		auto buffer = OutgoingLineBuffer(this.socket, command, target);
349 
350 		formattedWrite((const(char)[] chunk) {
351 			if(!chunk.length)
352 				return;
353 
354 			if(!buffer.hasMessage)
355 				chunk = chunk.stripNewlinesLeft;
356 
357 			while(chunk.length > buffer.capacity)
358 			{
359 				immutable newlinePos = chunk[0 .. buffer.capacity].indexOfNewline;
360 				immutable hasNewline = newlinePos != -1;
361 				immutable chunkEnd = hasNewline? newlinePos : buffer.capacity;
362 
363 				buffer.consume(chunk, chunkEnd);
364 				buffer.flush();
365 
366 				if(hasNewline)
367 					chunk = chunk.stripNewlinesLeft; // normalize consecutive newline characters
368 			}
369 
370 			auto newlinePos = chunk.indexOfNewline;
371 			while(newlinePos != -1)
372 			{
373 				buffer.consume(chunk, newlinePos);
374 				buffer.flush();
375 				chunk = chunk.stripNewlinesLeft;
376 				newlinePos = chunk.indexOfNewline; // normalize consecutive newline characters
377 			}
378 
379 			buffer.consume(chunk, chunk.length);
380 		}, messageFormat, formatArgs);
381 
382 		if(buffer.hasMessage)
383 			buffer.flush();
384 	}
385 
386 	/**
387 	 * Send lines of chat to a channel or user.
388 	 * Each line in $(D message) is sent as one _message.
389 	 * Lines exceeding the IRC _message length limit will be
390 	 * split up into multiple messages.
391 	 * Params:
392 	 *   target = channel or nick name to _send to
393 	 *   message = _message(s) to _send. Can contain multiple lines.
394 	 * Throws:
395 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
396 	 */
397 	void send(in char[] target, in char[] message)
398 	{
399 		sendMessage("PRIVMSG", target, message);
400 	}
401 
402 	/// Ditto
403 	void send(Range)(in char[] target, Range message)
404 		if(isInputRange!Range && isSomeChar!(ElementType!Range))
405 	{
406 		sendMessage("PRIVMSG", target, message);
407 	}
408 
409 	/**
410 	* Send formatted lines of chat to a channel or user.
411 	* Each line in the formatted result is sent as one message.
412 	* Lines exceeding the IRC message length limit will be
413 	* split up into multiple messages.
414 	* Params:
415 	*   target = channel or nick name to _send to
416 	*   fmt = message format
417 	*   fmtArgs = format arguments
418 	* Throws:
419 	*   $(DPREF exception, UnconnectedClientException) if this client is not connected.
420 	* See_Also:
421 	*   $(STDREF format, formattedWrite)
422 	*/
423 	void sendf(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs)
424 	{
425 		sendMessage("PRIVMSG", target, fmt, fmtArgs);
426 	}
427 
428 	/**
429 	 * Send notices to a channel or user.
430 	 * Each line in $(D message) is sent as one _notice.
431 	 * Lines exceeding the IRC _message length limit will be
432 	 * split up into multiple notices.
433 	 * Params:
434 	 *   target = channel or nick name to _notice
435 	 *   message = notices(s) to send. Can contain multiple lines.
436 	 * Throws:
437 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
438 	 */
439 	void notice(in char[] target, in char[] message)
440 	{
441 		sendMessage("NOTICE", target, message);
442 	}
443 
444 	/// Ditto
445 	void notice(Range)(in char[] target, Range message)
446 		if(isInputRange!Range && isSomeChar!(ElementType!Range))
447 	{
448 		sendMessage("NOTICE", target, message);
449 	}
450 
451 	/**
452 	 * Send formatted notices to a channel or user.
453 	 * Each line in the formatted result is sent as one notice.
454 	 * Lines exceeding the IRC message length limit will be
455 	 * split up into multiple notices.
456 	 * Params:
457 	 *   target = channel or nick name to _send to
458 	 *   fmt = message format
459 	 *   fmtArgs = format arguments
460 	 * Throws:
461 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
462 	 * See_Also:
463 	 *   $(STDREF format, formattedWrite)
464 	 */
465 	void noticef(FormatArgs...)(in char[] target, in char[] fmt, FormatArgs fmtArgs)
466 	{
467 		sendMessage("NOTICE", target, fmt, fmtArgs);
468 	}
469 
470 	/**
471 	 * Send a CTCP _query to a channel or user.
472 	 */
473 	// TODO: reuse buffer for output
474 	void ctcpQuery(in char[] target, in char[] query)
475 	{
476 		send(target, ctcpMessage(query));
477 	}
478 
479 	/// Ditto
480 	void ctcpQuery(in char[] target, in char[] tag, in char[] data)
481 	{
482 		send(target, ctcpMessage(tag, data));
483 	}
484 
485 	/**
486 	 * Send a CTCP _reply to a user.
487 	 */
488 	void ctcpReply(in char[] targetNick, in char[] reply)
489 	{
490 		notice(targetNick, ctcpMessage(reply));
491 	}
492 
493 	/// Ditto
494 	void ctcpReply(in char[] targetNick, in char[] tag, in char[] data)
495 	{
496 		notice(targetNick, ctcpMessage(tag, data));
497 	}
498 
499 	/**
500 	 * Send a CTCP _error message reply.
501 	 * Params:
502 	 *   invalidData = data that caused the _error
503 	 *   error = human-readable _error message
504 	 */
505 	void ctcpError(in char[] targetNick, in char[] invalidData, in char[] error)
506 	{
507 		notice(targetNick, ctcpMessage("ERRMSG", format("%s :%s", invalidData, error)));
508 	}
509 
510 	/**
511 	 * Check if this client is _connected.
512 	 */
513 	bool connected() const @property
514 	{
515 		return _connected;
516 	}
517 
518 	/**
519 	 * Address of the server this client is currently connected to,
520 	 * or null if this client is not connected.
521 	 */
522 	inout(Address) serverAddress() inout pure @property
523 	{
524 		return m_address;
525 	}
526 
527 	/**
528 	 * Real name of the user for this client.
529 	 *
530 	 * Cannot be changed after connecting.
531 	 */
532 	string realName() const pure @property
533 	{
534 		return m_name;
535 	}
536 
537 	/// Ditto
538 	void realName(string newRealName) @property
539 	{
540 		enforce(!connected, "cannot change real name while connected");
541 		enforce(!newRealName.empty);
542 		m_name = newRealName;
543 	}
544 
545 	/**
546 	 * User name of the user for this client.
547 	 *
548 	 * Cannot be changed after connecting.
549 	 */
550 	string userName() const pure @property
551 	{
552 		return m_user;
553 	}
554 
555 	/// Ditto
556 	void userName(string newUserName) @property
557 	{
558 		enforce(!connected, "cannot change user-name while connected");
559 		enforce(!newUserName.empty);
560 		m_user = newUserName;
561 	}
562 
563 	/**
564 	 * Nick name of the user for this client.
565 	 *
566 	 * Setting this property when connected can cause the $(MREF IrcClient.onNickInUse) event to fire.
567 	 */
568 	string nickName() const pure @property
569 	{
570 		return m_nick;
571 	}
572 
573 	/// Ditto
574 	void nickName(in char[] newNick) @property
575 	{
576 		enforce(!newNick.empty);
577 
578 		if(enforceMaxNickLength)
579 			enforce(newNick.length <= _maxNickLength,
580 				`desired nick name "%s" (%s bytes) is too long; nick name must be within %s bytes`
581 				.format(newNick, newNick.length, _maxNickLength));
582 
583 		if(connected) // m_nick will be set later if the nick is accepted.
584 			writef("NICK %s", newNick);
585 		else
586 			m_nick = newNick.idup;
587 	}
588 
589 	/// Ditto
590 	// Duplicated to show up nicer in DDoc - previously used a template and aliases
591 	void nickName(string newNick) @property
592 	{
593 		enforce(!newNick.empty);
594 
595 		if(enforceMaxNickLength)
596 			enforce(newNick.length <= _maxNickLength,
597 				`desired nick name "%s" (%s bytes) is too long; nick name must be within %s bytes`
598 				.format(newNick, newNick.length, _maxNickLength));
599 
600 		if(connected) // m_nick will be set later if the nick is accepted.
601 			writef("NICK %s", newNick);
602 		else
603 			m_nick = newNick;
604 	}
605 
606 	/// Ditto
607 	deprecated alias nick = nickName;
608 
609 	/**
610 	 * The name of the IRC network the server is part of, or $(D null)
611 	 * if the server has not advertised the network name.
612 	 */
613 	string networkName() const @property nothrow @nogc pure
614 	{
615 		return _networkName;
616 	}
617 
618 	/**
619 	 * The maximum number of characters (bytes) allowed in this user's nick name.
620 	 *
621 	 * The limit is network-specific.
622 	 */
623 	ushort maxNickNameLength() const @property nothrow @nogc pure
624 	{
625 		return _maxNickLength;
626 	}
627 
628 	/**
629 	 * Add or remove user modes to/from this user.
630 	 */
631 	void addUserModes(in char[] modes...)
632 	{
633 		writef("MODE %s +%s", m_nick, modes);
634 	}
635 
636 	/// Ditto
637 	void removeUserModes(in char[] modes...)
638 	{
639 		writef("MODE %s -%s", m_nick, modes);
640 	}
641 
642 	private void editChannelModes(Modes, Args)(char editAction, in char[] channel, Modes modes, Args args)
643 		if(allSatisfy!(isInputRange, Modes, Args) && isSomeChar!(ElementType!Modes) && isSomeString!(ElementType!Args))
644 	in {
645 		assert(modes.length == args.length);
646 		assert(modes.length <= messageModeLimit);
647 	} body {
648 		writef("MODE %s %c%s %-(%s%| %)", editAction, channel, modes, args);
649 	}
650 
651 	private void editChannelList(char editAction, in char[] channel, char list, in char[][] addresses...)
652 	{
653 		import std.range : chunks;
654 
655 		enforce(channelListModes.canFind(list),
656 			`specified channel mode "` ~ list ~ `" is not a list mode`);
657 
658 		foreach(chunk; addresses.chunks(messageModeLimit)) // TODO: split up if too long
659 		{
660 			if(messageModeLimit <= 16) // arbitrary number
661 			{
662 				char[16] modeBuffer = list;
663 				editChannelModes(editAction, channel, modeBuffer[0 .. chunk.length], chunk);
664 			}
665 			else
666 				editChannelModes(editAction, channel, list.repeat(chunk.length).byChar(), chunk);
667 		}
668 	}
669 
670 	/**
671 	 * Add or remove an address to/from a _channel list.
672 	Examples:
673 	Ban Alice and Bob from _channel #foo:
674 	------
675 	client.addToChannelList("#foo", 'b', "Alice!*@*", "Bob!*@*");
676 	------
677 	 */
678 	void addToChannelList(in char[] channel, char list, in char[][] addresses...)
679 	{
680 		editChannelList('+', channel, list, addresses);
681 	}
682 
683 	/// Ditto
684 	void removeFromChannelList(in char[] channel, char list, in char[][] addresses...)
685 	{
686 		editChannelList('-', channel, list, addresses);
687 	}
688 
689 	/**
690 	 * Add or remove channel modes in the given channel.
691 	 * Examples:
692 	 Give channel operator status (+o) to Alice and voice status (+v) to Bob in channel #foo:
693 	 ------
694 	client.addChannelModes("#foo", ChannelMode('o', "Alice"), ChannelMode('v', "Bob"));
695 	 ------
696 	 */
697 	struct ChannelMode
698 	{
699 		char mode; ///
700 		const(char)[] argument; /// Ditto
701 	}
702 
703 	/// Ditto
704 	void addChannelModes(in char[] channel, ChannelMode[] modes...)
705 	{
706 		import std.range : chunks;
707 
708 		foreach(chunk; modes.chunks(messageModeLimit)) // TODO: split up if too long
709 			writef("MODE %s +%s %-(%s%| %)", channel,
710 				modes.map!(pair => pair.mode),
711 				modes.map!(pair => pair.argument).filter!(arg => !arg.empty));
712 	}
713 
714 	/// Ditto
715 	void removeChannelModes(in char[] channel, ChannelMode[] modes...)
716 	{
717 		import std.range : chunks;
718 
719 		foreach(chunk; modes.chunks(messageModeLimit)) // TODO: split up if too long
720 			writef("MODE %s -%s %-(%s%| %)", channel,
721 				modes.map!(pair => pair.mode),
722 				modes.map!(pair => pair.argument));
723 	}
724 
725 	/**
726 	 * Join a _channel.
727 	 * Params:
728 	 *   channel = _channel to _join
729 	 * Throws:
730 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
731 	 */
732 	void join(in char[] channel)
733 	{
734 		writef("JOIN %s", channel);
735 	}
736 
737 	/**
738 	 * Join a passworded _channel.
739 	 * Params:
740 	 *   channel = _channel to _join
741 	 *   key = _channel password
742 	 * Throws:
743 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
744 	 */
745 	void join(in char[] channel, in char[] key)
746 	{
747 		writef("JOIN %s :%s", channel, key);
748 	}
749 
750 	/**
751 	 * Leave a _channel.
752 	 * Params:
753 	 *   channel = _channel to leave
754 	 * Throws:
755 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
756 	 */
757 	void part(in char[] channel)
758 	{
759 		writef("PART %s", channel);
760 		//fireEvent(onMePart, channel);
761 	}
762 
763 	/**
764 	 * Leave a _channel with a parting _message.
765 	 * Params:
766 	 *   channel = _channel to leave
767 	 *   message = parting _message
768 	 * Throws:
769 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
770 	 */
771 	void part(in char[] channel, in char[] message)
772 	{
773 		writef("PART %s :%s", channel, message);
774 	}
775 
776 	/**
777 	 * Kick one or more _users from a _channel.
778 	 *
779 	 * This _user must have channel operator status in $(D channel).
780 	 * Params:
781 	 *   channel = _channel to kick user(s) from
782 	 *   users = _user(s) to kick
783 	 *   comment = _comment to broadcast with the _kick message,
784 	 *      which typically contains the reason the _user is being kicked
785 	 */
786 	void kick()(in char[] channel, in char[] user)
787 	{
788 		writef("KICK %s %s", channel, user);
789 	}
790 
791 	/// Ditto
792 	void kick()(in char[] channel, in char[] user, in char[] comment)
793 	{
794 		writef("KICK %s %s :%s", channel, user, comment);
795 	}
796 
797 	/// Ditto
798 	void kick(Range)(in char[] channel, Range users)
799 		if(isInputRange!Range && isSomeString!(ElementType!Range))
800 	{
801 		writef("KICK %s %(%s%|,%)", channel, users);
802 	}
803 
804 	/// Ditto
805 	void kick(Range)(in char[] channel, Range users, in char[] comment)
806 		if(isInputRange!Range && isSomeString!(ElementType!Range))
807 	{
808 		writef("KICK %s %(%s%|,%) :%s", channel, users, comment);
809 	}
810 
811 	/**
812 	 * Kick users from channels in a single message.
813 	 *
814 	 * $(D channelUserPairs) must be a range of $(STDREF typecons, Tuple)
815 	 * pairs of strings, where the first string is the name of a channel
816 	 * and the second string is the user to kick from that channel.
817 	 */
818 	void kick(Range)(Range channelUserPairs)
819 		if(isInputRange!Range &&
820 		   isTuple!(ElementType!Range) && ElementType!Range.length == 2 &&
821 		   allSatisfy!(isSomeString, ElementType!Range.Types))
822 	{
823 		writef("KICK %(%s%|,%) %(%s%|,%)",
824 			   channelUserPairs.map!(pair => pair[0]),
825 			   channelUserPairs.map!(pair => pair[1]));
826 	}
827 
828 	/// Ditto
829 	void kick(Range)(Range channelUserPairs, in char[] comment)
830 		if(isInputRange!Range &&
831 		   isTuple!(ElementType!Range) && ElementType!Range.length == 2 &&
832 		   allSatisfy!(isSomeString, ElementType!Range.Types))
833 	{
834 		writef("KICK %(%s%|,%) %(%s%|,%) :%s",
835 			   channelUserPairs.map!(pair => pair[0]),
836 			   channelUserPairs.map!(pair => pair[1]),
837 			   comment);
838 	}
839 
840 	/**
841 	 * Query the user name and host name of up to 5 users.
842 	 * Params:
843 	 *   nicks = between 1 and 5 nick names to query
844 	 * See_Also:
845 	 *   $(MREF IrcClient.onUserhostReply)
846 	 */
847 	void queryUserhost(in char[][] nicks...)
848 	{
849 		version(assert)
850 		{
851 			import core.exception;
852 			if(nicks.length < 1 || nicks.length > 5)
853 				throw new RangeError();
854 		}
855 		writef("USERHOST %-(%s%| %)", nicks);
856 	}
857 
858 	/**
859 	 * Query information about a particular user.
860 	 * Params:
861 	 *   nick = target user's nick name
862 	 * See_Also:
863 	 *   $(MREF IrcClient.onWhoisReply)
864 	 */
865 	void queryWhois(in char[] nick)
866 	{
867 		writef("WHOIS %s", nick);
868 	}
869 
870 	/**
871 	 * Query the list of members in the given channels.
872 	 * See_Also:
873 	 *   $(MREF IrcClient.onNameList)
874 	 */
875 	void queryNames(in char[][] channels...)
876 	{
877 		// TODO: support automatic splitting of messages
878 		//writef("NAMES %s", channels.map!(channel => channel[]).joiner(","));
879 
880 		// NOTE: one message per channel because some servers ignore subsequent channels (confirmed: IRCd-Hybrid)
881 		foreach(channel; channels)
882 			writef("NAMES %s", channel);
883 	}
884 
885 	/**
886 	 * Leave and disconnect from the server.
887 	 * Params:
888 	 *   message = comment sent in _quit notification
889 	 * Throws:
890 	 *   $(DPREF exception, UnconnectedClientException) if this client is not connected.
891 	 */
892 	void quit(in char[] message)
893 	{
894 		writef("QUIT :%s", message);
895 		socket.close();
896 		_connected = false;
897 	}
898 
899 	/// Invoked when this client has successfully connected to a server.
900 	void delegate()[] onConnect;
901 
902 	/**
903 	 * Invoked when a message is picked up by the user for this client.
904 	 * Params:
905 	 *   user = _user who sent the message
906 	 *   target = message _target. This is either the nick of this client in the case of a personal
907 	 *   message, or the name of the channel which the message was sent to.
908 	 */
909 	void delegate(IrcUser user, in char[] target, in char[] message)[] onMessage;
910 
911 	/**
912 	 * Invoked when a notice is picked up by the user for this client.
913 	 * Params:
914 	 *   user = _user who sent the notice
915 	 *   target = notice _target. This is either the nick of this client in the case of a personal
916 	 *   notice, or the name of the channel which the notice was sent to.
917 	 */
918 	void delegate(IrcUser user, in char[] target, in char[] message)[] onNotice;
919 
920 	/**
921 	 * Invoked when a user receives a new nickname.
922 	 *
923 	 * When the user is this user, the $(MREF IrcClient.nick) property will return the old nickname
924 	 * until after all $(D _onNickChange) callbacks have been invoked.
925 	 * Params:
926 	 *   user = user which nickname was changed; the $(D nick) field contains the old nickname. Can be this user
927 	 *   newNick = new nickname of user
928 	 */
929 	void delegate(IrcUser user, in char[] newNick)[] onNickChange;
930 
931 	/**
932 	 * Invoked following a call to $(MREF IrcClient.join) when the _channel was successfully joined.
933 	 * Params:
934 	 *   channel = _channel that was successfully joined
935 	 */
936 	void delegate(in char[] channel)[] onSuccessfulJoin;
937 
938 	/**
939 	 * Invoked when another user joins a channel that this user is a member of.
940 	 * Params:
941 	 *   user = joining user
942 	 *   channel = channel that was joined
943 	 */
944 	void delegate(IrcUser user, in char[] channel)[] onJoin;
945 
946 	/**
947 	 * Invoked when a user parts a channel that this user is a member of.
948 	 * Also invoked when this user parts a channel.
949 	 * Params:
950 	 *   user = parting user
951 	 *   channel = channel that was parted
952 	 */
953 	void delegate(IrcUser user, in char[] channel)[] onPart;
954 
955 	// TODO: public?
956 	// package void delegate(in char[] channel)[] onMePart;
957 
958 	/**
959 	* Invoked when another user disconnects from the network.
960 	* Params:
961 	*   user = disconnecting user
962 	*   comment = quit message
963 	*/
964 	void delegate(IrcUser user, in char[] comment)[] onQuit;
965 
966 	/**
967 	 * Invoked when a user is kicked (forcefully removed) from a channel that this user is a member of.
968 	 * Params:
969 	 *   kicker = user that initiated the kick
970 	 *   channel = channel from which the user was kicked
971 	 *   kickedNick = nickname of the user that was kicked
972 	 *   comment = comment sent with the kick; usually describing the reason the user was kicked. Can be null
973 	 */
974 	void delegate(IrcUser kicker, in char[] channel, in char[] kickedNick, in char[] comment)[] onKick;
975 
976 	/**
977 	 * Invoked when a list of member nick names for a channel are received.
978 	 *
979 	 * The list is received after a successful join to a channel by this user,
980 	 * or when explicitly queried with $(MREF IrcClient.queryNames).
981 	 *
982 	 * The list for a single invocation is partial;
983 	 * the event can be invoked several times for the same channel
984 	 * as a response to a single trigger. The list is signaled complete
985 	 * when $(MREF IrcClient.onNameListEnd) is invoked.
986 	 * Params:
987 	 *    channel = channel of which the users are members
988 	 *    nickNames = list of member nicknames
989 	 */
990 	void delegate(in char[] channel, in char[][] nickNames)[] onNameList;
991 
992 	/**
993 	 * Invoked when the complete list of members of a _channel have been received.
994 	 * All invocations of $(D onNameList) between invocations of this event
995 	 * are part of the same member list.
996 	 * See_Also:
997 	 *    $(MREF IrcClient.onNameList)
998 	 */
999 	void delegate(in char[] channel)[] onNameListEnd;
1000 
1001 	/**
1002 	 * Invoked when a CTCP query is received in a message.
1003 	 * $(MREF IrcClient.onMessage) is not invoked for the given message
1004 	 * when onCtcpQuery has a non-zero number of registered handlers.
1005 	 * Note:
1006 	 *   This callback is only invoked when there is a CTCP message at the start
1007 	 *   of the message, and any subsequent CTCP messages in the same message are
1008 	 *   discarded. To handle multiple CTCP queries in one message, use
1009 	 *   $(MREF IrcClient.onMessage) with $(DPREF ctcp, ctcpExtract).
1010 	 */
1011 	void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpQuery;
1012 
1013 	/**
1014 	 * Invoked when a CTCP reply is received in a notice.
1015 	 * $(MREF IrcClient.onNotice) is not invoked for the given notice
1016 	 * when onCtcpReply has a non-zero number of registered handlers.
1017 	 * Note:
1018 	 *   This callback is only invoked when there is a CTCP message at the start
1019 	 *   of the notice, and any subsequent CTCP messages in the same notice are
1020 	 *   discarded. To handle multiple CTCP replies in one notice, use
1021 	 *   $(MREF IrcClient.onNotice) with $(DPREF ctcp, ctcpExtract).
1022 	 */
1023 	void delegate(IrcUser user, in char[] source, in char[] tag, in char[] data)[] onCtcpReply;
1024 
1025 	/**
1026 	 * Invoked when the requested nick name of the user for this client is already in use.
1027 	 *
1028 	 * Return a non-null string to provide a new nick. No further callbacks in the list
1029 	 * are called once a callback provides a nick.
1030 	 * Params:
1031 	 *   newNick = the nick name that was requested.
1032 	 * Note:
1033 	 *   The current nick name can be read from the $(MREF IrcClient.nick) property of this client.
1034 	 */
1035 	const(char)[] delegate(in char[] newNick)[] onNickInUse;
1036 
1037 	/**
1038 	 * Invoked when a _channel is joined, a _topic is set in a _channel or when
1039 	 * the current _topic was requested.
1040 	 *
1041 	 * Params:
1042 	 *   channel
1043 	 *   topic = _topic or new _topic for channel
1044 	 */
1045 	void delegate(in char[] channel, in char[] topic)[] onTopic;
1046 
1047 	/**
1048 	 * Invoked when a _channel is joined or when the current _topic was requested.
1049 	 *
1050 	 * Params:
1051 	 *   channel
1052 	 *   nick = _nick name of user who set the topic
1053 	 *   time = _time the topic was set
1054 	 */
1055 	void delegate(in char[] channel, in char[] nick, in char[] time)[] onTopicInfo;
1056 
1057 	/**
1058 	 * Invoked with the reply of a userhost query.
1059 	 * See_Also:
1060 	 *   $(MREF IrcClient.queryUserhost)
1061 	 */
1062 	void delegate(in IrcUser[] users)[] onUserhostReply;
1063 	
1064 	/**
1065 	 * Invoked when a user invites us to a channel.
1066 	 * Params:
1067 	 *   channel = _channel channel we were invited to
1068 	 */
1069 	void delegate(in char[] channel)[] onInvite;
1070 
1071 	/**
1072 	 * Invoked when a WHOIS reply is received.
1073 	 * See_Also:
1074 	 *   $(MREF IrcClient.queryWhois)
1075 	 */
1076 	// TODO: document more, and maybe parse `channels`
1077 	void delegate(IrcUser userInfo, in char[] realName)[] onWhoisReply;
1078 
1079 	/// Ditto
1080 	void delegate(in char[] nick, in char[] serverHostName, in char[] serverInfo)[] onWhoisServerReply;
1081 
1082 	/// Ditto
1083 	void delegate(in char[] nick)[] onWhoisOperatorReply;
1084 
1085 	/// Ditto
1086 	void delegate(in char[] nick, int idleTime)[] onWhoisIdleReply;
1087 
1088 	/// Ditto
1089 	void delegate(in char[] nick, in char[][] channels)[] onWhoisChannelsReply;
1090 
1091 	/// Ditto
1092 	void delegate(in char[] nick, in char[] accountName)[] onWhoisAccountReply;
1093 
1094 	/// Ditto
1095 	void delegate(in char[] nick)[] onWhoisEnd;
1096 
1097 	protected:
1098 	IrcUser getUser(in char[] prefix)
1099 	{
1100 		return IrcUser.fromPrefix(prefix);
1101 	}
1102 
1103 	private:
1104 	void fireEvent(T, U...)(T[] event, U args)
1105 	{
1106 		foreach(cb; event)
1107 		{
1108 			cb(args);
1109 		}
1110 	}
1111 
1112 	bool ctcpCheck(void delegate(IrcUser, in char[], in char[], in char[])[] event,
1113 				   in char[] prefix,
1114 				   in char[] target,
1115 				   in char[] message)
1116 	{
1117 		if(event.empty || message[0] != CtcpToken.delimiter)
1118 			return false;
1119 
1120 		auto extractor = message.ctcpExtract();
1121 
1122 		if(extractor.empty)
1123 			return false;
1124 
1125 		// TODO: re-use buffer
1126 		auto ctcpMessage = cast(string)extractor.front.array();
1127 		auto tag = ctcpMessage.munch("^ ");
1128 
1129 		if(!ctcpMessage.empty && ctcpMessage.front == ' ')
1130 			ctcpMessage.popFront();
1131 
1132 		fireEvent(
1133 			event,
1134 			getUser(prefix),
1135 			target,
1136 			tag,
1137 			ctcpMessage
1138 		);
1139 
1140 		return true;
1141 	}
1142 
1143 	// TODO: Switch getting large, change to something more performant?
1144 	void handle(ref IrcLine line)
1145 	{
1146 		import std.conv : to;
1147 
1148 		switch(line.command)
1149 		{
1150 			case "PING":
1151 				writef("PONG :%s", line.arguments[0]);
1152 				break;
1153 			case "433":
1154 				void failed433(Exception cause)
1155 				{
1156 					socket.close();
1157 					_connected = false;
1158 					throw new IrcErrorException(this, `"433 Nick already in use" was unhandled`, cause);
1159 				}
1160 
1161 				auto failedNick = line.arguments[1];
1162 				bool handled = false;
1163 
1164 				foreach(cb; onNickInUse)
1165 				{
1166 					const(char)[] nextNickToTry;
1167 
1168 					try nextNickToTry = cb(failedNick);
1169 					catch(Exception e)
1170 						failed433(e);
1171 
1172 					if(nextNickToTry)
1173 					{
1174 						writef("NICK %s", nextNickToTry);
1175 						handled = true;
1176 						break;
1177 					}
1178 				}
1179 
1180 				if(!handled)
1181 					failed433(null);
1182 
1183 				break;
1184 			case "PRIVMSG":
1185 				auto prefix = line.prefix;
1186 				auto target = line.arguments[0];
1187 				auto message = line.arguments[1];
1188 
1189 				if(!ctcpCheck(onCtcpQuery, prefix, target, message))
1190 					fireEvent(onMessage, getUser(prefix), target, message);
1191 
1192 				break;
1193 			case "NOTICE":
1194 				auto prefix = line.prefix;
1195 				auto target = line.arguments[0];
1196 				auto notice = line.arguments[1];
1197 
1198 				if(!ctcpCheck(onCtcpReply, prefix, target, notice))
1199 					fireEvent(onNotice, getUser(prefix), target, notice);
1200 
1201 				break;
1202 			case "NICK":
1203 				auto user = getUser(line.prefix);
1204 				auto newNick = line.arguments[0];
1205 
1206 				scope(exit)
1207 				{
1208 					if(m_nick == user.nickName)
1209 						m_nick = newNick.idup;
1210 				}
1211 
1212 				fireEvent(onNickChange, user, newNick);
1213 				break;
1214 			case "JOIN":
1215 				auto user = getUser(line.prefix);
1216 
1217 				if(user.nickName == m_nick)
1218 					fireEvent(onSuccessfulJoin, line.arguments[0]);
1219 				else
1220 					fireEvent(onJoin, user, line.arguments[0]);
1221 
1222 				break;
1223 			case "353": // TODO: operator/voice status etc. should be propagated to callbacks
1224 				// line.arguments[0] == client.nick
1225 				version(none) auto type = line.arguments[1];
1226 				auto channelName = line.arguments[2];
1227 
1228 				auto names = line.arguments[3].split();
1229 
1230 				// Strip channel modes from nick names (TODO: present them to the user in some way)
1231 				foreach(ref name; names)
1232 				{
1233 					while(prefixedChannelModes.map!(pair => pair[0]).canFind(name[0]))
1234 						name = name[1 .. $];
1235 				}
1236 
1237 				fireEvent(onNameList, channelName, names);
1238 				break;
1239 			case "366":
1240 				fireEvent(onNameListEnd, line.arguments[1]);
1241 				break;
1242 			case "PART":
1243 				fireEvent(onPart, getUser(line.prefix), line.arguments[0]);
1244 				break;
1245 			case "QUIT":
1246 				fireEvent(onQuit, getUser(line.prefix),
1247 					line.arguments.length? line.arguments[0] : null);
1248 				break;
1249 			case "KICK":
1250 				fireEvent(onKick,
1251 					getUser(line.prefix),
1252 					line.arguments[0],
1253 					line.arguments[1],
1254 					line.arguments.length > 2? line.arguments[2] : null);
1255 				break;
1256 			case "302":
1257 				IrcUser[5] users;
1258 				auto n = IrcUser.parseUserhostReply(users, line.arguments[1]);
1259 
1260 				fireEvent(onUserhostReply, users[0 .. n]);
1261 				break;
1262 			case "332":
1263 				fireEvent(onTopic, line.arguments[1], line.arguments[2]);
1264 				break;
1265 			case "333":
1266 				fireEvent(onTopicInfo, line.arguments[1], line.arguments[2], line.arguments[3]);
1267 				break;
1268 			// WHOIS replies
1269 			case "311":
1270 				auto user = IrcUser(
1271 					line.arguments[1], // Nick
1272 					line.arguments[2]); // Username
1273 
1274 				fireEvent(onWhoisReply, user, line.arguments[5]);
1275 				break;
1276 			case "312":
1277 				fireEvent(onWhoisServerReply, line.arguments[1], line.arguments[2], line.arguments[3]);
1278 				break;
1279 			case "313":
1280 				fireEvent(onWhoisOperatorReply, line.arguments[1]);
1281 				break;
1282 			case "317":
1283 				import std.conv : to;
1284 				fireEvent(onWhoisIdleReply, line.arguments[1], to!int(line.arguments[2]));
1285 				break;
1286 			case "319":
1287 				auto nickName = line.arguments[1];
1288 				auto channels = split(line.arguments[2]);
1289 
1290 				// Strip channel modes from channel names (TODO: present them to the user in some way)
1291 				foreach(ref channel; channels)
1292 				{
1293 					while(prefixedChannelModes.map!(pair => pair[0]).canFind(channel[0]))
1294 						channel = channel[1 .. $];
1295 				}
1296 
1297 				fireEvent(onWhoisChannelsReply, nickName, channels);
1298 				break;
1299 			case "318":
1300 				fireEvent(onWhoisEnd, line.arguments[1]);
1301 				break;
1302 			// Non-standard WHOIS replies
1303 			case "307": // UnrealIRCd?
1304 				if(line.arguments[0] == m_nick)
1305 					fireEvent(onWhoisAccountReply, line.arguments[1], line.arguments[1]);
1306 				break;
1307 			case "330": // Freenode
1308 				fireEvent(onWhoisAccountReply, line.arguments[1], line.arguments[2]);
1309 				break;
1310 			// End of WHOIS replies
1311 			case "ERROR":
1312 				_connected = false;
1313 				throw new IrcErrorException(this, line.arguments[0].idup);
1314 			case "005": // ISUPPORT
1315 				// TODO: handle "\xHH" sequences
1316 				auto tokens = line.arguments[1 .. $ - 1]; // trim nick name and "are supported by this server"
1317 				foreach(const token; tokens)
1318 				{
1319 					if(token[0] == '-') // Negation
1320 					{
1321 						auto parameter = token[1 .. $];
1322 						switch(parameter)
1323 						{
1324 							case "NICKLEN":
1325 								_maxNickLength = defaultMaxNickLength;
1326 								enforceMaxNickLength = false;
1327 								break;
1328 							default:
1329 								debug(Dirk) std.stdio.writefln(`Unhandled negation of ISUPPORT parameter "%s"`, parameter);
1330 								break;
1331 						}
1332 					}
1333 					else
1334 					{
1335 						auto sepPos = token.indexOf('=');
1336 						const(char)[] parameter, value;
1337 						if(sepPos == -1)
1338 						{
1339 							parameter = token;
1340 							value = null;
1341 						}
1342 						else
1343 						{
1344 							parameter = token[0 .. sepPos];
1345 							value = token[sepPos + 1 .. $]; // May be empty
1346 						}
1347 
1348 						debug(Dirk) std.stdio.writefln(`ISUPPORT parameter "%s" has value "%s"`, parameter, value);
1349 
1350 						switch(parameter)
1351 						{
1352 							case "PREFIX":
1353 								if(value.empty)
1354 									prefixedChannelModes = defaultPrefixedChannelModes;
1355 								else
1356 								{
1357 									assert(value[0] == '(');
1358 									auto endParenPos = value.indexOf(')');
1359 									assert(endParenPos != -1 && endParenPos != value.length - 1);
1360 									auto modes = value[1 .. endParenPos];
1361 									auto prefixes = value[endParenPos + 1 .. $];
1362 									assert(modes.length == prefixes.length);
1363 									auto newChannelModes = new char[2][](modes.length);
1364 									foreach(immutable i, ref pair; newChannelModes)
1365 										pair = [prefixes[i], modes[i]];
1366 									prefixedChannelModes = newChannelModes;
1367 									debug(Dirk) std.stdio.writefln("ISUPPORT PREFIX: %s", prefixedChannelModes);
1368 								}
1369 								break;
1370 							case "CHANMODES":
1371 								assert(!value.empty);
1372 								const(char)[][4] modeTypes; // Types A, B, C and D
1373 
1374 								value.splitter(',')
1375 									.takeExactly(4)
1376 									.copy(modeTypes[]);
1377 
1378 								if(channelListModes != modeTypes[0])
1379 									channelListModes = modeTypes[0].idup;
1380 
1381 								if(channelParameterizedModes != modeTypes[1])
1382 									channelParameterizedModes = modeTypes[1].idup;
1383 
1384 								if(channelNullaryRemovableModes != modeTypes[2])
1385 									channelNullaryRemovableModes = modeTypes[2].idup;
1386 
1387 								if(channelSettingModes != modeTypes[3])
1388 									channelSettingModes = modeTypes[3].idup;
1389 								break;
1390 							case "NICKLEN":
1391 								assert(!value.empty);
1392 								_maxNickLength = to!(typeof(_maxNickLength))(value);
1393 								enforceMaxNickLength = true;
1394 								break;
1395 							case "NETWORK":
1396 								assert(!value.empty);
1397 								_networkName = value.idup;
1398 								break;
1399 							default:
1400 								debug(Dirk) std.stdio.writefln(`Unhandled ISUPPORT parameter "%s"`, parameter);
1401 								break;
1402 						}
1403 					}
1404 				}
1405 
1406 				break;
1407 			case "001":
1408 				m_nick = line.arguments[0].idup;
1409 				fireEvent(onConnect);
1410 				break;
1411 			case "INVITE":
1412 				fireEvent(onInvite, line.arguments[1]);
1413 				break;
1414 			default:
1415 				debug(Dirk) std.stdio.writefln(`Unhandled command "%s"`, line.command);
1416 				break;
1417 		}
1418 	}
1419 }