1 /++
2 + Module containing IRC client guts. Parses and dispatches to appropriate
3 + handlers/
4 +/
5 module virc.client.skeleton;
6 import std.algorithm.comparison : among;
7 import std.algorithm.iteration : chunkBy, cumulativeFold, filter, map, splitter;
8 import std.algorithm.searching : canFind, endsWith, find, findSplit, findSplitAfter, findSplitBefore, skipOver, startsWith;
9 import std.array : array;
10 import std.ascii : isDigit;
11 import std.conv : parse, text;
12 import std.datetime;
13 import std.exception : enforce;
14 import std.format : format, formattedWrite;
15 import std.meta : AliasSeq;
16 import std.range.primitives : ElementType, isInputRange, isOutputRange;
17 import std.range : chain, empty, front, put, walkLength;
18 import std.traits : isCopyable, Parameters, Unqual;
19 import std.typecons : Nullable, RefCounted, refCounted;
20 import std.utf : byCodeUnit;
21 
22 import virc.common;
23 import virc.encoding;
24 import virc.client.internaladdresslist;
25 import virc.ircsplitter;
26 import virc.ircv3.batch;
27 import virc.ircv3.sasl;
28 import virc.ircv3.tags;
29 import virc.ircmessage;
30 import virc.message;
31 import virc.modes;
32 import virc.numerics;
33 import virc.target;
34 import virc.usermask;
35 
36 /++
37 +
38 +/
39 struct NickInfo {
40 	///
41 	string nickname;
42 	///
43 	string username;
44 	///
45 	string realname;
46 }
47 
48 /++
49 +
50 +/
51 enum supportedCaps = AliasSeq!(
52 	"account-notify", // http://ircv3.net/specs/extensions/account-notify-3.1.html
53 	"account-tag", // http://ircv3.net/specs/extensions/account-tag-3.2.html
54 	"away-notify", // http://ircv3.net/specs/extensions/away-notify-3.1.html
55 	"batch", // http://ircv3.net/specs/extensions/batch-3.2.html
56 	"cap-notify", // http://ircv3.net/specs/extensions/cap-notify-3.2.html
57 	"chghost", // http://ircv3.net/specs/extensions/chghost-3.2.html
58 	"echo-message", // http://ircv3.net/specs/extensions/echo-message-3.2.html
59 	"extended-join", // http://ircv3.net/specs/extensions/extended-join-3.1.html
60 	"invite-notify", // http://ircv3.net/specs/extensions/invite-notify-3.2.html
61 	"draft/metadata-2", //
62 	"message-tags", //https://ircv3.net/specs/extensions/message-tags
63 	"draft/metadata-notify-2", //
64 	"draft/multiline", // https://ircv3.net/specs/extensions/multiline
65 	//"monitor", // http://ircv3.net/specs/core/monitor-3.2.html
66 	"multi-prefix", // http://ircv3.net/specs/extensions/multi-prefix-3.1.html
67 	"sasl", // http://ircv3.net/specs/extensions/sasl-3.1.html and http://ircv3.net/specs/extensions/sasl-3.2.html
68 	"server-time", // http://ircv3.net/specs/extensions/server-time-3.2.html
69 	"userhost-in-names", // http://ircv3.net/specs/extensions/userhost-in-names-3.2.html
70 );
71 
72 /++
73 +
74 +/
75 auto ircClient(Output output, NickInfo info, SASLMechanism[] saslMechs = [], string password = string.init) {
76 	auto client = IRCClient(output);
77 	client.nickinfo.username = info.username;
78 	client.nickinfo.realname = info.realname;
79 	client.nickinfo.nickname = info.nickname;
80 	if (password != string.init) {
81 		client.password = password;
82 	}
83 	client.saslMechs = saslMechs;
84 	client.initialize();
85 	return client;
86 }
87 
88 /++
89 +
90 +/
91 struct Server {
92 	///
93 	MyInfo myInfo;
94 	///
95 	ISupport iSupport;
96 }
97 /++
98 +
99 +/
100 enum RFC1459Commands {
101 	privmsg = "PRIVMSG",
102 	notice = "NOTICE",
103 	info = "INFO",
104 	admin = "ADMIN",
105 	trace = "TRACE",
106 	connect = "CONNECT",
107 	time = "TIME",
108 	links = "LINKS",
109 	stats = "STATS",
110 	version_ = "VERSION",
111 	kick = "KICK",
112 	invite = "INVITE",
113 	list = "LIST",
114 	names = "NAMES",
115 	topic = "TOPIC",
116 	mode = "MODE",
117 	part = "PART",
118 	join = "JOIN",
119 	squit = "SQUIT",
120 	quit = "QUIT",
121 	oper = "OPER",
122 	server = "SERVER",
123 	user = "USER",
124 	nick = "NICK",
125 	pass = "PASS",
126 	who = "WHO",
127 	whois = "WHOIS",
128 	whowas = "WHOWAS",
129 	kill = "KILL",
130 	ping = "PING",
131 	pong = "PONG",
132 	error = "ERROR",
133 	away = "AWAY",
134 	rehash = "REHASH",
135 	restart = "RESTART",
136 	summon = "SUMMON",
137 	users = "USERS",
138 	wallops = "WALLOPS",
139 	userhost = "USERHOST",
140 	ison = "ISON",
141 }
142 /++
143 +
144 +/
145 enum RFC2812Commands {
146 	service = "SERVICE"
147 }
148 
149 import virc.ircv3 : IRCV3Commands;
150 alias ClientNoOpCommands = AliasSeq!(
151 	RFC1459Commands.server,
152 	RFC1459Commands.user,
153 	RFC1459Commands.pass,
154 	RFC1459Commands.whois,
155 	RFC1459Commands.whowas,
156 	RFC1459Commands.kill,
157 	RFC1459Commands.who,
158 	RFC1459Commands.oper,
159 	RFC1459Commands.squit,
160 	RFC1459Commands.summon,
161 	RFC1459Commands.pong, //UNIMPLEMENTED
162 	RFC1459Commands.error, //UNIMPLEMENTED
163 	RFC1459Commands.userhost,
164 	RFC1459Commands.version_,
165 	RFC1459Commands.names,
166 	RFC1459Commands.away,
167 	RFC1459Commands.connect,
168 	RFC1459Commands.trace,
169 	RFC1459Commands.links,
170 	RFC1459Commands.stats,
171 	RFC1459Commands.ison,
172 	RFC1459Commands.restart,
173 	RFC1459Commands.users,
174 	RFC1459Commands.list,
175 	RFC1459Commands.admin,
176 	RFC1459Commands.rehash,
177 	RFC1459Commands.time,
178 	RFC1459Commands.info,
179 	RFC2812Commands.service,
180 	IRCV3Commands.starttls, //DO NOT IMPLEMENT
181 	IRCV3Commands.batch, //SPECIAL CASE
182 	IRCV3Commands.monitor,
183 	Numeric.RPL_HOSTHIDDEN,
184 	Numeric.RPL_ENDOFNAMES,
185 	Numeric.RPL_ENDOFMONLIST,
186 	Numeric.RPL_ENDOFWHO,
187 	Numeric.RPL_LOCALUSERS,
188 	Numeric.RPL_GLOBALUSERS,
189 	Numeric.RPL_YOURHOST,
190 	Numeric.RPL_YOURID,
191 	Numeric.RPL_CREATED,
192 	Numeric.RPL_LISTSTART,
193 	Numeric.RPL_LISTEND,
194 	Numeric.RPL_TEXT,
195 	Numeric.RPL_ADMINME,
196 	Numeric.RPL_ADMINLOC1,
197 	Numeric.RPL_ADMINLOC2,
198 	Numeric.RPL_ADMINEMAIL,
199 	Numeric.RPL_WHOISCERTFP,
200 	Numeric.RPL_WHOISHOST,
201 	Numeric.RPL_WHOISMODE
202 );
203 
204 /++
205 +
206 +/
207 struct ChannelState {
208 	Channel channel;
209 	string topic;
210 	InternalAddressList users;
211 	Mode[] modes;
212 	void toString(T)(T sink) const if (isOutputRange!(T, const(char))) {
213 		formattedWrite!"Channel: %s\n"(sink, channel);
214 		formattedWrite!"\tTopic: %s\n"(sink, topic);
215 		formattedWrite!"\tUsers:\n"(sink);
216 		foreach (user; users.list) {
217 			formattedWrite!"\t\t%s\n"(sink, user);
218 		}
219 	}
220 }
221 unittest {
222 	import std.outbuffer;
223 	ChannelState(Channel("#test"), "Words").toString(new OutBuffer);
224 }
225 /++
226 + Types of errors.
227 +/
228 enum ErrorType {
229 	///Insufficient privileges for command. See message for missing privilege.
230 	noPrivs,
231 	///Monitor list is full.
232 	monListFull,
233 	///Server has no MOTD.
234 	noMOTD,
235 	///No server matches client-provided server mask.
236 	noSuchServer,
237 	///User is not an IRC operator.
238 	noPrivileges,
239 	///Malformed message received from server.
240 	malformed,
241 	///Message received unexpectedly.
242 	unexpected,
243 	///Unhandled command or numeric.
244 	unrecognized,
245 	///Bad input from client.
246 	badUserInput,
247 	///No key matched
248 	keyNotSet,
249 	///Action could not be performed now, try again later
250 	waitAndRetry,
251 	///Too many metadata subscriptions
252 	tooManySubs,
253 	///Standard replies: FAIL
254 	standardFail
255 }
256 /++
257 + Struct holding data about non-fatal errors.
258 +/
259 struct IRCError {
260 	ErrorType type;
261 	string message;
262 }
263 /++
264 + Channels in a WHOIS response.
265 +/
266 struct WhoisChannel {
267 	Channel name;
268 	string prefix;
269 }
270 
271 /++
272 + Full response to a WHOIS.
273 +/
274 struct WhoisResponse {
275 	bool isOper;
276 	bool isSecure;
277 	bool isRegistered;
278 	Nullable!string username;
279 	Nullable!string hostname;
280 	Nullable!string realname;
281 	Nullable!SysTime connectedTime;
282 	Nullable!Duration idleTime;
283 	Nullable!string connectedTo;
284 	Nullable!string account;
285 	WhoisChannel[string] channels;
286 }
287 
288 /++
289 + Metadata update.
290 +/
291 struct MetadataValue {
292 	///Visibility of this value. Exact meaning is defined by the server implementation.
293 	string visibility;
294 	///Main payload
295 	string value;
296 
297 	alias value this;
298 }
299 
300 interface Output {
301 	void put(char) @safe;
302 }
303 
304 enum ChannelListUpdateType {
305 	added,
306 	removed,
307 	updated
308 }
309 
310 /++
311 + IRC client implementation.
312 +/
313 struct IRCClient {
314 	import virc.ircv3 : Capability, CapabilityServerSubcommands, IRCV3Commands;
315 	Output output;
316 	///
317 	Server server;
318 	///
319 	Capability[] capsEnabled;
320 	///User metadata received so far
321 	MetadataValue[string][User] userMetadata;
322 	///Channel metadata received so far
323 	MetadataValue[string][Channel] channelMetadata;
324 
325 	NickInfo nickinfo;
326 	Nullable!string password;
327 	///
328 	ChannelState[string] channels;
329 
330 	///SASL mechanisms available for usage
331 	SASLMechanism[] saslMechs;
332 	///
333 	InternalAddressList internalAddressList;
334 
335 	///
336 	void delegate(const Capability, const MessageMetadata) @safe onReceiveCapList;
337 	///
338 	void delegate(const Capability, const MessageMetadata) @safe onReceiveCapLS;
339 	///
340 	void delegate(const Capability, const MessageMetadata) @safe onReceiveCapAck;
341 	///
342 	void delegate(const Capability, const MessageMetadata) @safe onReceiveCapNak;
343 	///
344 	void delegate(const Capability, const MessageMetadata) @safe onReceiveCapDel;
345 	///
346 	void delegate(const Capability, const MessageMetadata) @safe onReceiveCapNew;
347 	///
348 	void delegate(const User, const SysTime, const MessageMetadata) @safe onUserOnline;
349 	///
350 	void delegate(const User, const MessageMetadata) @safe onUserOffline;
351 	///
352 	void delegate(const User, const MessageMetadata) @safe onLogin;
353 	///
354 	void delegate(const User, const MessageMetadata) @safe onLogout;
355 	///
356 	void delegate(const User, const string, const MessageMetadata) @safe onOtherUserAwayReply;
357 	///
358 	void delegate(const User, const MessageMetadata) @safe onBack;
359 	///
360 	void delegate(const User, const MessageMetadata) @safe onMonitorList;
361 	///
362 	void delegate(const User, const User, const MessageMetadata) @safe onNick;
363 	///
364 	void delegate(const User, const User, const Channel, const MessageMetadata) @safe onInvite;
365 	///
366 	void delegate(const User, const Channel, const MessageMetadata) @safe onJoin;
367 	///
368 	void delegate(const User, const Channel, const string, const MessageMetadata) @safe onPart;
369 	///
370 	void delegate(const User, const Channel, const User, const string, const MessageMetadata) @safe onKick;
371 	///
372 	void delegate(const User, const string, const MessageMetadata) @safe onQuit;
373 	///
374 	void delegate(const User, const Target, const ModeChange, const MessageMetadata) @safe onMode;
375 	///
376 	void delegate(const User, const Target, const Message, const MessageMetadata) @safe onMessage;
377 	///
378 	void delegate(const User, const WhoisResponse) @safe onWhois;
379 	///
380 	void delegate(const User, const string, const MessageMetadata) @safe onWallops;
381 	///
382 	void delegate(const ChannelListResult, const MessageMetadata) @safe onList;
383 	///
384 	void delegate(const User, const User, const MessageMetadata) @safe onChgHost;
385 	///
386 	void delegate(const LUserClient, const MessageMetadata) @safe onLUserClient;
387 	///
388 	void delegate(const LUserOp, const MessageMetadata) @safe onLUserOp;
389 	///
390 	void delegate(const LUserChannels, const MessageMetadata) @safe onLUserChannels;
391 	///
392 	void delegate(const LUserMe, const MessageMetadata) @safe onLUserMe;
393 	///
394 	void delegate(const NamesReply, const MessageMetadata) @safe onNamesReply;
395 	///
396 	void delegate(const WHOXReply, const MessageMetadata) @safe onWHOXReply;
397 	///
398 	void delegate(const TopicReply, const MessageMetadata) @safe onTopicReply;
399 	///
400 	void delegate(const User, const Channel, const string, const MessageMetadata) @safe onTopicChange;
401 	///
402 	void delegate(const User, const MessageMetadata) @safe onUnAwayReply;
403 	///
404 	void delegate(const User, const MessageMetadata) @safe onAwayReply;
405 	///
406 	void delegate(const TopicWhoTime, const MessageMetadata) @safe onTopicWhoTimeReply;
407 	///
408 	void delegate(const VersionReply, const MessageMetadata) @safe onVersionReply;
409 	///
410 	void delegate(const RehashingReply, const MessageMetadata) @safe onServerRehashing;
411 	///
412 	void delegate(const MessageMetadata) @safe onYoureOper;
413 	///Called when an RPL_ISON message is received
414 	void delegate(const User, const MessageMetadata) @safe onIsOn;
415 	///Called when a metadata subscription list is received
416 	void delegate(const string, const MessageMetadata) @safe onMetadataSubList;
417 	///
418 	void delegate(const IRCError, const MessageMetadata) @safe onError;
419 	///
420 	void delegate(const MessageMetadata) @safe onRaw;
421 	///
422 	void delegate() @safe onConnect;
423 	/// Called whenever a channel user list is updated
424 	void delegate(const User, const User, const Channel, ChannelListUpdateType) @safe onChannelListUpdate;
425 	///
426 	debug void delegate(const string) @safe onSend;
427 
428 	static struct ClientState {
429 		bool invalid = true;
430 		bool isRegistered;
431 		ulong capReqCount = 0;
432 		BatchProcessor batchProcessor;
433 		bool isAuthenticating;
434 		bool authenticationSucceeded;
435 		string[] supportedSASLMechs;
436 		SASLMechanism selectedSASLMech;
437 		bool autoSelectSASLMech;
438 		string receivedSASLAuthenticationText;
439 		bool _isAway;
440 		ulong maxMetadataSubscriptions;
441 		ulong maxMetadataSelfKeys;
442 		size_t maxMetadataValueBytes;
443 		const(string)[] metadataSubscribedKeys;
444 		Capability[string] availableCapabilities;
445 		WhoisResponse[string] whoisCache;
446 		size_t multilineMaxBytes = size_t.max;
447 		size_t multilineMaxLines = size_t.max;
448 		size_t batchCounter = 0;
449 	}
450 	private ClientState state;
451 
452 	bool isAuthenticated() @safe {
453 		return state.authenticationSucceeded;
454 	}
455 
456 	void initialize(NickInfo info) @safe {
457 		nickinfo = info;
458 		initialize();
459 	}
460 	void initialize() @safe {
461 		state = state.init;
462 		state.invalid = false;
463 		write("CAP LS 302");
464 		register();
465 	}
466 	public void ping() @safe {
467 
468 	}
469 	public void names() @safe {
470 		write("NAMES");
471 	}
472 	public void ping(const string nonce) @safe {
473 		write!"PING :%s"(nonce);
474 	}
475 	public void lUsers() @safe {
476 		write!"LUSERS";
477 	}
478 	private void pong(const string nonce) @safe {
479 		write!"PONG :%s"(nonce);
480 	}
481 	public void put(string line) @safe {
482 		import std.conv : asOriginalType;
483 		import std.meta : NoDuplicates;
484 		import std.string : representation;
485 		import std.traits : EnumMembers;
486 		debug(verboseirc) import std.experimental.logger : trace;
487 		//Chops off terminating \r\n. Everything after is ignored, according to spec.
488 		line = findSplitBefore(line, "\r\n")[0];
489 		debug(verboseirc) trace("←: ", line);
490 		assert(isValid, "Received data after invalidation");
491 		if (line.empty) {
492 			return;
493 		}
494 		state.batchProcessor.put(line);
495 		foreach (batch; state.batchProcessor) {
496 			state.batchProcessor.popFront();
497 			bool multilineMessageStarted;
498 			Message multilineMessage;
499 			Target multilineMessageTarget;
500 			User multilineMessageUser;
501 			MessageMetadata multilineMessageMetadata;
502 			multilineMessageMetadata.tags = batch.info.tags;
503 			foreach (parsed; batch.lines) {
504 				auto metadata = MessageMetadata();
505 				metadata.batch = parsed.batch;
506 				metadata.tags = parsed.tags;
507 				if("time" in parsed.tags) {
508 					metadata.time = parseTime(parsed.tags);
509 				} else {
510 					metadata.time = Clock.currTime(UTC());
511 				}
512 				if ("account" in parsed.tags) {
513 					if (!parsed.sourceUser.isNull) {
514 						parsed.sourceUser.get.account = parsed.tags["account"];
515 					}
516 				}
517 				if (!parsed.sourceUser.isNull) {
518 					internalAddressList.update(parsed.sourceUser.get);
519 					if (parsed.sourceUser.get.nickname in internalAddressList) {
520 						parsed.sourceUser = internalAddressList[parsed.sourceUser.get.nickname];
521 					}
522 				}
523 				if (metadata.batch.type == "draft/multiline") {
524 					auto split = parsed.args;
525 					multilineMessageUser = parsed.sourceUser.get;
526 					multilineMessageTarget = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes);
527 					split.popFront();
528 					multilineMessage.type = parsed.verb == "NOTICE" ? MessageType.notice : MessageType.privmsg;
529 					if (("draft/multiline-concat" !in parsed.tags) && (multilineMessageStarted)) {
530 						multilineMessage.msg ~= "\n";
531 					}
532 					multilineMessage.msg ~= split.front;
533 					multilineMessageStarted = true;
534 					continue;
535 				}
536 
537 				if (parsed.verb.filter!(x => !isDigit(x)).empty) {
538 					metadata.messageNumeric = cast(Numeric)parsed.verb;
539 				}
540 				metadata.original = parsed.raw;
541 				tryCall!"onRaw"(metadata);
542 
543 				switchy: switch (parsed.verb) {
544 					//TOO MANY TEMPLATE INSTANTIATIONS! uncomment when compiler fixes this!
545 					//alias Numerics = NoDuplicates!(EnumMembers!Numeric);
546 					alias Numerics = AliasSeq!(Numeric.RPL_WELCOME, Numeric.RPL_ISUPPORT, Numeric.RPL_LIST, Numeric.RPL_YOURHOST, Numeric.RPL_CREATED, Numeric.RPL_LISTSTART, Numeric.RPL_LISTEND, Numeric.RPL_ENDOFMONLIST, Numeric.RPL_ENDOFNAMES, Numeric.RPL_YOURID, Numeric.RPL_LOCALUSERS, Numeric.RPL_GLOBALUSERS, Numeric.RPL_HOSTHIDDEN, Numeric.RPL_TEXT, Numeric.RPL_MYINFO, Numeric.RPL_LOGON, Numeric.RPL_MONONLINE, Numeric.RPL_MONOFFLINE, Numeric.RPL_MONLIST, Numeric.RPL_LUSERCLIENT, Numeric.RPL_LUSEROP, Numeric.RPL_LUSERCHANNELS, Numeric.RPL_LUSERME, Numeric.RPL_TOPIC, Numeric.RPL_NAMREPLY, Numeric.RPL_TOPICWHOTIME, Numeric.RPL_SASLSUCCESS, Numeric.RPL_LOGGEDIN, Numeric.RPL_VERSION, Numeric.ERR_MONLISTFULL, Numeric.ERR_NOMOTD, Numeric.ERR_NICKLOCKED, Numeric.ERR_SASLFAIL, Numeric.ERR_SASLTOOLONG, Numeric.ERR_SASLABORTED, Numeric.RPL_REHASHING, Numeric.ERR_NOPRIVS, Numeric.RPL_YOUREOPER, Numeric.ERR_NOSUCHSERVER, Numeric.ERR_NOPRIVILEGES, Numeric.RPL_AWAY, Numeric.RPL_UNAWAY, Numeric.RPL_NOWAWAY, Numeric.RPL_ENDOFWHOIS, Numeric.RPL_WHOISUSER, Numeric.RPL_WHOISSECURE, Numeric.RPL_WHOISOPERATOR, Numeric.RPL_WHOISREGNICK, Numeric.RPL_WHOISIDLE, Numeric.RPL_WHOISSERVER, Numeric.RPL_WHOISACCOUNT, Numeric.RPL_ADMINEMAIL, Numeric.RPL_ADMINLOC1, Numeric.RPL_ADMINLOC2, Numeric.RPL_ADMINME, Numeric.RPL_WHOISHOST, Numeric.RPL_WHOISMODE, Numeric.RPL_WHOISCERTFP, Numeric.RPL_WHOISCHANNELS, Numeric.RPL_ISON, Numeric.RPL_WHOISKEYVALUE, Numeric.RPL_KEYVALUE, Numeric.RPL_KEYNOTSET, Numeric.ERR_METADATASYNCLATER, Numeric.RPL_METADATASUBOK, Numeric.RPL_METADATAUNSUBOK, Numeric.RPL_METADATASUBS, Numeric.RPL_WHOSPCRPL, Numeric.RPL_ENDOFWHO);
547 
548 					static foreach (cmd; AliasSeq!(NoDuplicates!(EnumMembers!IRCV3Commands), NoDuplicates!(EnumMembers!RFC1459Commands), NoDuplicates!(EnumMembers!RFC2812Commands), Numerics)) {
549 						case cmd:
550 							static if (!cmd.asOriginalType.among(ClientNoOpCommands)) {
551 								rec!cmd(parsed, metadata);
552 							}
553 							break switchy;
554 					}
555 					default: recUnknownCommand(parsed.verb, metadata); break;
556 				}
557 			}
558 			if (batch.info.type == "draft/multiline") {
559 				recMessageCommon(multilineMessageUser, multilineMessageTarget, multilineMessage, multilineMessageMetadata);
560 			}
561 		}
562 	}
563 	void put(const immutable(ubyte)[] rawString) @safe {
564 		put(rawString.toUTF8String);
565 	}
566 	private void tryEndRegistration() @safe {
567 		if (state.capReqCount == 0 && !state.isAuthenticating && !state.isRegistered) {
568 			endRegistration();
569 		}
570 	}
571 	private void endAuthentication() @safe {
572 		state.isAuthenticating = false;
573 		tryEndRegistration();
574 	}
575 	private void endRegistration() @safe {
576 		write("CAP END");
577 	}
578 	public void sendRaw(string message) @safe {
579 		write(message);
580 	}
581 	public void capList() @safe {
582 		write("CAP LIST");
583 	}
584 	public void list() @safe {
585 		write("LIST");
586 	}
587 	public void away(const string message) @safe {
588 		write!"AWAY :%s"(message);
589 	}
590 	public void away() @safe {
591 		write("AWAY");
592 	}
593 	public void whois(const string nick) @safe {
594 		write!"WHOIS %s"(nick);
595 	}
596 	public void monitorClear() @safe {
597 		assert(monitorIsEnabled);
598 		write("MONITOR C");
599 	}
600 	public void monitorList() @safe {
601 		assert(monitorIsEnabled);
602 		write("MONITOR L");
603 	}
604 	public void monitorStatus() @safe {
605 		assert(monitorIsEnabled);
606 		write("MONITOR S");
607 	}
608 	public void monitorAdd(T)(T users) if (isInputRange!T && is(ElementType!T == User)) {
609 		assert(monitorIsEnabled);
610 		writeList!("MONITOR + ", ",")(users.map!(x => x.nickname));
611 	}
612 	public void monitorRemove(T)(T users) if (isInputRange!T && is(ElementType!T == User)) {
613 		assert(monitorIsEnabled);
614 		writeList!("MONITOR - ", ",")(users.map!(x => x.nickname));
615 	}
616 	public bool isAway() const @safe {
617 		return state._isAway;
618 	}
619 	public bool monitorIsEnabled() @safe {
620 		return capsEnabled.canFind("MONITOR");
621 	}
622 	public void quit(const string msg) @safe {
623 		write!"QUIT :%s"(msg);
624 		state = state.init;
625 		state.invalid = false;
626 	}
627 	public void changeNickname(const string nick) @safe {
628 		write!"NICK %s"(nick);
629 	}
630 	public void join(T,U)(T chans, U keys) if (isInputRange!T && isInputRange!U) {
631 		auto filteredKeys = keys.filter!(x => !x.empty);
632 		if (!filteredKeys.empty) {
633 			write!"JOIN %-(%s,%) %-(%s,%)"(chans, filteredKeys);
634 		} else {
635 			write!"JOIN %-(%s,%)"(chans);
636 		}
637 	}
638 	public void join(const string chan, const string key = "") @safe {
639 		import std.range : only;
640 		join(only(chan), only(key));
641 	}
642 	public void join(const Channel chan, const string key = "") @safe {
643 		import std.range : only;
644 		join(only(chan.text), only(key));
645 	}
646 	public void msg(const string target, const string message, IRCTags tags = IRCTags.init) @safe {
647 		outMessageCommon!"PRIVMSG"(target, message, tags);
648 	}
649 	size_t startBatch(string fmt, T...)(string type, IRCTags tags, T args) {
650 		writeTags!("BATCH +%s %s"~fmt)(tags, state.batchCounter, type, args);
651 		return state.batchCounter++;
652 	}
653 	void endBatch(size_t id) @safe {
654 		write!"BATCH -%s"(id);
655 	}
656 	public void tagMsg(const string target, IRCTags tags = IRCTags.init) @safe {
657 		outMessageCommon!"TAGMSG"(target, "", tags);
658 	}
659 	public void wallops(const string message) @safe {
660 		write!"WALLOPS :%s"(message);
661 	}
662 	public void msg(const Target target, const Message message, IRCTags tags = IRCTags.init) @safe {
663 		msg(target.targetText, message.text, tags);
664 	}
665 	public void tagMsg(const Target target, IRCTags tags = IRCTags.init) @safe {
666 		tagMsg(target.targetText, tags);
667 	}
668 	public void ctcp(const Target target, const string command, const string args) @safe {
669 		msg(target, Message("\x01"~command~" "~args~"\x01"));
670 	}
671 	public void ctcp(const Target target, const string command) @safe {
672 		msg(target, Message("\x01"~command~"\x01"));
673 	}
674 	public void ctcpReply(const Target target, const string command, const string args) @safe {
675 		notice(target, Message("\x01"~command~" "~args~"\x01"));
676 	}
677 	public void notice(const string target, const string message, IRCTags tags = IRCTags.init) @safe {
678 		outMessageCommon!"NOTICE"(target, message, tags);
679 	}
680 	public void notice(const Target target, const Message message, IRCTags tags = IRCTags.init) @safe {
681 		notice(target.targetText, message.text, tags);
682 	}
683 	private void outMessageCommon(string cmd)(scope const string target, scope const string message, IRCTags tags) {
684 		import std.algorithm.comparison : min;
685 		import std.format : sformat;
686 		import virc.style : checkFormatting, ControlCharacters;
687 		enum breathingRoom = 5;
688 		const maxLength = 512 - 14 - breathingRoom - me.nickname.length - me.ident.get("").length - me.host.get("").length - target.length;
689 		if (message.length < maxLength && !message.canFind("\n")) {
690 			writeTags!(cmd~" %s :%s")(tags, target, message);
691 		} else {
692 			const batch = isEnabled(Capability("draft/multiline"));
693 			auto batchRemainingLines = state.multilineMaxLines;
694 			auto batchRemainingBytes = state.multilineMaxBytes;
695 			auto id = batch ? startBatch!" %s"("draft/multiline", tags, target) : 0;
696 			char[20] formattingBuf;
697 			char[] formatting;
698 			bool startNewBatch;
699 			foreach (line; message.splitter("\n")) {
700 				IRCTags batchTag;
701 				do {
702 					if (startNewBatch) {
703 						endBatch(id);
704 						id = startBatch!" %s"("draft/multiline", tags, target);
705 						batchTag = batchTag.init;
706 						startNewBatch = false;
707 					}
708 					if (batch) {
709 						batchTag.batch(id.text);
710 					} else {
711 						batchTag = tags;
712 					}
713 					const printLine = line[0 .. min(formatting.length + line.length, maxLength, batchRemainingBytes) - formatting.length];
714 					const activeFormatting = checkFormatting(printLine);
715 					writeTags!(cmd~" %s :%s%s")(batchTag, target, formatting, printLine);
716 					line = line[min(line.length, maxLength, batchRemainingBytes) .. $];
717 					if (batch) {
718 						batchTag.multilineConcat();
719 					}
720 					if (--batchRemainingLines == 0) {
721 						startNewBatch = true;
722 						batchRemainingBytes = state.multilineMaxBytes;
723 						batchRemainingLines = state.multilineMaxLines;
724 					}
725 					batchRemainingBytes -= formatting.length + printLine.length;
726 					if (batchRemainingBytes == 0) {
727 						startNewBatch = true;
728 						batchRemainingBytes = state.multilineMaxBytes;
729 						batchRemainingLines = state.multilineMaxLines;
730 					}
731 					formatting = sformat!"%s%s%s%s%s%s%s"(formattingBuf,
732 						activeFormatting.bold ? [cast(char)ControlCharacters.bold] : "",
733 						activeFormatting.underline ? [cast(char)ControlCharacters.underline] : "",
734 						activeFormatting.strikethrough ? [cast(char)ControlCharacters.strikethrough] : "",
735 						activeFormatting.italic ? [cast(char)ControlCharacters.italic] : "",
736 						activeFormatting.reverse ? [cast(char)ControlCharacters.reverse] : "",
737 						activeFormatting.monospace ? [cast(char)ControlCharacters.monospace] : "",
738 						activeFormatting.colour,
739 					);
740 				} while (line.length > 0);
741 			}
742 			if (batch) {
743 				endBatch(id);
744 			}
745 		}
746 	}
747 	public void changeTopic(const Target target, const string topic) @safe {
748 		write!"TOPIC %s :%s"(target, topic);
749 	}
750 	public void oper(const string name, const string pass) @safe {
751 		assert(!name.canFind(" ") && !pass.canFind(" "));
752 		write!"OPER %s %s"(name, pass);
753 	}
754 	public void rehash() @safe {
755 		write!"REHASH";
756 	}
757 	public void restart() @safe {
758 		write!"RESTART";
759 	}
760 	public void squit(const string server, const string reason) @safe {
761 		assert(!server.canFind(" "));
762 		write!"SQUIT %s :%s"(server, reason);
763 	}
764 	public void version_() @safe {
765 		write!"VERSION"();
766 	}
767 	public void version_(const string serverMask) @safe {
768 		write!"VERSION %s"(serverMask);
769 	}
770 	public void kick(const Channel chan, const User nick, const string message = "") @safe {
771 		assert(message.length < server.iSupport.kickLength, "Kick message length exceeded");
772 		write!"KICK %s %s :%s"(chan, nick, message);
773 	}
774 	public void isOn(const string[] nicknames...) @safe {
775 		write!"ISON %-(%s %)"(nicknames);
776 	}
777 	public void isOn(const User[] users...) @safe {
778 		write!"ISON %-(%s %)"(users.map!(x => x.nickname));
779 	}
780 	public void admin(const string server = "") @safe {
781 		if (server == "") {
782 			write!"ADMIN"();
783 		} else {
784 			write!"ADMIN %s"(server);
785 		}
786 	}
787 	public auto ownMetadata() const @safe {
788 		if (me !in userMetadata) {
789 			return null;
790 		}
791 		return userMetadata[me];
792 	}
793 	public void setMetadata(const User user, const string key, const string data) @safe {
794 		write!"METADATA %s SET %s :%s"(user, key, data);
795 	}
796 	public void setMetadata(const Channel channel, const string key, const string data) @safe {
797 		write!"METADATA %s SET %s :%s"(channel, key, data);
798 	}
799 	public void setMetadata(const string key, const string data) @safe {
800 		write!"METADATA * SET %s :%s"(key, data);
801 	}
802 	public void getMetadata(const Channel channel, const string[] keys...) @safe {
803 		write!"METADATA %s GET %-(%s %)"(channel, keys);
804 	}
805 	public void getMetadata(const User user, const string[] keys...) @safe {
806 		write!"METADATA %s GET %-(%s %)"(user, keys);
807 	}
808 	public void listMetadata(const Channel channel) @safe {
809 		write!"METADATA %s LIST"(channel);
810 	}
811 	public void listMetadata(const User user) @safe {
812 		write!"METADATA %s LIST"(user);
813 	}
814 	public void subscribeMetadata(const string[] keys...) @safe {
815 		write!"METADATA * SUB %-(%s %)"(keys);
816 	}
817 	public void unsubscribeMetadata(const string[] keys...) @safe {
818 		write!"METADATA * UNSUB %-(%s %)"(keys);
819 	}
820 	public void listSubscribedMetadata() @safe {
821 		write!"METADATA * SUBS"();
822 	}
823 	public void syncMetadata(const User user) @safe {
824 		write!"METADATA %s SYNC"(user);
825 	}
826 	public void syncMetadata(const Channel channel) @safe {
827 		write!"METADATA %s SYNC"(channel);
828 	}
829 	public void syncMetadata() @safe {
830 		write!"METADATA * SYNC"();
831 	}
832 	public void clearMetadata(const User user) @safe {
833 		write!"METADATA %s CLEAR"(user);
834 	}
835 	public void clearMetadata(const Channel channel) @safe {
836 		write!"METADATA %s CLEAR"(channel);
837 	}
838 	public void clearMetadata() @safe {
839 		write!"METADATA * CLEAR"();
840 	}
841 	public bool isSubscribed(const string key) @safe {
842 		return state.metadataSubscribedKeys.canFind(key);
843 	}
844 	private void sendAuthenticatePayload(const string payload) @safe {
845 		import std.base64 : Base64;
846 		import std.range : chunks;
847 		import std.string : representation;
848 		if (payload == "") {
849 			write!"AUTHENTICATE +"();
850 		} else {
851 			auto str = Base64.encode(payload.representation);
852 			size_t lastChunkSize = 0;
853 			foreach (chunk; str.byCodeUnit.chunks(400)) {
854 				write!"AUTHENTICATE %s"(chunk);
855 				lastChunkSize = chunk.length;
856 			}
857 			if (lastChunkSize == 400) {
858 				write!"AUTHENTICATE +"();
859 			}
860 		}
861 	}
862 	private void user(const string username_, const string realname_) @safe {
863 		write!"USER %s 0 * :%s"(username_, realname_);
864 	}
865 	private void pass(const string pass) @safe {
866 		write!"PASS :%s"(pass);
867 	}
868 	private void register() @safe {
869 		assert(!state.isRegistered);
870 		if (!password.isNull) {
871 			pass(password.get);
872 		}
873 		changeNickname(nickinfo.nickname);
874 		user(nickinfo.username, nickinfo.realname);
875 	}
876 	private void write(string fmt, T...)(scope T args) {
877 		writeTags!(fmt, T)(IRCTags.init, args);
878 	}
879 	private void writeTags(string fmt, T...)(IRCTags tags, scope T args) {
880 		import std.range : put;
881 		debug(verboseirc) import std.experimental.logger : tracef;
882 		const sendTags = !tags.empty && isEnabled(Capability("message-tags"));
883 		debug(verboseirc) tracef("→: %s"~fmt, sendTags ? format!"@%s "(tags) : "", args);
884 		if (sendTags) {
885 			formattedWrite!"@%s "(output, tags);
886 		}
887 		formattedWrite!fmt(output, args);
888 		put(output, "\r\n");
889 		debug {
890 			tryCall!"onSend"(format!fmt(args));
891 		}
892 		static if (is(typeof(output.flush()))) {
893 			output.flush();
894 		}
895 	}
896 	private void write(const scope string text) @safe {
897 		write!"%s"(text);
898 	}
899 	private void writeList(string prefix, string separator, T)(T range) if (isInputRange!T && is(Unqual!(ElementType!T) == string)) {
900 		write!(prefix~"%-(%s"~separator~"%)")(range);
901 	}
902 	private bool isEnabled(const Capability cap) @safe {
903 		return capsEnabled.canFind(cap);
904 	}
905 	private void tryCall(string func, T...)(const T params) {
906 		if (__traits(getMember, this, func) !is null) {
907 			__traits(getMember, this, func)(params);
908 		}
909 	}
910 	auto me() const @safe {
911 		assert(nickinfo.nickname in internalAddressList);
912 		return internalAddressList[nickinfo.nickname];
913 	}
914 	//Message parsing functions follow
915 	private void rec(string cmd : IRCV3Commands.cap)(IRCMessage message, const MessageMetadata metadata) {
916 		auto tokens = message.args;
917 		immutable username = tokens.front; //Unused?
918 		tokens.popFront();
919 		immutable subCommand = tokens.front;
920 		tokens.popFront();
921 		immutable terminator = !tokens.skipOver("*");
922 		auto args = tokens
923 			.front
924 			.splitter(" ")
925 			.filter!(x => x != "")
926 			.map!(x => Capability(x));
927 		final switch (cast(CapabilityServerSubcommands) subCommand) {
928 			case CapabilityServerSubcommands.ls:
929 				recCapLS(args, metadata);
930 				break;
931 			case CapabilityServerSubcommands.list:
932 				recCapList(args, metadata);
933 				break;
934 			case CapabilityServerSubcommands.acknowledge:
935 				recCapAck(args, metadata);
936 				break;
937 			case CapabilityServerSubcommands.notAcknowledge:
938 				recCapNak(args, metadata);
939 				break;
940 			case CapabilityServerSubcommands.new_:
941 				recCapNew(args, metadata);
942 				break;
943 			case CapabilityServerSubcommands.delete_:
944 				recCapDel(args, metadata);
945 				break;
946 		}
947 	}
948 	private void recCapLS(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) {
949 		auto requestCaps = caps.filter!(among!supportedCaps).map!(x => Capability(x.name));
950 		state.capReqCount += requestCaps.save().walkLength;
951 		if (!requestCaps.empty) {
952 			write!"CAP REQ :%-(%s %)"(requestCaps);
953 		}
954 		foreach (ref cap; caps) {
955 			state.availableCapabilities[cap.name] = cap;
956 			tryCall!"onReceiveCapLS"(cap, metadata);
957 		}
958 	}
959 	private void recCapList(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) {
960 		foreach (ref cap; caps) {
961 			state.availableCapabilities[cap.name] = cap;
962 			tryCall!"onReceiveCapList"(cap, metadata);
963 		}
964 	}
965 	private void recCapAck(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) {
966 		import std.range : hasLength;
967 		capsEnabled ~= caps.save().array;
968 		foreach (ref cap; caps) {
969 			enableCapability(cap);
970 			tryCall!"onReceiveCapAck"(state.availableCapabilities[cap.name], metadata);
971 			static if (!hasLength!T) {
972 				capAcknowledgementCommon(1);
973 			}
974 		}
975 		static if (hasLength!T) {
976 			capAcknowledgementCommon(caps.length);
977 		}
978 	}
979 	private void recCapNak(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) {
980 		import std.range : hasLength;
981 		foreach (ref cap; caps) {
982 			tryCall!"onReceiveCapNak"(state.availableCapabilities[cap.name], metadata);
983 			static if (!hasLength!T) {
984 				capAcknowledgementCommon(1);
985 			}
986 		}
987 		static if (hasLength!T) {
988 			capAcknowledgementCommon(caps.length);
989 		}
990 	}
991 	private void capAcknowledgementCommon(const size_t count) @safe {
992 		state.capReqCount -= count;
993 		tryEndRegistration();
994 	}
995 	private void recCapNew(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) {
996 		auto requestCaps = caps.filter!(among!supportedCaps).map!(x => Capability(x.name));
997 		state.capReqCount += requestCaps.save().walkLength;
998 		if (!requestCaps.empty) {
999 			write!"CAP REQ :%-(%s %)"(requestCaps);
1000 		}
1001 		foreach (ref cap; caps) {
1002 			state.availableCapabilities[cap.name] = cap;
1003 			tryCall!"onReceiveCapNew"(cap, metadata);
1004 		}
1005 	}
1006 	private void recCapDel(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) {
1007 		import std.algorithm.mutation : remove;
1008 		import std.algorithm.searching : countUntil;
1009 		foreach (ref cap; caps) {
1010 			state.availableCapabilities.remove(cap.name);
1011 			auto findCap = countUntil(capsEnabled, cap);
1012 			if (findCap > -1) {
1013 				capsEnabled = capsEnabled.remove(findCap);
1014 			}
1015 			tryCall!"onReceiveCapDel"(cap, metadata);
1016 		}
1017 	}
1018 	private void enableCapability(const Capability cap) @safe {
1019 		import virc.keyvaluesplitter : splitKeyValues;
1020 		import std.conv : to;
1021 		const capDetails = state.availableCapabilities[cap.name];
1022 		switch (cap.name) {
1023 			case "sasl":
1024 				state.supportedSASLMechs = capDetails.value.splitter(",").array;
1025 				startSASL();
1026 				break;
1027 			case "draft/metadata-2":
1028 				state.maxMetadataSubscriptions = ulong.max;
1029 				state.maxMetadataSelfKeys = ulong.max;
1030 				state.maxMetadataValueBytes = size_t.max;
1031 				foreach (kv; capDetails.value.splitKeyValues) {
1032 					switch (kv.key) {
1033 						case "max-subs":
1034 							if (!kv.value.isNull) {
1035 								state.maxMetadataSubscriptions = kv.value.get.to!ulong;
1036 							}
1037 							break;
1038 						case "max-keys":
1039 							if (!kv.value.isNull) {
1040 								state.maxMetadataSelfKeys = kv.value.get.to!ulong;
1041 							}
1042 							break;
1043 						case "max-value-bytes":
1044 							if (!kv.value.isNull) {
1045 								state.maxMetadataValueBytes = kv.value.get.to!ulong;
1046 							}
1047 							break;
1048 						default: break;
1049 					}
1050 				}
1051 				break;
1052 			case "draft/multiline":
1053 				state.multilineMaxLines = size_t.max;
1054 				foreach (kv; capDetails.value.splitKeyValues) {
1055 					switch (kv.key) {
1056 						case "max-lines":
1057 							if (!kv.value.isNull) {
1058 								state.multilineMaxLines = kv.value.get.to!size_t;
1059 							}
1060 							break;
1061 						case "max-bytes":
1062 							if (!kv.value.isNull) {
1063 								state.multilineMaxBytes = kv.value.get.to!size_t;
1064 							}
1065 							break;
1066 						default: break;
1067 					}
1068 				}
1069 				break;
1070 			default: break;
1071 		}
1072 	}
1073 	private void startSASL() @safe {
1074 		if (state.supportedSASLMechs.empty && !saslMechs.empty) {
1075 			state.autoSelectSASLMech = true;
1076 			saslAuth(saslMechs.front);
1077 		} else if (!state.supportedSASLMechs.empty && !saslMechs.empty) {
1078 			foreach (id, mech; saslMechs) {
1079 				if (state.supportedSASLMechs.canFind(mech.name)) {
1080 					saslAuth(mech);
1081 				}
1082 			}
1083 		}
1084 	}
1085 	private void saslAuth(SASLMechanism mech) @safe {
1086 		state.selectedSASLMech = mech;
1087 		write!"AUTHENTICATE %s"(mech.name);
1088 		state.isAuthenticating = true;
1089 	}
1090 	private void rec(string cmd : RFC1459Commands.kick)(IRCMessage message, const MessageMetadata metadata) {
1091 		auto split = message.args;
1092 		auto source = message.sourceUser.get;
1093 		if (split.empty) {
1094 			return;
1095 		}
1096 		Channel channel = Channel(split.front);
1097 		split.popFront();
1098 		if (split.empty) {
1099 			return;
1100 		}
1101 		User victim = User(split.front);
1102 		split.popFront();
1103 		string msg;
1104 
1105 		if (!split.empty) {
1106 			msg = split.front;
1107 		}
1108 
1109 		tryCall!"onChannelListUpdate"(victim, victim, channel, ChannelListUpdateType.removed);
1110 		tryCall!"onKick"(source, channel, victim, msg, metadata);
1111 	}
1112 	private void rec(string cmd : RFC1459Commands.wallops)(IRCMessage message, const MessageMetadata metadata) {
1113 		tryCall!"onWallops"(message.sourceUser.get, message.args.front, metadata);
1114 	}
1115 	private void rec(string cmd : RFC1459Commands.mode)(IRCMessage message, const MessageMetadata metadata) {
1116 		auto split = message.args;
1117 		auto source = message.sourceUser.get;
1118 		auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes);
1119 		split.popFront();
1120 		ModeType[char] modeTypes;
1121 		if (target.isChannel) {
1122 			modeTypes = server.iSupport.channelModeTypes;
1123 		} else {
1124 			//there are no user mode types.
1125 		}
1126 		auto modes = parseModeString(split, modeTypes);
1127 		foreach (mode; modes) {
1128 			tryCall!"onMode"(source, target, mode, metadata);
1129 		}
1130 	}
1131 	private void rec(string cmd : RFC1459Commands.join)(IRCMessage message, const MessageMetadata metadata) {
1132 		auto split = message.args;
1133 		auto channel = Channel(split.front);
1134 		auto source = message.sourceUser.get;
1135 		split.popFront();
1136 		if (isEnabled(Capability("extended-join"))) {
1137 			if (split.front != "*") {
1138 				source.account = split.front;
1139 			}
1140 			split.popFront();
1141 			source.realName = split.front;
1142 			split.popFront();
1143 		}
1144 		if (server.iSupport.whoX) {
1145 			write!"WHO %s %%uihsnflar"(channel);
1146 		}
1147 		if (channel.name !in channels) {
1148 			channels[channel.name] = ChannelState(Channel(channel.name));
1149 		}
1150 		internalAddressList.update(source);
1151 		if (source.nickname in internalAddressList) {
1152 			channels[channel.name].users.update(internalAddressList[source.nickname]);
1153 		}
1154 		tryCall!"onChannelListUpdate"(source, source, channel, ChannelListUpdateType.added);
1155 		tryCall!"onJoin"(source, channel, metadata);
1156 	}
1157 	private void rec(string cmd : RFC1459Commands.part)(IRCMessage message, const MessageMetadata metadata) {
1158 		import std.algorithm.mutation : remove;
1159 		import std.algorithm.searching : countUntil;
1160 		auto split = message.args;
1161 		auto user = message.sourceUser.get;
1162 		auto channel = Channel(split.front);
1163 		split.popFront();
1164 		string msg;
1165 		if (!split.empty) {
1166 			msg = split.front;
1167 		}
1168 		if ((channel.name in channels) && (user.nickname in channels[channel.name].users)) {
1169 			channels[channel.name].users.invalidate(user.nickname);
1170 		}
1171 		if ((user == me) && (channel.name in channels)) {
1172 			channels.remove(channel.name);
1173 		}
1174 		tryCall!"onChannelListUpdate"(user, user, channel, ChannelListUpdateType.removed);
1175 		tryCall!"onPart"(user, channel, msg, metadata);
1176 	}
1177 	private void rec(string cmd : RFC1459Commands.notice)(IRCMessage message, const MessageMetadata metadata) {
1178 		auto split = message.args;
1179 		auto user = message.sourceUser.get;
1180 		auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes);
1181 		split.popFront();
1182 		auto msg = Message(split.front, MessageType.notice);
1183 		recMessageCommon(user, target, msg, metadata);
1184 	}
1185 	private void rec(string cmd : RFC1459Commands.privmsg)(IRCMessage message, const MessageMetadata metadata) {
1186 		auto split = message.args;
1187 		auto user = message.sourceUser.get;
1188 		auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes);
1189 		split.popFront();
1190 		if (split.empty) {
1191 			return;
1192 		}
1193 		auto msg = Message(split.front, MessageType.privmsg);
1194 		recMessageCommon(user, target, msg, metadata);
1195 	}
1196 	private void recMessageCommon(const User user, const Target target, Message msg, const MessageMetadata metadata) @safe {
1197 		if (user.nickname == nickinfo.nickname) {
1198 			msg.isEcho = true;
1199 		}
1200 		tryCall!"onMessage"(user, target, msg, metadata);
1201 	}
1202 	private void rec(string cmd : Numeric.RPL_ISUPPORT)(IRCMessage message, const MessageMetadata metadata) {
1203 		auto split = message.args;
1204 		switch (split.save().canFind("UHNAMES", "NAMESX")) {
1205 			case 1:
1206 				if (!isEnabled(Capability("userhost-in-names"))) {
1207 					write("PROTOCTL UHNAMES");
1208 				}
1209 				break;
1210 			case 2:
1211 				if (!isEnabled(Capability("multi-prefix"))) {
1212 					write("PROTOCTL NAMESX");
1213 				}
1214 				break;
1215 			default: break;
1216 		}
1217 		parseNumeric!(Numeric.RPL_ISUPPORT)(split, server.iSupport);
1218 	}
1219 	private void rec(string cmd : Numeric.RPL_WELCOME)(IRCMessage message, const MessageMetadata metadata) {
1220 		state.isRegistered = true;
1221 		if (!message.args.empty && (message.args.front != "*") && (User(message.args.front).nickname != nickinfo.nickname)) {
1222 			nickinfo.nickname = User(message.args.front).nickname;
1223 		}
1224 		auto meUser = User();
1225 		meUser.mask.nickname = nickinfo.nickname;
1226 		meUser.mask.ident = nickinfo.username;
1227 		meUser.mask.host = "127.0.0.1";
1228 		internalAddressList.update(meUser);
1229 		tryCall!"onConnect"();
1230 	}
1231 	private void rec(string cmd : Numeric.RPL_LOGGEDIN)(IRCMessage message, const MessageMetadata metadata) {
1232 		import virc.numerics.sasl : parseNumeric;
1233 		if (state.isAuthenticating || isAuthenticated) {
1234 			auto parsed = parseNumeric!(Numeric.RPL_LOGGEDIN)(message.args);
1235 			auto user = User(parsed.get.mask);
1236 			user.account = parsed.get.account;
1237 			internalAddressList.update(user);
1238 		}
1239 	}
1240 	private void rec(string cmd)(IRCMessage message, const MessageMetadata metadata) if (cmd.among(Numeric.ERR_NICKLOCKED, Numeric.ERR_SASLFAIL, Numeric.ERR_SASLTOOLONG, Numeric.ERR_SASLABORTED)) {
1241 		endAuthentication();
1242 	}
1243 	private void rec(string cmd : Numeric.RPL_MYINFO)(IRCMessage message, const MessageMetadata metadata) {
1244 		server.myInfo = parseNumeric!(Numeric.RPL_MYINFO)(message.args).get;
1245 	}
1246 	private void rec(string cmd : Numeric.RPL_LUSERCLIENT)(IRCMessage message, const MessageMetadata metadata) {
1247 		tryCall!"onLUserClient"(parseNumeric!(Numeric.RPL_LUSERCLIENT)(message.args), metadata);
1248 	}
1249 	private void rec(string cmd : Numeric.RPL_LUSEROP)(IRCMessage message, const MessageMetadata metadata) {
1250 		tryCall!"onLUserOp"(parseNumeric!(Numeric.RPL_LUSEROP)(message.args), metadata);
1251 	}
1252 	private void rec(string cmd : Numeric.RPL_LUSERCHANNELS)(IRCMessage message, const MessageMetadata metadata) {
1253 		tryCall!"onLUserChannels"(parseNumeric!(Numeric.RPL_LUSERCHANNELS)(message.args), metadata);
1254 	}
1255 	private void rec(string cmd : Numeric.RPL_LUSERME)(IRCMessage message, const MessageMetadata metadata) {
1256 		tryCall!"onLUserMe"(parseNumeric!(Numeric.RPL_LUSERME)(message.args), metadata);
1257 	}
1258 	private void rec(string cmd : Numeric.RPL_YOUREOPER)(IRCMessage message, const MessageMetadata metadata) {
1259 		tryCall!"onYoureOper"(metadata);
1260 	}
1261 	private void rec(string cmd : Numeric.ERR_NOMOTD)(IRCMessage message, const MessageMetadata metadata) {
1262 		tryCall!"onError"(IRCError(ErrorType.noMOTD), metadata);
1263 	}
1264 	private void rec(string cmd : Numeric.RPL_SASLSUCCESS)(IRCMessage message, const MessageMetadata metadata) {
1265 		if (state.selectedSASLMech) {
1266 			state.authenticationSucceeded = true;
1267 		}
1268 		endAuthentication();
1269 	}
1270 	private void rec(string cmd : Numeric.RPL_LIST)(IRCMessage message, const MessageMetadata metadata) {
1271 		auto channel = parseNumeric!(Numeric.RPL_LIST)(message.args, server.iSupport.channelModeTypes);
1272 		tryCall!"onList"(channel, metadata);
1273 	}
1274 	private void rec(string cmd : RFC1459Commands.ping)(IRCMessage message, const MessageMetadata) {
1275 		pong(message.args.front);
1276 	}
1277 	private void rec(string cmd : Numeric.RPL_ISON)(IRCMessage message, const MessageMetadata metadata) {
1278 		auto reply = parseNumeric!(Numeric.RPL_ISON)(message.args);
1279 		if (!reply.isNull) {
1280 			foreach (online; reply.get.online) {
1281 				internalAddressList.update(User(online));
1282 				tryCall!"onIsOn"(internalAddressList[online], metadata);
1283 			}
1284 		} else {
1285 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1286 		}
1287 	}
1288 	private void rec(string cmd : Numeric.RPL_MONONLINE)(IRCMessage message, const MessageMetadata metadata) {
1289 		auto users = parseNumeric!(Numeric.RPL_MONONLINE)(message.args);
1290 		foreach (user; users) {
1291 			tryCall!"onUserOnline"(user, SysTime.init, metadata);
1292 		}
1293 	}
1294 	private void rec(string cmd : Numeric.RPL_MONOFFLINE)(IRCMessage message, const MessageMetadata metadata) {
1295 		auto users = parseNumeric!(Numeric.RPL_MONOFFLINE)(message.args);
1296 		foreach (user; users) {
1297 			tryCall!"onUserOffline"(user, metadata);
1298 		}
1299 	}
1300 	private void rec(string cmd : Numeric.RPL_MONLIST)(IRCMessage message, const MessageMetadata metadata) {
1301 		auto users = parseNumeric!(Numeric.RPL_MONLIST)(message.args);
1302 		foreach (user; users) {
1303 			tryCall!"onMonitorList"(user, metadata);
1304 		}
1305 	}
1306 	private void rec(string cmd : Numeric.ERR_MONLISTFULL)(IRCMessage message, const MessageMetadata metadata) {
1307 		auto err = parseNumeric!(Numeric.ERR_MONLISTFULL)(message.args);
1308 		tryCall!"onError"(IRCError(ErrorType.monListFull), metadata);
1309 	}
1310 	private void rec(string cmd : Numeric.RPL_VERSION)(IRCMessage message, const MessageMetadata metadata) {
1311 		auto versionReply = parseNumeric!(Numeric.RPL_VERSION)(message.args);
1312 		tryCall!"onVersionReply"(versionReply.get, metadata);
1313 	}
1314 	private void rec(string cmd : Numeric.RPL_LOGON)(IRCMessage message, const MessageMetadata metadata) {
1315 		auto reply = parseNumeric!(Numeric.RPL_LOGON)(message.args);
1316 		tryCall!"onUserOnline"(reply.user, reply.timeOccurred, metadata);
1317 	}
1318 	private void rec(string cmd : IRCV3Commands.chghost)(IRCMessage message, const MessageMetadata metadata) {
1319 		User target;
1320 		auto split = message.args;
1321 		auto user = message.sourceUser.get;
1322 		target.mask.nickname = user.nickname;
1323 		target.mask.ident = split.front;
1324 		split.popFront();
1325 		target.mask.host = split.front;
1326 		internalAddressList.update(target);
1327 		tryCall!"onChgHost"(user, target, metadata);
1328 	}
1329 	private void rec(string cmd : Numeric.RPL_TOPICWHOTIME)(IRCMessage message, const MessageMetadata metadata) {
1330 		auto reply = parseNumeric!(Numeric.RPL_TOPICWHOTIME)(message.args);
1331 		if (!reply.isNull) {
1332 			tryCall!"onTopicWhoTimeReply"(reply.get, metadata);
1333 		}
1334 	}
1335 	private void rec(string cmd : Numeric.RPL_AWAY)(IRCMessage message, const MessageMetadata metadata) {
1336 		auto reply = parseNumeric!(Numeric.RPL_AWAY)(message.args);
1337 		if (!reply.isNull) {
1338 			tryCall!"onOtherUserAwayReply"(reply.get.user, reply.get.message, metadata);
1339 		}
1340 	}
1341 	private void rec(string cmd : Numeric.RPL_UNAWAY)(IRCMessage message, const MessageMetadata metadata) {
1342 		tryCall!"onUnAwayReply"(message.sourceUser.get, metadata);
1343 		state._isAway = false;
1344 	}
1345 	private void rec(string cmd : Numeric.RPL_NOWAWAY)(IRCMessage message, const MessageMetadata metadata) {
1346 		tryCall!"onAwayReply"(message.sourceUser.get, metadata);
1347 		state._isAway = true;
1348 	}
1349 	private void rec(string cmd : Numeric.RPL_TOPIC)(IRCMessage message, const MessageMetadata metadata) {
1350 		auto reply = parseNumeric!(Numeric.RPL_TOPIC)(message.args);
1351 		if (!reply.isNull) {
1352 			tryCall!"onTopicReply"(reply.get, metadata);
1353 		}
1354 	}
1355 	private void rec(string cmd : RFC1459Commands.topic)(IRCMessage message, const MessageMetadata metadata) {
1356 		auto split = message.args;
1357 		auto user = message.sourceUser.get;
1358 		if (split.empty) {
1359 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1360 			return;
1361 		}
1362 		auto target = Channel(split.front);
1363 		split.popFront();
1364 		if (split.empty) {
1365 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1366 			return;
1367 		}
1368 		auto msg = split.front;
1369 		tryCall!"onTopicChange"(user, target, msg, metadata);
1370 	}
1371 	private void rec(string cmd : RFC1459Commands.nick)(IRCMessage message, const MessageMetadata metadata) {
1372 		auto split = message.args;
1373 		if (!split.empty) {
1374 			auto old = message.sourceUser.get;
1375 			auto newNick = split.front;
1376 			internalAddressList.renameTo(old, newNick);
1377 			foreach (ref channel; channels) {
1378 				if (old.nickname in channel.users) {
1379 					channel.users.renameTo(old, newNick);
1380 				}
1381 			}
1382 			auto new_ = internalAddressList[newNick];
1383 			if (old.nickname == nickinfo.nickname) {
1384 				nickinfo.nickname = new_.nickname;
1385 			}
1386 			tryCall!"onNick"(old, new_, metadata);
1387 		}
1388 	}
1389 	private void rec(string cmd : RFC1459Commands.invite)(IRCMessage message, const MessageMetadata metadata) {
1390 		auto split = message.args;
1391 		auto inviter = message.sourceUser.get;
1392 		if (!split.empty) {
1393 			User invited;
1394 			if (split.front in internalAddressList) {
1395 				invited = internalAddressList[split.front];
1396 			} else {
1397 				invited = User(split.front);
1398 			}
1399 			split.popFront();
1400 			if (!split.empty) {
1401 				auto channel = Channel(split.front);
1402 				tryCall!"onInvite"(inviter, invited, channel, metadata);
1403 			}
1404 		}
1405 	}
1406 	private void rec(string cmd : RFC1459Commands.quit)(IRCMessage message, const MessageMetadata metadata) {
1407 		auto split = message.args;
1408 		auto user = message.sourceUser.get;
1409 		string msg;
1410 		if (!split.empty) {
1411 			msg = split.front;
1412 		}
1413 		foreach (ref channel; channels) {
1414 			if (user.nickname in channel.users) {
1415 				tryCall!"onChannelListUpdate"(user, user, channel.channel, ChannelListUpdateType.added);
1416 			}
1417 		}
1418 		if (isMe(user)) {
1419 			state.invalid = true;
1420 		}
1421 		tryCall!"onQuit"(user, msg, metadata);
1422 		internalAddressList.invalidate(user.nickname);
1423 	}
1424 	private void recUnknownCommand(const string cmd, const MessageMetadata metadata) @safe {
1425 		if (cmd.filter!(x => !x.isDigit).empty) {
1426 			recUnknownNumeric(cmd, metadata);
1427 		} else {
1428 			tryCall!"onError"(IRCError(ErrorType.unrecognized, cmd), metadata);
1429 			debug(verboseirc) import std.experimental.logger : trace;
1430 			debug(verboseirc) trace(" Unknown command: ", metadata.original);
1431 		}
1432 	}
1433 	private void rec(string cmd : Numeric.RPL_NAMREPLY)(IRCMessage message, const MessageMetadata metadata) {
1434 		auto reply = parseNumeric!(Numeric.RPL_NAMREPLY)(message.args);
1435 		foreach (user; reply.get.users(server.iSupport.prefixes)) {
1436 			internalAddressList.update(User(user.name));
1437 		}
1438 		if (reply.get.channel in channels) {
1439 			foreach (user; reply.get.users(server.iSupport.prefixes)) {
1440 				const newUser = User(user.name);
1441 				channels[reply.get.channel].users.update(newUser);
1442 				if (newUser != me) {
1443 					tryCall!"onChannelListUpdate"(newUser, newUser, Channel(reply.get.channel), ChannelListUpdateType.added);
1444 				}
1445 			}
1446 		}
1447 		if (!reply.isNull) {
1448 			tryCall!"onNamesReply"(reply.get, metadata);
1449 		}
1450 	}
1451 	private void rec(string cmd : Numeric.RPL_WHOSPCRPL)(IRCMessage message, const MessageMetadata metadata) {
1452 		auto reply = parseNumeric!(Numeric.RPL_WHOSPCRPL)(message.args, "uihsnflar");
1453 		if (!reply.isNull) {
1454 			User user;
1455 			user.account = reply.get.account;
1456 			user.realName = reply.get.realname;
1457 			user.mask.ident = reply.get.ident;
1458 			user.mask.host = reply.get.host;
1459 			user.mask.nickname = reply.get.nick.get;
1460 			auto oldUser = internalAddressList[user.mask.nickname];
1461 			internalAddressList.update(user);
1462 			foreach (ref channel; channels) {
1463 				if (user.nickname in channel.users) {
1464 					tryCall!"onChannelListUpdate"(user, oldUser, channel.channel, ChannelListUpdateType.updated);
1465 				}
1466 			}
1467 			tryCall!"onWHOXReply"(reply.get, metadata);
1468 		}
1469 	}
1470 	private void rec(string cmd : Numeric.RPL_REHASHING)(IRCMessage message, const MessageMetadata metadata) {
1471 		auto reply = parseNumeric!(Numeric.RPL_REHASHING)(message.args);
1472 		if (!reply.isNull) {
1473 			tryCall!"onServerRehashing"(reply.get, metadata);
1474 		}
1475 	}
1476 	private void rec(string cmd : Numeric.ERR_NOPRIVS)(IRCMessage message, const MessageMetadata metadata) {
1477 		auto reply = parseNumeric!(Numeric.ERR_NOPRIVS)(message.args);
1478 		if (!reply.isNull) {
1479 			tryCall!"onError"(IRCError(ErrorType.noPrivs, reply.get.priv), metadata);
1480 		} else {
1481 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1482 		}
1483 	}
1484 	private void rec(string cmd : Numeric.ERR_NOPRIVILEGES)(IRCMessage message, const MessageMetadata metadata) {
1485 		tryCall!"onError"(IRCError(ErrorType.noPrivileges), metadata);
1486 	}
1487 	private void rec(string cmd : Numeric.ERR_NOSUCHSERVER)(IRCMessage message, const MessageMetadata metadata) {
1488 		auto reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(message.args);
1489 		if (!reply.isNull) {
1490 			tryCall!"onError"(IRCError(ErrorType.noSuchServer, reply.get.serverMask), metadata);
1491 		} else {
1492 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1493 		}
1494 	}
1495 	private void rec(string cmd : Numeric.RPL_ENDOFWHOIS)(IRCMessage message, const MessageMetadata metadata) {
1496 		auto reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(message.args);
1497 		if (!reply.isNull) {
1498 			if (reply.get.user.nickname in state.whoisCache) {
1499 				tryCall!"onWhois"(reply.get.user, state.whoisCache[reply.get.user.nickname]);
1500 				state.whoisCache.remove(reply.get.user.nickname);
1501 			} else {
1502 				tryCall!"onError"(IRCError(ErrorType.unexpected, "empty WHOIS data returned"), metadata);
1503 			}
1504 		} else {
1505 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1506 		}
1507 	}
1508 	private void rec(string cmd : Numeric.RPL_WHOISUSER)(IRCMessage message, const MessageMetadata metadata) {
1509 		auto reply = parseNumeric!(Numeric.RPL_WHOISUSER)(message.args);
1510 		if (!reply.isNull) {
1511 			if (reply.get.user.nickname !in state.whoisCache) {
1512 				state.whoisCache[reply.get.user.nickname] = WhoisResponse();
1513 			}
1514 			state.whoisCache[reply.get.user.nickname].username = reply.get.username;
1515 			state.whoisCache[reply.get.user.nickname].hostname = reply.get.hostname;
1516 			state.whoisCache[reply.get.user.nickname].realname = reply.get.realname;
1517 		} else {
1518 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1519 		}
1520 	}
1521 	private void rec(string cmd : Numeric.RPL_WHOISSECURE)(IRCMessage message, const MessageMetadata metadata) {
1522 		auto reply = parseNumeric!(Numeric.RPL_WHOISSECURE)(message.args);
1523 		if (!reply.isNull) {
1524 			if (reply.get.user.nickname !in state.whoisCache) {
1525 				state.whoisCache[reply.get.user.nickname] = WhoisResponse();
1526 			}
1527 			state.whoisCache[reply.get.user.nickname].isSecure = true;
1528 		} else {
1529 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1530 		}
1531 	}
1532 	private void rec(string cmd : Numeric.RPL_WHOISOPERATOR)(IRCMessage message, const MessageMetadata metadata) {
1533 		auto reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(message.args);
1534 		if (!reply.isNull) {
1535 			if (reply.get.user.nickname !in state.whoisCache) {
1536 				state.whoisCache[reply.get.user.nickname] = WhoisResponse();
1537 			}
1538 			state.whoisCache[reply.get.user.nickname].isOper = true;
1539 		} else {
1540 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1541 		}
1542 	}
1543 	private void rec(string cmd : Numeric.RPL_WHOISREGNICK)(IRCMessage message, const MessageMetadata metadata) {
1544 		auto reply = parseNumeric!(Numeric.RPL_WHOISREGNICK)(message.args);
1545 		if (!reply.isNull) {
1546 			if (reply.get.user.nickname !in state.whoisCache) {
1547 				state.whoisCache[reply.get.user.nickname] = WhoisResponse();
1548 			}
1549 			state.whoisCache[reply.get.user.nickname].isRegistered = true;
1550 		} else {
1551 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1552 		}
1553 	}
1554 	private void rec(string cmd : Numeric.RPL_WHOISIDLE)(IRCMessage message, const MessageMetadata metadata) {
1555 		auto reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(message.args);
1556 		if (!reply.isNull) {
1557 			if (reply.user.get.nickname !in state.whoisCache) {
1558 				state.whoisCache[reply.user.get.nickname] = WhoisResponse();
1559 			}
1560 			state.whoisCache[reply.user.get.nickname].idleTime = reply.idleTime;
1561 			state.whoisCache[reply.user.get.nickname].connectedTime = reply.connectedTime;
1562 		} else {
1563 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1564 		}
1565 	}
1566 	private void rec(string cmd : Numeric.RPL_WHOISSERVER)(IRCMessage message, const MessageMetadata metadata) {
1567 		auto reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(message.args);
1568 		if (!reply.isNull) {
1569 			if (reply.get.user.nickname !in state.whoisCache) {
1570 				state.whoisCache[reply.get.user.nickname] = WhoisResponse();
1571 			}
1572 			state.whoisCache[reply.get.user.nickname].connectedTo = reply.get.server;
1573 		} else {
1574 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1575 		}
1576 	}
1577 	private void rec(string cmd : Numeric.RPL_WHOISACCOUNT)(IRCMessage message, const MessageMetadata metadata) {
1578 		auto reply = parseNumeric!(Numeric.RPL_WHOISACCOUNT)(message.args);
1579 		if (!reply.isNull) {
1580 			if (reply.get.user.nickname !in state.whoisCache) {
1581 				state.whoisCache[reply.get.user.nickname] = WhoisResponse();
1582 			}
1583 			state.whoisCache[reply.get.user.nickname].account = reply.get.account;
1584 		} else {
1585 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1586 		}
1587 	}
1588 	private void rec(string cmd : IRCV3Commands.metadata)(IRCMessage message, const MessageMetadata metadata) {
1589 		auto split = message.args;
1590 		auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes);
1591 		split.popFront();
1592 		auto key = split.front;
1593 		split.popFront();
1594 		auto visibility = split.front;
1595 		split.popFront();
1596 		if (split.empty) {
1597 			deleteMetadataCommon(target, key);
1598 		} else {
1599 			setMetadataCommon(target, visibility, key, split.front);
1600 		}
1601 	}
1602 	private void rec(string cmd : Numeric.RPL_WHOISKEYVALUE)(IRCMessage message, const MessageMetadata metadata) {
1603 		auto split = message.args;
1604 		string prefixes;
1605 		foreach (k,v; server.iSupport.prefixes) {
1606 			prefixes ~= v;
1607 		}
1608 		auto reply = parseNumeric!(Numeric.RPL_WHOISKEYVALUE)(split, prefixes, server.iSupport.channelTypes);
1609 		if (!reply.isNull) {
1610 			setMetadataCommon(reply.get.target, reply.get.visibility, reply.get.key, reply.get.value);
1611 		} else {
1612 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1613 		}
1614 	}
1615 	private void rec(string cmd : Numeric.RPL_KEYVALUE)(IRCMessage message, const MessageMetadata metadata) {
1616 		auto split = message.args;
1617 		string prefixes;
1618 		foreach (k,v; server.iSupport.prefixes) {
1619 			prefixes ~= v;
1620 		}
1621 		auto reply = parseNumeric!(Numeric.RPL_KEYVALUE)(split, prefixes, server.iSupport.channelTypes);
1622 		if (!reply.isNull) {
1623 			if (reply.get.value.isNull) {
1624 				deleteMetadataCommon(reply.get.target, reply.get.key);
1625 			} else {
1626 				setMetadataCommon(reply.get.target, reply.get.visibility, reply.get.key, reply.get.value.get);
1627 			}
1628 		} else {
1629 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1630 		}
1631 	}
1632 	private void setMetadataCommon(Target target, string visibility, string key, string value) @safe pure {
1633 		if (target.isUser && target.user == User("*")) {
1634 			userMetadata[me][key] = MetadataValue(visibility, value);
1635 		} else if (target.isChannel) {
1636 			channelMetadata[target.channel.get][key] = MetadataValue(visibility, value);
1637 		} else if (target.isUser) {
1638 			userMetadata[target.user.get][key] = MetadataValue(visibility, value);
1639 		}
1640 	}
1641 	private void deleteMetadataCommon(Target target, string key) @safe pure {
1642 		if (target.isUser && target.user == User("*")) {
1643 			userMetadata[me].remove(key);
1644 		} else if (target.isChannel) {
1645 			channelMetadata[target.channel.get].remove(key);
1646 		} else if (target.isUser) {
1647 			userMetadata[target.user.get].remove(key);
1648 		}
1649 	}
1650 	private void rec(string cmd : Numeric.RPL_METADATASUBOK)(IRCMessage message, const MessageMetadata metadata) {
1651 		auto parsed = parseNumeric!(Numeric.RPL_METADATASUBOK)(message.args);
1652 		if (!parsed.isNull) {
1653 			foreach (sub; parsed.get) {
1654 				if (!state.metadataSubscribedKeys.canFind(sub)) {
1655 					state.metadataSubscribedKeys ~= sub;
1656 				}
1657 			}
1658 		} else {
1659 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1660 		}
1661 	}
1662 	private void rec(string cmd : Numeric.RPL_METADATAUNSUBOK)(IRCMessage message, const MessageMetadata metadata) {
1663 		auto parsed = parseNumeric!(Numeric.RPL_METADATAUNSUBOK)(message.args);
1664 		if (!parsed.isNull) {
1665 			state.metadataSubscribedKeys = state.metadataSubscribedKeys.filter!(x => !parsed.get.canFind(x)).array;
1666 		} else {
1667 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1668 		}
1669 	}
1670 	private void rec(string cmd : Numeric.RPL_METADATASUBS)(IRCMessage message, const MessageMetadata metadata) {
1671 		auto reply = parseNumeric!(Numeric.RPL_METADATASUBS)(message.args);
1672 		if (!reply.isNull) {
1673 			foreach (sub; reply.get) {
1674 				tryCall!"onMetadataSubList"(sub, metadata);
1675 			}
1676 		} else {
1677 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1678 		}
1679 	}
1680 	private void rec(string cmd : Numeric.RPL_KEYNOTSET)(IRCMessage message, const MessageMetadata metadata) {
1681 		auto split = message.args;
1682 		string prefixes;
1683 		foreach (k,v; server.iSupport.prefixes) {
1684 			prefixes ~= v;
1685 		}
1686 		auto err = parseNumeric!(Numeric.RPL_KEYNOTSET)(split, prefixes, server.iSupport.channelTypes);
1687 		tryCall!"onError"(IRCError(ErrorType.keyNotSet, err.get.humanReadable), metadata);
1688 	}
1689 	private void rec(string cmd : Numeric.ERR_METADATASYNCLATER)(IRCMessage message, const MessageMetadata metadata) {
1690 		auto split = message.args;
1691 		string prefixes;
1692 		foreach (k,v; server.iSupport.prefixes) {
1693 			prefixes ~= v;
1694 		}
1695 		auto err = parseNumeric!(Numeric.ERR_METADATASYNCLATER)(split, prefixes, server.iSupport.channelTypes);
1696 		tryCall!"onError"(IRCError(ErrorType.waitAndRetry), metadata);
1697 	}
1698 	private void rec(string cmd : Numeric.RPL_WHOISCHANNELS)(IRCMessage message, const MessageMetadata metadata) {
1699 		string prefixes;
1700 		foreach (k,v; server.iSupport.prefixes) {
1701 			prefixes ~= v;
1702 		}
1703 		auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(message.args, prefixes, server.iSupport.channelTypes);
1704 		if (!reply.isNull) {
1705 			if (reply.get.user.nickname !in state.whoisCache) {
1706 				state.whoisCache[reply.get.user.nickname] = WhoisResponse();
1707 			}
1708 			foreach (channel; reply.get.channels) {
1709 				auto whoisChannel = WhoisChannel();
1710 				whoisChannel.name = channel.channel;
1711 				if (!channel.prefix.isNull) {
1712 					whoisChannel.prefix = channel.prefix.get;
1713 				}
1714 				state.whoisCache[reply.get.user.nickname].channels[channel.channel.name] = whoisChannel;
1715 			}
1716 		} else {
1717 			tryCall!"onError"(IRCError(ErrorType.malformed), metadata);
1718 		}
1719 	}
1720 	private void recUnknownNumeric(const string cmd, const MessageMetadata metadata) @safe {
1721 		tryCall!"onError"(IRCError(ErrorType.unrecognized, cmd), metadata);
1722 		debug(verboseirc) import std.experimental.logger : trace;
1723 		debug(verboseirc) trace("Unhandled numeric: ", cast(Numeric)cmd, " ", metadata.original);
1724 	}
1725 	private void rec(string cmd : IRCV3Commands.account)(IRCMessage message, const MessageMetadata metadata) {
1726 		auto split = message.args;
1727 		if (!split.empty) {
1728 			auto user = message.sourceUser.get;
1729 			auto newAccount = split.front;
1730 			internalAddressList.update(user);
1731 			auto newUser = internalAddressList[user.nickname];
1732 			if (newAccount == "*") {
1733 				newUser.account.nullify();
1734 			} else {
1735 				newUser.account = newAccount;
1736 			}
1737 			internalAddressList.updateExact(newUser);
1738 		}
1739 	}
1740 	private void rec(string cmd : IRCV3Commands.authenticate)(IRCMessage message, const MessageMetadata metadata) {
1741 		import std.base64 : Base64;
1742 		auto split = message.args;
1743 		if (split.front != "+") {
1744 			state.receivedSASLAuthenticationText ~= Base64.decode(split.front);
1745 		}
1746 		if ((state.selectedSASLMech) && (split.front == "+" || (split.front.length < 400))) {
1747 			state.selectedSASLMech.put(state.receivedSASLAuthenticationText);
1748 			if (state.selectedSASLMech.empty) {
1749 				sendAuthenticatePayload("");
1750 			} else {
1751 				sendAuthenticatePayload(state.selectedSASLMech.front);
1752 				state.selectedSASLMech.popFront();
1753 			}
1754 			state.receivedSASLAuthenticationText = [];
1755 		}
1756 	}
1757 	private void rec(string cmd : IRCV3Commands.tagmsg)(IRCMessage message, const MessageMetadata metadata) {
1758 		auto split = message.args;
1759 		auto user = message.sourceUser.get;
1760 		auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes);
1761 		auto msg = Message("", MessageType.tagmsg);
1762 		recMessageCommon(user, target, msg, metadata);
1763 	}
1764 	private void rec(string cmd : IRCV3Commands.note)(IRCMessage message, const MessageMetadata metadata) {
1765 	}
1766 	private void rec(string cmd : IRCV3Commands.warn)(IRCMessage message, const MessageMetadata metadata) {
1767 	}
1768 	private void rec(string cmd : IRCV3Commands.fail)(IRCMessage message, const MessageMetadata metadata) {
1769 		tryCall!"onError"(IRCError(ErrorType.standardFail, cmd), metadata);
1770 	}
1771 	bool isMe(const User user) const pure @safe nothrow {
1772 		return user == me;
1773 	}
1774 	bool isValid() const pure @safe nothrow {
1775 		return !state.invalid;
1776 	}
1777 	bool isRegistered() const pure @safe nothrow {
1778 		return state.isRegistered;
1779 	}
1780 }
1781 version(unittest) {
1782 	import std.algorithm : equal, sort, until;
1783 	import std.array : appender, array;
1784 	import std.range: drop, empty, tail;
1785 	import std.stdio : writeln;
1786 	import std.string : lineSplitter, representation;
1787 	import std.typecons : Tuple, tuple;
1788 	import virc.ircv3 : Capability;
1789 	static immutable testClientInfo = NickInfo("someone", "ident", "real name!");
1790 	static immutable testUser = User(testClientInfo.nickname, testClientInfo.username, "example.org");
1791 	mixin template Test() {
1792 		bool lineReceived;
1793 		void onRaw(const MessageMetadata) @safe pure {
1794 			lineReceived = true;
1795 		}
1796 	}
1797 	void setupFakeConnection(T)(ref T client) {
1798 		if (!client.onError) {
1799 			client.onError = (const IRCError error, const MessageMetadata metadata) {
1800 				writeln(metadata.time, " - ", error.type, " - ", metadata.original);
1801 			};
1802 		}
1803 		client.put(":localhost 001 someone :Welcome to the TestNet IRC Network "~testUser.text);
1804 		client.put(":localhost 002 someone :Your host is localhost, running version IRCd-2.0");
1805 		client.put(":localhost 003 someone :This server was created 20:21:33 Oct  21 2016");
1806 		client.put(":localhost 004 someone localhost IRCd-2.0 BGHIRSWcdgikorswx ABCDFGIJKLMNOPQRSTYabcefghijklmnopqrstuvz FIJLYabefghjkloqv");
1807 		client.put(":localhost 005 someone AWAYLEN=200 CALLERID=g CASEMAPPING=rfc1459 CHANMODES=IYbeg,k,FJLfjl,ABCDGKMNOPQRSTcimnprstuz CHANNELLEN=31 CHANTYPES=# CHARSET=ascii ELIST=MU ESILENCE EXCEPTS=e EXTBAN=,ABCNOQRSTUcjmprsz FNC INVEX=I :are supported by this server");
1808 		client.put(":localhost 005 someone KICKLEN=255 MAP MAXBANS=60 MAXCHANNELS=25 MAXPARA=32 MAXTARGETS=20 MODES=10 NAMESX NETWORK=TestNet NICKLEN=31 OPERLOG OVERRIDE PREFIX=(qaohv)~&@%+ :are supported by this server");
1809 		client.put(":localhost 005 someone REMOVE SECURELIST SILENCE=32 SSL=[::]:6697 STARTTLS STATUSMSG=~&@%+ TOPICLEN=307 UHNAMES USERIP VBANLIST WALLCHOPS WALLVOICES WATCH=1000 WHOX :are supported by this server");
1810 		assert(client.isRegistered);
1811 		assert(client.server.iSupport.userhostsInNames == true);
1812 	}
1813 	void initializeCaps(T)(ref T client) {
1814 		initializeWithCaps(client, [Capability("multi-prefix"), Capability("server-time"), Capability("sasl", "EXTERNAL")]);
1815 	}
1816 	void initializeWithCaps(T)(ref T client, Capability[] caps) {
1817 		foreach (i, cap; caps) {
1818 			client.put(":localhost CAP * LS " ~ ((i+1 == caps.length) ? "" : "* ")~ ":" ~ cap.toString);
1819 			client.put(":localhost CAP * ACK :" ~ cap.name);
1820 		}
1821 		setupFakeConnection(client);
1822 	}
1823 	class Wrapper : Output {
1824 		import std.array : Appender;
1825 		Appender!string buffer;
1826 		override void put(char c) @safe {
1827 			buffer.put(c);
1828 		}
1829 		this(Appender!string buf) @safe {
1830 			buffer = buf;
1831 		}
1832 	}
1833 	auto data(Output o) @safe {
1834 		return (cast(Wrapper)o).buffer.data;
1835 	}
1836 	auto spawnNoBufferClient(string password = string.init) {
1837 		auto buffer = appender!(string);
1838 		return ircClient(new Wrapper(buffer), testClientInfo, [], password);
1839 	}
1840 }
1841 //Test the basics
1842 @safe unittest {
1843 	auto client = spawnNoBufferClient();
1844 	bool lineReceived;
1845 	client.onRaw = (_) {
1846 		lineReceived = true;
1847 	};
1848 	client.put("");
1849 	assert(lineReceived == false);
1850 	client.put("\r\n");
1851 	assert(lineReceived == false);
1852 	client.put("hello");
1853 	assert(lineReceived == true);
1854 	assert(!client.isRegistered);
1855 	client.put(":localhost 001 someone :words");
1856 	assert(client.isRegistered);
1857 	client.put(":localhost 001 someone :words");
1858 	assert(client.isRegistered);
1859 }
1860 //Auto-decoding test
1861 @safe unittest {
1862 	auto client = spawnNoBufferClient();
1863 	bool lineReceived;
1864 	client.onRaw = (_) {
1865 		lineReceived = true;
1866 	};
1867 	client.put("\r\n".representation);
1868 	assert(lineReceived == false);
1869 }
1870 @safe unittest {
1871 	import virc.ircv3 : Capability;
1872 	{ //password test
1873 		auto client = spawnNoBufferClient("Example");
1874 
1875 		assert(client.output.data.lineSplitter.until!(x => x.startsWith("USER")).canFind("PASS :Example"));
1876 	}
1877 	//Request capabilities (IRC v3.2)
1878 	{
1879 		auto client = spawnNoBufferClient();
1880 		client.put(":localhost CAP * LS :multi-prefix sasl");
1881 		client.put(":localhost CAP * ACK :multi-prefix sasl");
1882 
1883 		auto lineByLine = client.output.data.lineSplitter;
1884 
1885 		assert(lineByLine.front == "CAP LS 302");
1886 		lineByLine.popFront();
1887 		lineByLine.popFront();
1888 		lineByLine.popFront();
1889 		//sasl not yet supported
1890 		assert(lineByLine.front == "CAP REQ :multi-prefix sasl");
1891 		lineByLine.popFront();
1892 		assert(!lineByLine.empty);
1893 		assert(lineByLine.front == "CAP END");
1894 	}
1895 	//Request capabilities NAK (IRC v3.2)
1896 	{
1897 		auto client = spawnNoBufferClient();
1898 		Capability[] capabilities;
1899 		client.onReceiveCapNak = (const Capability cap, const MessageMetadata) {
1900 			capabilities ~= cap;
1901 		};
1902 		client.put(":localhost CAP * LS :multi-prefix");
1903 		client.put(":localhost CAP * NAK :multi-prefix");
1904 
1905 
1906 		auto lineByLine = client.output.data.lineSplitter;
1907 
1908 		assert(lineByLine.front == "CAP LS 302");
1909 		lineByLine.popFront();
1910 		lineByLine.popFront();
1911 		lineByLine.popFront();
1912 		//sasl not yet supported
1913 		assert(lineByLine.front == "CAP REQ :multi-prefix");
1914 		lineByLine.popFront();
1915 		assert(!lineByLine.empty);
1916 		assert(lineByLine.front == "CAP END");
1917 
1918 		assert(!client.capsEnabled.canFind("multi-prefix"));
1919 		assert(capabilities.length == 1);
1920 		assert(capabilities[0] == Capability("multi-prefix"));
1921 	}
1922 	//Request capabilities, multiline (IRC v3.2)
1923 	{
1924 		auto client = spawnNoBufferClient();
1925 		auto lineByLine = client.output.data.lineSplitter();
1926 
1927 		Capability[] capabilities;
1928 		client.onReceiveCapLS = (const Capability cap, const MessageMetadata) {
1929 			capabilities ~= cap;
1930 		};
1931 
1932 		assert(lineByLine.front == "CAP LS 302");
1933 
1934 		put(client, ":localhost CAP * LS * :multi-prefix extended-join account-notify batch invite-notify tls");
1935 		put(client, ":localhost CAP * LS * :cap-notify server-time example.org/dummy-cap=dummyvalue example.org/second-dummy-cap");
1936 		put(client, ":localhost CAP * LS :userhost-in-names sasl=EXTERNAL,DH-AES,DH-BLOWFISH,ECDSA-NIST256P-CHALLENGE,PLAIN");
1937 		auto lines = client.output.data.lineSplitter().array;
1938 		assert(lines[5] == "CAP REQ :userhost-in-names sasl");
1939 		assert(capabilities.length == 12);
1940 		setupFakeConnection(client);
1941 	}
1942 	//CAP LIST multiline (IRC v3.2)
1943 	{
1944 		auto client = spawnNoBufferClient();
1945 		Capability[] capabilities;
1946 		client.onReceiveCapList = (const Capability cap, const MessageMetadata) {
1947 			capabilities ~= cap;
1948 		};
1949 		setupFakeConnection(client);
1950 		client.capList();
1951 		client.put(":localhost CAP modernclient LIST * :example.org/example-cap example.org/second-example-cap account-notify");
1952 		client.put(":localhost CAP modernclient LIST :invite-notify batch example.org/third-example-cap");
1953 		assert(
1954 			capabilities.array.sort().equal(
1955 				[
1956 					Capability("account-notify"),
1957 					Capability("batch"),
1958 					Capability("example.org/example-cap"),
1959 					Capability("example.org/second-example-cap"),
1960 					Capability("example.org/third-example-cap"),
1961 					Capability("invite-notify")
1962 				]
1963 		));
1964 	}
1965 	//CAP NEW, DEL (IRCv3.2 - cap-notify)
1966 	{
1967 		auto client = spawnNoBufferClient();
1968 		Capability[] capabilitiesNew;
1969 		Capability[] capabilitiesDeleted;
1970 		client.onReceiveCapNew = (const Capability cap, const MessageMetadata) {
1971 			capabilitiesNew ~= cap;
1972 		};
1973 		client.onReceiveCapDel = (const Capability cap, const MessageMetadata) {
1974 			capabilitiesDeleted ~= cap;
1975 		};
1976 		initializeWithCaps(client, [Capability("cap-notify"), Capability("userhost-in-names"), Capability("multi-prefix"), Capability("away-notify")]);
1977 
1978 		assert(client.capsEnabled.length == 4);
1979 
1980 		client.put(":irc.example.com CAP modernclient NEW :batch");
1981 		assert(capabilitiesNew == [Capability("batch")]);
1982 		client.put(":irc.example.com CAP modernclient ACK :batch");
1983 		assert(
1984 			client.capsEnabled.sort().equal(
1985 				[
1986 					Capability("away-notify"),
1987 					Capability("batch"),
1988 					Capability("cap-notify"),
1989 					Capability("multi-prefix"),
1990 					Capability("userhost-in-names")
1991 				]
1992 		));
1993 
1994 		client.put(":irc.example.com CAP modernclient DEL :userhost-in-names multi-prefix away-notify");
1995 		assert(
1996 			capabilitiesDeleted.array.sort().equal(
1997 				[
1998 					Capability("away-notify"),
1999 					Capability("multi-prefix"),
2000 					Capability("userhost-in-names")
2001 				]
2002 		));
2003 		assert(
2004 			client.capsEnabled.sort().equal(
2005 				[
2006 					Capability("batch"),
2007 					Capability("cap-notify")
2008 				]
2009 		));
2010 		client.put(":irc.example.com CAP modernclient NEW :account-notify");
2011 		auto lineByLine = client.output.data.lineSplitter();
2012 		assert(lineByLine.array[$-1] == "CAP REQ :account-notify");
2013 		client.put(":irc.example.com CAP modernclient NEW :sasl=new");
2014 		auto lineByLine2 = client.output.data.lineSplitter();
2015 		assert(lineByLine2.array[$-1] == "CAP REQ :sasl");
2016 	}
2017 	{ //JOIN
2018 		auto client = spawnNoBufferClient();
2019 		Tuple!(const User, "user", const Channel, "channel")[] joins;
2020 		client.onJoin = (const User user, const Channel chan, const MessageMetadata) {
2021 			joins ~= tuple!("user", "channel")(user, chan);
2022 		};
2023 		TopicWhoTime topicWhoTime;
2024 		bool topicWhoTimeReceived;
2025 		client.onTopicWhoTimeReply = (const TopicWhoTime twt, const MessageMetadata) {
2026 			assert(!topicWhoTimeReceived);
2027 			topicWhoTimeReceived = true;
2028 			topicWhoTime = twt;
2029 		};
2030 		Tuple!(const User, "user", const Channel, "channel", ChannelListUpdateType, "type")[] updates;
2031 		client.onChannelListUpdate = (const User user, const User old, const Channel chan, ChannelListUpdateType type) {
2032 			updates ~= tuple!("user", "channel", "type")(user, chan, type);
2033 		};
2034 		NamesReply[] namesReplies;
2035 		client.onNamesReply = (const NamesReply reply, const MessageMetadata) {
2036 			namesReplies ~= reply;
2037 		};
2038 		TopicReply topicReply;
2039 		bool topicReplyReceived;
2040 		client.onTopicReply = (const TopicReply tr, const MessageMetadata) {
2041 			assert(!topicReplyReceived);
2042 			topicReplyReceived = true;
2043 			topicReply = tr;
2044 		};
2045 		setupFakeConnection(client);
2046 		client.join("#test");
2047 		client.put(":someone!ident@hostmask JOIN :#test");
2048 		client.put(":localhost 332 someone #test :a topic");
2049 		client.put(":localhost 333 someone #test someoneElse :1496821983");
2050 		client.put(":localhost 353 someone = #test :someone!ident@hostmask +@another!user@somewhere");
2051 		client.put(":localhost 366 someone #test :End of /NAMES list.");
2052 		client.put(":localhost 324 someone #test :+nt");
2053 		client.put(":localhost 329 someone #test :1496821983");
2054 		assert("someone" in client.internalAddressList);
2055 		assert(client.internalAddressList["someone"] == User("someone!ident@hostmask"));
2056 		assert(client.internalAddressList["someone"].account.isNull);
2057 		client.put(":localhost 354 someone ident 127.0.0.1 hostmask localhost someone H@r 0 SomeoneAccount * :a real name");
2058 		client.put(":localhost 354 someone ident 127.0.0.2 somewhere localhost another H@r 66 SomeoneElseAccount * :a different real name");
2059 		client.put(":localhost 315 someone #test :End of WHO list");
2060 
2061 		assert(joins.length == 1);
2062 		with(joins[0]) {
2063 			assert(user.nickname == "someone");
2064 			assert(channel.name == "#test");
2065 		}
2066 		assert("someone" in client.channels["#test"].users);
2067 		assert(client.channels["#test"].channel.name == "#test");
2068 		assert(client.channels["#test"].users["someone"] == User("someone!ident@hostmask"));
2069 		assert("someone" in client.internalAddressList);
2070 		assert(client.internalAddressList["someone"] == User("someone!ident@hostmask"));
2071 		assert(client.internalAddressList["someone"].account.get == "SomeoneAccount");
2072 
2073 		assert(topicWhoTimeReceived);
2074 		assert(topicReplyReceived);
2075 
2076 		with(topicReply) {
2077 			assert(channel == "#test");
2078 			assert(topic == "a topic");
2079 		}
2080 
2081 		with (topicWhoTime) {
2082 			//TODO: remove when lack of these imports no longer produces warnings
2083 			import std.datetime : SysTime;
2084 			import virc.common : User;
2085 			assert(channel == "#test");
2086 			assert(setter == User("someoneElse"));
2087 			assert(timestamp == SysTime(DateTime(2017, 6, 7, 7, 53, 3), UTC()));
2088 		}
2089 		//TODO: Add 366, 324, 329 tests
2090 		auto lineByLine = client.output.data.lineSplitter();
2091 		assert(lineByLine.array[$-2] == "JOIN #test");
2092 		assert(namesReplies.length == 1);
2093 		assert(namesReplies[0].users(['o': '@', 'v': '+']).array.length == 2);
2094 		assert(updates.length == 4);
2095 		with(updates[0]) {
2096 			assert(user.nickname == "someone");
2097 			assert(user.account.isNull);
2098 			assert(channel.name == "#test");
2099 		}
2100 		with(updates[1]) {
2101 			assert(user.nickname == "another");
2102 			assert(user.account.isNull);
2103 			assert(channel.name == "#test");
2104 		}
2105 		with(updates[2]) {
2106 			assert(user.nickname == "someone");
2107 			assert(!user.account.isNull);
2108 			assert(user.account.get == "SomeoneAccount");
2109 			assert(channel.name == "#test");
2110 		}
2111 		with(updates[3]) {
2112 			assert(user.nickname == "another");
2113 			assert(!user.account.isNull);
2114 			assert(user.account.get == "SomeoneElseAccount");
2115 			assert(channel.name == "#test");
2116 		}
2117 	}
2118 	{ //Channel list example
2119 		auto client = spawnNoBufferClient();
2120 		const(ChannelListResult)[] channels;
2121 		client.onList = (const ChannelListResult chan, const MessageMetadata) {
2122 			channels ~= chan;
2123 		};
2124 		setupFakeConnection(client);
2125 		client.list();
2126 		client.put("321 someone Channel :Users Name");
2127 		client.put("322 someone #test 4 :[+fnt 200:2] some words");
2128 		client.put("322 someone #test2 6 :[+fnst 100:2] some more words");
2129 		client.put("322 someone #test3 1 :no modes?");
2130 		client.put("323 someone :End of channel list.");
2131 		assert(channels.length == 3);
2132 		with(channels[0]) {
2133 			//TODO: remove when lack of import no longer produces a warning
2134 			import virc.common : Topic;
2135 			assert(name == "#test");
2136 			assert(userCount == 4);
2137 			assert(topic == Topic("some words"));
2138 		}
2139 		with(channels[1]) {
2140 			//TODO: remove when lack of import no longer produces a warning
2141 			import virc.common : Topic;
2142 			assert(name == "#test2");
2143 			assert(userCount == 6);
2144 			assert(topic == Topic("some more words"));
2145 		}
2146 		with(channels[2]) {
2147 			//TODO: remove when lack of import no longer produces a warning
2148 			import virc.common : Topic;
2149 			assert(name == "#test3");
2150 			assert(userCount == 1);
2151 			assert(topic == Topic("no modes?"));
2152 		}
2153 	}
2154 	{ //server-time http://ircv3.net/specs/extensions/server-time-3.2.html
2155 		auto client = spawnNoBufferClient();
2156 		User[] users;
2157 		const(Channel)[] channels;
2158 		client.onJoin = (const User user, const Channel chan, const MessageMetadata metadata) {
2159 			users ~= user;
2160 			channels ~= chan;
2161 			assert(metadata.time == SysTime(DateTime(2012, 6, 30, 23, 59, 59), 419.msecs, UTC()));
2162 		};
2163 		setupFakeConnection(client);
2164 		client.put("@time=2012-06-30T23:59:59.419Z :John!~john@1.2.3.4 JOIN #chan");
2165 		assert(users.length == 1);
2166 		assert(users[0].nickname == "John");
2167 		assert(channels.length == 1);
2168 		assert(channels[0].name == "#chan");
2169 	}
2170 	{ //monitor
2171 		auto client = spawnNoBufferClient();
2172 		User[] users;
2173 		const(MessageMetadata)[] metadata;
2174 		client.onUserOnline = (const User user, const SysTime, const MessageMetadata) {
2175 			users ~= user;
2176 		};
2177 		client.onUserOffline = (const User user, const MessageMetadata) {
2178 			users ~= user;
2179 		};
2180 		client.onMonitorList = (const User user, const MessageMetadata) {
2181 			users ~= user;
2182 		};
2183 		client.onError = (const IRCError error, const MessageMetadata received) {
2184 			assert(error.type == ErrorType.monListFull);
2185 			metadata ~= received;
2186 		};
2187 		setupFakeConnection(client);
2188 		client.put(":localhost 730 someone :John!test@example.net,Bob!test2@example.com");
2189 		assert(users.length == 2);
2190 		with (users[0]) {
2191 			assert(nickname == "John");
2192 			assert(ident == "test");
2193 			assert(host == "example.net");
2194 		}
2195 		with (users[1]) {
2196 			assert(nickname == "Bob");
2197 			assert(ident == "test2");
2198 			assert(host == "example.com");
2199 		}
2200 
2201 		users.length = 0;
2202 
2203 		client.put(":localhost 731 someone :John");
2204 		assert(users.length == 1);
2205 		assert(users[0].nickname == "John");
2206 
2207 		users.length = 0;
2208 
2209 		client.put(":localhost 732 someone :John,Bob");
2210 		client.put(":localhost 733 someone :End of MONITOR list");
2211 		assert(users.length == 2);
2212 		assert(users[0].nickname == "John");
2213 		assert(users[1].nickname == "Bob");
2214 
2215 		client.put(":localhost 734 someone 5 Earl :Monitor list is full.");
2216 		assert(metadata.length == 1);
2217 		assert(metadata[0].messageNumeric.get == Numeric.ERR_MONLISTFULL);
2218 	}
2219 	{ //extended-join http://ircv3.net/specs/extensions/extended-join-3.1.html
2220 		auto client = spawnNoBufferClient();
2221 
2222 		User[] users;
2223 		client.onJoin = (const User user, const Channel, const MessageMetadata) {
2224 			users ~= user;
2225 		};
2226 
2227 		initializeWithCaps(client, [Capability("extended-join")]);
2228 
2229 		client.put(":nick!user@host JOIN #channelname accountname :Real Name");
2230 		auto user = User("nick!user@host");
2231 		user.account = "accountname";
2232 		user.realName = "Real Name";
2233 		assert(users.front == user);
2234 
2235 		user.account.nullify();
2236 		users = [];
2237 		client.put(":nick!user@host JOIN #channelname * :Real Name");
2238 		assert(users.front == user);
2239 	}
2240 	{ //test for blank caps
2241 		auto client = spawnNoBufferClient();
2242 		put(client, ":localhost CAP * LS * : ");
2243 		setupFakeConnection(client);
2244 		assert(client.isRegistered);
2245 	}
2246 	{ //example taken from RFC2812, section 3.2.2
2247 		auto client = spawnNoBufferClient();
2248 
2249 		User[] users;
2250 		const(Channel)[] channels;
2251 		string lastMsg;
2252 		client.onPart = (const User user, const Channel chan, const string msg, const MessageMetadata) {
2253 			users ~= user;
2254 			channels ~= chan;
2255 			lastMsg = msg;
2256 		};
2257 
2258 		setupFakeConnection(client);
2259 
2260 		client.put(":WiZ!jto@tolsun.oulu.fi PART #playzone :I lost");
2261 		immutable user = User("WiZ!jto@tolsun.oulu.fi");
2262 		assert(users.front == user);
2263 		assert(channels.front == Channel("#playzone"));
2264 		assert(lastMsg == "I lost");
2265 	}
2266 	{ //PART tests
2267 		auto client = spawnNoBufferClient();
2268 
2269 		Tuple!(const User, "user", const Channel, "channel", string, "message")[] parts;
2270 		client.onPart = (const User user, const Channel chan, const string msg, const MessageMetadata) {
2271 			parts ~= tuple!("user", "channel", "message")(user, chan, msg);
2272 		};
2273 
2274 		setupFakeConnection(client);
2275 
2276 		client.put(":"~testUser.text~" JOIN #example");
2277 		client.put(":SomeoneElse JOIN #example");
2278 		assert("#example" in client.channels);
2279 		assert("SomeoneElse" in client.channels["#example"].users);
2280 		client.put(":SomeoneElse PART #example :bye forever");
2281 		assert("SomeoneElse" !in client.channels["#example"].users);
2282 		client.put(":"~testUser.text~" PART #example :see ya");
2283 		assert("#example" !in client.channels);
2284 
2285 		client.put(":"~testUser.text~" JOIN #example");
2286 		client.put(":SomeoneElse JOIN #example");
2287 		assert("#example" in client.channels);
2288 		assert("SomeoneElse" in client.channels["#example"].users);
2289 		client.put(":SomeoneElse PART #example");
2290 		assert("SomeoneElse" !in client.channels["#example"].users);
2291 		client.put(":"~testUser.text~" PART #example");
2292 		assert("#example" !in client.channels);
2293 
2294 		assert(parts.length == 4);
2295 		with (parts[0]) {
2296 			assert(user == User("SomeoneElse"));
2297 			assert(channel == Channel("#example"));
2298 			assert(message == "bye forever");
2299 		}
2300 		with (parts[1]) {
2301 			assert(user == client.me);
2302 			assert(channel == Channel("#example"));
2303 			assert(message == "see ya");
2304 		}
2305 		with (parts[2]) {
2306 			assert(user == User("SomeoneElse"));
2307 			assert(channel == Channel("#example"));
2308 		}
2309 		with (parts[3]) {
2310 			assert(user == client.me);
2311 			assert(channel == Channel("#example"));
2312 		}
2313 	}
2314 	{ //http://ircv3.net/specs/extensions/chghost-3.2.html
2315 		auto client = spawnNoBufferClient();
2316 
2317 		User[] users;
2318 		client.onChgHost = (const User user, const User newUser, const MessageMetadata) {
2319 			users ~= user;
2320 			users ~= newUser;
2321 		};
2322 
2323 		setupFakeConnection(client);
2324 		client.put(":nick!user@host JOIN #test");
2325 		assert("nick" in client.internalAddressList);
2326 		assert(client.internalAddressList["nick"] == User("nick!user@host"));
2327 		client.put(":nick!user@host CHGHOST user new.host.goes.here");
2328 		assert(users[0] == User("nick!user@host"));
2329 		assert(users[1] == User("nick!user@new.host.goes.here"));
2330 		assert(client.internalAddressList["nick"] == User("nick!user@new.host.goes.here"));
2331 		client.put(":nick!user@host CHGHOST newuser host");
2332 		assert(users[2] == User("nick!user@host"));
2333 		assert(users[3] == User("nick!newuser@host"));
2334 		assert(client.internalAddressList["nick"] == User("nick!newuser@host"));
2335 		client.put(":nick!user@host CHGHOST newuser new.host.goes.here");
2336 		assert(users[4] == User("nick!user@host"));
2337 		assert(users[5] == User("nick!newuser@new.host.goes.here"));
2338 		assert(client.internalAddressList["nick"] == User("nick!newuser@new.host.goes.here"));
2339 		client.put(":tim!~toolshed@backyard CHGHOST b ckyard");
2340 		assert(users[6] == User("tim!~toolshed@backyard"));
2341 		assert(users[7] == User("tim!b@ckyard"));
2342 		assert(client.internalAddressList["tim"] == User("tim!b@ckyard"));
2343 		client.put(":tim!b@ckyard CHGHOST ~toolshed backyard");
2344 		assert(users[8] == User("tim!b@ckyard"));
2345 		assert(users[9] == User("tim!~toolshed@backyard"));
2346 		assert(client.internalAddressList["tim"] == User("tim!~toolshed@backyard"));
2347 	}
2348 	{ //PING? PONG!
2349 		auto client = spawnNoBufferClient();
2350 
2351 		setupFakeConnection(client);
2352 		client.put("PING :words");
2353 		auto lineByLine = client.output.data.lineSplitter();
2354 		assert(lineByLine.array[$-1] == "PONG :words");
2355 	}
2356 	{ //echo-message http://ircv3.net/specs/extensions/echo-message-3.2.html
2357 		auto client = spawnNoBufferClient();
2358 		Message[] messages;
2359 		client.onMessage = (const User, const Target, const Message msg, const MessageMetadata) {
2360 			messages ~= msg;
2361 		};
2362 		setupFakeConnection(client);
2363 		client.msg("Attila", "hi");
2364 		client.put(":"~testUser.text~" PRIVMSG Attila :hi");
2365 		assert(messages.length > 0);
2366 		assert(messages[0].isEcho);
2367 
2368 		client.msg("#ircv3", "back from \x02lunch\x0F");
2369 		client.put(":"~testUser.text~" PRIVMSG #ircv3 :back from lunch");
2370 		assert(messages.length > 1);
2371 		assert(messages[1].isEcho);
2372 	}
2373 	{ //Test self-tracking
2374 		auto client = spawnNoBufferClient();
2375 		setupFakeConnection(client);
2376 		assert(client.me.nickname == testUser.nickname);
2377 		client.changeNickname("Testface");
2378 		client.put(":"~testUser.nickname~" NICK Testface");
2379 		assert(client.me.nickname == "Testface");
2380 	}
2381 }
2382 @safe unittest {
2383 	{ //QUIT and invalidation check
2384 		import core.exception : AssertError;
2385 		import std.exception : assertThrown;
2386 		auto client = spawnNoBufferClient();
2387 
2388 		setupFakeConnection(client);
2389 		client.quit("I'm out");
2390 		auto lineByLine = client.output.data.lineSplitter();
2391 		assert(lineByLine.array[$-1] == "QUIT :I'm out");
2392 		client.put(":"~testUser.nickname~" QUIT");
2393 		assert(!client.isValid);
2394 	}
2395 }
2396 @safe unittest {
2397 	{ //NAMES
2398 		auto client = spawnNoBufferClient();
2399 		NamesReply[] replies;
2400 		client.onNamesReply = (const NamesReply reply, const MessageMetadata) {
2401 			replies ~= reply;
2402 		};
2403 
2404 		setupFakeConnection(client);
2405 
2406 		client.names();
2407 		client.put(":localhost 353 someone = #channel :User1 User2 @User3 +User4");
2408 		client.put(":localhost 353 someone @ #channel2 :User5 User2 @User6 +User7");
2409 		client.put(":localhost 353 someone * #channel3 :User1 User2 @User3 +User4");
2410 		client.put(":localhost 366 someone :End of NAMES list");
2411 		assert(replies.length == 3);
2412 		assert(replies[0].chanFlag == NamReplyFlag.public_);
2413 		assert(replies[1].chanFlag == NamReplyFlag.secret);
2414 		assert(replies[2].chanFlag == NamReplyFlag.private_);
2415 	}
2416 	{ //WATCH stuff
2417 		auto client = spawnNoBufferClient();
2418 		User[] users;
2419 		SysTime[] times;
2420 		client.onUserOnline = (const User user, const SysTime time, const MessageMetadata) {
2421 			users ~= user;
2422 			times ~= time;
2423 		};
2424 		setupFakeConnection(client);
2425 		client.put(":localhost 600 someone someoneElse someIdent example.net 911248013 :logged on");
2426 
2427 		assert(users.length == 1);
2428 		assert(users[0] == User("someoneElse!someIdent@example.net"));
2429 		assert(times.length == 1);
2430 		assert(times[0] == SysTime(DateTime(1998, 11, 16, 20, 26, 53), UTC()));
2431 	}
2432 	{ //LUSER stuff
2433 		auto client = spawnNoBufferClient();
2434 		bool lUserMeReceived;
2435 		bool lUserChannelsReceived;
2436 		bool lUserOpReceived;
2437 		bool lUserClientReceived;
2438 		LUserMe lUserMe;
2439 		LUserClient lUserClient;
2440 		LUserOp lUserOp;
2441 		LUserChannels lUserChannels;
2442 		client.onLUserMe = (const LUserMe param, const MessageMetadata) {
2443 			assert(!lUserMeReceived);
2444 			lUserMeReceived = true;
2445 			lUserMe = param;
2446 		};
2447 		client.onLUserChannels = (const LUserChannels param, const MessageMetadata) {
2448 			assert(!lUserChannelsReceived);
2449 			lUserChannelsReceived = true;
2450 			lUserChannels = param;
2451 		};
2452 		client.onLUserOp = (const LUserOp param, const MessageMetadata) {
2453 			assert(!lUserOpReceived);
2454 			lUserOpReceived = true;
2455 			lUserOp = param;
2456 		};
2457 		client.onLUserClient = (const LUserClient param, const MessageMetadata) {
2458 			assert(!lUserClientReceived);
2459 			lUserClientReceived = true;
2460 			lUserClient = param;
2461 		};
2462 		setupFakeConnection(client);
2463 		client.lUsers();
2464 		client.put(":localhost 251 someone :There are 8 users and 0 invisible on 2 servers");
2465 		client.put(":localhost 252 someone 1 :operator(s) online");
2466 		client.put(":localhost 254 someone 1 :channels formed");
2467 		client.put(":localhost 255 someone :I have 1 clients and 1 servers");
2468 
2469 		assert(lUserMeReceived);
2470 		assert(lUserChannelsReceived);
2471 		assert(lUserOpReceived);
2472 		assert(lUserClientReceived);
2473 
2474 		assert(lUserMe.message == "I have 1 clients and 1 servers");
2475 		assert(lUserClient.message == "There are 8 users and 0 invisible on 2 servers");
2476 		assert(lUserOp.numOperators == 1);
2477 		assert(lUserOp.message == "operator(s) online");
2478 		assert(lUserChannels.numChannels == 1);
2479 		assert(lUserChannels.message == "channels formed");
2480 	}
2481 	{ //PRIVMSG and NOTICE stuff
2482 		auto client = spawnNoBufferClient();
2483 		Tuple!(const User, "user", const Target, "target", const Message, "message")[] messages;
2484 		client.onMessage = (const User user, const Target target, const Message msg, const MessageMetadata) {
2485 			messages ~= tuple!("user", "target", "message")(user, target, msg);
2486 		};
2487 
2488 		setupFakeConnection(client);
2489 
2490 		client.put(":someoneElse!somebody@somewhere PRIVMSG someone :words go here");
2491 		assert(messages.length == 1);
2492 		with (messages[0]) {
2493 			assert(user == User("someoneElse!somebody@somewhere"));
2494 			assert(!target.isChannel);
2495 			assert(target.isNickname);
2496 			assert(target == User("someone"));
2497 			assert(message == "words go here");
2498 			assert(message.isReplyable);
2499 			assert(!message.isEcho);
2500 		}
2501 		client.put(":ohno!it's@me PRIVMSG #someplace :more words go here");
2502 		assert(messages.length == 2);
2503 		with (messages[1]) {
2504 			assert(user == User("ohno!it's@me"));
2505 			assert(target.isChannel);
2506 			assert(!target.isNickname);
2507 			assert(target == Channel("#someplace"));
2508 			assert(message == "more words go here");
2509 			assert(message.isReplyable);
2510 			assert(!message.isEcho);
2511 		}
2512 
2513 		client.put(":someoneElse2!somebody2@somewhere2 NOTICE someone :words don't go here");
2514 		assert(messages.length == 3);
2515 		with(messages[2]) {
2516 			assert(user == User("someoneElse2!somebody2@somewhere2"));
2517 			assert(!target.isChannel);
2518 			assert(target.isNickname);
2519 			assert(target == User("someone"));
2520 			assert(message == "words don't go here");
2521 			assert(!message.isReplyable);
2522 			assert(!message.isEcho);
2523 		}
2524 
2525 		client.put(":ohno2!it's2@me4 NOTICE #someplaceelse :more words might go here");
2526 		assert(messages.length == 4);
2527 		with(messages[3]) {
2528 			assert(user == User("ohno2!it's2@me4"));
2529 			assert(target.isChannel);
2530 			assert(!target.isNickname);
2531 			assert(target == Channel("#someplaceelse"));
2532 			assert(message == "more words might go here");
2533 			assert(!message.isReplyable);
2534 			assert(!message.isEcho);
2535 		}
2536 
2537 		client.put(":someoneElse2!somebody2@somewhere2 NOTICE someone :\x01ACTION did the thing\x01");
2538 		assert(messages.length == 5);
2539 		with(messages[4]) {
2540 			assert(user == User("someoneElse2!somebody2@somewhere2"));
2541 			assert(!target.isChannel);
2542 			assert(target.isNickname);
2543 			assert(target == User("someone"));
2544 			assert(message.isCTCP);
2545 			assert(message.ctcpArgs == "did the thing");
2546 			assert(message.ctcpCommand == "ACTION");
2547 			assert(!message.isReplyable);
2548 			assert(!message.isEcho);
2549 		}
2550 
2551 		client.put(":ohno2!it's2@me4 NOTICE #someplaceelse :\x01ACTION did not do the thing\x01");
2552 		assert(messages.length == 6);
2553 		with(messages[5]) {
2554 			assert(user == User("ohno2!it's2@me4"));
2555 			assert(target.isChannel);
2556 			assert(!target.isNickname);
2557 			assert(target == Channel("#someplaceelse"));
2558 			assert(message.isCTCP);
2559 			assert(message.ctcpArgs == "did not do the thing");
2560 			assert(message.ctcpCommand == "ACTION");
2561 			assert(!message.isReplyable);
2562 			assert(!message.isEcho);
2563 		}
2564 
2565 		client.msg("#channel", "ohai");
2566 		client.notice("#channel", "ohi");
2567 		client.msg("someoneElse", "ohay");
2568 		client.notice("someoneElse", "ohello");
2569 		Target channelTarget;
2570 		channelTarget.channel = Channel("#channel");
2571 		Target userTarget;
2572 		userTarget.user = User("someoneElse");
2573 		client.msg(channelTarget, Message("ohai"));
2574 		client.notice(channelTarget, Message("ohi"));
2575 		client.msg(userTarget, Message("ohay"));
2576 		client.notice(userTarget, Message("ohello"));
2577 		auto lineByLine = client.output.data.lineSplitter();
2578 		foreach (i; 0..5) //skip the initial handshake
2579 			lineByLine.popFront();
2580 		assert(lineByLine.array == ["PRIVMSG #channel :ohai", "NOTICE #channel :ohi", "PRIVMSG someoneElse :ohay", "NOTICE someoneElse :ohello", "PRIVMSG #channel :ohai", "NOTICE #channel :ohi", "PRIVMSG someoneElse :ohay", "NOTICE someoneElse :ohello"]);
2581 	}
2582 	{ //PING? PONG!
2583 		auto client = spawnNoBufferClient();
2584 
2585 		setupFakeConnection(client);
2586 		client.ping("hooray");
2587 		client.put(":localhost PONG localhost :hooray");
2588 
2589 		client.put(":localhost PING :hoorah");
2590 
2591 		auto lineByLine = client.output.data.lineSplitter();
2592 		assert(lineByLine.array[$-2] == "PING :hooray");
2593 		assert(lineByLine.array[$-1] == "PONG :hoorah");
2594 	}
2595 	{ //Mode change test
2596 		auto client = spawnNoBufferClient();
2597 		Tuple!(const User, "user", const Target, "target", const ModeChange, "change")[] changes;
2598 
2599 		client.onMode = (const User user, const Target target, const ModeChange mode, const MessageMetadata) {
2600 			changes ~= tuple!("user", "target", "change")(user, target, mode);
2601 		};
2602 
2603 		setupFakeConnection(client);
2604 		client.join("#test");
2605 		client.put(":"~testUser.text~" JOIN #test "~testUser.nickname);
2606 		client.put(":someone!ident@host JOIN #test");
2607 		client.put(":someoneElse!user@host2 MODE #test +s");
2608 		client.put(":someoneElse!user@host2 MODE #test -s");
2609 		client.put(":someoneElse!user@host2 MODE #test +kp 2");
2610 		client.put(":someoneElse!user@host2 MODE someone +r");
2611 		client.put(":someoneElse!user@host2 MODE someone +k");
2612 
2613 		assert(changes.length == 6);
2614 		with (changes[0]) {
2615 			assert(target == Channel("#test"));
2616 			assert(user == User("someoneElse!user@host2"));
2617 		}
2618 		with (changes[1]) {
2619 			assert(target == Channel("#test"));
2620 			assert(user == User("someoneElse!user@host2"));
2621 		}
2622 		with (changes[2]) {
2623 			assert(target == Channel("#test"));
2624 			assert(user == User("someoneElse!user@host2"));
2625 		}
2626 		with (changes[3]) {
2627 			assert(target == Channel("#test"));
2628 			assert(user == User("someoneElse!user@host2"));
2629 		}
2630 		with (changes[4]) {
2631 			assert(target == User("someone"));
2632 			assert(user == User("someoneElse!user@host2"));
2633 		}
2634 		with (changes[5]) {
2635 			assert(target == User("someone"));
2636 			assert(user == User("someoneElse!user@host2"));
2637 		}
2638 	}
2639 	{ //client join stuff
2640 		auto client = spawnNoBufferClient();
2641 		client.join("#test");
2642 		assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test");
2643 		client.join(Channel("#test2"));
2644 		assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test2");
2645 		client.join("#test3", "key");
2646 		assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test3 key");
2647 		client.join("#test4", "key2");
2648 		assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test4 key2");
2649 	}
2650 	{ //account-tag examples from http://ircv3.net/specs/extensions/account-tag-3.2.html
2651 		auto client = spawnNoBufferClient();
2652 		User[] privmsgUsers;
2653 		client.onMessage = (const User user, const Target, const Message, const MessageMetadata) {
2654 			privmsgUsers ~= user;
2655 		};
2656 		setupFakeConnection(client);
2657 
2658 		client.put(":user PRIVMSG #atheme :Hello everyone.");
2659 		client.put(":user ACCOUNT hax0r");
2660 		client.put("@account=hax0r :user PRIVMSG #atheme :Now I'm logged in.");
2661 		client.put("@account=hax0r :user ACCOUNT bob");
2662 		client.put("@account=bob :user PRIVMSG #atheme :I switched accounts.");
2663 		with(privmsgUsers[0]) {
2664 			assert(account.isNull);
2665 		}
2666 		with(privmsgUsers[1]) {
2667 			assert(account.get == "hax0r");
2668 		}
2669 		with(privmsgUsers[2]) {
2670 			assert(account.get == "bob");
2671 		}
2672 		assert(client.internalAddressList["user"].account == "bob");
2673 	}
2674 	{ //account-notify - http://ircv3.net/specs/extensions/account-notify-3.1.html
2675 		auto client = spawnNoBufferClient();
2676 		setupFakeConnection(client);
2677 		client.put(":nick!user@host ACCOUNT accountname");
2678 		assert(client.internalAddressList["nick"].account.get == "accountname");
2679 		client.put(":nick!user@host ACCOUNT *");
2680 		assert(client.internalAddressList["nick"].account.isNull);
2681 	}
2682 	{ //monitor - http://ircv3.net/specs/core/monitor-3.2.html
2683 		auto client = spawnNoBufferClient();
2684 		initializeWithCaps(client, [Capability("MONITOR")]);
2685 
2686 		assert(client.monitorIsEnabled);
2687 
2688 		client.monitorAdd([User("Someone")]);
2689 		client.monitorRemove([User("Someone")]);
2690 		client.monitorClear();
2691 		client.monitorList();
2692 		client.monitorStatus();
2693 
2694 		const lineByLine = client.output.data.lineSplitter().drop(5).array;
2695 		assert(lineByLine == ["MONITOR + Someone", "MONITOR - Someone", "MONITOR C", "MONITOR L", "MONITOR S"]);
2696 	}
2697 	{ //No MOTD test
2698 		auto client = spawnNoBufferClient();
2699 		bool errorReceived;
2700 		client.onError = (const IRCError error, const MessageMetadata) {
2701 			assert(!errorReceived);
2702 			errorReceived = true;
2703 			assert(error.type == ErrorType.noMOTD);
2704 		};
2705 		setupFakeConnection(client);
2706 		client.put("422 someone :MOTD File is missing");
2707 		assert(errorReceived);
2708 	}
2709 	{ //NICK tests
2710 		auto client = spawnNoBufferClient();
2711 		Tuple!(const User, "old", const User, "new_")[] nickChanges;
2712 		client.onNick = (const User old, const User new_, const MessageMetadata) {
2713 			nickChanges ~= tuple!("old", "new_")(old, new_);
2714 		};
2715 
2716 		setupFakeConnection(client);
2717 		client.put(":WiZ JOIN #testchan");
2718 		client.put(":dan- JOIN #testchan");
2719 
2720 
2721 		client.put(":WiZ NICK Kilroy");
2722 
2723 		assert(nickChanges.length == 1);
2724 		with(nickChanges[0]) {
2725 			assert(old.nickname == "WiZ");
2726 			assert(new_.nickname == "Kilroy");
2727 		}
2728 
2729 		assert("Kilroy" in client.internalAddressList);
2730 		assert("Kilroy" in client.channels["#testchan"].users);
2731 		assert("WiZ" !in client.channels["#testchan"].users);
2732 
2733 		client.put(":dan-!d@localhost NICK Mamoped");
2734 
2735 		assert(nickChanges.length == 2);
2736 		with(nickChanges[1]) {
2737 			assert(old.nickname == "dan-");
2738 			assert(new_.nickname == "Mamoped");
2739 		}
2740 
2741 		assert("Mamoped" in client.internalAddressList);
2742 		assert("Mamoped" in client.channels["#testchan"].users);
2743 		assert("dan-" !in client.channels["#testchan"].users);
2744 
2745 		//invalid, so we shouldn't see anything
2746 		client.put(":a NICK");
2747 		assert(nickChanges.length == 2);
2748 	}
2749 	{ //QUIT tests
2750 		auto client = spawnNoBufferClient();
2751 
2752 		Tuple!(const User, "user", string, "message")[] quits;
2753 		client.onQuit = (const User user, const string msg, const MessageMetadata) {
2754 			quits ~= tuple!("user", "message")(user, msg);
2755 		};
2756 
2757 		setupFakeConnection(client);
2758 
2759 		client.put(":dan-!d@localhost QUIT :Quit: Bye for now!");
2760 		assert(quits.length == 1);
2761 		with (quits[0]) {
2762 			assert(user == User("dan-!d@localhost"));
2763 			assert(message == "Quit: Bye for now!");
2764 		}
2765 		client.put(":nomessage QUIT");
2766 		assert(quits.length == 2);
2767 		with(quits[1]) {
2768 			assert(user == User("nomessage"));
2769 			assert(message == "");
2770 		}
2771 	}
2772 	{ //Batch stuff
2773 		auto client = spawnNoBufferClient();
2774 
2775 		Tuple!(const User, "user", const MessageMetadata, "metadata")[] quits;
2776 		client.onQuit = (const User user, const string, const MessageMetadata metadata) {
2777 			quits ~= tuple!("user", "metadata")(user, metadata);
2778 		};
2779 
2780 		setupFakeConnection(client);
2781 
2782 		client.put(`:irc.host BATCH +yXNAbvnRHTRBv netsplit irc.hub other.host`);
2783 		client.put(`@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host`);
2784 		client.put(`@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host`);
2785 		client.put(`:nick!user@host PRIVMSG #channel :This is not in batch, so processed immediately`);
2786 		client.put(`@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`);
2787 
2788 		assert(quits.length == 0);
2789 
2790 		client.put(`:irc.host BATCH -yXNAbvnRHTRBv`);
2791 
2792 
2793 		assert(quits.length == 3);
2794 		with(quits[0]) {
2795 			assert(metadata.batch.type == "netsplit");
2796 		}
2797 		client.put(`:SomeUser!Someplace@Somewhere BATCH +2 something`);
2798 		client.put(`@batch=2 :SomeUser!Someplace@Somewhere PRIVMSG #channel :hello`);
2799 		client.put(`@batch=2 :SomeUser!Someplace@Somewhere PRIVMSG #channel :hello again`);
2800 		client.put(`:SomeUser!Someplace@Somewhere BATCH -2`);
2801 	}
2802 	{ //INVITE tests
2803 		auto client = spawnNoBufferClient();
2804 
2805 		Tuple!(const User, "inviter", const User, "invited",  const Channel, "channel")[] invites;
2806 		client.onInvite = (const User inviter, const User invited, const Channel channel, const MessageMetadata) {
2807 			invites ~= tuple!("inviter", "invited", "channel")(inviter, invited, channel);
2808 		};
2809 
2810 		setupFakeConnection(client);
2811 
2812 		//Ensure the internal address list gets used for invited users as well
2813 		client.internalAddressList.update(User("Wiz!ident@host"));
2814 
2815 		client.put(":Angel INVITE Wiz #Dust");
2816 		assert(invites.length == 1);
2817 		with(invites[0]) {
2818 			assert(inviter.nickname == "Angel");
2819 			assert(invited.nickname == "Wiz");
2820 			assert(invited.host == "host");
2821 			assert(channel == Channel("#Dust"));
2822 		}
2823 
2824 		client.put(":ChanServ!ChanServ@example.com INVITE Attila #channel");
2825 		assert(invites.length == 2);
2826 		with(invites[1]) {
2827 			assert(inviter.nickname == "ChanServ");
2828 			assert(invited.nickname == "Attila");
2829 			assert(channel == Channel("#channel"));
2830 		}
2831 	}
2832 	{ //VERSION tests
2833 		auto client = spawnNoBufferClient();
2834 
2835 		VersionReply[] replies;
2836 		client.onVersionReply = (const VersionReply reply, const MessageMetadata) {
2837 			replies ~= reply;
2838 		};
2839 
2840 		setupFakeConnection(client);
2841 
2842 		client.version_();
2843 		client.put(format!":localhost 351 %s example-1.0 localhost :not a beta"(testUser.nickname));
2844 		with (replies[0]) {
2845 			assert(version_ == "example-1.0");
2846 			assert(server == "localhost");
2847 			assert(comments == "not a beta");
2848 		}
2849 		client.version_("*.example");
2850 		client.put(format!":localhost 351 %s example-1.0 test.example :not a beta"(testUser.nickname));
2851 		with (replies[1]) {
2852 			assert(version_ == "example-1.0");
2853 			assert(server == "test.example");
2854 			assert(comments == "not a beta");
2855 		}
2856 	}
2857 	{ //SASL test
2858 		auto client = spawnNoBufferClient();
2859 		client.saslMechs = [new SASLPlain("jilles", "jilles", "sesame")];
2860 		client.put(":localhost CAP * LS :sasl");
2861 		client.put(":localhost CAP whoever ACK :sasl");
2862 		client.put("AUTHENTICATE +");
2863 		client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname);
2864 		client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful");
2865 
2866 		assert(client.output.data.canFind("AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU="));
2867 		assert(client.isAuthenticated == true);
2868 		assert(client.me.account.get == testUser.nickname);
2869 	}
2870 	{ //SASL 3.2 test
2871 		auto client = spawnNoBufferClient();
2872 		client.saslMechs = [new SASLPlain("jilles", "jilles", "sesame")];
2873 		client.put(":localhost CAP * LS :sasl=UNKNOWN,PLAIN,EXTERNAL");
2874 		client.put(":localhost CAP whoever ACK :sasl");
2875 		client.put("AUTHENTICATE +");
2876 		client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname);
2877 		client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful");
2878 
2879 		assert(client.output.data.canFind("AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU="));
2880 		assert(client.isAuthenticated == true);
2881 		assert(client.me.account.get == testUser.nickname);
2882 	}
2883 	{ //SASL 3.2 test
2884 		auto client = spawnNoBufferClient();
2885 		client.saslMechs = [new SASLExternal];
2886 		client.put(":localhost CAP * LS :sasl=UNKNOWN,EXTERNAL");
2887 		client.put(":localhost CAP whoever ACK :sasl");
2888 		client.put("AUTHENTICATE +");
2889 		client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname);
2890 		client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful");
2891 
2892 		assert(client.output.data.canFind("AUTHENTICATE +"));
2893 		assert(client.isAuthenticated == true);
2894 		assert(client.me.account.get == testUser.nickname);
2895 	}
2896 	{ //SASL 3.2 test (bogus)
2897 		auto client = spawnNoBufferClient();
2898 		client.saslMechs = [new SASLPlain("jilles", "jilles", "sesame")];
2899 		client.put(":localhost CAP * LS :sasl=UNKNOWN,EXTERNAL");
2900 		client.put(":localhost CAP whoever ACK :sasl");
2901 		client.put("AUTHENTICATE +");
2902 		client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname);
2903 		client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful");
2904 
2905 		assert(!client.output.data.canFind("AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU="));
2906 		assert(client.isAuthenticated == false);
2907 		//assert(client.me.account.get.isNull);
2908 	}
2909 	{ //SASL post-registration test
2910 		auto client = spawnNoBufferClient();
2911 		client.saslMechs = [new SASLExternal()];
2912 		setupFakeConnection(client);
2913 		client.capList();
2914 		client.put(":localhost CAP * LIST :sasl=UNKNOWN,PLAIN,EXTERNAL");
2915 	}
2916 	{ //KICK tests
2917 		auto client = spawnNoBufferClient();
2918 		Tuple!(const User, "kickedBy", const User, "kicked",  const Channel, "channel", string, "message")[] kicks;
2919 		client.onKick = (const User kickedBy, const Channel channel, const User kicked, const string message, const MessageMetadata) {
2920 			kicks ~= tuple!("kickedBy", "kicked", "channel", "message")(kickedBy, kicked, channel, message);
2921 		};
2922 		setupFakeConnection(client);
2923 		client.kick(Channel("#test"), User("Example"), "message");
2924 		auto lineByLine = client.output.data.lineSplitter();
2925 		assert(lineByLine.array[$-1] == "KICK #test Example :message");
2926 
2927 		client.put(":WiZ KICK #Finnish John");
2928 
2929 		assert(kicks.length == 1);
2930 		with(kicks[0]) {
2931 			assert(kickedBy == User("WiZ"));
2932 			assert(channel == Channel("#Finnish"));
2933 			assert(kicked == User("John"));
2934 			assert(message == "");
2935 		}
2936 
2937 		client.put(":Testo KICK #example User :Now with kick message!");
2938 
2939 		assert(kicks.length == 2);
2940 		with(kicks[1]) {
2941 			assert(kickedBy == User("Testo"));
2942 			assert(channel == Channel("#example"));
2943 			assert(kicked == User("User"));
2944 			assert(message == "Now with kick message!");
2945 		}
2946 
2947 		client.put(":WiZ!jto@tolsun.oulu.fi KICK #Finnish John");
2948 
2949 		assert(kicks.length == 3);
2950 		with(kicks[2]) {
2951 			assert(kickedBy == User("WiZ!jto@tolsun.oulu.fi"));
2952 			assert(channel == Channel("#Finnish"));
2953 			assert(kicked == User("John"));
2954 			assert(message == "");
2955 		}
2956 
2957 		client.put(":User KICK");
2958 		assert(kicks.length == 3);
2959 
2960 		client.put(":User KICK #channel");
2961 		assert(kicks.length == 3);
2962 	}
2963 	{ //REHASH tests
2964 		auto client = spawnNoBufferClient();
2965 		RehashingReply[] replies;
2966 		client.onServerRehashing = (const RehashingReply reply, const MessageMetadata) {
2967 			replies ~= reply;
2968 		};
2969 		IRCError[] errors;
2970 		client.onError = (const IRCError error, const MessageMetadata) {
2971 			errors ~= error;
2972 		};
2973 
2974 		setupFakeConnection(client);
2975 		client.rehash();
2976 		auto lineByLine = client.output.data.lineSplitter();
2977 		assert(lineByLine.array[$-1] == "REHASH");
2978 		client.put(":localhost 382 Someone ircd.conf :Rehashing config");
2979 
2980 		assert(replies.length == 1);
2981 		with (replies[0]) {
2982 			import virc.common : User;
2983 			assert(me == User("Someone"));
2984 			assert(configFile == "ircd.conf");
2985 			assert(message == "Rehashing config");
2986 		}
2987 
2988 		client.put(":localhost 382 Nope");
2989 
2990 		assert(replies.length == 1);
2991 
2992 		client.put(":localhost 723 Someone rehash :Insufficient oper privileges");
2993 		client.put(":localhost 723 Someone");
2994 		assert(errors.length == 2);
2995 		with(errors[0]) {
2996 			assert(type == ErrorType.noPrivs);
2997 		}
2998 		with(errors[1]) {
2999 			assert(type == ErrorType.malformed);
3000 		}
3001 	}
3002 	{ //ISON tests
3003 		auto client = spawnNoBufferClient();
3004 		const(User)[] users;
3005 		client.onIsOn = (const User user, const MessageMetadata) {
3006 			users ~= user;
3007 		};
3008 		setupFakeConnection(client);
3009 
3010 		client.isOn("phone", "trillian", "WiZ", "jarlek", "Avalon", "Angel", "Monstah");
3011 
3012 		client.put(":localhost 303 Someone :trillian");
3013 		client.put(":localhost 303 Someone :WiZ");
3014 		client.put(":localhost 303 Someone :jarlek");
3015 		client.put(":localhost 303 Someone :Angel");
3016 		client.put(":localhost 303 Someone :Monstah");
3017 
3018 		assert(users.length == 5);
3019 		assert(users[0].nickname == "trillian");
3020 		assert(users[1].nickname == "WiZ");
3021 		assert(users[2].nickname == "jarlek");
3022 		assert(users[3].nickname == "Angel");
3023 		assert(users[4].nickname == "Monstah");
3024 	}
3025 	{ //OPER tests
3026 		auto client = spawnNoBufferClient();
3027 		bool received;
3028 		client.onYoureOper = (const MessageMetadata) {
3029 			received = true;
3030 		};
3031 		setupFakeConnection(client);
3032 
3033 		client.oper("foo", "bar");
3034 		auto lineByLine = client.output.data.lineSplitter();
3035 		assert(lineByLine.array[$-1] == "OPER foo bar");
3036 		client.put(":localhost 381 Someone :You are now an IRC operator");
3037 		assert(received);
3038 	}
3039 	{ //SQUIT tests
3040 		auto client = spawnNoBufferClient();
3041 		IRCError[] errors;
3042 		client.onError = (const IRCError error, const MessageMetadata) {
3043 			errors ~= error;
3044 		};
3045 		setupFakeConnection(client);
3046 
3047 		client.squit("badserver.example.net", "Bad link");
3048 		auto lineByLine = client.output.data.lineSplitter();
3049 		assert(lineByLine.array[$-1] == "SQUIT badserver.example.net :Bad link");
3050 		client.put(":localhost 481 Someone :Permission Denied- You're not an IRC operator");
3051 		client.put(":localhost 402 Someone badserver.example.net :No such server");
3052 		client.put(":localhost 402 Someone");
3053 		assert(errors.length == 3);
3054 		with(errors[0]) {
3055 			assert(type == ErrorType.noPrivileges);
3056 		}
3057 		with(errors[1]) {
3058 			assert(type == ErrorType.noSuchServer);
3059 		}
3060 		with(errors[2]) {
3061 			assert(type == ErrorType.malformed);
3062 		}
3063 	}
3064 	{ //AWAY tests
3065 		auto client = spawnNoBufferClient();
3066 		Tuple!(const User, "user", string, "message")[] aways;
3067 		client.onOtherUserAwayReply = (const User awayUser, const string msg, const MessageMetadata) {
3068 			aways ~= tuple!("user", "message")(awayUser, msg);
3069 		};
3070 		bool unAwayReceived;
3071 		client.onUnAwayReply = (const User, const MessageMetadata) {
3072 			unAwayReceived = true;
3073 		};
3074 		bool awayReceived;
3075 		client.onAwayReply = (const User, const MessageMetadata) {
3076 			awayReceived = true;
3077 		};
3078 		setupFakeConnection(client);
3079 
3080 		client.away("Laughing at salads");
3081 		client.put(":localhost 306 Someone :You have been marked as being away");
3082 		assert(client.isAway);
3083 		assert(awayReceived);
3084 
3085 		client.away();
3086 		client.put(":localhost 305 Someone :You are no longer marked as being away");
3087 		assert(!client.isAway);
3088 		assert(unAwayReceived);
3089 
3090 		client.put(":localhost 301 Someone AwayUser :User on fire");
3091 
3092 		assert(aways.length == 1);
3093 		with (aways[0]) {
3094 			assert(user == User("AwayUser"));
3095 			assert(message == "User on fire");
3096 		}
3097 	}
3098 	{ //ADMIN tests
3099 		auto client = spawnNoBufferClient();
3100 
3101 		setupFakeConnection(client);
3102 
3103 		client.admin("localhost");
3104 		client.admin();
3105 		auto lineByLine = client.output.data.lineSplitter();
3106 		assert(lineByLine.array[$-2] == "ADMIN localhost");
3107 		assert(lineByLine.array[$-1] == "ADMIN");
3108 		client.put(":localhost 256 Someone :Administrative info for localhost");
3109 		client.put(":localhost 257 Someone :Name     - Admin");
3110 		client.put(":localhost 258 Someone :Nickname - Admin");
3111 		client.put(":localhost 259 Someone :E-Mail   - Admin@localhost");
3112 	}
3113 	{ //WHOIS tests
3114 		auto client = spawnNoBufferClient();
3115 		const(WhoisResponse)[] responses;
3116 		client.onWhois = (const User, const WhoisResponse whois) {
3117 			responses ~= whois;
3118 		};
3119 		setupFakeConnection(client);
3120 		client.whois("someoneElse");
3121 
3122 		client.put(":localhost 276 Someone someoneElse :has client certificate 0");
3123 		client.put(":localhost 311 Someone someoneElse someUsername someHostname * :Some Real Name");
3124 		client.put(":localhost 312 Someone someoneElse example.net :The real world is out there");
3125 		client.put(":localhost 313 Someone someoneElse :is an IRC operator");
3126 		client.put(":localhost 317 Someone someoneElse 1000 1500000000 :seconds idle, signon time");
3127 		client.put(":localhost 319 Someone someoneElse :+#test #test2");
3128 		client.put(":localhost 330 Someone someoneElse someoneElseAccount :is logged in as");
3129 		client.put(":localhost 378 Someone someoneElse :is connecting from someoneElse@127.0.0.5 127.0.0.5");
3130 		client.put(":localhost 671 Someone someoneElse :is using a secure connection");
3131 		client.put(":localhost 379 Someone someoneElse :is using modes +w");
3132 		client.put(":localhost 307 Someone someoneElse :is a registered nick");
3133 
3134 		assert(responses.length == 0);
3135 		client.put(":localhost 318 Someone someoneElse :End of /WHOIS list");
3136 
3137 		assert(responses.length == 1);
3138 		with(responses[0]) {
3139 			assert(isSecure);
3140 			assert(isOper);
3141 			assert(isRegistered);
3142 			assert(username.get == "someUsername");
3143 			assert(hostname.get == "someHostname");
3144 			assert(realname.get == "Some Real Name");
3145 			assert(connectedTime.get == SysTime(DateTime(2017, 7, 14, 2, 40, 0), UTC()));
3146 			assert(idleTime.get == 1000.seconds);
3147 			assert(connectedTo.get == "example.net");
3148 			assert(account.get == "someoneElseAccount");
3149 			assert(channels.length == 2);
3150 			assert("#test" in channels);
3151 			assert(channels["#test"].prefix == "+");
3152 			assert("#test2" in channels);
3153 			assert(channels["#test2"].prefix == "");
3154 		}
3155 	}
3156 	{ //RESTART tests
3157 		auto client = spawnNoBufferClient();
3158 		setupFakeConnection(client);
3159 
3160 		client.restart();
3161 		auto lineByLine = client.output.data.lineSplitter();
3162 		assert(lineByLine.array[$-1] == "RESTART");
3163 	}
3164 	{ //WALLOPS tests
3165 		auto client = spawnNoBufferClient();
3166 		string[] messages;
3167 		client.onWallops = (const User, const string msg, const MessageMetadata) {
3168 			messages ~= msg;
3169 		};
3170 		setupFakeConnection(client);
3171 
3172 		client.wallops("Test message!");
3173 		auto lineByLine = client.output.data.lineSplitter();
3174 		assert(lineByLine.array[$-1] == "WALLOPS :Test message!");
3175 
3176 		client.put(":OtherUser!someone@somewhere WALLOPS :Test message reply!");
3177 		assert(messages.length == 1);
3178 		assert(messages[0] == "Test message reply!");
3179 
3180 	}
3181 	{ //CTCP tests
3182 		auto client = spawnNoBufferClient();
3183 		setupFakeConnection(client);
3184 		client.ctcp(Target(User("test")), "ping");
3185 		client.ctcp(Target(User("test")), "action", "does the thing.");
3186 		client.ctcpReply(Target(User("test")), "ping", "1000000000");
3187 
3188 		auto lineByLine = client.output.data.lineSplitter();
3189 		assert(lineByLine.array[$-3] == "PRIVMSG test :\x01ping\x01");
3190 		assert(lineByLine.array[$-2] == "PRIVMSG test :\x01action does the thing.\x01");
3191 		assert(lineByLine.array[$-1] == "NOTICE test :\x01ping 1000000000\x01");
3192 	}
3193 	{ //TOPIC tests
3194 		auto client = spawnNoBufferClient();
3195 		Tuple!(const User, "user", const Channel, "channel", string, "message")[] topics;
3196 		IRCError[] errors;
3197 		client.onTopicChange = (const User user, const Channel channel, const string msg, const MessageMetadata) {
3198 			topics ~= tuple!("user", "channel", "message")(user, channel, msg);
3199 		};
3200 		client.onError = (const IRCError error, const MessageMetadata) {
3201 			errors ~= error;
3202 		};
3203 
3204 		setupFakeConnection(client);
3205 		client.changeTopic(Target(Channel("#test")), "This is a new topic");
3206 		client.put(":"~testUser.text~" TOPIC #test :This is a new topic");
3207 		client.put(":"~testUser.text~" TOPIC #test"); //Malformed
3208 		client.put(":"~testUser.text~" TOPIC"); //Malformed
3209 
3210 		auto lineByLine = client.output.data.lineSplitter();
3211 		assert(lineByLine.array[$-1] == "TOPIC #test :This is a new topic");
3212 		assert(topics.length == 1);
3213 		with(topics[0]) {
3214 			assert(channel == Channel("#test"));
3215 			assert(message == "This is a new topic");
3216 		}
3217 		assert(errors.length == 2);
3218 		assert(errors[0].type == ErrorType.malformed);
3219 		assert(errors[1].type == ErrorType.malformed);
3220 	}
3221 	//Request capabilities (IRC v3.2) - Missing prefix
3222 	{
3223 		auto client = spawnNoBufferClient();
3224 		client.put("CAP * LS :multi-prefix sasl");
3225 		client.put("CAP * ACK :multi-prefix sasl");
3226 
3227 		auto lineByLine = client.output.data.lineSplitter;
3228 		lineByLine.popFront();
3229 		lineByLine.popFront();
3230 		lineByLine.popFront();
3231 		//sasl not yet supported
3232 		assert(lineByLine.front == "CAP REQ :multi-prefix sasl");
3233 		lineByLine.popFront();
3234 		assert(!lineByLine.empty);
3235 		assert(lineByLine.front == "CAP END");
3236 	}
3237 	{ //METADATA tests
3238 		auto client = spawnNoBufferClient();
3239 		IRCError[] errors;
3240 		client.onError = (const IRCError error, const MessageMetadata md) {
3241 			errors ~= error;
3242 		};
3243 		string[] subs;
3244 		client.onMetadataSubList = (const string str, const MessageMetadata) {
3245 			subs ~= str;
3246 		};
3247 		initializeWithCaps(client, [Capability("draft/metadata-2", "foo,max-subs=50,max-keys=25,bar"), Capability("draft/metadata-notify-2")]);
3248 
3249 
3250 		assert(client.state.maxMetadataSubscriptions == 50);
3251 		assert(client.state.maxMetadataSelfKeys == 25);
3252 
3253 		client.setMetadata("url", "http://www.example.com");
3254 		assert(client.output.data.lineSplitter().array[$-1] == "METADATA * SET url :http://www.example.com");
3255 		client.put(":irc.example.com 761 client * url * :http://www.example.com");
3256 		assert(client.ownMetadata["url"] == "http://www.example.com");
3257 
3258 		client.setMetadata("url", "http://www.example.com");
3259 		client.put("FAIL METADATA LIMIT_REACHED :Metadata limit reached");
3260 		assert(errors.length == 1);
3261 		with(errors[0]) {
3262 			assert(type == ErrorType.standardFail);
3263 		}
3264 
3265 		client.setMetadata(User("user1"), "url", "http://www.example.com");
3266 		assert(client.output.data.lineSplitter().array[$ - 1] == "METADATA user1 SET url :http://www.example.com");
3267 		client.put("FAIL METADATA KEY_NO_PERMISSION url user1 :You do not have permission to set 'url' on 'user1'");
3268 		assert(errors.length == 2);
3269 		with(errors[1]) {
3270 			assert(type == ErrorType.standardFail);
3271 		}
3272 
3273 		client.setMetadata(Channel("#example"), "url", "http://www.example.com");
3274 		assert(client.output.data.lineSplitter().array[$ - 1] == "METADATA #example SET url :http://www.example.com");
3275 		client.put(":irc.example.com 761 client #example url * :http://www.example.com");
3276 		assert(client.channelMetadata[Channel("#example")]["url"] == "http://www.example.com");
3277 
3278 		client.setMetadata(User("$a:user"), "url", "http://www.example.com");
3279 		client.put("FAIL METADATA INVALID_TARGET $a:user :Invalid target.");
3280 		assert(errors.length == 3);
3281 		with(errors[2]) {
3282 			assert(type == ErrorType.standardFail);
3283 		}
3284 
3285 		client.setMetadata(User("user1"), "$url$", "http://www.example.com");
3286 		client.put("FAIL METADATA KEY_INVALID $url$ user1 :Invalid key.");
3287 		assert(errors.length == 4);
3288 		with(errors[3]) {
3289 			assert(type == ErrorType.standardFail);
3290 		}
3291 
3292 		client.setMetadata("url", "http://www.example.com");
3293 		client.put("FAIL METADATA RATE_LIMITED * url 5 :Rate-limit reached. You're going too fast! Try again in 5 seconds.");
3294 		assert(errors.length == 5);
3295 		with(errors[4]) {
3296 			assert(type == ErrorType.standardFail);
3297 		}
3298 
3299 		client.setMetadata("url", "http://www.example.com");
3300 		client.put("FAIL METADATA RATE_LIMITED * url * :Rate-limit reached. You're going too fast!");
3301 		assert(errors.length == 6);
3302 		with(errors[5]) {
3303 			assert(type == ErrorType.standardFail);
3304 		}
3305 
3306 		client.put(":irc.example.com METADATA user1 account * :user1");
3307 		assert(client.userMetadata[User("user1")]["account"] == "user1");
3308 
3309 		client.put(":user1!~user@somewhere.example.com METADATA #example url * :http://www.example.com");
3310 		assert(client.channelMetadata[Channel("#example")]["url"] == "http://www.example.com");
3311 
3312 		client.put(":irc.example.com METADATA #example wiki-url * :http://wiki.example.com");
3313 		assert(client.channelMetadata[Channel("#example")]["wiki-url"] == "http://wiki.example.com");
3314 
3315 		client.listMetadata(User("user1"));
3316 		client.put(":irc.example.com BATCH +VUN2ot metadata");
3317 		client.put("@batch=VUN2ot :irc.example.com 761 client user1 url * :http://www.example.com");
3318 		client.put("@batch=VUN2ot :irc.example.com 761 client user1 im.xmpp * :user1@xmpp.example.com");
3319 		client.put("@batch=VUN2ot :irc.example.com 761 client user1 bot-likeliness-score visible-only-for-admin :42");
3320 		client.put(":irc.example.com BATCH -VUN2ot");
3321 		assert(client.userMetadata[User("user1")]["url"] == "http://www.example.com");
3322 		assert(client.userMetadata[User("user1")]["im.xmpp"] == "user1@xmpp.example.com");
3323 		assert(client.userMetadata[User("user1")]["bot-likeliness-score"] == "42");
3324 
3325 		client.userMetadata.remove(User("user1"));
3326 
3327 		client.getMetadata(User("user1"), "blargh", "splot", "im.xmpp");
3328 		client.put(":irc.example.com BATCH +gWkCiV metadata");
3329 		client.put("@batch=gWkCiV 766 client user1 blargh :No matching key");
3330 		client.put("@batch=gWkCiV 766 client user1 splot :No matching key");
3331 		client.put("@batch=gWkCiV :irc.example.com 761 client user1 im.xmpp * :user1@xmpp.example.com");
3332 		client.put(":irc.example.com BATCH -gWkCiV");
3333 		assert("blargh" !in client.userMetadata[User("user1")]);
3334 		assert("splot" !in client.userMetadata[User("user1")]);
3335 		assert(client.userMetadata[User("user1")]["im.xmpp"] == "user1@xmpp.example.com");
3336 		assert(errors.length == 8);
3337 		with(errors[6]) {
3338 			assert(type == ErrorType.keyNotSet);
3339 		}
3340 		with(errors[7]) {
3341 			assert(type == ErrorType.keyNotSet);
3342 		}
3343 
3344 		client.join(Channel("#smallchan"));
3345 		client.put(":modernclient!modernclient@example.com JOIN #smallchan");
3346 		client.put(":irc.example.com 353 modernclient @ #smallchan :user1 user2 user3 user4 user5 ...");
3347 		client.put(":irc.example.com 353 modernclient @ #smallchan :user51 user52 user53 user54 ...");
3348 		client.put(":irc.example.com 353 modernclient @ #smallchan :user101 user102 user103 user104 ...");
3349 		client.put(":irc.example.com 353 modernclient @ #smallchan :user151 user152 user153 user154 ...");
3350 		client.put(":irc.example.com 366 modernclient #smallchan :End of /NAMES list.");
3351 		client.put(":irc.example.com BATCH +UwZ67M metadata");
3352 		client.put("@batch=UwZ67M :irc.example.com METADATA user2 bar * :second example value ");
3353 		client.put("@batch=UwZ67M :irc.example.com METADATA user1 foo * :third example value");
3354 		client.put("@batch=UwZ67M :irc.example.com METADATA user1 bar * :this is another example value");
3355 		client.put("@batch=UwZ67M :irc.example.com METADATA user3 website * :www.example.com");
3356 		client.put(":irc.example.com BATCH -UwZ67M");
3357 		assert(client.userMetadata[User("user1")]["foo"] == "third example value");
3358 		assert(client.userMetadata[User("user1")]["bar"] == "this is another example value");
3359 		assert(client.userMetadata[User("user2")]["bar"] == "second example value ");
3360 		assert(client.userMetadata[User("user3")]["website"] == "www.example.com");
3361 
3362 		client.join(Channel("#bigchan"));
3363 		client.put(":modernclient!modernclient@example.com JOIN #bigchan");
3364 		client.put(":irc.example.com 353 modernclient @ #bigchan :user1 user2 user3 user4 user5 ...");
3365 		client.put(":irc.example.com 353 modernclient @ #bigchan :user51 user52 user53 user54 ...");
3366 		client.put(":irc.example.com 353 modernclient @ #bigchan :user101 user102 user103 user104 ...");
3367 		client.put(":irc.example.com 353 modernclient @ #bigchan :user151 user152 user153 user154 ...");
3368 		client.put(":irc.example.com 366 modernclient #bigchan :End of /NAMES list.");
3369 		client.put(":irc.example.com 774 modernclient #bigchan 4");
3370 		assert(errors.length == 9);
3371 		with(errors[8]) {
3372 			assert(type == ErrorType.waitAndRetry);
3373 		}
3374 
3375 		client.syncMetadata(Channel("#bigchan"));
3376 		client.put(":irc.example.com 774 modernclient #bigchan 6");
3377 		assert(errors.length == 10);
3378 		with(errors[9]) {
3379 			assert(type == ErrorType.waitAndRetry);
3380 		}
3381 
3382 		client.syncMetadata(Channel("#bigchan"));
3383 		client.put(":irc.example.com BATCH +O5J6rk metadata");
3384 		client.put("@batch=O5J6rk :irc.example.com METADATA user52 foo * :example value 1");
3385 		client.put("@batch=O5J6rk :irc.example.com METADATA user2 bar * :second example value ");
3386 		client.put("@batch=O5J6rk :irc.example.com METADATA user1 foo * :third example value");
3387 		client.put("@batch=O5J6rk :irc.example.com METADATA user1 bar * :this is another example value");
3388 		client.put("@batch=O5J6rk :irc.example.com METADATA user152 baz * :Lorem ipsum");
3389 		client.put("@batch=O5J6rk :irc.example.com METADATA user3 website * :www.example.com");
3390 		client.put("@batch=O5J6rk :irc.example.com METADATA user152 bar * :dolor sit amet");
3391 		client.put(":irc.example.com BATCH -O5J6rk");
3392 		assert(client.userMetadata[User("user1")]["foo"] == "third example value");
3393 		assert(client.userMetadata[User("user1")]["bar"] == "this is another example value");
3394 		assert(client.userMetadata[User("user2")]["bar"] == "second example value ");
3395 		assert(client.userMetadata[User("user3")]["website"] == "www.example.com");
3396 		assert(client.userMetadata[User("user52")]["foo"] == "example value 1");
3397 		assert(client.userMetadata[User("user152")]["baz"] == "Lorem ipsum");
3398 		assert(client.userMetadata[User("user152")]["bar"] == "dolor sit amet");
3399 
3400 		client.subscribeMetadata("avatar", "website", "foo", "bar");
3401 		client.put(":irc.example.com 770 modernclient avatar website foo bar");
3402 		assert(client.isSubscribed("avatar"));
3403 		assert(client.isSubscribed("website"));
3404 		assert(client.isSubscribed("foo"));
3405 		assert(client.isSubscribed("bar"));
3406 		client.unsubscribeMetadata("foo", "bar");
3407 		client.put(":irc.example.com 771 modernclient bar foo");
3408 		assert(!client.isSubscribed("foo"));
3409 		assert(!client.isSubscribed("bar"));
3410 
3411 		client.subscribeMetadata("avatar", "website", "foo", "bar", "baz");
3412 		client.put(":irc.example.com 770 modernclient avatar website");
3413 		client.put(":irc.example.com 770 modernclient foo");
3414 		client.put(":irc.example.com 770 modernclient bar baz");
3415 		assert(client.isSubscribed("avatar"));
3416 		assert(client.isSubscribed("website"));
3417 		assert(client.isSubscribed("foo"));
3418 		assert(client.isSubscribed("bar"));
3419 		assert(client.isSubscribed("baz"));
3420 
3421 		client.subscribeMetadata("foo", "$url", "bar");
3422 		client.put(":irc.example.com 770 modernclient foo bar");
3423 		client.put("FAIL METADATA KEY_INVALID $url :Invalid key");
3424 		assert(errors.length == 11);
3425 		with(errors[10]) {
3426 			assert(type == ErrorType.standardFail);
3427 		}
3428 
3429 		// uh oh zone
3430 		client.state.metadataSubscribedKeys = [];
3431 		client.subscribeMetadata("website", "avatar", "foo", "bar", "baz");
3432 		client.put(":irc.example.com 770 modernclient website avatar foo bar baz");
3433 		client.subscribeMetadata("email", "city");
3434 		client.put("FAIL METADATA TOO_MANY_SUBS email :Too many subscriptions!");
3435 		client.listSubscribedMetadata();
3436 		client.put(":irc.example.com 772 modernclient website avatar foo bar baz");
3437 		assert(errors.length == 12);
3438 		with(errors[11]) {
3439 			assert(type == ErrorType.standardFail);
3440 		}
3441 		assert(client.isSubscribed("website"));
3442 		assert(client.isSubscribed("avatar"));
3443 		assert(client.isSubscribed("foo"));
3444 		assert(client.isSubscribed("bar"));
3445 		assert(client.isSubscribed("baz"));
3446 		assert(!client.isSubscribed("email"));
3447 		assert(!client.isSubscribed("city"));
3448 
3449 		client.state.metadataSubscribedKeys = [];
3450 		client.subscribeMetadata("website", "avatar", "foo");
3451 		client.put(":irc.example.com 770 modernclient website avatar foo");
3452 		client.subscribeMetadata("email", "city", "country", "bar", "baz");
3453 		client.put("FAIL METADATA TOO_MANY_SUBS country :Too many subscriptions!");
3454 		client.put(":irc.example.com 770 modernclient email city");
3455 		client.listSubscribedMetadata();
3456 		client.put(":irc.example.com 772 modernclient website avatar city foo email");
3457 		assert(errors.length == 13);
3458 		with(errors[12]) {
3459 			assert(type == ErrorType.standardFail);
3460 		}
3461 		assert(client.isSubscribed("website"));
3462 		assert(client.isSubscribed("avatar"));
3463 		assert(client.isSubscribed("foo"));
3464 		assert(client.isSubscribed("email"));
3465 		assert(client.isSubscribed("city"));
3466 		assert(!client.isSubscribed("country"));
3467 		assert(!client.isSubscribed("bar"));
3468 		assert(!client.isSubscribed("baz"));
3469 
3470 		client.state.metadataSubscribedKeys = [];
3471 		client.subscribeMetadata("avatar", "website");
3472 		client.put(":irc.example.com 770 modernclient avatar website");
3473 		client.subscribeMetadata("foo", "avatar", "website");
3474 		client.put("FAIL METADATA TOO_MANY_SUBS website :Too many subscriptions!");
3475 		client.put(":irc.example.com 770 modernclient foo");
3476 		client.listSubscribedMetadata();
3477 		client.put(":irc.example.com 772 modernclient avatar foo website");
3478 		assert(errors.length == 14);
3479 		with(errors[13]) {
3480 			assert(type == ErrorType.standardFail);
3481 		}
3482 		assert(client.isSubscribed("avatar"));
3483 		assert(client.isSubscribed("foo"));
3484 		assert(client.isSubscribed("website"));
3485 		assert(!client.isSubscribed("country"));
3486 		assert(!client.isSubscribed("bar"));
3487 		assert(!client.isSubscribed("baz"));
3488 
3489 		client.state.metadataSubscribedKeys = [];
3490 		client.subscribeMetadata("website", "avatar", "foo", "bar", "baz");
3491 		client.put(":irc.example.com 770 modernclient website avatar foo bar baz");
3492 		client.listSubscribedMetadata();
3493 		client.put(":irc.example.com 772 modernclient avatar bar baz foo website");
3494 		assert(client.isSubscribed("avatar"));
3495 		assert(client.isSubscribed("foo"));
3496 		assert(client.isSubscribed("website"));
3497 		assert(client.isSubscribed("bar"));
3498 		assert(client.isSubscribed("baz"));
3499 
3500 		client.state.metadataSubscribedKeys = [];
3501 		client.subscribeMetadata("website", "avatar", "foo", "bar", "baz");
3502 		client.put(":irc.example.com 770 modernclient website avatar foo bar baz");
3503 		client.listSubscribedMetadata();
3504 		client.put(":irc.example.com 772 modernclient avatar");
3505 		client.put(":irc.example.com 772 modernclient bar baz");
3506 		client.put(":irc.example.com 772 modernclient foo website");
3507 		assert(client.isSubscribed("avatar"));
3508 		assert(client.isSubscribed("foo"));
3509 		assert(client.isSubscribed("website"));
3510 		assert(client.isSubscribed("bar"));
3511 		assert(client.isSubscribed("baz"));
3512 
3513 		client.state.metadataSubscribedKeys = [];
3514 		client.listSubscribedMetadata();
3515 
3516 		client.state.metadataSubscribedKeys = [];
3517 		client.subscribeMetadata("website", "avatar", "foo", "bar", "baz");
3518 		client.put(":irc.example.com 770 modernclient website avatar foo bar baz");
3519 		client.listSubscribedMetadata();
3520 		client.put(":irc.example.com 772 modernclient avatar bar baz foo website");
3521 		client.unsubscribeMetadata("bar", "foo", "baz");
3522 		client.put(":irc.example.com 771 modernclient baz foo bar");
3523 		client.listSubscribedMetadata();
3524 		client.put(":irc.example.com 772 modernclient avatar website");
3525 		assert(client.isSubscribed("avatar"));
3526 		assert(client.isSubscribed("website"));
3527 
3528 		client.state.metadataSubscribedKeys = [];
3529 		client.subscribeMetadata("website", "avatar", "foo", "bar", "baz");
3530 		client.put(":irc.example.com 770 modernclient website avatar foo bar baz");
3531 		client.listSubscribedMetadata();
3532 		client.put(":irc.example.com 772 modernclient avatar bar baz foo website");
3533 		client.subscribeMetadata("avatar", "website");
3534 		client.put(":irc.example.com 770 modernclient avatar website");
3535 		client.listSubscribedMetadata();
3536 		client.put(":irc.example.com 772 modernclient avatar bar baz foo website");
3537 		assert(client.isSubscribed("avatar"));
3538 		assert(client.isSubscribed("website"));
3539 		assert(client.isSubscribed("foo"));
3540 		assert(client.isSubscribed("bar"));
3541 		assert(client.isSubscribed("baz"));
3542 
3543 		client.state.metadataSubscribedKeys = [];
3544 		client.subscribeMetadata("avatar", "avatar");
3545 		client.put(":irc.example.com 770 modernclient avatar");
3546 		client.listSubscribedMetadata();
3547 		client.put(":irc.example.com 772 modernclient avatar");
3548 		assert(client.isSubscribed("avatar"));
3549 
3550 		client.state.metadataSubscribedKeys = [];
3551 		client.subscribeMetadata("avatar", "avatar");
3552 		client.put(":irc.example.com 770 modernclient avatar avatar");
3553 		client.listSubscribedMetadata();
3554 		client.put(":irc.example.com 772 modernclient avatar");
3555 		assert(client.isSubscribed("avatar"));
3556 
3557 		client.state.metadataSubscribedKeys = [];
3558 		client.listSubscribedMetadata();
3559 		client.unsubscribeMetadata("website");
3560 		client.put(":irc.example.com 771 modernclient website");
3561 		assert(!client.isSubscribed("website"));
3562 		client.listSubscribedMetadata();
3563 		client.subscribeMetadata("website");
3564 		client.put(":irc.example.com 770 modernclient website");
3565 		client.listSubscribedMetadata();
3566 		client.put(":irc.example.com 772 modernclient website");
3567 		assert(client.isSubscribed("website"));
3568 
3569 		client.state.metadataSubscribedKeys = [];
3570 		client.listSubscribedMetadata();
3571 		client.put(":irc.example.com 772 modernclient website");
3572 		client.unsubscribeMetadata("website", "website");
3573 		client.put(":irc.example.com 771 modernclient website");
3574 		assert(!client.isSubscribed("website"));
3575 
3576 		client.state.metadataSubscribedKeys = [];
3577 		client.listSubscribedMetadata();
3578 		client.put(":irc.example.com 772 modernclient website");
3579 		client.unsubscribeMetadata("website", "website");
3580 		client.put(":irc.example.com 771 modernclient website website");
3581 		assert(!client.isSubscribed("website"));
3582 
3583 		client.state.metadataSubscribedKeys = [];
3584 		client.subscribeMetadata("avatar", "secretkey", "website");
3585 		client.put("FAIL METADATA KEY_NO_PERMISSION modernclient secretkey :You do not have permission to do that.");
3586 		client.put(":irc.example.com 770 modernclient avatar website");
3587 		client.listSubscribedMetadata();
3588 		client.put(":irc.example.com 772 modernclient avatar website");
3589 		assert(!client.isSubscribed("secretkey"));
3590 		assert(client.isSubscribed("website"));
3591 		assert(client.isSubscribed("avatar"));
3592 		assert(errors.length == 15);
3593 		with(errors[14]) {
3594 			assert(type == ErrorType.standardFail);
3595 		}
3596 
3597 		client.state.metadataSubscribedKeys = [];
3598 		client.subscribeMetadata("$invalid1", "secretkey1", "$invalid2", "secretkey2", "website");
3599 		client.put("FAIL METADATA KEY_NO_PERMISSION modernclient secretkey1 :You do not have permission to do that.");
3600 		client.put("FAIL METADATA KEY_INVALID $invalid1 modernclient :Invalid key");
3601 		client.put("FAIL METADATA KEY_NO_PERMISSION modernclient secretkey2 :You do not have permission to do that.");
3602 		client.put("FAIL METADATA KEY_INVALID $invalid2 modernclient :Invalid key");
3603 		client.put(":irc.example.com 770 modernclient website");
3604 		client.listSubscribedMetadata();
3605 		client.put(":irc.example.com 772 modernclient website");
3606 		assert(!client.isSubscribed("$invalid1"));
3607 		assert(!client.isSubscribed("secretkey1"));
3608 		assert(!client.isSubscribed("$invalid2"));
3609 		assert(!client.isSubscribed("secretkey2"));
3610 		assert(client.isSubscribed("website"));
3611 		assert(errors.length == 19);
3612 		with(errors[15]) {
3613 			assert(type == ErrorType.standardFail);
3614 		}
3615 		with(errors[16]) {
3616 			assert(type == ErrorType.standardFail);
3617 		}
3618 		with(errors[17]) {
3619 			assert(type == ErrorType.standardFail);
3620 		}
3621 		with(errors[18]) {
3622 			assert(type == ErrorType.standardFail);
3623 		}
3624 
3625 		// end of uh oh zone
3626 
3627 	}
3628 	{ //Multiline messages
3629 		auto client = spawnNoBufferClient();
3630 		initializeWithCaps(client, [Capability("draft/multiline", "max-bytes=1000,max-lines=100"), Capability("message-tags")]);
3631 		{
3632 			client.msg("someoneElse", "This is a message\nwith newlines in it.\nDo not be alarmed.");
3633 			auto lines = client.output.data.lineSplitter.array;
3634 			assert(lines[$ - 5] == "BATCH +0 draft/multiline someoneElse");
3635 			assert(lines[$ - 4] == "@batch=0 PRIVMSG someoneElse :This is a message");
3636 			assert(lines[$ - 3] == "@batch=0 PRIVMSG someoneElse :with newlines in it.");
3637 			assert(lines[$ - 2] == "@batch=0 PRIVMSG someoneElse :Do not be alarmed.");
3638 			assert(lines[$ - 1] == "BATCH -0");
3639 		}
3640 		{
3641 			client.msg("someoneElse", "This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long message.");
3642 			auto lines = client.output.data.lineSplitter.array;
3643 			assert(lines[$ - 4] == "BATCH +1 draft/multiline someoneElse");
3644 			assert(lines[$ - 3] == "@batch=1 PRIVMSG someoneElse :This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very ");
3645 			assert(lines[$ - 2] == "@batch=1;draft/multiline-concat PRIVMSG someoneElse :very very very very very very very very very very very very very very long message.");
3646 			assert(lines[$ - 1] == "BATCH -1");
3647 		}
3648 		{
3649 			client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting.");
3650 			auto lines = client.output.data.lineSplitter.array;
3651 			assert(lines[$ - 4] == "BATCH +2 draft/multiline someoneElse");
3652 			assert(lines[$ - 3] == "@batch=2 PRIVMSG someoneElse :\x1FThis is a simple message");
3653 			assert(lines[$ - 2] == "@batch=2 PRIVMSG someoneElse :\x1Fwith newlines and formatting.");
3654 			assert(lines[$ - 1] == "BATCH -2");
3655 		}
3656 		{
3657 			IRCTags reply;
3658 			reply.reply("replyid");
3659 			client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting.", reply);
3660 			auto lines = client.output.data.lineSplitter.array;
3661 			assert(lines[$ - 4] == "@+draft/reply=replyid BATCH +3 draft/multiline someoneElse");
3662 			assert(lines[$ - 3] == "@batch=3 PRIVMSG someoneElse :\x1FThis is a simple message");
3663 			assert(lines[$ - 2] == "@batch=3 PRIVMSG someoneElse :\x1Fwith newlines and formatting.");
3664 			assert(lines[$ - 1] == "BATCH -3");
3665 		}
3666 		{
3667 			client.notice("someoneElse", "This is a message\nwith newlines in it.\nDo not be alarmed.");
3668 			auto lines = client.output.data.lineSplitter.array;
3669 			assert(lines[$ - 5] == "BATCH +4 draft/multiline someoneElse");
3670 			assert(lines[$ - 4] == "@batch=4 NOTICE someoneElse :This is a message");
3671 			assert(lines[$ - 3] == "@batch=4 NOTICE someoneElse :with newlines in it.");
3672 			assert(lines[$ - 2] == "@batch=4 NOTICE someoneElse :Do not be alarmed.");
3673 			assert(lines[$ - 1] == "BATCH -4");
3674 		}
3675 		{
3676 			Message[] messages;
3677 			const(MessageMetadata)[] metadata;
3678 			client.onMessage = (const User, const Target, const Message msg, const MessageMetadata m) {
3679 				messages ~= msg;
3680 				metadata ~= m;
3681 			};
3682 			client.put("@msgid=xxx;account=account :n!u@h BATCH +123 draft/multiline #channel");
3683 			client.put("@batch=123 :n!u@h PRIVMSG #channel hello");
3684 			client.put("@batch=123 :n!u@h PRIVMSG #channel :");
3685 			client.put("@batch=123 :n!u@h PRIVMSG #channel :how is ");
3686 			client.put("@batch=123;draft/multiline-concat :n!u@h PRIVMSG #channel :everyone?");
3687 			client.put("BATCH -123");
3688 			assert(messages.length == 1);
3689 			assert(messages[0].type == MessageType.privmsg);
3690 			assert(messages[0].msg == "hello\n\nhow is everyone?");
3691 			assert(metadata[0].tags["msgid"] == "xxx");
3692 			client.put("@msgid=xxx2;account=account :n!u@h BATCH +123 draft/multiline #channel");
3693 			client.put("@batch=123 :n!u@h PRIVMSG #channel :");
3694 			client.put("@batch=123 :n!u@h PRIVMSG #channel hello");
3695 			client.put("BATCH -123");
3696 			assert(messages.length == 2);
3697 			assert(messages[1].msg == "\nhello");
3698 			assert(metadata[1].tags["msgid"] == "xxx2");
3699 			client.put("@msgid=xxx3;account=account :n!u@h BATCH +123 draft/multiline #channel");
3700 			client.put("@batch=123 :n!u@h PRIVMSG #channel hello");
3701 			client.put("@batch=123 :n!u@h PRIVMSG #channel :");
3702 			client.put("BATCH -123");
3703 			assert(messages.length == 3);
3704 			assert(messages[2].msg == "hello\n");
3705 			assert(metadata[2].tags["msgid"] == "xxx3");
3706 			client.put("@msgid=xxx;account=account :n!u@h BATCH +123 draft/multiline #channel");
3707 			client.put("@batch=123 :n!u@h NOTICE #channel hello");
3708 			client.put("@batch=123 :n!u@h NOTICE #channel :");
3709 			client.put("@batch=123 :n!u@h NOTICE #channel :how is ");
3710 			client.put("@batch=123;draft/multiline-concat :n!u@h NOTICE #channel :everyone?");
3711 			client.put("BATCH -123");
3712 			assert(messages.length == 4);
3713 			assert(messages[3].type == MessageType.notice);
3714 			assert(messages[3].msg == "hello\n\nhow is everyone?");
3715 			assert(metadata[3].tags["msgid"] == "xxx");
3716 		}
3717 	}
3718 	{ //Multiline messages, low line limit
3719 		auto client = spawnNoBufferClient();
3720 		initializeWithCaps(client, [Capability("draft/multiline", "max-bytes=1000,max-lines=2"), Capability("message-tags")]);
3721 		{
3722 			client.msg("someoneElse", "This is a message\nwith newlines in it.\nDo not be alarmed.\nIt's fine.");
3723 			auto lines = client.output.data.lineSplitter.array;
3724 			assert(lines[$ - 8] == "BATCH +0 draft/multiline someoneElse");
3725 			assert(lines[$ - 7] == "@batch=0 PRIVMSG someoneElse :This is a message");
3726 			assert(lines[$ - 6] == "@batch=0 PRIVMSG someoneElse :with newlines in it.");
3727 			assert(lines[$ - 5] == "BATCH -0");
3728 			assert(lines[$ - 4] == "BATCH +1 draft/multiline someoneElse");
3729 			assert(lines[$ - 3] == "@batch=1 PRIVMSG someoneElse :Do not be alarmed.");
3730 			assert(lines[$ - 2] == "@batch=1 PRIVMSG someoneElse :It's fine.");
3731 			assert(lines[$ - 1] == "BATCH -1");
3732 		}
3733 	}
3734 	{ //Multiline messages, low byte limit
3735 		auto client = spawnNoBufferClient();
3736 		initializeWithCaps(client, [Capability("draft/multiline", "max-bytes=600,max-lines=100"), Capability("message-tags")]);
3737 		{
3738 			client.msg("someoneElse", "This is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long message");
3739 			auto lines = client.output.data.lineSplitter.array;
3740 			assert(lines[$ - 7] == "BATCH +0 draft/multiline someoneElse");
3741 			assert(lines[$ - 6] == "@batch=0 PRIVMSG someoneElse :This is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very v");
3742 			assert(lines[$ - 5] == "@batch=0;draft/multiline-concat PRIVMSG someoneElse :ery very very very very very very very very very very very very very very very very very very very very very very very very very very very ");
3743 			assert(lines[$ - 4] == "BATCH -0");
3744 			assert(lines[$ - 3] == "BATCH +1 draft/multiline someoneElse");
3745 			assert(lines[$ - 2] == "@batch=1 PRIVMSG someoneElse :very very very very very very very very very very long message");
3746 			assert(lines[$ - 1] == "BATCH -1");
3747 		}
3748 	}
3749 	{ //Multiline messages (no cap)
3750 		auto client = spawnNoBufferClient();
3751 		initializeWithCaps(client, [Capability("message-tags")]);
3752 		{
3753 			client.msg("someoneElse", "This is a message\nwith newlines in it.\nDo not be alarmed.");
3754 			auto lines = client.output.data.lineSplitter.array;
3755 			assert(lines[$ - 3] == "PRIVMSG someoneElse :This is a message");
3756 			assert(lines[$ - 2] == "PRIVMSG someoneElse :with newlines in it.");
3757 			assert(lines[$ - 1] == "PRIVMSG someoneElse :Do not be alarmed.");
3758 		}
3759 		{
3760 			client.msg("someoneElse", "This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long message.");
3761 			auto lines = client.output.data.lineSplitter.array;
3762 			assert(lines[$ - 2] == "PRIVMSG someoneElse :This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very ");
3763 			assert(lines[$ - 1] == "PRIVMSG someoneElse :very very very very very very very very very very very very very very long message.");
3764 		}
3765 		{
3766 			client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting.");
3767 			auto lines = client.output.data.lineSplitter.array;
3768 			assert(lines[$ - 2] == "PRIVMSG someoneElse :\x1FThis is a simple message");
3769 			assert(lines[$ - 1] == "PRIVMSG someoneElse :\x1Fwith newlines and formatting.");
3770 		}
3771 		{
3772 			IRCTags reply;
3773 			reply.reply("replyid");
3774 			client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting.", reply);
3775 			auto lines = client.output.data.lineSplitter.array;
3776 			assert(lines[$ - 2] == "@+draft/reply=replyid PRIVMSG someoneElse :\x1FThis is a simple message");
3777 			assert(lines[$ - 1] == "@+draft/reply=replyid PRIVMSG someoneElse :\x1Fwith newlines and formatting.");
3778 		}
3779 	}
3780 	{ //message-tags
3781 		auto client = spawnNoBufferClient();
3782 		Tuple!(const User, "user", const Target, "target", const Message, "message", const MessageMetadata, "metadata")[] messages;
3783 		client.onMessage = (const User user, const Target target, const Message msg, const MessageMetadata metadata) {
3784 			messages ~= tuple!("user", "target", "message", "metadata")(user, target, msg, metadata);
3785 		};
3786 
3787 		setupFakeConnection(client);
3788 
3789 		// a basic example
3790 		client.put("@msgid=abc;+example-client-tag=example-value :nick!user@example.com TAGMSG #channel");
3791 		assert(messages.length == 1);
3792 		with (messages[0]) {
3793 			assert(user == User("nick!user@example.com"));
3794 			assert(target.isChannel);
3795 			assert(!target.isNickname);
3796 			assert(target == Channel("#channel"));
3797 			assert(message == "");
3798 			assert(message.isReplyable);
3799 			assert(metadata.tags["msgid"] == "abc");
3800 			assert(metadata.tags["+example-client-tag"] == "example-value");
3801 		}
3802 		// no tags
3803 		client.put(":nick!user@example.com TAGMSG #channel");
3804 		assert(messages.length == 2);
3805 		with (messages[1]) {
3806 			assert(user == User("nick!user@example.com"));
3807 			assert(target.isChannel);
3808 			assert(!target.isNickname);
3809 			assert(target == Channel("#channel"));
3810 			assert(message == "");
3811 			assert(message.isReplyable);
3812 			assert(metadata.tags == null);
3813 		}
3814 	}
3815 }