1 /++
2 +
3 +/
4 module virc.numerics.rfc1459;
5 
6 import virc.modes : ModeType;
7 import virc.numerics.definitions;
8 
9 /++
10 +
11 +/
12 struct LUserClient {
13 	///
14 	string message;
15 	///
16 	this(string msg) pure @safe {
17 		message = msg;
18 	}
19 }
20 /++
21 +
22 +/
23 struct LUserMe {
24 	///
25 	string message;
26 	///
27 	this(string msg) pure @safe {
28 		message = msg;
29 	}
30 }
31 /++
32 +
33 +/
34 struct LUserChannels {
35 	///
36 	ulong numChannels;
37 	///
38 	string message;
39 	///
40 	this(string chans, string msg) pure @safe {
41 		import std.conv : to;
42 		numChannels = chans.to!ulong;
43 		message = msg;
44 	}
45 }
46 /++
47 +
48 +/
49 struct LUserOp {
50 	///
51 	ulong numOperators;
52 	///
53 	string message;
54 	///
55 	this(string ops, string msg) pure @safe {
56 		import std.conv : to;
57 		numOperators = ops.to!ulong;
58 		message = msg;
59 	}
60 }
61 /++
62 + RPL_VERSION reply contents.
63 +/
64 struct VersionReply {
65 	import virc.common : User;
66 	User me;
67 	///The responding server's version string.
68 	string version_;
69 	///The server hostmask responding to the version query
70 	string server;
71 	///Contents depend on server, but are usually related to version
72 	string comments;
73 }
74 
75 /++
76 + RPL_REHASHING reply contents.
77 +/
78 struct RehashingReply {
79 	import virc.common : User;
80 	User me;
81 	///The config file being rehashed.
82 	string configFile;
83 	///Message displayed to user.
84 	string message;
85 }
86 
87 /++
88 +
89 +/
90 //251 :There are <users> users and <services> services on <servers> servers
91 //TODO: Find out if this is safe to parse
92 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSERCLIENT, T)(T input) {
93 	input.popFront();
94 	return LUserClient(input.front);
95 }
96 ///
97 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSERCLIENT
98 	import std.range : only;
99 	{
100 		immutable luser = parseNumeric!(Numeric.RPL_LUSERCLIENT)(only("someone", "There are 42 users and 43 services on 44 servers"));
101 		assert(luser.message == "There are 42 users and 43 services on 44 servers");
102 	}
103 }
104 /++
105 +
106 +/
107 //252 <opers> :operator(s) online
108 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSEROP, T)(T input) {
109 	input.popFront();
110 	auto ops = input.front;
111 	input.popFront();
112 	auto msg = input.front;
113 	auto output = LUserOp(ops, msg);
114 	return output;
115 }
116 ///
117 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSEROP
118 	import std.range : only;
119 	{
120 		immutable luser = parseNumeric!(Numeric.RPL_LUSEROP)(only("someone", "45", "operator(s) online"));
121 		assert(luser.numOperators == 45);
122 		assert(luser.message == "operator(s) online");
123 	}
124 }
125 
126 /++
127 +
128 +/
129 //254 <channels> :channels formed
130 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSERCHANNELS, T)(T input) {
131 	input.popFront();
132 	auto chans = input.front;
133 	input.popFront();
134 	auto msg = input.front;
135 	auto output = LUserChannels(chans, msg);
136 	return output;
137 }
138 ///
139 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSERCHANNELS
140 	import std.range : only;
141 	{
142 		immutable luser = parseNumeric!(Numeric.RPL_LUSERCHANNELS)(only("someone", "46", "channels formed"));
143 		assert(luser.numChannels == 46);
144 		assert(luser.message == "channels formed");
145 	}
146 }
147 /++
148 +
149 +/
150 //255 :I have <clients> clients and <servers> servers
151 //TODO: Find out if this is safe to parse
152 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSERME, T)(T input) {
153 	input.popFront();
154 	return LUserMe(input.front);
155 }
156 ///
157 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSERME
158 	import std.range : only;
159 	{
160 		immutable luser = parseNumeric!(Numeric.RPL_LUSERME)(only("someone", "I have 47 clients and 48 servers"));
161 		assert(luser.message == "I have 47 clients and 48 servers");
162 	}
163 }
164 
165 struct ChannelListResult {
166 	import virc.common : Topic;
167 	import virc.modes : Mode;
168 	string name;
169 	uint userCount;
170 	Topic topic;
171 	Mode[] modes;
172 }
173 
174 /++
175 +
176 +/
177 //322 <username> <channel> <count> :[\[<modes\] ]<topic>
178 auto parseNumeric(Numeric numeric : Numeric.RPL_LIST, T)(T input, ModeType[char] channelModeTypes) {
179 	import std.algorithm.iteration : filter, map;
180 	import std.algorithm.searching : findSplitAfter, startsWith;
181 	import std.array : array;
182 	import std.conv : parse;
183 	import virc.common : Channel, Topic;
184 	import virc.modes : Change, parseModeString;
185 	//Note: RFC2812 makes no mention of the modes being included.
186 	//Seems to be a de-facto standard, supported by several softwares.
187 	ChannelListResult channel;
188 	//username doesn't really help us here. skip it
189 	input.popFront();
190 	channel.name = input.front;
191 	input.popFront();
192 	auto str = input.front;
193 	channel.userCount = parse!uint(str);
194 	input.popFront();
195 	if (input.front.startsWith("[+")) {
196 		auto splitTopicStr = input.front.findSplitAfter("] ");
197 		channel.topic = Topic(splitTopicStr[1]);
198 		channel.modes = parseModeString(splitTopicStr[0][1..$], channelModeTypes).filter!(x => x.change == Change.set).map!(x => x.mode).array;
199 	} else {
200 		channel.topic = Topic(input.front);
201 	}
202 	return channel;
203 }
204 ///
205 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LIST
206 	import std.algorithm.searching : canFind;
207 	import std.range : only;
208 	import std.typecons : Nullable;
209 	import virc.common : Topic;
210 	import virc.modes : Mode;
211 	{
212 		immutable listEntry = parseNumeric!(Numeric.RPL_LIST)(only("someone", "#test", "4", "[+fnt 200:2] some words"), ['n': ModeType.d, 't': ModeType.d, 'f': ModeType.c]);
213 		assert(listEntry.name == "#test");
214 		assert(listEntry.userCount == 4);
215 		assert(listEntry.topic == Topic("some words"));
216 		assert(listEntry.modes.canFind(Mode(ModeType.d, 'n')));
217 		assert(listEntry.modes.canFind(Mode(ModeType.d, 't')));
218 		assert(listEntry.modes.canFind(Mode(ModeType.c, 'f', Nullable!string("100:2"))));
219 	}
220 	{
221 		immutable listEntry = parseNumeric!(Numeric.RPL_LIST)(only("someone", "#test2", "6", "[+fnst 100:2] some more words"), ['n': ModeType.d, 't': ModeType.d, 'f': ModeType.c]);
222 		assert(listEntry.name == "#test2");
223 		assert(listEntry.userCount == 6);
224 		assert(listEntry.topic == Topic("some more words"));
225 		assert(listEntry.modes.canFind(Mode(ModeType.d, 'n')));
226 		assert(listEntry.modes.canFind(Mode(ModeType.d, 't')));
227 		assert(listEntry.modes.canFind(Mode(ModeType.d, 's')));
228 		assert(listEntry.modes.canFind(Mode(ModeType.c, 'f', Nullable!string("100:2"))));
229 	}
230 	{
231 		immutable listEntry = parseNumeric!(Numeric.RPL_LIST)(only("someone", "#test3", "1", "no modes?"), ['n': ModeType.d, 't': ModeType.d, 'f': ModeType.c]);
232 		assert(listEntry.name == "#test3");
233 		assert(listEntry.userCount == 1);
234 		assert(listEntry.topic == Topic("no modes?"));
235 		assert(listEntry.modes.length == 0);
236 	}
237 }
238 struct TopicReply {
239 	string channel;
240 	string topic;
241 }
242 /++
243 + Parser for RPL_TOPIC.
244 +
245 + Format is `332 <client><channel> :<topic>`
246 +/
247 auto parseNumeric(Numeric numeric : Numeric.RPL_TOPIC, T)(T input) {
248 	import std.typecons : Nullable;
249 	Nullable!TopicReply output = TopicReply();
250 	if (input.empty) {
251 		output.nullify;
252 		return output;
253 	}
254 	input.popFront();
255 	if (input.empty) {
256 		output.nullify;
257 		return output;
258 	}
259 	output.get.channel = input.front;
260 	input.popFront();
261 	if (input.empty) {
262 		output.nullify;
263 		return output;
264 	}
265 	output.get.topic = input.front;
266 	return output;
267 }
268 ///
269 @safe pure nothrow @nogc unittest {
270 	import std.range : only, takeNone;
271 	{
272 		immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(only("someone", "#channel", "This is the topic!"));
273 		assert(topic.get.channel == "#channel");
274 		assert(topic.get.topic == "This is the topic!");
275 	}
276 	{
277 		immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(takeNone(only("")));
278 		assert(topic.isNull);
279 	}
280 	{
281 		immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(only("someone"));
282 		assert(topic.isNull);
283 	}
284 	{
285 		immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(only("someone", "#channel"));
286 		assert(topic.isNull);
287 	}
288 }
289 struct NamesReply {
290 	import std.algorithm : splitter;
291 	import std.traits : ReturnType;
292 	import std.typecons : No;
293 	NamReplyFlag chanFlag;
294 	string channel;
295 	string _users;
296 	static struct User {
297 		string modes;
298 		string name;
299 		this(string str, char[char] prefixes) @safe pure nothrow {
300 			size_t nameStart;
301 			foreach (idx, char chr; str) {
302 				bool found;
303 				foreach (mode; prefixes.keys) {
304 					const prefix = prefixes[mode];
305 					if (chr == prefix) {
306 						found = true;
307 						modes ~= mode;
308 						break;
309 					}
310 				}
311 				if (!found) {
312 					nameStart = idx;
313 					break;
314 				}
315 			}
316 			name = str[nameStart .. $];
317 		}
318 	}
319 	auto users(char[char] prefixes) inout nothrow {
320 		import std.algorithm.iteration : map;
321 		return _users.splitter(" ").map!(x => User(x, prefixes));
322 	}
323 }
324 /++
325 +
326 +/
327 enum NamReplyFlag : string {
328 	///Channel will never be shown to users that aren't in it
329 	secret = "@",
330 	///Channel will have its name replaced for users that aren't in it
331 	private_ = "*",
332 	///Non-private and non-secret channel.
333 	public_ = "=",
334 	///ditto
335 	other = public_
336 }
337 /++
338 + Parser for RPL_VERSION
339 +
340 + Format is `351 <client> <version> <server> :<comments>`
341 +/
342 auto parseNumeric(Numeric numeric : Numeric.RPL_VERSION, T)(T input) {
343 	import virc.numerics.magicparser : autoParse;
344 	return autoParse!VersionReply(input);
345 }
346 ///
347 @safe pure nothrow @nogc unittest {
348 	import std.algorithm.searching : canFind;
349 	import std.array : array;
350 	import std.range : only, takeNone;
351 	import virc.ircsplitter : IRCSplitter;
352 	{
353 		auto versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone", "ircd-seven-1.1.4(20170104-717fbca8dbac,charybdis-3.4-dev)", "localhost", "eHIKMpSZ6 TS6ow 7IZ"));
354 		assert(versionReply.get.version_ == "ircd-seven-1.1.4(20170104-717fbca8dbac,charybdis-3.4-dev)");
355 		assert(versionReply.get.server == "localhost");
356 		assert(versionReply.get.comments == "eHIKMpSZ6 TS6ow 7IZ");
357 	}
358 	{
359 		immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(takeNone(only("")));
360 		assert(versionReply.isNull);
361 	}
362 	{
363 		immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone"));
364 		assert(versionReply.isNull);
365 	}
366 	{
367 		immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone", "ircd-seven-1.1.4(20170104-717fbca8dbac,charybdis-3.4-dev)"));
368 		assert(versionReply.isNull);
369 	}
370 	{
371 		immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone", "", "localhost"));
372 		assert(versionReply.isNull);
373 	}
374 }
375 
376 /++
377 + Parser for RPL_NAMREPLY
378 +
379 + Format is `353 <client> =/*/@ <channel> :<prefix[es]><usermask>[ <prefix[es]><usermask>...]`
380 +/
381 auto parseNumeric(Numeric numeric : Numeric.RPL_NAMREPLY, T)(T input) {
382 	import std.algorithm : splitter;
383 	import std.typecons : Nullable;
384 	Nullable!NamesReply output = NamesReply();
385 	if (input.empty) {
386 		output.nullify();
387 		return output;
388 	}
389 	input.popFront();
390 	if (input.empty) {
391 		output.nullify();
392 		return output;
393 	}
394 	output.get.chanFlag = cast(NamReplyFlag)input.front;
395 	input.popFront();
396 	if (input.empty) {
397 		output.nullify();
398 		return output;
399 	}
400 	output.get.channel = input.front;
401 	input.popFront();
402 	if (input.empty) {
403 		output.nullify();
404 		return output;
405 	}
406 	output.get._users = input.front;
407 	return output;
408 }
409 ///
410 @safe pure nothrow unittest {
411 	import std.algorithm.searching : canFind;
412 	import std.algorithm.comparison : equal;
413 	import std.array : array;
414 	import std.range : only, takeNone;
415 	import virc.ircsplitter : IRCSplitter;
416 	{
417 		auto namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone = #channel :User1 User2 @User3 +User4"));
418 		assert(namReply.get.chanFlag == NamReplyFlag.public_);
419 		assert(namReply.get.channel == "#channel");
420 		auto users = namReply.get.users(['o': '@', 'v': '+']);
421 		with(users.front) {
422 			assert(modes.equal(""));
423 			assert(name == "User1");
424 		}
425 		users.popFront();
426 		with(users.front) {
427 			assert(modes.equal(""));
428 			assert(name == "User2");
429 		}
430 		users.popFront();
431 		with(users.front) {
432 			assert(modes.equal("o"));
433 			assert(name == "User3");
434 		}
435 		users.popFront();
436 		with(users.front) {
437 			assert(modes.equal("v"));
438 			assert(name == "User4");
439 		}
440 	}
441 	{
442 		immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone = #channel"));
443 		assert(namReply.isNull);
444 	}
445 	{
446 		immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone ="));
447 		assert(namReply.isNull);
448 	}
449 	{
450 		immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone"));
451 		assert(namReply.isNull);
452 	}
453 	{
454 		immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(takeNone(only("")));
455 		assert(namReply.isNull);
456 	}
457 }
458 /++
459 + Parser for RPL_REHASHING
460 +
461 + Format is `382 <client> <config file> :Rehashing]`
462 +/
463 auto parseNumeric(Numeric numeric : Numeric.RPL_REHASHING, T)(T input) {
464 	import virc.numerics.magicparser : autoParse;
465 	return autoParse!RehashingReply(input);
466 }
467 ///
468 @safe pure nothrow unittest {
469 	import std.range : only, takeNone;
470 	{
471 		auto reply = parseNumeric!(Numeric.RPL_REHASHING)(only("someone", "ircd.conf", "Rehashing"));
472 		assert(reply.get.configFile == "ircd.conf");
473 		assert(reply.get.message == "Rehashing");
474 	}
475 	{
476 		immutable reply = parseNumeric!(Numeric.RPL_REHASHING)(only("someone", "ircd.conf"));
477 		assert(reply.isNull);
478 	}
479 	{
480 		immutable reply = parseNumeric!(Numeric.RPL_REHASHING)(only("someone"));
481 		assert(reply.isNull);
482 	}
483 	{
484 		immutable reply = parseNumeric!(Numeric.RPL_REHASHING)(takeNone(only("")));
485 		assert(reply.isNull);
486 	}
487 }
488 
489 struct NoSuchServerError {
490 	import virc.common : User;
491 	User me;
492 	///Server mask that failed to match any servers.
493 	string serverMask;
494 	///User-readable error message
495 	string message;
496 }
497 /++
498 + Parser for ERR_NOSUCHSERVER
499 +
500 + Format is `402 <client> <server> :No such server`
501 +/
502 auto parseNumeric(Numeric numeric : Numeric.ERR_NOSUCHSERVER, T)(T input) {
503 	import virc.numerics.magicparser : autoParse;
504 	return autoParse!NoSuchServerError(input);
505 }
506 ///
507 @safe pure nothrow unittest {
508 	import std.range : only, takeNone;
509 	{
510 		auto reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(only("someone", "badserver.example.net", "No such server"));
511 		assert(reply.get.serverMask == "badserver.example.net");
512 		assert(reply.get.message == "No such server");
513 	}
514 	{
515 		immutable reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(only("someone", "badserver.example.net"));
516 		assert(reply.isNull);
517 	}
518 	{
519 		immutable reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(only("someone"));
520 		assert(reply.isNull);
521 	}
522 	{
523 		immutable reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(takeNone(only("")));
524 		assert(reply.isNull);
525 	}
526 }
527 struct AwayReply {
528 	import virc.common : User;
529 	User me;
530 	///User that's away.
531 	User user;
532 	///User's away message.
533 	string message;
534 }
535 /++
536 + Parser for RPL_AWAY
537 +
538 + Format is `301 <client> <nick> <message>`
539 +/
540 auto parseNumeric(Numeric numeric : Numeric.RPL_AWAY, T)(T input) {
541 	import virc.numerics.magicparser : autoParse;
542 	return autoParse!AwayReply(input);
543 }
544 ///
545 @safe pure nothrow unittest {
546 	import virc.common : User;
547 	import std.range : only, takeNone;
548 	{
549 		auto reply = parseNumeric!(Numeric.RPL_AWAY)(only("someone", "awayuser", "On fire"));
550 		assert(reply.get.user == User("awayuser"));
551 		assert(reply.get.message == "On fire");
552 	}
553 	{
554 		immutable reply = parseNumeric!(Numeric.RPL_AWAY)(only("someone", "awayuser"));
555 		assert(reply.isNull);
556 	}
557 	{
558 		immutable reply = parseNumeric!(Numeric.RPL_AWAY)(only("someone"));
559 		assert(reply.isNull);
560 	}
561 	{
562 		immutable reply = parseNumeric!(Numeric.RPL_AWAY)(takeNone(only("")));
563 		assert(reply.isNull);
564 	}
565 }
566 /++
567 + WHOIS reply containing only a nickname and human-readable message.
568 +/
569 struct InfolessWhoisReply {
570 	import virc.common : User;
571 	User me;
572 	///User whose query is complete.
573 	User user;
574 	///Human-readable numeric message.
575 	string message;
576 }
577 /++
578 + Parser for RPL_ENDOFWHOIS
579 +
580 + Format is `318 <client> <nick> :End of /WHOIS list`
581 +/
582 auto parseNumeric(Numeric numeric : Numeric.RPL_ENDOFWHOIS, T)(T input) {
583 	import virc.numerics.magicparser : autoParse;
584 	return autoParse!InfolessWhoisReply(input);
585 }
586 ///
587 @safe pure nothrow unittest {
588 	import virc.common : User;
589 	import std.range : only, takeNone;
590 	{
591 		auto reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(only("someone", "whoisuser", "End of /WHOIS list"));
592 		assert(reply.get.user == User("whoisuser"));
593 	}
594 	{
595 		immutable reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(only("someone", "whoisuser"));
596 		assert(reply.isNull);
597 	}
598 	{
599 		immutable reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(only("someone"));
600 		assert(reply.isNull);
601 	}
602 	{
603 		immutable reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(takeNone(only("")));
604 		assert(reply.isNull);
605 	}
606 }
607 /++
608 + Parser for RPL_WHOISOPERATOR
609 +
610 + Format is `313 <client> <nick> :is an IRC operator`
611 +/
612 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISOPERATOR, T)(T input) {
613 	import virc.numerics.magicparser : autoParse;
614 	return autoParse!InfolessWhoisReply(input);
615 }
616 ///
617 @safe pure nothrow unittest {
618 	import virc.common : User;
619 	import std.range : only, takeNone;
620 	{
621 		auto reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(only("someone", "whoisuser", "is an IRC operator"));
622 		assert(reply.get.user == User("whoisuser"));
623 	}
624 	{
625 		immutable reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(only("someone", "whoisuser"));
626 		assert(reply.isNull);
627 	}
628 	{
629 		immutable reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(only("someone"));
630 		assert(reply.isNull);
631 	}
632 	{
633 		immutable reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(takeNone(only("")));
634 		assert(reply.isNull);
635 	}
636 }
637 /++
638 +
639 +/
640 struct WhoisUserReply {
641 	import virc.common : User;
642 	User me;
643 	///User whose query is complete.
644 	User user;
645 	///User's username.
646 	string username;
647 	///User's hostname.
648 	string hostname;
649 	//Reserved. Just a *.
650 	string reserved;
651 	///User's realname.
652 	string realname;
653 }
654 /++
655 + Parser for RPL_WHOISUSER
656 +
657 + Format is `311 <client> <nick> <user> <host> * :<real name>`
658 +/
659 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISUSER, T)(T input) {
660 	import virc.numerics.magicparser : autoParse;
661 	return autoParse!WhoisUserReply(input);
662 }
663 ///
664 @safe pure nothrow unittest {
665 	import virc.common : User;
666 	import std.range : only, takeNone;
667 	{
668 		immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername", "someHostname", "*", "a real name"));
669 		assert(reply.get.user == User("whoisuser"));
670 		assert(reply.get.username == "someUsername");
671 		assert(reply.get.hostname == "someHostname");
672 		assert(reply.get.realname == "a real name");
673 	}
674 	{
675 		immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername", "someHostname", "*"));
676 		assert(reply.isNull);
677 	}
678 	{
679 		immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername", "someHostname"));
680 		assert(reply.isNull);
681 	}
682 	{
683 		immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername"));
684 		assert(reply.isNull);
685 	}
686 	{
687 		immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser"));
688 		assert(reply.isNull);
689 	}
690 	{
691 		immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone"));
692 		assert(reply.isNull);
693 	}
694 	{
695 		immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(takeNone(only("")));
696 		assert(reply.isNull);
697 	}
698 }
699 /++
700 + Newer form of RPL_WHOISIDLE. Also provides the time at which the user
701 + connected.
702 +/
703 struct WhoisIdleReplyNew {
704 	import core.time : Duration;
705 	import std.datetime.systime : SysTime;
706 	import virc.common : User;
707 	User me;
708 	///User whose query is complete.
709 	User user;
710 	///How long the user has been idle.
711 	Duration idleTime;
712 	///Time at which the user connected.
713 	SysTime connectedTime;
714 	///Human-readable numeric message.
715 	string message;
716 }
717 /++
718 + Older form of RPL_WHOISIDLE. Does not have user connection time.
719 +/
720 struct WhoisIdleReplyOld {
721 	import core.time : Duration;
722 	import virc.common : User;
723 	User me;
724 	///User whose query is complete.
725 	User user;
726 	///How long the user has been idle.
727 	Duration idleTime;
728 	///Human-readable numeric message.
729 	string message;
730 }
731 /++
732 + Parser for RPL_WHOISIDLE
733 +
734 + Format is `317 <client> <nick> <idle time in seconds> :seconds idle` or
735 + `317 <client> <nick> <idle time in seconds> <time connected> :seconds idle, signon time`
736 +/
737 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISIDLE, T)(T input) {
738 	import virc.numerics.magicparser : autoParse;
739 	return autoParse!(WhoisIdleReplyNew, WhoisIdleReplyOld)(input);
740 }
741 ///
742 @safe pure nothrow unittest {
743 	import core.time : seconds;
744 	import std.datetime : DateTime, SysTime, UTC;
745 	import std.range : only, takeNone;
746 	import virc.common : User;
747 	static immutable testTime = SysTime(DateTime(2017, 07, 14, 02, 40, 00), UTC());
748 	{
749 		immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser", "1500", "1500000000", "seconds idle, signon time"));
750 		assert(reply.user == User("whoisuser"));
751 		assert(reply.idleTime == 1500.seconds);
752 		assert(reply.connectedTime.get == testTime);
753 	}
754 	{
755 		immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser", "1500", "seconds idle"));
756 		assert(reply.user == User("whoisuser"));
757 		assert(reply.idleTime == 1500.seconds);
758 		assert(reply.connectedTime.isNull);
759 	}
760 	{
761 		immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser", "1500"));
762 		assert(reply.isNull);
763 	}
764 	{
765 		immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser"));
766 		assert(reply.isNull);
767 	}
768 	{
769 		immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone"));
770 		assert(reply.isNull);
771 	}
772 	{
773 		immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(takeNone(only("")));
774 		assert(reply.isNull);
775 	}
776 }
777 /++
778 +
779 +/
780 struct WhoisServerReply {
781 	import virc.common : User;
782 	User me;
783 	///User whose query is complete.
784 	User user;
785 	///Hostmask of server the user is connected to.
786 	string server;
787 	///Server description.
788 	string serverDescription;
789 }
790 /++
791 + Parser for RPL_WHOISSERVER
792 +
793 + Format is `312 <client> <nick> <server mask> :<server description>`
794 +/
795 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISSERVER, T)(T input) {
796 	import virc.numerics.magicparser : autoParse;
797 	return autoParse!WhoisServerReply(input);
798 }
799 ///
800 @safe pure nothrow unittest {
801 	import std.range : only, takeNone;
802 	import virc.common : User;
803 	{
804 		immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone", "whoisuser", "example.net", "Mysterious example server"));
805 		assert(reply.get.user == User("whoisuser"));
806 		assert(reply.get.server == "example.net");
807 		assert(reply.get.serverDescription == "Mysterious example server");
808 	}
809 	{
810 		immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone", "whoisuser", "example.net"));
811 		assert(reply.isNull);
812 	}
813 	{
814 		immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone", "whoisuser"));
815 		assert(reply.isNull);
816 	}
817 	{
818 		immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone"));
819 		assert(reply.isNull);
820 	}
821 	{
822 		immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(takeNone(only("")));
823 		assert(reply.isNull);
824 	}
825 }
826 /++
827 +
828 +/
829 struct WhoisChannelReply {
830 	import virc.common : User;
831 	User me;
832 	///User whose query is complete.
833 	User user;
834 	///Channel details of this user, including name and prefix.
835 	WhoisChannelReplyChannel[] channels;
836 }
837 /++
838 +
839 +/
840 struct WhoisChannelReplyChannel {
841 	import std.typecons : Nullable;
842 	import virc.common : Channel;
843 	///Mode prefix applied to this user on this channel.
844 	Nullable!string prefix;
845 	///Name of the channel.
846 	Channel channel;
847 }
848 /++
849 + Parser for RPL_WHOISCHANNELS
850 +
851 + Format is `319 <client> <nick> <server mask> :<server description>`
852 +/
853 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISCHANNELS, T)(T input, string prefixes, string channelTypes) {
854 	import std.algorithm.iteration : splitter;
855 	import std.typecons : Nullable;
856 	import virc.target : Target;
857 	import virc.numerics.magicparser : autoParse;
858 	struct Reduced {
859 		import virc.common : User;
860 		User me;
861 		User user;
862 		string channels;
863 	}
864 	Nullable!WhoisChannelReply output;
865 	auto parsed = autoParse!Reduced(input);
866 	if (!parsed.isNull) {
867 		output = WhoisChannelReply();
868 		output.get.me = parsed.get.me;
869 		output.get.user = parsed.get.user;
870 		auto split = parsed.get.channels.splitter(" ");
871 		foreach (rawChan; split) {
872 			auto parsedChannel = Target(rawChan, prefixes, channelTypes);
873 			if (parsedChannel.isChannel) {
874 				auto channel = WhoisChannelReplyChannel();
875 				channel.prefix = parsedChannel.prefixes;
876 				channel.channel = parsedChannel.channel.get;
877 				output.get.channels ~= channel;
878 			}
879 		}
880 	}
881 	return output;
882 }
883 ///
884 /+@safe pure nothrow +/unittest {
885 	import std.range : only, takeNone;
886 	import virc.common : User;
887 	import virc.numerics.isupport : defaultModePrefixes;
888 	{
889 		auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(only("someone", "whoisuser", "#test3 +#test"), defaultModePrefixes, "#");
890 		assert(reply.get.user == User("whoisuser"));
891 		assert(reply.get.channels.length == 2);
892 		with(reply.get.channels[0]) {
893 			assert(channel == Channel("#test3"));
894 			assert(prefix.isNull);
895 		}
896 		with(reply.get.channels[1]) {
897 			assert(channel == Channel("#test"));
898 			assert(prefix.get == "+");
899 		}
900 	}
901 	{
902 		auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(only("someone", "whoisuser"), defaultModePrefixes, "#");
903 		assert(reply.isNull);
904 	}
905 	{
906 		auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(only("someone"), defaultModePrefixes, "#");
907 		assert(reply.isNull);
908 	}
909 	{
910 		auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(takeNone(only("")), defaultModePrefixes, "#");
911 		assert(reply.isNull);
912 	}
913 }
914 
915 auto parseNumeric(Numeric numeric : Numeric.RPL_ISON, T)(T input) {
916 	import std.algorithm.iteration : splitter;
917 	import std.typecons : Nullable, Tuple;
918 	import virc.common : User;
919 	Nullable!(Tuple!(User, "user", typeof("".splitter(" ")), "online")) output = Tuple!(User, "user", typeof("".splitter(" ")), "online")();
920 	if (input.empty) {
921 		return output.init;
922 	}
923 	output.get.user = User(input.front);
924 	input.popFront();
925 	if (input.empty) {
926 		return output.init;
927 	}
928 	output.get.online = input.front.splitter(" ");
929 	return output;
930 }
931 unittest {
932 	import std.array : array;
933 	import std.range : only, takeNone;
934 	import virc.common : User;
935 	assert(parseNumeric!(Numeric.RPL_ISON)(takeNone(only(""))).isNull);
936 	assert(parseNumeric!(Numeric.RPL_ISON)(only("someone")).isNull);
937 	{
938 		auto reply = parseNumeric!(Numeric.RPL_ISON)(only("someone", "user1 user2 user3"));
939 		assert(reply.get.user == User("someone"));
940 		assert(reply.get.online.array == ["user1", "user2", "user3"]);
941 	}
942 }