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