1 /**
2  * A simple IRC server.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.net.irc.server;
15 
16 import std.algorithm;
17 import std.conv;
18 import std.datetime;
19 import std.exception;
20 import std.range;
21 import std.regex;
22 import std.socket;
23 import std.string;
24 
25 import ae.net.asockets;
26 import ae.utils.array;
27 import ae.sys.log;
28 import ae.utils.exception;
29 import ae.utils.meta;
30 import ae.utils.text;
31 
32 import ae.net.irc.common;
33 
34 private alias std..string.indexOf indexOf;
35 
36 /// IRC server.
37 class IrcServer
38 {
39 	// This class is currently intentionally written for readability, not performance.
40 	// Performance and scalability could be greatly improved by using numeric indices for users and channels
41 	// instead of associative arrays.
42 
43 	string hostname; /// Hostname to announce.  Defaults to the current machine's hostname.
44 	string password; /// If set, require this password to be specified using PASS.
45 	string network; /// If set, announce as the "NETWORK=".
46 	/// Require that nicknames match the given regular expression.
47 	string nicknameValidationPattern = "^[a-zA-Z][a-zA-Z0-9\\-`\\|\\[\\]\\{\\}_^]{0,14}$";
48 	uint nicknameMaxLength = 15; /// For the announced capabilities.
49 	string serverVersion = "ae.net.irc.server"; /// Announced in MOTD.
50 	string[] motd; /// Additional MOTD lines to send.
51 	string chanTypes = "#&"; /// Character prefixes indicating channels.
52 	SysTime creationTime; /// Announced in MOTD. Defaults to class construction time.
53 	string operPassword; /// If set, allow obtaining OPER status using the specified password.
54 
55 	/// Channels can't be created by users, and don't disappear when they're empty
56 	bool staticChannels;
57 	/// If set, masks all IPs to the given mask
58 	string addressMask;
59 
60 	Logger log; /// Optional log.
61 
62 	/// Abstract client connection and information.
63 	abstract static class Client
64 	{
65 		/// How to convert the IRC 8-bit data to and from UTF-8 (D strings must be valid UTF-8).
66 		string function(in char[]) decoder = &rawToUTF8, encoder = &UTF8ToRaw;
67 
68 		/// Registration details
69 		string nickname, password;
70 		/// ditto
71 		string username, hostname, servername, realname;
72 		bool identified; /// Pretend that we obtained the username from an `ident` server?
73 		/// Full `"nick!user@host"`. `publicPrefix` is what everyone except the user themself and opers see.
74 		string prefix, publicPrefix;
75 		/// Away reason, if away.
76 		string away;
77 
78 		bool registered; /// Registration completed successfully?
79 		Modes modes; /// User modes.
80 		MonoTime lastActivity; ///
81 
82 		Channel[] getJoinedChannels()
83 		{
84 			Channel[] result;
85 			foreach (channel; server.channels)
86 				if (nickname.normalized in channel.members)
87 					result ~= channel;
88 			return result;
89 		} ///
90 
91 		string realHostname() { return remoteAddress ? remoteAddress.toAddrString : "null"; } ///
92 		string publicHostname() { return server.addressMask ? server.addressMask : realHostname; } ///
93 		bool realHostnameVisibleTo(Client viewer)
94 		{
95 			return server.addressMask is null
96 				|| viewer is this
97 				|| viewer.modes.flags['o']; // Oper
98 		} ///
99 		string hostnameAsVisibleTo(Client viewer) { return realHostnameVisibleTo(viewer) ? realHostname : publicHostname; } ///
100 		string prefixAsVisibleTo(Client viewer) { return realHostnameVisibleTo(viewer) ? prefix : publicPrefix; } ///
101 
102 	protected:
103 		IrcServer server;
104 		Address remoteAddress;
105 
106 		this(IrcServer server, Address remoteAddress)
107 		{
108 			this.server = server;
109 			lastActivity = MonoTime.currTime;
110 			server.clients.add(this);
111 
112 			this.remoteAddress = remoteAddress;
113 
114 			server.log("New IRC connection from " ~ remoteAddress.toString);
115 		}
116 
117 		void onReadLine(string line)
118 		{
119 			try
120 			{
121 				if (decoder) line = decoder(line);
122 
123 				if (!connConnected())
124 					return; // A previous line in the same buffer caused a disconnect
125 
126 				enforce(line.indexOf('\0')<0 && line.indexOf('\r')<0 && line.indexOf('\n')<0, "Forbidden character");
127 
128 				auto parameters = line.ircSplit();
129 				if (!parameters.length)
130 					return;
131 
132 				auto command = parameters.shift.toUpper();
133 				onCommand(command, parameters);
134 			}
135 			catch (CaughtException e)
136 			{
137 				if (connConnected())
138 					disconnect(e.msg);
139 			}
140 		}
141 
142 		void onCommand(string command, scope string[] parameters...)
143 		{
144 			switch (command)
145 			{
146 				case "PASS":
147 					if (registered)
148 						return sendReply(Reply.ERR_ALREADYREGISTRED, "You may not reregister");
149 					if (parameters.length != 1)
150 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
151 					password = parameters[0];
152 					break;
153 				case "NICK":
154 					if (parameters.length != 1)
155 						return sendReply(Reply.ERR_NONICKNAMEGIVEN, "No nickname given");
156 					if (!registered)
157 					{
158 						nickname = parameters[0];
159 						checkRegistration();
160 					}
161 					else
162 					{
163 						auto newNick = parameters[0];
164 						if (!newNick.match(server.nicknameValidationPattern))
165 							return sendReply(Reply.ERR_ERRONEUSNICKNAME, newNick, "Erroneous nickname");
166 						if (newNick.normalized in server.nicknames)
167 						{
168 							if (newNick.normalized != nickname.normalized)
169 								sendReply(Reply.ERR_NICKNAMEINUSE, newNick, "Nickname is already in use");
170 							return;
171 						}
172 
173 						changeNick(newNick);
174 					}
175 					break;
176 				case "USER":
177 					if (registered)
178 						return sendReply(Reply.ERR_ALREADYREGISTRED, "You may not reregister");
179 					if (parameters.length != 4)
180 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
181 					username   = parameters[0];
182 					hostname   = parameters[1];
183 					servername = parameters[2];
184 					realname   = parameters[3];
185 					checkRegistration();
186 					break;
187 
188 				case "PING":
189 					if (!registered)
190 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered"); // KVIrc needs this.
191 					sendReply("PONG", parameters);
192 					break;
193 				case "PONG":
194 					break;
195 				case "QUIT":
196 					if (parameters.length)
197 						disconnect("Quit: " ~ parameters[0]);
198 					else
199 						disconnect("Quit");
200 					break;
201 				case "JOIN":
202 					if (!registered)
203 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
204 					if (parameters.length < 1)
205 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
206 					string[] keys = parameters.length > 1 ? parameters[1].split(",") : null;
207 					foreach (i, channame; parameters[0].split(","))
208 					{
209 						auto key = i < keys.length ? keys[i] : null;
210 						if (!server.isChannelName(channame))
211 							{ sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; }
212 						auto normchan = channame.normalized;
213 						if (!mayJoin(normchan))
214 							continue;
215 						auto pchannel = normchan in server.channels;
216 						Channel channel;
217 						if (pchannel)
218 							channel = *pchannel;
219 						else
220 						{
221 							if (server.staticChannels)
222 								{ sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; }
223 							else
224 								channel = server.createChannel(channame);
225 						}
226 						if (nickname.normalized in channel.members)
227 							continue; // already on channel
228 						if (channel.modes.strings['k'] && channel.modes.strings['k'] != key)
229 							{ sendReply(Reply.ERR_BADCHANNELKEY, channame, "Cannot join channel (+k)"); continue; }
230 						if (channel.modes.masks['b'].any!(mask => prefix.maskMatch(mask)))
231 							{ sendReply(Reply.ERR_BANNEDFROMCHAN, channame, "Cannot join channel (+b)"); continue; }
232 						join(channel);
233 					}
234 					lastActivity = MonoTime.currTime;
235 					break;
236 				case "PART":
237 					if (!registered)
238 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
239 					if (parameters.length < 1) // TODO: part reason
240 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
241 					string reason = parameters.length < 2 ? null : parameters[1];
242 					foreach (channame; parameters[0].split(","))
243 					{
244 						auto pchan = channame.normalized in server.channels;
245 						if (!pchan)
246 							{ sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; }
247 						auto chan = *pchan;
248 						if (nickname.normalized !in chan.members)
249 							{ sendReply(Reply.ERR_NOTONCHANNEL, channame, "You're not on that channel"); continue; }
250 						part(chan, reason);
251 					}
252 					lastActivity = MonoTime.currTime;
253 					break;
254 				case "MODE":
255 					if (!registered)
256 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
257 					if (parameters.length < 1)
258 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
259 					auto target = parameters.shift;
260 					if (server.isChannelName(target))
261 					{
262 						auto pchannel = target.normalized in server.channels;
263 						if (!pchannel)
264 							return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel");
265 						auto channel = *pchannel;
266 						auto pmember = nickname.normalized in channel.members;
267 						if (!pmember)
268 							return sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel");
269 						if (!parameters.length)
270 							return sendChannelModes(channel);
271 						return setChannelModes(channel, parameters);
272 					}
273 					else
274 					{
275 						auto pclient = target.normalized in server.nicknames;
276 						if (!pclient)
277 							return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel");
278 						auto client = *pclient;
279 						if (parameters.length)
280 						{
281 							if (client !is this)
282 								return sendReply(Reply.ERR_USERSDONTMATCH, "Cannot change mode for other users");
283 							return setUserModes(parameters);
284 						}
285 						else
286 							return sendUserModes(client);
287 					}
288 				case "LIST":
289 					if (!registered)
290 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
291 					foreach (channel; getChannelList())
292 						if (!(channel.modes.flags['p'] || channel.modes.flags['s']) || nickname.normalized in channel.members)
293 							sendReply(Reply.RPL_LIST, channel.name, channel.members.length.text, channel.topic ? channel.topic : "");
294 					sendReply(Reply.RPL_LISTEND, "End of LIST");
295 					break;
296 				case "MOTD":
297 					if (!registered)
298 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
299 					sendMotd();
300 					break;
301 				case "NAMES":
302 					if (!registered)
303 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
304 					if (parameters.length < 1)
305 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
306 					foreach (channame; parameters[0].split(","))
307 					{
308 						auto pchan = channame.normalized in server.channels;
309 						if (!pchan)
310 							{ sendReply(Reply.ERR_NOSUCHCHANNEL, channame, "No such channel"); continue; }
311 						auto channel = *pchan;
312 						auto pmember = nickname.normalized in channel.members;
313 						if (!pmember)
314 							{ sendReply(Reply.ERR_NOTONCHANNEL, channame, "You're not on that channel"); continue; }
315 						sendNames(channel);
316 					}
317 					break;
318 				case "WHO":
319 				{
320 					if (!registered)
321 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
322 					auto mask = parameters.length ? parameters[0].among("", "*", "0") ? null : parameters[0] : null;
323 					string[string] result;
324 					foreach (channel; server.channels)
325 					{
326 						auto inChannel = nickname.normalized in channel.members;
327 						if (!inChannel && channel.modes.flags['s'])
328 							continue;
329 						foreach (member; channel.members)
330 							if (inChannel || !member.client.modes.flags['i'])
331 								if (!mask || channel.name.maskMatch(mask) || member.client.nickname.maskMatch(mask) || member.client.hostnameAsVisibleTo(this).maskMatch(mask))
332 								{
333 									auto phit = member.client.nickname in result;
334 									if (phit)
335 										*phit = "*";
336 									else
337 										result[member.client.nickname] = channel.name;
338 								}
339 					}
340 
341 					foreach (client; server.nicknames)
342 						if (!client.modes.flags['i'])
343 							if (!mask || client.nickname.maskMatch(mask) || client.hostnameAsVisibleTo(this).maskMatch(mask))
344 								if (client.nickname !in result)
345 									result[client.nickname] = "*";
346 
347 					foreach (nickname, channel; result)
348 					{
349 						auto client = server.nicknames[nickname.normalized];
350 						sendReply(Reply.RPL_WHOREPLY,
351 							channel,
352 							client.username,
353 							safeHostname(client.hostnameAsVisibleTo(this)),
354 							server.hostname,
355 							nickname,
356 							"H",
357 							"0 " ~ client.realname,
358 						);
359 					}
360 					sendReply(Reply.RPL_ENDOFWHO, mask ? mask : "*", "End of WHO list");
361 					break;
362 				}
363 				case "WHOIS":
364 				{
365 					if (!registered)
366 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
367 					// Contrary to the RFC, and similar to e.g. Freenode, we don't support masks here.
368 					if (parameters.length < 1)
369 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
370 					foreach (nick; parameters[0].split(","))
371 					{
372 						auto pclient = nick.normalized in server.nicknames;
373 						if (!pclient)
374 							{ sendReply(Reply.ERR_NOSUCHNICK, nick, "No such nick/channel"); continue; }
375 						auto client = *pclient;
376 
377 						// RPL_WHOISUSER
378 						sendReply(Reply.RPL_WHOISUSER,
379 							client.nickname,
380 							client.username,
381 							safeHostname(client.hostnameAsVisibleTo(this)),
382 							"*",
383 							client.realname,
384 						);
385 						// RPL_WHOISCHANNELS
386 						server.channels.byValue
387 							// Channel contents visible?
388 							.filter!(channel => !channel.modes.flags['s'] || this.nickname.normalized in channel.members)
389 							// Get channel member mode + name if target in channel, or null
390 							.map!(channel => (nick.normalized in channel.members).I!(pmember => pmember ? pmember.modeChar() ~ channel.name : null))
391 							.filter!(name => name !is null)
392 							.chunks(10)
393 							.each!(chunk => sendReply(Reply.RPL_WHOISCHANNELS, client.nickname, chunk.join(" ")));
394 						// RPL_WHOISOPERATOR
395 						if (client.modes.flags['o'])
396 							sendReply(Reply.RPL_WHOISOPERATOR, client.nickname, "is an IRC operator");
397 						// RPL_WHOISIDLE
398 						sendReply(Reply.RPL_WHOISIDLE, client.nickname,
399 							(MonoTime.currTime - client.lastActivity).total!"seconds".text,
400 							"seconds idle");
401 					}
402 					// RPL_ENDOFWHOIS
403 					sendReply(Reply.RPL_ENDOFWHOIS, parameters[0], "End of WHOIS list");
404 					break;
405 				}
406 				case "TOPIC":
407 				{
408 					if (!registered)
409 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
410 					if (parameters.length < 1)
411 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
412 					auto target = parameters.shift;
413 					auto pchannel = target.normalized in server.channels;
414 					if (!pchannel)
415 						return sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel");
416 					auto channel = *pchannel;
417 					auto pmember = nickname.normalized in channel.members;
418 					if (!pmember)
419 						return sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel");
420 					if (!parameters.length)
421 						return sendTopic(channel);
422 					if (channel.modes.flags['t'] && (pmember.modes & Channel.Member.Modes.op) == 0)
423 						return sendReply(Reply.ERR_CHANOPRIVSNEEDED, target, "You're not channel operator");
424 					return setChannelTopic(channel, parameters[0]);
425 				}
426 				case "ISON":
427 					if (!registered)
428 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
429 					sendReply(Reply.RPL_ISON, parameters.filter!(nick => nick.normalized in server.nicknames).join(" "));
430 					break;
431 				case "USERHOST":
432 				{
433 					if (!registered)
434 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
435 					string[] replies;
436 					foreach (nick; parameters)
437 					{
438 						auto pclient = nick.normalized in server.nicknames;
439 						if (!pclient)
440 							continue;
441 						auto client = *pclient;
442 						replies ~= "%s%s=%s%s@%s".format(
443 							nick,
444 							client.modes.flags['o'] ? "*" : "",
445 							client.away ? "+" : "-",
446 							client.username,
447 							client.hostnameAsVisibleTo(this),
448 						);
449 					}
450 					sendReply(Reply.RPL_USERHOST, replies.join(" "));
451 					break;
452 				}
453 				case "LUSERS":
454 					sendLusers();
455 					break;
456 				case "PRIVMSG":
457 				case "NOTICE":
458 				{
459 					if (!registered)
460 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
461 					if (parameters.length < 2)
462 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
463 					auto message = parameters[1];
464 					if (!message.length)
465 						return sendReply(Reply.ERR_NOTEXTTOSEND, command, "No text to send");
466 					foreach (target; parameters[0].split(","))
467 					{
468 						if (server.isChannelName(target))
469 						{
470 							auto pchannel = target.normalized in server.channels;
471 							if (!pchannel)
472 								{ sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); continue; }
473 							auto channel = *pchannel;
474 							auto pmember = nickname.normalized in channel.members;
475 							if (pmember) // On channel?
476 							{
477 								if (channel.modes.flags['m'] && (pmember.modes & Channel.Member.Modes.bypassM) == 0)
478 									{ sendReply(Reply.ERR_CANNOTSENDTOCHAN, target, "Cannot send to channel"); continue; }
479 							}
480 							else
481 							{
482 								if (channel.modes.flags['n']) // No external messages
483 									{ sendReply(Reply.ERR_NOTONCHANNEL, target, "You're not on that channel"); continue; }
484 							}
485 							sendToChannel(channel, command, message);
486 						}
487 						else
488 						{
489 							auto pclient = target.normalized in server.nicknames;
490 							if (!pclient)
491 								{ sendReply(Reply.ERR_NOSUCHNICK, target, "No such nick/channel"); continue; }
492 							sendToClient(*pclient, command, message);
493 						}
494 					}
495 					lastActivity = MonoTime.currTime;
496 					break;
497 				}
498 				case "OPER":
499 					if (!registered)
500 						return sendReply(Reply.ERR_NOTREGISTERED, "You have not registered");
501 					if (parameters.length < 1)
502 						return sendReply(Reply.ERR_NEEDMOREPARAMS, command, "Not enough parameters");
503 					if (!server.operPassword || parameters[$-1] != server.operPassword)
504 						return sendReply(Reply.ERR_PASSWDMISMATCH, "Password incorrect");
505 					modes.flags['o'] = true;
506 					sendReply(Reply.RPL_YOUREOPER, "You are now an IRC operator");
507 					sendUserModes(this);
508 					foreach (channel; server.channels)
509 						if (nickname.normalized in channel.members)
510 							setChannelMode(channel, nickname, Channel.Member.Mode.op, true);
511 					break;
512 
513 				default:
514 					if (registered)
515 						return sendReply(Reply.ERR_UNKNOWNCOMMAND, command, "Unknown command");
516 			}
517 		}
518 
519 		final void onInactivity()
520 		{
521 			sendLine("PING %s".format(Clock.currTime.stdTime));
522 		}
523 
524 		void disconnect(string why)
525 		{
526 			if (registered)
527 				unregister(why);
528 			sendLine("ERROR :Closing Link: %s[%s@%s] (%s)".format(nickname, username, realHostname, why));
529 			connDisconnect(why);
530 		}
531 
532 		void onDisconnect(string reason, DisconnectType type)
533 		{
534 			if (registered)
535 				unregister(reason);
536 			server.clients.remove(this);
537 			server.log("IRC: %s disconnecting: %s".format(remoteAddress, reason));
538 		}
539 
540 		void checkRegistration()
541 		{
542 			assert(!registered);
543 
544 			if (nickname.length && username.length)
545 			{
546 				if (server.password && password != server.password)
547 				{
548 					password = null;
549 					return sendReply(Reply.ERR_PASSWDMISMATCH, "Password incorrect");
550 				}
551 				if (nickname.normalized in server.nicknames)
552 				{
553 					scope(exit) nickname = null;
554 					return sendReply(Reply.ERR_NICKNAMEINUSE, nickname, "Nickname is already in use");
555 				}
556 				if (!nickname.match(server.nicknameValidationPattern))
557 				{
558 					scope(exit) nickname = null;
559 					return sendReply(Reply.ERR_ERRONEUSNICKNAME, nickname, "Erroneous nickname");
560 				}
561 				if (!username.match(`[a-zA-Z]+`))
562 					return disconnect("Invalid username");
563 
564 				// All OK
565 				register();
566 			}
567 		}
568 
569 		void register()
570 		{
571 			if (!identified)
572 				username = "~" ~ username;
573 			update();
574 
575 			registered = true;
576 			server.nicknames[nickname.normalized] = this;
577 			auto userCount = server.nicknames.length;
578 			if (server.maxUsers < userCount)
579 				server.maxUsers = userCount;
580 			sendReply(Reply.RPL_WELCOME      , "Welcome, %s!".format(nickname));
581 			sendReply(Reply.RPL_YOURHOST     , "Your host is %s, running %s".format(server.hostname, server.serverVersion));
582 			sendReply(Reply.RPL_CREATED      , "This server was created %s".format(server.creationTime));
583 			sendReply(Reply.RPL_MYINFO       , server.hostname, server.serverVersion, UserModes.supported, ChannelModes.supported, null);
584 			sendReply(cast(Reply)         005, server.capabilities ~ ["are supported by this server"]);
585 			sendLusers();
586 			sendReply(cast(Reply)         265, "Current local  users: %d  Max: %d".format(userCount, server.maxUsers));
587 			sendReply(cast(Reply)         266, "Current global users: %d  Max: %d".format(userCount, server.maxUsers));
588 			sendReply(cast(Reply)         250, "Highest connection count: %d (%d clients) (%d since server was (re)started)".format(server.maxUsers, server.maxUsers, server.totalConnections));
589 			sendMotd();
590 		}
591 
592 		void update()
593 		{
594 			prefix       = "%s!%s@%s".format(nickname, username, realHostname  );
595 			publicPrefix = "%s!%s@%s".format(nickname, username, publicHostname);
596 		}
597 
598 		void unregister(string why)
599 		{
600 			assert(registered);
601 			auto channels = getJoinedChannels();
602 			foreach (channel; channels)
603 				channel.remove(this);
604 			foreach (client; server.allClientsInChannels(channels))
605 				client.sendCommand(this, "QUIT", why);
606 			server.nicknames.remove(nickname.normalized);
607 			registered = false;
608 		}
609 
610 		void changeNick(string newNick)
611 		{
612 			auto channels = getJoinedChannels();
613 			auto witnesses = server.whoCanSee(this);
614 
615 			foreach (channel; channels)
616 			{
617 				auto pmember = nickname.normalized in channel.members;
618 				assert(pmember);
619 				auto member = *pmember;
620 				channel.members.remove(nickname.normalized);
621 				channel.members[newNick.normalized] = member;
622 			}
623 
624 			foreach (client; witnesses)
625 				client.sendCommand(this, "NICK", newNick, null);
626 
627 			server.nicknames.remove(nickname.normalized);
628 			server.nicknames[newNick.normalized] = this;
629 
630 			nickname = newNick;
631 			update();
632 		}
633 
634 		void sendMotd()
635 		{
636 			sendReply(Reply.RPL_MOTDSTART    , "- %s Message of the Day - ".format(server.hostname));
637 			foreach (line; server.motd)
638 				sendReply(Reply.RPL_MOTD, "- %s".format(line));
639 			sendReply(Reply.RPL_ENDOFMOTD    , "End of /MOTD command.");
640 		}
641 
642 		void sendLusers()
643 		{
644 			sendReply(Reply.RPL_LUSERCLIENT  , "There are %d users and %d services on %d servers".format(
645 				server.clients.byKey.filter!(client => cast(NetworkClient)client && client.registered).walkLength,
646 				server.clients.byKey.filter!(client => !cast(NetworkClient)client).walkLength,
647 				1,
648 			));
649 			sendReply(Reply.RPL_LUSEROP      , server.clients.byKey.filter!(client => client.modes.flags['o']).walkLength.text, "IRC Operators online");
650 			sendReply(Reply.RPL_LUSERUNKNOWN , server.clients.byKey.filter!(client => cast(NetworkClient)client && !client.registered).walkLength.text, "unknown connection(s)");
651 			sendReply(Reply.RPL_LUSERCHANNELS, server.channels.length.text, "channels formed");
652 			sendReply(Reply.RPL_LUSERME      , "I have %d clients and %d servers".format(
653 				server.clients.byKey.filter!(client => client.registered).walkLength,
654 				0,
655 			));
656 		}
657 
658 		bool mayJoin(string name)
659 		{
660 			return true;
661 		}
662 
663 		void join(Channel channel)
664 		{
665 			channel.add(this);
666 			foreach (member; channel.members)
667 				member.client.sendCommand(this, "JOIN", channel.name);
668 			sendTopic(channel);
669 			sendNames(channel);
670 			auto pmember = nickname.normalized in channel.members;
671 			// Sync OPER status with (initial) channel op status
672 			if (server.staticChannels || modes.flags['o'])
673 				setChannelMode(channel, nickname, Channel.Member.Mode.op, modes.flags['o']);
674 		}
675 
676 		// For server-imposed mode changes.
677 		void setChannelMode(Channel channel, string nickname, Channel.Member.Mode mode, bool value)
678 		{
679 			auto pmember = nickname.normalized in channel.members;
680 			if (pmember.modeSet(mode) == value)
681 				return;
682 
683 			pmember.setMode(mode, value);
684 			auto c = ChannelModes.memberModeChars[mode];
685 			foreach (member; channel.members)
686 				member.client.sendCommand(server.hostname, "MODE", channel.name, [value ? '+' : '-', c], nickname, null);
687 			server.channelChanged(channel);
688 		}
689 
690 		void part(Channel channel, string reason=null)
691 		{
692 			foreach (member; channel.members)
693 				member.client.sendCommand(this, "PART", channel.name, reason);
694 			channel.remove(this);
695 		}
696 
697 		void sendToChannel(Channel channel, string command, string message)
698 		{
699 			foreach (member; channel.members)
700 				if (member.client !is this)
701 					member.client.sendCommand(this, command, channel.name, message);
702 		}
703 
704 		void sendToClient(Client client, string command, string message)
705 		{
706 			client.sendCommand(this, command, client.nickname, message);
707 		}
708 
709 		void sendTopic(Channel channel)
710 		{
711 			if (channel.topic)
712 				sendReply(Reply.RPL_TOPIC, channel.name, channel.topic);
713 			else
714 				sendReply(Reply.RPL_NOTOPIC, channel.name, "No topic is set");
715 		}
716 
717 		void sendNames(Channel channel)
718 		{
719 			foreach (chunk; channel.members.values.chunks(10)) // can't use byValue - https://issues.dlang.org/show_bug.cgi?id=11761
720 				sendReply(Reply.RPL_NAMREPLY, channel.modes.flags['s'] ? "@" : channel.modes.flags['p'] ? "*" : "=", channel.name, chunk.map!q{a.displayName}.join(" "));
721 			sendReply(Reply.RPL_ENDOFNAMES, channel.name, "End of /NAMES list");
722 		}
723 
724 		/// For LIST
725 		Channel[] getChannelList()
726 		{
727 			return server.channels.values;
728 		}
729 
730 		void sendChannelModes(Channel channel)
731 		{
732 			string modes = "+";
733 			string[] modeParams;
734 			foreach (char c; 0..char.max)
735 				final switch (ChannelModes.modeTypes[c])
736 				{
737 					case ChannelModes.Type.none:
738 					case ChannelModes.Type.member:
739 						break;
740 					case ChannelModes.Type.mask:
741 						// sent after RPL_CHANNELMODEIS
742 						break;
743 					case ChannelModes.Type.flag:
744 						if (channel.modes.flags[c])
745 							modes ~= c;
746 						break;
747 					case ChannelModes.Type.str:
748 						if (channel.modes.strings[c])
749 						{
750 							modes ~= c;
751 							auto value = channel.modes.strings[c];
752 							assert(value.length, "Empty string channel parameter: " ~ c);
753 							modeParams ~= value;
754 						}
755 						break;
756 					case ChannelModes.Type.number:
757 						if (channel.modes.numbers[c])
758 						{
759 							modes ~= c;
760 							modeParams ~= channel.modes.numbers[c].text;
761 						}
762 						break;
763 				}
764 			sendReply(Reply.RPL_CHANNELMODEIS, [channel.name, modes] ~ modeParams ~ [string.init]);
765 		}
766 
767 		void sendChannelModeMasks(Channel channel, char mode)
768 		{
769 			switch (mode)
770 			{
771 				case 'b':
772 					sendChannelMaskList(channel, channel.modes.masks[mode], Reply.RPL_BANLIST, Reply.RPL_ENDOFBANLIST, "End of channel ban list");
773 					break;
774 				default:
775 					assert(false);
776 			}
777 		}
778 
779 		void sendChannelMaskList(Channel channel, string[] masks, Reply lineReply, Reply endReply, string endText)
780 		{
781 			foreach (mask; masks)
782 				sendReply(lineReply, channel.name, mask, null);
783 			sendReply(endReply, channel.name, endText);
784 		}
785 
786 		void setChannelTopic(Channel channel, string topic)
787 		{
788 			channel.topic = topic;
789 			foreach (ref member; channel.members)
790 				member.client.sendCommand(this, "TOPIC", channel.name, topic);
791 			server.channelChanged(channel);
792 		}
793 
794 		void setChannelModes(Channel channel, string[] modes)
795 		{
796 			auto pself = nickname.normalized in channel.members;
797 			bool op = (pself.modes & Channel.Member.Modes.op) != 0;
798 
799 			string[2] effectedChars;
800 			string[][2] effectedParams;
801 
802 			scope(exit) // Broadcast effected options
803 			{
804 				string[] parameters;
805 				foreach (adding; 0..2)
806 					if (effectedChars[adding].length)
807 						parameters ~= [(adding ? "+" : "-") ~ effectedChars[adding]] ~ effectedParams[adding];
808 				if (parameters.length)
809 				{
810 					assert(op);
811 					parameters = ["MODE", channel.name] ~ parameters ~ [string.init];
812 					foreach (ref member; channel.members)
813 						member.client.sendCommand(this, parameters);
814 				}
815 			}
816 
817 			while (modes.length)
818 			{
819 				auto chars = modes.shift;
820 
821 				bool adding = true;
822 				foreach (c; chars)
823 					if (c == '+')
824 						adding = true;
825 					else
826 					if (c == '-')
827 						adding = false;
828 					else
829 					final switch (ChannelModes.modeTypes[c])
830 					{
831 						case ChannelModes.Type.none:
832 							sendReply(Reply.ERR_UNKNOWNMODE, [c], "is unknown mode char to me for %s".format(channel.name));
833 							break;
834 						case ChannelModes.Type.flag:
835 							if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator");
836 							if (adding != channel.modes.flags[c])
837 							{
838 								channel.modes.flags[c] = adding;
839 								effectedChars[adding] ~= c;
840 							}
841 							break;
842 						case ChannelModes.Type.member:
843 						{
844 							if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator");
845 							if (!modes.length)
846 								{ sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; }
847 							auto memberName = modes.shift;
848 							auto pmember = memberName.normalized in channel.members;
849 							if (!pmember)
850 								{ sendReply(Reply.ERR_USERNOTINCHANNEL, memberName, channel.name, "They aren't on that channel"); continue; }
851 							auto mode = ChannelModes.memberModes[c];
852 							if (pmember.modeSet(mode) != adding)
853 							{
854 								pmember.setMode(mode, adding);
855 								effectedChars[adding] ~= c;
856 								effectedParams[adding] ~= memberName;
857 							}
858 							break;
859 						}
860 						case ChannelModes.Type.mask:
861 						{
862 							if (!modes.length)
863 								return sendChannelModeMasks(channel, c);
864 							if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator");
865 							auto mask = modes.shift;
866 							if (adding)
867 							{
868 								if (channel.modes.masks[c].canFind(mask))
869 									continue;
870 								channel.modes.masks[c] ~= mask;
871 							}
872 							else
873 							{
874 								auto index = channel.modes.masks[c].countUntil(mask);
875 								if (index < 0)
876 									continue;
877 								channel.modes.masks[c] = channel.modes.masks[c][0..index] ~ channel.modes.masks[c][index+1..$];
878 							}
879 							effectedChars[adding] ~= c;
880 							effectedParams[adding] ~= mask;
881 							break;
882 						}
883 						case ChannelModes.Type.str:
884 							if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator");
885 							if (adding)
886 							{
887 								if (!modes.length)
888 									{ sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; }
889 								auto str = modes.shift;
890 								if (channel.modes.strings[c] == str)
891 									continue;
892 								channel.modes.strings[c] = str;
893 								effectedChars[adding] ~= c;
894 								effectedParams[adding] ~= str;
895 							}
896 							else
897 							{
898 								if (!channel.modes.strings[c])
899 									continue;
900 								channel.modes.strings[c] = null;
901 								effectedChars[adding] ~= c;
902 							}
903 							break;
904 						case ChannelModes.Type.number:
905 							if (!op) return sendReply(Reply.ERR_CHANOPRIVSNEEDED, channel.name, "You're not channel operator");
906 							if (adding)
907 							{
908 								if (!modes.length)
909 									{ sendReply(Reply.ERR_NEEDMOREPARAMS, "MODE", "Not enough parameters"); continue; }
910 								auto numText = modes.shift;
911 								auto num = numText.to!long;
912 								if (channel.modes.numbers[c] == num)
913 									continue;
914 								channel.modes.numbers[c] = num;
915 								effectedChars[adding] ~= c;
916 								effectedParams[adding] ~= numText;
917 							}
918 							else
919 							{
920 								if (!channel.modes.numbers[c])
921 									continue;
922 								channel.modes.numbers[c] = 0;
923 								effectedChars[adding] ~= c;
924 							}
925 							break;
926 					}
927 			}
928 			server.channelChanged(channel);
929 		}
930 
931 		void setUserModes(string[] modes)
932 		{
933 			while (modes.length)
934 			{
935 				auto chars = modes.shift;
936 
937 				bool adding = true;
938 				foreach (c; chars)
939 					if (c == '+')
940 						adding = true;
941 					else
942 					if (c == '-')
943 						adding = false;
944 					else
945 					final switch (UserModes.modeTypes[c])
946 					{
947 						case UserModes.Type.none:
948 							sendReply(Reply.ERR_UMODEUNKNOWNFLAG, "Unknown MODE flag");
949 							break;
950 						case UserModes.Type.flag:
951 							if (UserModes.isSettable[c])
952 								this.modes.flags[c] = adding;
953 							break;
954 					}
955 			}
956 		}
957 
958 		void sendUserModes(Client client)
959 		{
960 			string modeString = "+";
961 			foreach (char c, on; modes.flags)
962 				if (on)
963 					modeString ~= c;
964 			return sendReply(Reply.RPL_UMODEIS, modeString, null);
965 		}
966 
967 		void sendCommand(Client from, string[] parameters...)
968 		{
969 			return sendCommand(from.prefixAsVisibleTo(this), parameters);
970 		}
971 
972 		void sendCommand(string from, string[] parameters...)
973 		{
974 			assert(parameters.length, "At least one parameter expected");
975 			foreach (parameter; parameters[0..$-1])
976 				assert(parameter.length && parameter[0] != ':' && parameter.indexOf(' ') < 0, "Invalid parameter: " ~ parameter);
977 			if (parameters[$-1] is null)
978 				parameters = parameters[0..$-1];
979 			else
980 				parameters = parameters[0..$-1] ~ [":" ~ parameters[$-1]];
981 			auto line = ":%s %-(%s %)".format(from, parameters);
982 			sendLine(line);
983 		}
984 
985 		void sendReply(Reply reply, string[] parameters...)
986 		{
987 			return sendReply("%03d".format(reply), parameters);
988 		}
989 
990 		void sendReply(string command, string[] parameters...)
991 		{
992 			return sendCommand(server.hostname, [command, nickname ? nickname : "*"] ~ parameters);
993 		}
994 
995 		void sendServerNotice(string text)
996 		{
997 			sendReply("NOTICE", "*** Notice -- " ~ text);
998 		}
999 
1000 		void sendLine(string line)
1001 		{
1002 			if (encoder) line = encoder(line);
1003 			connSendLine(line);
1004 		}
1005 
1006 		abstract bool connConnected();
1007 		abstract void connSendLine(string line);
1008 		abstract void connDisconnect(string reason);
1009 	}
1010 
1011 	/// `Client` implementation backed by a real network connection.
1012 	static class NetworkClient : Client
1013 	{
1014 	protected:
1015 		IrcConnection conn;
1016 
1017 		this(IrcServer server, IrcConnection incoming, Address remoteAddress)
1018 		{
1019 			super(server, remoteAddress);
1020 
1021 			conn = incoming;
1022 			conn.handleReadLine = &onReadLine;
1023 			conn.handleInactivity = &onInactivity;
1024 			conn.handleDisconnect = &onDisconnect;
1025 		}
1026 
1027 		override bool connConnected()
1028 		{
1029 			return conn.state == ConnectionState.connected;
1030 		}
1031 
1032 		override void connSendLine(string line)
1033 		{
1034 			conn.send(line);
1035 		}
1036 
1037 		override void connDisconnect(string reason)
1038 		{
1039 			conn.disconnect(reason);
1040 		}
1041 	}
1042 
1043 	HashSet!Client clients; /// All clients
1044 	Client[string] nicknames; /// Registered clients only
1045 
1046 	/// Statistics
1047 	ulong maxUsers, totalConnections;
1048 
1049 	/// IRC channel information.
1050 	final class Channel
1051 	{
1052 		string name; /// Channel name (including any leading `'#'`).
1053 		string topic; /// Channel topic, if any.
1054 
1055 		Modes modes; /// Channel modes.
1056 
1057 		/// Channel member (entry for a user who is in the channel).
1058 		struct Member
1059 		{
1060 			/// A mode that a user may or may not have when in a channel.
1061 			enum Mode
1062 			{
1063 				op,    /// Channel operator. Can change channel properties.
1064 				voice, /// Has voice. May speak even when banned or the channel is moderated.
1065 			}
1066 
1067 			/// Bitmask for modes that a user has in the channel.
1068 			enum Modes
1069 			{
1070 				none  = 0,               ///
1071 				op    = 1 << Mode.op,    ///
1072 				voice = 1 << Mode.voice, ///
1073 
1074 				bypassM = op | voice,    /// Modes which bypass +m.
1075 			}
1076 
1077 			Client client; ///
1078 			Modes modes; ///
1079 
1080 			/// Does this member have the given mode?
1081 			bool modeSet(Mode mode) { return (modes & (1 << mode)) != 0; }
1082 			void setMode(Mode mode, bool value)
1083 			{
1084 				auto modeMask = 1 << mode;
1085 				if (value)
1086 					modes |= modeMask;
1087 				else
1088 					modes &= ~modeMask;
1089 			} /// Set (enable or disable) the given mode for this channel member.
1090 
1091 			/// Returns the character used to indicate the user's highest channel mode
1092 			/// (e.g. `'@'` or `'+'`).
1093 			string modeChar()
1094 			{
1095 				foreach (mode; Mode.init..enumLength!Mode)
1096 					if ((1 << mode) & modes)
1097 						return [ChannelModes.memberModePrefixes[mode]];
1098 				return "";
1099 			}
1100 			/// Returns this member's name as it would appear in a RPL_NAMREPLY listing,
1101 			/// i.e. `modeChar` plus nickname.
1102 			string displayName() { return modeChar ~ client.nickname; }
1103 		}
1104 
1105 		Member[string] members; /// Channel members. The key is the normalized nickname.
1106 
1107 		this(string name)
1108 		{
1109 			this.name = name;
1110 			modes.flags['t'] = modes.flags['n'] = true;
1111 		} ///
1112 
1113 		void add(Client client)
1114 		{
1115 			auto modes = staticChannels || members.length ? Member.Modes.none : Member.Modes.op;
1116 			members[client.nickname.normalized] = Member(client, modes);
1117 		} ///
1118 
1119 		void remove(Client client)
1120 		{
1121 			members.remove(client.nickname.normalized);
1122 			if (!staticChannels && !members.length && !modes.flags['P'])
1123 				channels.remove(name.normalized);
1124 		} ///
1125 	}
1126 
1127 	Channel[string] channels; /// All channels on this server.
1128 
1129 	TcpServer conn; /// Listening socket.
1130 
1131 	this()
1132 	{
1133 		conn = new TcpServer;
1134 		conn.handleAccept = &onAccept;
1135 
1136 		hostname = Socket.hostName;
1137 		creationTime = Clock.currTime;
1138 	} ///
1139 
1140 	/// Listen on the given address.
1141 	/// If port is 0, listen on a random available port.
1142 	/// Returns the actual listening port.
1143 	ushort listen(ushort port=6667, string addr = null)
1144 	{
1145 		port = conn.listen(port, addr);
1146 		return port;
1147 	}
1148 
1149 	/// Creates a new channel. The default modes are "+nt".
1150 	Channel createChannel(string name)
1151 	{
1152 		return channels[name.normalized] = new Channel(name);
1153 	}
1154 
1155 	/// Stop listening and disconnect all clients.
1156 	void close(string reason)
1157 	{
1158 		conn.close();
1159 		foreach (client; clients.keys)
1160 			client.disconnect("Server is shutting down" ~ (reason.length ? ": " ~ reason : ""));
1161 	}
1162 
1163 protected:
1164 	Client createClient(TcpConnection incoming)
1165 	{
1166 		return new NetworkClient(this, new IrcConnection(incoming), incoming.remoteAddress);
1167 	}
1168 
1169 	void onAccept(TcpConnection incoming)
1170 	{
1171 		createClient(incoming);
1172 		totalConnections++;
1173 	}
1174 
1175 	Client[string] allClientsInChannels(Channel[] channels)
1176 	{
1177 		Client[string] result;
1178 		foreach (channel; channels)
1179 			foreach (ref member; channel.members)
1180 				result[member.client.nickname.normalized] = member.client;
1181 		return result;
1182 	}
1183 
1184 	/// Clients who can see the given client (are in the same channer).
1185 	/// Includes the target client himself.
1186 	Client[string] whoCanSee(Client who)
1187 	{
1188 		auto clients = allClientsInChannels(who.getJoinedChannels());
1189 		clients[who.nickname.normalized] = who;
1190 		return clients;
1191 	}
1192 
1193 	bool isChannelName(string target)
1194 	{
1195 		foreach (prefix; chanTypes)
1196 			if (target.startsWith(prefix))
1197 				return true;
1198 		return false;
1199 	}
1200 
1201 	string[] capabilities()
1202 	{
1203 		string[] result;
1204 		result ~= "PREFIX=(%s)%s".format(ChannelModes.memberModeChars, ChannelModes.memberModePrefixes);
1205 		result ~= "CHANTYPES=" ~ chanTypes;
1206 		result ~= "CHANMODES=%-(%s,%)".format(
1207 			[ChannelModes.Type.mask, ChannelModes.Type.str, ChannelModes.Type.number, ChannelModes.Type.flag].map!(type => ChannelModes.byType(type))
1208 		);
1209 		if (network)
1210 			result ~= "NETWORK=" ~ network;
1211 		result ~= "CASEMAPPING=rfc1459";
1212 		result ~= "NICKLEN=" ~ text(nicknameMaxLength);
1213 		return result;
1214 	}
1215 
1216 	/// Persistence hook
1217 	void channelChanged(Channel channel)
1218 	{
1219 	}
1220 }
1221 
1222 /// Check if the given string matches the given mask
1223 /// (e.g. when enforcing +b modes).
1224 bool maskMatch(string subject, string mask)
1225 {
1226 	import std.path;
1227 	return globMatch!(CaseSensitive.no)(subject, mask);
1228 }
1229 
1230 /// Encode a host name to be sent in a WHO / WHOIS reply.
1231 string safeHostname(string s)
1232 {
1233 	assert(s.length);
1234 	if (s[0] == ':')
1235 		s = '0' ~ s;
1236 	return s;
1237 }
1238 
1239 /// The method used when normalizing user and channel names for lookup.
1240 alias rfc1459toUpper normalized;
1241 
1242 /// Split an IRC line into parameters.
1243 string[] ircSplit(string line)
1244 {
1245 	auto colon = line.indexOf(":");
1246 	if (colon < 0)
1247 		return line.split;
1248 	else
1249 		return line[0..colon].strip.split ~ [line[colon+1..$]];
1250 }
1251 
1252 /// Represents channel modes.
1253 struct Modes
1254 {
1255 	/// A mode may be a flag (e.g. +t), a string (e.g. +k), a number (e.g. +l), or a list of strings (generally masks, e.g. +b).
1256 	bool[char.max] flags;
1257 	string[char.max] strings; /// ditto
1258 	long[char.max] numbers; /// ditto
1259 	string[][char.max] masks; /// ditto
1260 }
1261 
1262 /// Common declarations for `ChannelModes` and `UserModes`.
1263 mixin template CommonModes()
1264 {
1265 //static immutable:
1266 	/// The type of the given mode character.
1267 	Type[char.max] modeTypes;
1268 	/// List of modes supported by this module.
1269 	string supported()       pure { return modeTypes.length.iota.filter!(m => modeTypes[m]        ).map!(m => cast(char)m).array; }
1270 	/// List of modes of the given type supported by this module.
1271 	string byType(Type type) pure { return modeTypes.length.iota.filter!(m => modeTypes[m] == type).map!(m => cast(char)m).array; }
1272 }
1273 
1274 /// Encodes static information about channel modes supported by this module.
1275 struct ChannelModes
1276 {
1277 static immutable:
1278 	enum Type
1279 	{
1280 		none,   ///
1281 		flag,	///
1282 		member,	///
1283 		mask,	///
1284 		str,	///
1285 		number,	///
1286 	} /// Mode types.
1287 	mixin CommonModes;
1288 	IrcServer.Channel.Member.Mode[char.max] memberModes; /// Mappings from channel to member modes.
1289 	/// ditto
1290 	char[enumLength!(IrcServer.Channel.Member.Mode)] memberModeChars, memberModePrefixes;
1291 
1292 	shared static this()
1293 	{
1294 		foreach (c; "ntpsP")
1295 			modeTypes[c] = Type.flag;
1296 		foreach (c; "ov")
1297 			modeTypes[c] = Type.member;
1298 		foreach (c; "b")
1299 			modeTypes[c] = Type.mask;
1300 		foreach (c; "k")
1301 			modeTypes[c] = Type.str;
1302 
1303 		memberModes['o'] = IrcServer.Channel.Member.Mode.op;
1304 		memberModes['v'] = IrcServer.Channel.Member.Mode.voice;
1305 
1306 		memberModeChars    = "ov";
1307 		memberModePrefixes = "@+";
1308 	}
1309 }
1310 
1311 /// Encodes static information about user modes supported by this module.
1312 struct UserModes
1313 {
1314 static immutable:
1315 	enum Type
1316 	{
1317 		none, ///
1318 		flag, ///
1319 	} /// Mode types.
1320 	mixin CommonModes;
1321 	bool[char.max] isSettable; /// Can users change this mode for themselves?
1322 
1323 	shared static this()
1324 	{
1325 		foreach (c; "io")
1326 			modeTypes[c] = Type.flag;
1327 		foreach (c; "i")
1328 			isSettable[c] = true;
1329 	}
1330 }