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