text: "use strict";Object.defineProperty(exports, "__esModule", {value: true});/**
   * Users
   * Pokemon Showdown - http://pokemonshowdown.com/
   *
   * Most of the communication with users happens here.
   *
   * There are two object types this file introduces:
   * User and Connection.
   *
   * A User object is a user, identified by username. A guest has a
   * username in the form "Guest 12". Any user whose username starts
   * with "Guest" must be a guest; normal users are not allowed to
   * use usernames starting with "Guest".
   *
   * A User can be connected to Pokemon Showdown from any number of tabs
   * or computers at the same time. Each connection is represented by
   * a Connection object. A user tracks its connections in
   * user.connections - if this array is empty, the user is offline.
   *
   * Get a user by username with Users.get
   * (scroll down to its definition for details)
   *
   * @license MIT license
   */
  
  'use strict';
  
  
  
  
  
  
  
  
  
  const PLAYER_SYMBOL = '\u2606';
  const HOST_SYMBOL = '\u2605';
  
  const THROTTLE_DELAY = 600;
  const THROTTLE_BUFFER_LIMIT = 6;
  const THROTTLE_MULTILINE_WARN = 3;
  const THROTTLE_MULTILINE_WARN_STAFF = 6;
  
  const PERMALOCK_CACHE_TIME = 30 * 24 * 60 * 60 * 1000;
  
  const DEFAULT_TRAINER_SPRITES = [1, 2, 101, 102, 169, 170, 265, 266];
  
  var _fs = require('../.lib-dist/fs');
  
  const MINUTES = 60 * 1000;
  const IDLE_TIMER = 60 * MINUTES;
  const STAFF_IDLE_TIMER = 30 * MINUTES;
  
  
  
  /*********************************************************
   * Utility functions
   *********************************************************/
  
  // Low-level functions for manipulating Users.users and Users.prevUsers
  // Keeping them all here makes it easy to ensure they stay consistent
  
  function move(user, newUserid) {
  	if (user.userid === newUserid) return true;
  	if (!user) return false;
  
  	// doing it this way mathematically ensures no cycles
  	prevUsers.delete(newUserid);
  	prevUsers.set(user.userid, newUserid);
  
  	users.delete(user.userid);
  	user.userid = newUserid;
  	users.set(newUserid, user);
  
  	user.forcedPublic = null;
  	if (Config.forcedpublicprefixes) {
  		for (const prefix of Config.forcedpublicprefixes) {
  			if (user.userid.startsWith(toID(prefix))) {
  				user.forcedPublic = prefix;
  				break;
  			}
  		}
  	}
  
  	return true;
  }
  function add(user) {
  	if (user.userid) throw new Error(`Adding a user that already exists`);
  
  	numUsers++;
  	user.guestNum = numUsers;
  	user.name = `Guest ${numUsers}`;
  	user.userid = toID(user.name);
  
  	if (users.has(user.userid)) throw new Error(`userid taken: ${user.userid}`);
  	users.set(user.userid, user);
  }
  function deleteUser(user) {
  	prevUsers.delete('guest' + user.guestNum );
  	users.delete(user.userid);
  }
  function merge(toRemain, toDestroy) {
  	prevUsers.delete(toRemain.userid);
  	prevUsers.set(toDestroy.userid, toRemain.userid);
  }
  
  /**
   * Get a user.
   *
   * Usage:
   *   Users.get(userid or username)
   *
   * Returns the corresponding User object, or null if no matching
   * was found.
   *
   * By default, this function will track users across name changes.
   * For instance, if "Some dude" changed their name to "Some guy",
   * Users.get("Some dude") will give you "Some guy"s user object.
   *
   * If this behavior is undesirable, use Users.getExact.
   */
  function getUser(name, exactName = false) {
  	if (!name || name === '!') return null;
  	if ((name ).userid) return name ;
  	let userid = toID(name);
  	let i = 0;
  	if (!exactName) {
  		while (userid && !users.has(userid) && i < 1000) {
  			userid = prevUsers.get(userid);
  			i++;
  		}
  	}
  	return users.get(userid) || null;
  }
  
  /**
   * Get a user by their exact username.
   *
   * Usage:
   *   Users.getExact(userid or username)
   *
   * Like Users.get, but won't track across username changes.
   *
   * Users.get(userid or username, true) is equivalent to
   * Users.getExact(userid or username).
   * The former is not recommended because it's less readable.
   */
  function getExactUser(name) {
  	return getUser(name, true);
  }
  
  /**
   * Get a list of all users matching a list of userids and ips.
   *
   * Usage:
   *   Users.findUsers([userids], [ips])
   */
  function findUsers(userids, ips, options = {}) {
  	const matches = [];
  	if (options.forPunishment) ips = ips.filter(ip => !Punishments.sharedIps.has(ip));
  	for (const user of users.values()) {
  		if (!options.forPunishment && !user.named && !user.connected) continue;
  		if (!options.includeTrusted && user.trusted) continue;
  		if (userids.includes(user.userid)) {
  			matches.push(user);
  			continue;
  		}
  		for (const myIp of ips) {
  			if (myIp in user.ips) {
  				matches.push(user);
  				break;
  			}
  		}
  	}
  	return matches;
  }
  
  /*********************************************************
   * User groups
   *********************************************************/
  
  const usergroups = Object.create(null);
  function importUsergroups() {
  	// can't just say usergroups = {} because it's exported
  	for (const i in usergroups) delete usergroups[i];
  	return _fs.FS.call(void 0, 'config/usergroups.csv').readIfExists().then(data => {
  		for (const row of data.split("\n")) {
  			if (!row) continue;
  			const cells = row.split(",");
  			usergroups[toID(cells[0])] = (cells[1] || Config.groupsranking[0]) + cells[0];
  		}
  	});
  }
  function exportUsergroups() {
  	let buffer = '';
  	for (const i in usergroups) {
  		buffer += usergroups[i].substr(1).replace(/,/g, '') + ',' + usergroups[i].charAt(0) + "\n";
  	}
  	return _fs.FS.call(void 0, 'config/usergroups.csv').write(buffer);
  }
  // tslint:disable-next-line:no-floating-promises
  importUsergroups();
  
  function cacheGroupData() {
  	if (Config.groups) {
  		// Support for old config groups format.
  		// Should be removed soon.
  		console.error(
  			`You are using a deprecated version of user group specification in config.\n` +
  			`Support for this will be removed soon.\n` +
  			`Please ensure that you update your config.js to the new format (see config-example.js, line 220).\n`
  		);
  	} else {
  		Config.punishgroups = Object.create(null);
  		Config.groups = Object.create(null);
  		Config.groupsranking = [];
  	}
  
  	const groups = Config.groups;
  	const punishgroups = Config.punishgroups;
  	const cachedGroups = {};
  
  	function cacheGroup(sym, groupData) {
  		if (cachedGroups[sym] === 'processing') return false; // cyclic inheritance.
  
  		if (cachedGroups[sym] !== true && groupData['inherit']) {
  			cachedGroups[sym] = 'processing';
  			const inheritGroup = groups[groupData['inherit']];
  			if (cacheGroup(groupData['inherit'], inheritGroup)) {
  				// Add lower group permissions to higher ranked groups,
  				// preserving permissions specifically declared for the higher group.
  				for (const key in inheritGroup) {
  					if (key in groupData) continue;
  					groupData[key] = inheritGroup[key];
  				}
  			}
  			delete groupData['inherit'];
  		}
  		return (cachedGroups[sym] = true);
  	}
  
  	if (Config.grouplist) { // Using new groups format.
  		const grouplist = Config.grouplist;
  		const numGroups = grouplist.length;
  		for (let i = 0; i < numGroups; i++) {
  			const groupData = grouplist[i];
  
  			// punish groups
  			if (groupData.punishgroup) {
  				punishgroups[groupData.id] = groupData;
  				continue;
  			}
  
  			groupData.rank = numGroups - i - 1;
  			groups[groupData.symbol] = groupData;
  			Config.groupsranking.unshift(groupData.symbol);
  		}
  	}
  
  	for (const sym in groups) {
  		const groupData = groups[sym];
  		cacheGroup(sym, groupData);
  	}
  
  	// hardcode default punishgroups.
  	if (!punishgroups.locked) {
  		punishgroups.locked = {
  			name: 'Locked',
  			id: 'locked',
  			symbol: '\u203d',
  		};
  	}
  	if (!punishgroups.muted) {
  		punishgroups.muted = {
  			name: 'Muted',
  			id: 'muted',
  			symbol: '!',
  		};
  	}
  }
  cacheGroupData();
  
  function setOfflineGroup(name, group, forceTrusted) {
  	if (!group) throw new Error(`Falsy value passed to setOfflineGroup`);
  	const userid = toID(name);
  	const user = getExactUser(userid);
  	if (user) {
  		user.setGroup(group, forceTrusted);
  		return true;
  	}
  	if (group === Config.groupsranking[0] && !forceTrusted) {
  		delete usergroups[userid];
  	} else {
  		const usergroup = usergroups[userid];
  		name = usergroup ? usergroup.substr(1) : name;
  		usergroups[userid] = group + name;
  	}
  	// tslint:disable-next-line:no-floating-promises
  	exportUsergroups();
  	return true;
  }
  function isUsernameKnown(name) {
  	const userid = toID(name);
  	if (exports.Users.get(userid)) return true;
  	if (userid in usergroups) return true;
  	for (const room of Rooms.global.chatRooms) {
  		if (!room.auth) continue;
  		if (userid in room.auth) return true;
  	}
  	return false;
  }
  
  function isTrusted(name) {
  	if ((name ).trusted) return (name ).trusted;
  	const userid = toID(name);
  	if (userid in usergroups) return userid;
  	for (const room of Rooms.global.chatRooms) {
  		if (!room.isPrivate && !room.isPersonal && room.auth && userid in room.auth && room.auth[userid] !== '+') {
  			return userid;
  		}
  	}
  	return false;
  }
  
  /*********************************************************
   * User and Connection classes
   *********************************************************/
  
  const connections = new Map();
  
   class Connection {
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	constructor(
  		id,
  		worker,
  		socketid,
  		user,
  		ip,
  		protocol
  	) {
  		this.id = id;
  		this.socketid = socketid;
  		this.worker = worker;
  		this.inRooms = new Set();
  
  		/**
  		 * This can be null during initialization and after disconnecting,
  		 * but we're asserting it non-null for ease of use. The main risk
  		 * is async code, where you need to re-check that it's not null
  		 * before using it.
  		 */
  		this.user = user;
  
  		this.ip = ip || '';
  		this.protocol = protocol || '';
  
  		this.challenge = '';
  		this.autojoins = '';
  		this.lastActiveTime = Date.now();
  	}
  	sendTo(roomid, data) {
  		if (roomid && (roomid ).id) roomid = (roomid ).id;
  		if (roomid && roomid !== 'lobby') data = `>${roomid}\n${data}`;
  		Sockets.socketSend(this.worker, this.socketid, data);
  		Monitor.countNetworkUse(data.length);
  	}
  
  	send(data) {
  		Sockets.socketSend(this.worker, this.socketid, data);
  		Monitor.countNetworkUse(data.length);
  	}
  
  	destroy() {
  		Sockets.socketDisconnect(this.worker, this.socketid);
  		this.onDisconnect();
  	}
  	onDisconnect() {
  		connections.delete(this.id);
  		if (this.user) this.user.onDisconnect(this);
  		this.user = null;
  	}
  
  	popup(message) {
  		this.send(`|popup|` + message.replace(/\n/g, '||'));
  	}
  
  	joinRoom(room) {
  		if (this.inRooms.has(room.id)) return;
  		this.inRooms.add(room.id);
  		Sockets.roomAdd(this.worker, room.id, this.socketid);
  	}
  	leaveRoom(room) {
  		if (this.inRooms.has(room.id)) {
  			this.inRooms.delete(room.id);
  			Sockets.roomRemove(this.worker, room.id, this.socketid);
  		}
  	}
  	toString() {
  		let buf = this.user ? `${this.user.userid}[${this.user.connections.indexOf(this)}]` : `[disconnected]`;
  		buf += `:${this.ip}`;
  		if (this.protocol !== 'websocket') buf += `:${this.protocol}`;
  		return buf;
  	}
  } exports.Connection = Connection;
  
  
  
  const SETTINGS = [
  	'isSysop', 'isStaff', 'blockChallenges', 'blockPMs',
  	'ignoreTickets', 'lastConnected', 'inviteOnlyNextBattle',
  ];
  
  // User
   class User extends Chat.MessageContext {
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  
  	
  	
  	
  	
  	
  	
  	
  	
  
  	
  	
  	
  	
  	
  	
  	
  
  	
  	
  	
  	
  
  	
  	
  	
  	
  	
  	
  
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	constructor(connection) {
  		super(connection.user);
  		this.user = this;
  		this.mmrCache = Object.create(null);
  		this.guestNum = -1;
  		this.name = "";
  		this.named = false;
  		this.registered = false;
  		this.userid = '';
  		this.group = Config.groupsranking[0];
  		this.language = null;
  
  		this.avatar = DEFAULT_TRAINER_SPRITES[Math.floor(Math.random() * DEFAULT_TRAINER_SPRITES.length)];
  
  		this.connected = true;
  
  		if (connection.user) connection.user = this;
  		this.connections = [connection];
  		this.latestHost = '';
  		this.latestHostType = '';
  		this.ips = Object.create(null);
  		this.ips[connection.ip] = 1;
  		// Note: Using the user's latest IP for anything will usually be
  		//       wrong. Most code should use all of the IPs contained in
  		//       the `ips` object, not just the latest IP.
  		this.latestIp = connection.ip;
  		this.locked = null;
  		this.semilocked = null;
  		this.namelocked = null;
  		this.permalocked = null;
  		this.prevNames = Object.create(null);
  		this.inRooms = new Set();
  
  		/**
  		 * Set of room IDs
  		 */
  		this.games = new Set();
  		/** Millisecond timestamp for last battle decision */
  		this.lastDecision = 0;
  
  		// misc state
  		this.lastChallenge = 0;
  		this.lastPM = '';
  		this.team = '';
  		this.lastMatch = '';
  		this.forcedPublic = null;
  
  		// settings
  		this.isSysop = false;
  		this.isStaff = false;
  		this.blockChallenges = false;
  		this.blockPMs = false;
  		this.ignoreTickets = false;
  		this.lastConnected = 0;
  		this.inviteOnlyNextBattle = false;
  
  		// chat queue
  		this.chatQueue = null;
  		this.chatQueueTimeout = null;
  		this.lastChatMessage = 0;
  		this.lastCommand = '';
  
  		// for the anti-spamming mechanism
  		this.lastMessage = ``;
  		this.lastMessageTime = 0;
  		this.lastReportTime = 0;
  		this.s1 = '';
  		this.s2 = '';
  		this.s3 = '';
  
  		this.blockChallengesNotified = false;
  		this.blockPMsNotified = false;
  		this.punishmentNotified = false;
  		this.lockNotified = false;
  		this.autoconfirmed = '';
  		this.trusted = '';
  		// Used in punishments
  		this.trackRename = '';
  		this.statusType = 'online';
  		this.userMessage = '';
  		this.lastWarnedAt = 0;
  
  		// initialize
  		exports.Users.add(this);
  	}
  
  	sendTo(roomid, data) {
  		if (roomid && typeof roomid !== 'string') roomid = roomid.id;
  		if (roomid && roomid !== 'global' && roomid !== 'lobby') data = `>${roomid}\n${data}`;
  		for (const connection of this.connections) {
  			if (roomid && !connection.inRooms.has(roomid)) continue;
  			connection.send(data);
  			Monitor.countNetworkUse(data.length);
  		}
  	}
  	send(data) {
  		for (const connection of this.connections) {
  			connection.send(data);
  			Monitor.countNetworkUse(data.length);
  		}
  	}
  	popup(message) {
  		this.send(`|popup|` + message.replace(/\n/g, '||'));
  	}
  	getIdentity(roomid = '') {
  		if (this.locked || this.namelocked) {
  			const lockedSymbol = (Config.punishgroups && Config.punishgroups.locked ? Config.punishgroups.locked.symbol
  				: '\u203d');
  			return lockedSymbol + this.name;
  		}
  		if (roomid && roomid !== 'global') {
  			const room = Rooms(roomid);
  			if (!room) {
  				throw new Error(`Room doesn't exist: ${roomid}`);
  			}
  			if (room.isMuted(this)) {
  				const mutedSymbol = (Config.punishgroups && Config.punishgroups.muted ? Config.punishgroups.muted.symbol : '!');
  				return mutedSymbol + this.name;
  			}
  			return room.getAuth(this) + this.name;
  		}
  		if (this.semilocked) {
  			const mutedSymbol = (Config.punishgroups && Config.punishgroups.muted ? Config.punishgroups.muted.symbol : '!');
  			return mutedSymbol + this.name;
  		}
  		return this.group + this.name;
  	}
  	getIdentityWithStatus(roomid = '') {
  		const identity = this.getIdentity(roomid);
  		const status = this.statusType === 'online' ? '' : '@!';
  		return `${identity}${status}`;
  	}
  	getStatus() {
  		const statusMessage = this.statusType === 'busy' ? '!(Busy) ' : this.statusType === 'idle' ? '!(Idle) ' : '';
  		const status = statusMessage + (this.userMessage || '');
  		return status;
  	}
  	authAtLeast(minAuth, room = null) {
  		if (!minAuth || minAuth === ' ') return true;
  		if (minAuth === 'trusted' && this.trusted) return true;
  		if (minAuth === 'autoconfirmed' && this.autoconfirmed) return true;
  
  		if (minAuth === 'trusted' || minAuth === 'autoconfirmed') {
  			minAuth = Config.groupsranking[1];
  		}
  		if (!(minAuth in Config.groups)) return false;
  		const auth = (room && !this.can('makeroom') ? room.getAuth(this) : this.group);
  		return auth in Config.groups && Config.groups[auth].rank >= Config.groups[minAuth].rank;
  	}
  	can(permission, target = null, room = null) {
  		if (this.hasSysopAccess()) return true;
  
  		let groupData = Config.groups[this.group];
  		if (groupData && groupData['root']) {
  			return true;
  		}
  
  		let group = '';
  		let targetGroup = '';
  		let targetUser = null;
  
  		if (typeof target === 'string') {
  			targetGroup = target;
  		} else {
  			targetUser = target;
  		}
  
  		if (room && (room.auth || room.parent)) {
  			group = room.getAuth(this);
  			if (targetUser) targetGroup = room.getAuth(targetUser);
  			if (room.isPrivate === true && this.can('makeroom')) group = this.group;
  		} else {
  			group = this.group;
  			if (targetUser) targetGroup = targetUser.group;
  		}
  
  		groupData = Config.groups[group];
  
  		const roomIsTemporary = room && (room.isPersonal || room.battle);
  		if (roomIsTemporary && group === this.group && groupData.globalGroupInPersonalRoom) {
  			const newGroup = groupData.globalGroupInPersonalRoom;
  			if (Config.groups[newGroup].rank > groupData.rank) {
  				groupData = Config.groups[newGroup];
  			}
  		}
  
  		if (groupData && groupData[permission]) {
  			const jurisdiction = groupData[permission];
  			if (!targetUser && !targetGroup) {
  				return !!jurisdiction;
  			}
  			if (jurisdiction === true && permission !== 'jurisdiction') {
  				return this.can('jurisdiction', (targetUser || targetGroup), room);
  			}
  			if (typeof jurisdiction !== 'string') {
  				return !!jurisdiction;
  			}
  			if (jurisdiction.includes(targetGroup)) {
  				return true;
  			}
  			if (jurisdiction.includes('s') && targetUser === this) {
  				return true;
  			}
  			if (jurisdiction.includes('u') && Config.groupsranking.indexOf(group) > Config.groupsranking.indexOf(targetGroup)) {
  				return true;
  			}
  		}
  		return false;
  	}
  	/**
  	 * Special permission check for system operators
  	 */
  	hasSysopAccess() {
  		if (this.isSysop && Config.backdoor) {
  			// This is the Pokemon Showdown system operator backdoor.
  
  			// Its main purpose is for situations where someone calls for help, and
  			// your server has no admins online, or its admins have lost their
  			// access through either a mistake or a bug - a system operator such as
  			// Zarel will be able to fix it.
  
  			// This relies on trusting Pokemon Showdown. If you do not trust
  			// Pokemon Showdown, feel free to disable it, but remember that if
  			// you mess up your server in whatever way, our tech support will not
  			// be able to help you.
  			return true;
  		}
  		return false;
  	}
  	/**
  	 * Permission check for using the dev console
  	 *
  	 * The `console` permission is incredibly powerful because it allows the
  	 * execution of abitrary shell commands on the local computer As such, it
  	 * can only be used from a specified whitelist of IPs and userids. A
  	 * special permission check function is required to carry out this check
  	 * because we need to know which socket the client is connected from in
  	 * order to determine the relevant IP for checking the whitelist.
  	 */
  	hasConsoleAccess(connection) {
  		if (this.hasSysopAccess()) return true;
  		if (!this.can('console')) return false; // normal permission check
  
  		const whitelist = Config.consoleips || ['127.0.0.1'];
  		// on the IP whitelist OR the userid whitelist
  		return whitelist.includes(connection.ip) || whitelist.includes(this.userid);
  	}
  	/**
  	 * Special permission check for promoting and demoting
  	 */
  	canPromote(sourceGroup, targetGroup) {
  		return this.can('promote', sourceGroup) && this.can('promote', targetGroup);
  	}
  	resetName(isForceRenamed = false) {
  		return this.forceRename('Guest ' + this.guestNum, false, isForceRenamed);
  	}
  	updateIdentity(roomid = null) {
  		if (roomid) {
  			return Rooms(roomid).onUpdateIdentity(this);
  		}
  		for (const inRoomID of this.inRooms) {
  			Rooms(inRoomID).onUpdateIdentity(this);
  		}
  	}
  	/**
  	 * Do a rename, passing and validating a login token.
  	 *
  	 * @param name The name you want
  	 * @param token Signed assertion returned from login server
  	 * @param newlyRegistered Make sure this account will identify as registered
  	 * @param connection The connection asking for the rename
  	 */
  	async rename(name, token, newlyRegistered, connection) {
  		let userid = toID(name);
  		if (userid !== this.userid) {
  			for (const roomid of this.games) {
  				const room = Rooms(roomid);
  				if (!room || !room.game || room.game.ended) {
  					this.games.delete(roomid);
  					console.log(`desynced roomgame ${roomid} renaming ${this.userid} -> ${userid}`);
  					continue;
  				}
  				if (room.game.allowRenames || !this.named) continue;
  				this.popup(`You can't change your name right now because you're in ${room.game.title}, which doesn't allow renaming.`);
  				return false;
  			}
  		}
  
  		let challenge = '';
  		if (connection) {
  			challenge = connection.challenge;
  		}
  		if (!challenge) {
  			Monitor.warn(`verification failed; no challenge`);
  			return false;
  		}
  
  		if (!name) name = '';
  		if (!/[a-zA-Z]/.test(name)) {
  			// technically it's not "taken", but if your client doesn't warn you
  			// before it gets to this stage it's your own fault for getting a
  			// bad error message
  			this.send(`|nametaken||Your name must contain at least one letter.`);
  			return false;
  		}
  
  		if (userid.length > 18) {
  			this.send(`|nametaken||Your name must be 18 characters or shorter.`);
  			return false;
  		}
  		name = Chat.namefilter(name, this);
  		if (userid !== toID(name)) {
  			if (name) {
  				name = userid;
  			} else {
  				userid = '';
  			}
  		}
  		if (this.registered) newlyRegistered = false;
  
  		if (!userid) {
  			this.send(`|nametaken||Your name contains a banned word.`);
  			return false;
  		} else {
  			if (userid === this.userid && !newlyRegistered) {
  				return this.forceRename(name, this.registered);
  			}
  		}
  
  		if (!token || token.charAt(0) === ';') {
  			this.send(`|nametaken|${name}|Your authentication token was invalid.`);
  			return false;
  		}
  
  		const tokenSemicolonPos = token.indexOf(';');
  		const tokenData = token.substr(0, tokenSemicolonPos);
  		const tokenSig = token.substr(tokenSemicolonPos + 1);
  
  		const tokenDataSplit = tokenData.split(',');
  		const [signedChallenge, signedUserid, userType, signedDate, signedHostname] = tokenDataSplit;
  		if (signedHostname && Config.legalhosts && !Config.legalhosts.includes(signedHostname)) {
  			Monitor.warn(`forged assertion: ${tokenData}`);
  			this.send(`|nametaken|${name}|Your assertion is for the wrong server. This server is ${Config.legalhosts[0]}.`);
  			return false;
  		}
  
  		if (tokenDataSplit.length < 5) {
  			Monitor.warn(`outdated assertion format: ${tokenData}`);
  			this.send(`|nametaken|${name}|Your assertion is stale. This usually means that the clock on the server computer is incorrect. If this is your server, please set the clock to the correct time.`);
  			return false;
  		}
  
  		if (signedUserid !== userid) {
  			// userid mismatch
  			this.send(`|nametaken|${name}|Your verification signature doesn't match your new username.`);
  			return false;
  		}
  
  		if (signedChallenge !== challenge) {
  			// a user sent an invalid token
  			Monitor.debug(`verify token challenge mismatch: ${signedChallenge} <=> ${challenge}`);
  			this.send(`|nametaken|${name}|Your verification signature doesn't match your authentication token.`);
  			return false;
  		}
  
  		const expiry = Config.tokenexpiry || 25 * 60 * 60;
  		if (Math.abs(parseInt(signedDate) - Date.now() / 1000) > expiry) {
  			Monitor.warn(`stale assertion: ${tokenData}`);
  			this.send(`|nametaken|${name}|Your assertion is stale. This usually means that the clock on the server computer is incorrect. If this is your server, please set the clock to the correct time.`);
  			return false;
  		}
  
  		const success = await Verifier.verify(tokenData, tokenSig);
  		if (!success) {
  			Monitor.warn(`verify failed: ${token}`);
  			Monitor.warn(`challenge was: ${challenge}`);
  			this.send(`|nametaken|${name}|Your verification signature was invalid.`);
  			return false;
  		}
  
  		// future-proofing
  		this.s1 = tokenDataSplit[5];
  		this.s2 = tokenDataSplit[6];
  		this.s3 = tokenDataSplit[7];
  
  		this.handleRename(name, userid, newlyRegistered, userType);
  	}
  
  	handleRename(name, userid, newlyRegistered, userType) {
  		const conflictUser = users.get(userid);
  		if (conflictUser && !conflictUser.registered && conflictUser.connected) {
  			if (newlyRegistered && userType !== '1') {
  				if (conflictUser !== this) conflictUser.resetName();
  			} else {
  				this.send(`|nametaken|${name}|Someone is already using the name "${conflictUser.name}".`);
  				return false;
  			}
  		}
  
  		let registered = false;
  		// user types:
  		//   1: unregistered user
  		//   2: registered user
  		//   3: Pokemon Showdown system operator
  		//   4: autoconfirmed
  		//   5: permalocked
  		//   6: permabanned
  		if (userType !== '1') {
  			registered = true;
  
  			if (userType === '3') {
  				this.isSysop = true;
  				this.isStaff = true;
  				this.trusted = userid;
  				this.autoconfirmed = userid;
  			} else if (userType === '4') {
  				this.autoconfirmed = userid;
  			} else if (userType === '5') {
  				this.permalocked = userid;
  				Punishments.lock(this, Date.now() + PERMALOCK_CACHE_TIME, userid, `Permalocked as ${name}`);
  			} else if (userType === '6') {
  				Punishments.lock(this, Date.now() + PERMALOCK_CACHE_TIME, userid, `Permabanned as ${name}`);
  				this.disconnectAll();
  			}
  		}
  		if (exports.Users.isTrusted(userid)) {
  			this.trusted = userid;
  			this.autoconfirmed = userid;
  		}
  		if (this.trusted) {
  			this.locked = null;
  			this.namelocked = null;
  			this.permalocked = null;
  			this.semilocked = null;
  		}
  
  		let user = users.get(userid);
  		const possibleUser = exports.Users.get(userid);
  		if (possibleUser && possibleUser.namelocked) {
  			// allows namelocked users to be merged
  			user = possibleUser;
  		}
  		if (user && user !== this) {
  			// This user already exists; let's merge
  			user.merge(this);
  
  			exports.Users.merge(user, this);
  			for (const i in this.prevNames) {
  				if (!user.prevNames[i]) {
  					user.prevNames[i] = this.prevNames[i];
  				}
  			}
  			if (this.named) user.prevNames[this.userid] = this.name;
  			this.destroy();
  
  			Punishments.checkName(user, userid, registered);
  
  			Rooms.global.checkAutojoin(user);
  			Chat.loginfilter(user, this, userType);
  			return true;
  		}
  
  		Punishments.checkName(this, userid, registered);
  		if (this.namelocked) return false;
  
  		// rename success
  		if (!this.forceRename(name, registered)) {
  			return false;
  		}
  		Rooms.global.checkAutojoin(this);
  		Chat.loginfilter(this, null, userType);
  		return true;
  	}
  	forceRename(name, registered, isForceRenamed = false) {
  		// skip the login server
  		const userid = toID(name);
  
  		if (users.has(userid) && users.get(userid) !== this) {
  			return false;
  		}
  
  		const oldname = this.name;
  		const oldid = this.userid;
  		if (userid !== this.userid) {
  			this.cancelReady();
  
  			if (!exports.Users.move(this, userid)) {
  				return false;
  			}
  
  			// MMR is different for each userid
  			this.mmrCache = {};
  
  			this.updateGroup(registered);
  		} else if (registered) {
  			this.updateGroup(registered);
  		}
  
  		if (this.named && oldid !== userid) this.prevNames[oldid] = this.name;
  		this.name = name;
  
  		const joining = !this.named;
  		this.named = !userid.startsWith('guest') || !!this.namelocked;
  
  		if (isForceRenamed) this.userMessage = '';
  
  		for (const connection of this.connections) {
  			// console.log('' + name + ' renaming: socket ' + i + ' of ' + this.connections.length);
  			connection.send(this.getUpdateuserText());
  		}
  		for (const roomid of this.games) {
  			const room = Rooms(roomid);
  			if (!room) {
  				Monitor.warn(`while renaming, room ${roomid} expired for user ${this.userid} in rooms ${[...this.inRooms]} and games ${[...this.games]}`);
  				this.games.delete(roomid);
  				continue;
  			}
  			if (!room.game) {
  				Monitor.warn(`game desync for user ${this.userid} in room ${room.id}`);
  				this.games.delete(roomid);
  				continue;
  			}
  			room.game.onRename(this, oldid, joining, isForceRenamed);
  		}
  		for (const roomid of this.inRooms) {
  			Rooms(roomid).onRename(this, oldid, joining);
  		}
  		if (isForceRenamed) this.trackRename = oldname;
  		return true;
  	}
  	/**
  	 * @param updated the settings which have been updated or none for all settings.
  	 */
  	getUpdateuserText(...updated) {
  		const named = this.named ? 1 : 0;
  		const diff = {};
  		const settings = updated.length ? updated : SETTINGS;
  		for (const setting of settings) {
  			// @ts-ignore - dynamic lookup
  			diff[setting] = this[setting];
  		}
  		return `|updateuser|${this.getIdentityWithStatus()}|${named}|${this.avatar}|${JSON.stringify(diff)}`;
  	}
  	/**
  	 * @param updated the settings which have been updated or none for all settings.
  	 */
  	update(...updated) {
  		this.send(this.getUpdateuserText(...updated));
  	}
  	merge(oldUser) {
  		oldUser.cancelReady();
  		for (const roomid of oldUser.inRooms) {
  			Rooms(roomid).onLeave(oldUser);
  		}
  
  		if (this.locked === '#dnsbl' && !oldUser.locked) this.locked = null;
  		if (!this.locked && oldUser.locked === '#dnsbl') oldUser.locked = null;
  		if (oldUser.locked) this.locked = oldUser.locked;
  		if (oldUser.autoconfirmed) this.autoconfirmed = oldUser.autoconfirmed;
  
  		this.updateGroup(this.registered);
  		// We only propagate the 'busy' statusType through merging - merging is
  		// active enough that the user should no longer be in the 'idle' state.
  		// Doing this before merging connections ensures the updateuser message
  		// shows the correct idle state.
  		this.setStatusType((this.statusType === 'busy' || oldUser.statusType === 'busy') ? 'busy' : 'online');
  
  		for (const connection of oldUser.connections) {
  			this.mergeConnection(connection);
  		}
  		oldUser.inRooms.clear();
  		oldUser.connections = [];
  
  		if (oldUser.chatQueue) {
  			if (!this.chatQueue) this.chatQueue = [];
  			this.chatQueue.push(...oldUser.chatQueue);
  			oldUser.clearChatQueue();
  			if (!this.chatQueueTimeout) this.startChatQueue();
  		}
  
  		this.s1 = oldUser.s1;
  		this.s2 = oldUser.s2;
  		this.s3 = oldUser.s3;
  
  		// merge IPs
  		for (const ip in oldUser.ips) {
  			if (this.ips[ip]) {
  				this.ips[ip] += oldUser.ips[ip];
  			} else {
  				this.ips[ip] = oldUser.ips[ip];
  			}
  		}
  
  		if (oldUser.isSysop) {
  			this.isSysop = true;
  			oldUser.isSysop = false;
  		}
  
  		oldUser.ips = {};
  		this.latestIp = oldUser.latestIp;
  		this.latestHost = oldUser.latestHost;
  		this.latestHostType = oldUser.latestHostType;
  		this.userMessage = oldUser.userMessage || this.userMessage || '';
  
  		oldUser.markDisconnected();
  	}
  	mergeConnection(connection) {
  		// the connection has changed name to this user's username, and so is
  		// being merged into this account
  		this.connected = true;
  		this.connections.push(connection);
  		// console.log('' + this.name + ' merging: connection ' + connection.socket.id);
  		connection.send(this.getUpdateuserText());
  		connection.user = this;
  		for (const roomid of connection.inRooms) {
  			const room = Rooms(roomid);
  			if (!this.inRooms.has(roomid)) {
  				if (Punishments.checkNameInRoom(this, room.id)) {
  					// the connection was in a room that this user is banned from
  					connection.sendTo(room.id, `|deinit`);
  					connection.leaveRoom(room);
  					continue;
  				}
  				room.onJoin(this, connection);
  				this.inRooms.add(roomid);
  			}
  			if (room.game && room.game.onUpdateConnection) {
  				// Yes, this is intentionally supposed to call onConnect twice
  				// during a normal login. Override onUpdateConnection if you
  				// don't want this behavior.
  				room.game.onUpdateConnection(this, connection);
  			}
  		}
  		this.updateReady(connection);
  	}
  	debugData() {
  		let str = `${this.group}${this.name} (${this.userid})`;
  		for (const [i, connection] of this.connections.entries()) {
  			str += ` socket${i}[`;
  			str += [...connection.inRooms].join(`, `);
  			str += `]`;
  		}
  		if (!this.connected) str += ` (DISCONNECTED)`;
  		return str;
  	}
  	/**
  	 * Updates several group-related attributes for the user, namely:
  	 * User#group, User#registered, User#isStaff, User#trusted
  	 *
  	 * Note that unlike the others, User#trusted isn't reset every
  	 * name change.
  	 */
  	updateGroup(registered) {
  		if (!registered) {
  			this.registered = false;
  			this.group = Config.groupsranking[0];
  			this.isStaff = false;
  			return;
  		}
  		this.registered = true;
  		if (this.userid in usergroups) {
  			this.group = usergroups[this.userid].charAt(0);
  		} else {
  			this.group = Config.groupsranking[0];
  		}
  
  		if (Config.customavatars && Config.customavatars[this.userid]) {
  			this.avatar = Config.customavatars[this.userid];
  		}
  
  		this.isStaff = Config.groups[this.group] && (Config.groups[this.group].lock || Config.groups[this.group].root);
  		if (!this.isStaff) {
  			const staffRoom = Rooms('staff');
  			this.isStaff = (staffRoom && staffRoom.auth && staffRoom.auth[this.userid]);
  		}
  		if (this.trusted) {
  			if (this.locked && this.permalocked) {
  				Monitor.log(`[CrisisMonitor] Trusted user '${this.userid}' is ${this.permalocked !== this.userid ? `an alt of permalocked user '${this.permalocked}'` : `a permalocked user`}, and was automatically demoted from ${this.distrust()}.`);
  				return;
  			}
  			this.locked = null;
  			this.namelocked = null;
  		}
  		if (this.autoconfirmed && this.semilocked) {
  			if (this.semilocked.startsWith('#sharedip')) {
  				this.semilocked = null;
  			} else if (this.semilocked === '#dnsbl') {
  				this.popup(`You are locked because someone using your IP has spammed/hacked other websites. This usually means either you're using a proxy, you're in a country where other people commonly hack, or you have a virus on your computer that's spamming websites.`);
  				this.semilocked = '#dnsbl.';
  			}
  		}
  		if (this.blockPMs && this.can('lock') && !this.can('bypassall')) this.blockPMs = false;
  	}
  	/**
  	 * Set a user's group. Pass (' ', true) to force trusted
  	 * status without giving the user a group.
  	 */
  	setGroup(group, forceTrusted = false) {
  		if (!group) throw new Error(`Falsy value passed to setGroup`);
  		this.group = group.charAt(0);
  		const wasStaff = this.isStaff;
  		this.isStaff = Config.groups[this.group] && (Config.groups[this.group].lock || Config.groups[this.group].root);
  		if (!this.isStaff) {
  			const staffRoom = Rooms('staff');
  			this.isStaff = (staffRoom && staffRoom.auth && staffRoom.auth[this.userid]);
  		}
  		if (wasStaff !== this.isStaff) this.update('isStaff');
  		Rooms.global.checkAutojoin(this);
  		if (this.registered) {
  			if (forceTrusted || this.group !== Config.groupsranking[0]) {
  				usergroups[this.userid] = this.group + this.name;
  				this.trusted = this.userid;
  				this.autoconfirmed = this.userid;
  			} else {
  				delete usergroups[this.userid];
  				this.trusted = '';
  			}
  			// tslint:disable-next-line:no-floating-promises
  			exportUsergroups();
  		}
  	}
  	/**
  	 * Demotes a user from anything that grants trusted status.
  	 * Returns an array describing what the user was demoted from.
  	 */
  	distrust() {
  		if (!this.trusted) return;
  		const userid = this.trusted;
  		const removed = [];
  		if (usergroups[userid]) {
  			removed.push(usergroups[userid].charAt(0));
  		}
  		for (const room of Rooms.global.chatRooms) {
  			if (!room.isPrivate && room.auth && userid in room.auth && room.auth[userid] !== '+') {
  				removed.push(room.auth[userid] + room.id);
  				room.auth[userid] = '+';
  			}
  		}
  		this.trusted = '';
  		this.setGroup(Config.groupsranking[0]);
  		return removed;
  	}
  	markDisconnected() {
  		this.connected = false;
  		this.lastConnected = Date.now();
  		if (!this.registered) {
  			// for "safety"
  			this.group = Config.groupsranking[0];
  			this.isSysop = false; // should never happen
  			this.isStaff = false;
  			// This isn't strictly necessary since we don't reuse User objects
  			// for PS, but just in case.
  			// We're not resetting .trusted/.autoconfirmed so those accounts
  			// can still be locked after logout.
  		}
  		// NOTE: can't do a this.update(...) at this point because we're no longer connected.
  	}
  	onDisconnect(connection) {
  		for (const [i, connected] of this.connections.entries()) {
  			if (connected === connection) {
  				// console.log('DISCONNECT: ' + this.userid);
  				if (this.connections.length <= 1) {
  					this.markDisconnected();
  				}
  				for (const roomid of connection.inRooms) {
  					this.leaveRoom(Rooms(roomid), connection, true);
  				}
  				--this.ips[connection.ip];
  				this.connections.splice(i, 1);
  				break;
  			}
  		}
  		if (!this.connections.length) {
  			for (const roomid of this.inRooms) {
  				// should never happen.
  				Monitor.debug(`!! room miscount: ${roomid} not left`);
  				Rooms(roomid).onLeave(this);
  			}
  			// cleanup
  			this.inRooms.clear();
  			if (!this.named && !Object.keys(this.prevNames).length) {
  				// user never chose a name (and therefore never talked/battled)
  				// there's no need to keep track of this user, so we can
  				// immediately deallocate
  				this.destroy();
  			} else {
  				this.cancelReady();
  			}
  		}
  	}
  	disconnectAll() {
  		// Disconnects a user from the server
  		this.clearChatQueue();
  		let connection = null;
  		this.markDisconnected();
  		for (let i = this.connections.length - 1; i >= 0; i--) {
  			// console.log('DESTROY: ' + this.userid);
  			connection = this.connections[i];
  			for (const roomid of connection.inRooms) {
  				this.leaveRoom(Rooms(roomid), connection, true);
  			}
  			connection.destroy();
  		}
  		if (this.connections.length) {
  			// should never happen
  			throw new Error(`Failed to drop all connections for ${this.userid}`);
  		}
  		for (const roomid of this.inRooms) {
  			// should never happen.
  			throw new Error(`Room miscount: ${roomid} not left for ${this.userid}`);
  		}
  		this.inRooms.clear();
  	}
  	/**
  	 * If this user is included in the returned list of
  	 * alts (i.e. when forPunishment is true), they will always be the first element of that list.
  	 */
  	getAltUsers(includeTrusted = false, forPunishment = false) {
  		let alts = findUsers([this.getLastId()], Object.keys(this.ips), {includeTrusted, forPunishment});
  		alts = alts.filter(user => user !== this);
  		if (forPunishment) alts.unshift(this);
  		return alts;
  	}
  	getLastName() {
  		if (this.named) return this.name;
  		const prevNames = Object.keys(this.prevNames);
  		return "[" + (prevNames.length ? prevNames[prevNames.length - 1] : this.name) + "]";
  	}
  	getLastId() {
  		if (this.named) return this.userid;
  		const prevNames = Object.keys(this.prevNames);
  		return (prevNames.length ? prevNames[prevNames.length - 1] : this.userid) ;
  	}
  	async tryJoinRoom(roomid, connection) {
  		roomid = roomid && (roomid ).id ? (roomid ).id : roomid ;
  		const room = Rooms.search(roomid);
  		if (!room && roomid.startsWith('view-')) {
  			return Chat.resolvePage(roomid, this, connection);
  		}
  		if (!room || !room.checkModjoin(this)) {
  			if (!this.named) {
  				return Rooms.RETRY_AFTER_LOGIN;
  			} else {
  				connection.sendTo(roomid, `|noinit|nonexistent|The room "${roomid}" does not exist.`);
  				return false;
  			}
  		}
  		if ((room ).tour) {
  			const errorMessage = (room ).tour.onBattleJoin(room, this);
  			if (errorMessage) {
  				connection.sendTo(roomid, `|noinit|joinfailed|${errorMessage}`);
  				return false;
  			}
  		}
  		if (room.isPrivate) {
  			if (!this.named) {
  				return Rooms.RETRY_AFTER_LOGIN;
  			}
  		}
  
  		if (!this.can('bypassall') && Punishments.isRoomBanned(this, room.id)) {
  			connection.sendTo(roomid, `|noinit|joinfailed|You are banned from the room "${roomid}".`);
  			return false;
  		}
  
  		if (Rooms.aliases.get(roomid) === room.id) {
  			connection.send(`>${roomid}\n|deinit`);
  		}
  
  		this.joinRoom(room, connection);
  		return true;
  	}
  	joinRoom(roomid, connection = null) {
  		const room = Rooms(roomid);
  		if (!room) throw new Error(`Room not found: ${roomid}`);
  		if (!connection) {
  			for (const curConnection of this.connections) {
  				// only join full clients, not pop-out single-room
  				// clients
  				// (...no, pop-out rooms haven't been implemented yet)
  				if (curConnection.inRooms.has('global')) {
  					this.joinRoom(room, curConnection);
  				}
  			}
  			return;
  		}
  		if (!connection.inRooms.has(room.id)) {
  			if (!this.inRooms.has(room.id)) {
  				this.inRooms.add(room.id);
  				room.onJoin(this, connection);
  			}
  			connection.joinRoom(room);
  			room.onConnect(this, connection);
  		}
  	}
  	leaveRoom(
  		room,
  		connection = null,
  		force = false
  	) {
  		room = Rooms(room);
  		if (room.id === 'global') {
  			// you can't leave the global room except while disconnecting
  			if (!force) return false;
  			this.cancelReady();
  		}
  		if (!this.inRooms.has(room.id)) {
  			return false;
  		}
  		for (const curConnection of this.connections) {
  			if (connection && curConnection !== connection) continue;
  			if (curConnection.inRooms.has(room.id)) {
  				curConnection.sendTo(room.id, `|deinit`);
  				curConnection.leaveRoom(room);
  			}
  			if (connection) break;
  		}
  
  		let stillInRoom = false;
  		if (connection) {
  			// @ts-ignore TypeScript inferring wrong type for room
  			stillInRoom = this.connections.some(conn => conn.inRooms.has(room.id));
  		}
  		if (!stillInRoom) {
  			room.onLeave(this);
  			this.inRooms.delete(room.id);
  		}
  	}
  
  	cancelReady() {
  		// setting variables because this can't be short-circuited
  		const searchesCancelled = Ladders.cancelSearches(this);
  		const challengesCancelled = Ladders.clearChallenges(this.userid);
  		if (searchesCancelled || challengesCancelled) {
  			this.popup(`Your searches and challenges have been cancelled because you changed your username.`);
  		}
  		// cancel tour challenges
  		// no need for a popup because users can't change their name while in a tournament anyway
  		for (const roomid of this.games) {
  			const room = Rooms.get(roomid);
  			// @ts-ignore Tournaments aren't TS'd yet
  			if (room.game && room.game.cancelChallenge) room.game.cancelChallenge(this);
  		}
  	}
  	updateReady(connection = null) {
  		Ladders.updateSearch(this, connection);
  		Ladders.updateChallenges(this, connection);
  	}
  	updateSearch(connection = null) {
  		Ladders.updateSearch(this, connection);
  	}
  	/**
  	 * The user says message in room.
  	 * Returns false if the rest of the user's messages should be discarded.
  	 */
  	chat(message, room, connection) {
  		const now = Date.now();
  
  		if (message.startsWith('/cmd userdetails') || message.startsWith('>> ') || this.isSysop) {
  			// certain commands are exempt from the queue
  			Monitor.activeIp = connection.ip;
  			Chat.parse(message, room, this, connection);
  			Monitor.activeIp = null;
  			if (this.isSysop) return;
  			return false; // but end the loop here
  		}
  
  		let throttleDelay = THROTTLE_DELAY;
  		if (this.group !== ' ') throttleDelay /= 2;
  
  		if (this.chatQueueTimeout) {
  			if (!this.chatQueue) this.chatQueue = []; // this should never happen
  			if (this.chatQueue.length >= THROTTLE_BUFFER_LIMIT - 1) {
  				connection.sendTo(room, `|raw|` +
  					`<strong class="message-throttle-notice">Your message was not sent because you've been typing too quickly.</strong>`
  				);
  				return false;
  			} else {
  				this.chatQueue.push([message, room.id, connection]);
  			}
  		} else if (now < this.lastChatMessage + throttleDelay) {
  			this.chatQueue = [[message, room.id, connection]];
  			this.startChatQueue(throttleDelay - (now - this.lastChatMessage));
  		} else {
  			this.lastChatMessage = now;
  			Monitor.activeIp = connection.ip;
  			Chat.parse(message, room, this, connection);
  			Monitor.activeIp = null;
  		}
  	}
  	startChatQueue(delay = null) {
  		if (delay === null) {
  			delay = (this.group !== ' ' ? THROTTLE_DELAY / 2 : THROTTLE_DELAY) - (Date.now() - this.lastChatMessage);
  		}
  
  		this.chatQueueTimeout = setTimeout(
  			() => this.processChatQueue(),
  			delay
  		);
  	}
  	clearChatQueue() {
  		this.chatQueue = null;
  		if (this.chatQueueTimeout) {
  			clearTimeout(this.chatQueueTimeout);
  			this.chatQueueTimeout = null;
  		}
  	}
  	processChatQueue() {
  		this.chatQueueTimeout = null;
  		if (!this.chatQueue) return;
  		const queueElement = this.chatQueue.shift();
  		if (!queueElement) {
  			this.chatQueue = null;
  			return;
  		}
  		const [message, roomid, connection] = queueElement;
  		if (!connection.user) {
  			// connection disconnected, chat queue should not be big enough
  			// for recursion to be an issue, also didn't ES6 spec tail
  			// recursion at some point?
  			return this.processChatQueue();
  		}
  
  		this.lastChatMessage = new Date().getTime();
  
  		const room = Rooms(roomid);
  		if (room) {
  			Monitor.activeIp = connection.ip;
  			Chat.parse(message, room, this, connection);
  			Monitor.activeIp = null;
  		} else {
  			// room is expired, do nothing
  		}
  
  		let throttleDelay = THROTTLE_DELAY;
  		if (this.group !== ' ') throttleDelay /= 2;
  
  		if (this.chatQueue.length) {
  			this.chatQueueTimeout = setTimeout(
  				() => this.processChatQueue(), throttleDelay);
  		} else {
  			this.chatQueue = null;
  		}
  	}
  	setStatusType(type) {
  		if (type === this.statusType) return;
  		this.statusType = type;
  		this.updateIdentity();
  		this.update('statusType');
  	}
  	setUserMessage(message) {
  		if (message === this.userMessage) return;
  		this.userMessage = message;
  		this.updateIdentity();
  	}
  	clearStatus(type = this.statusType) {
  		this.statusType = type;
  		this.userMessage = '';
  		this.updateIdentity();
  	}
  	destroy() {
  		// deallocate user
  		for (const roomid of this.games) {
  			const room = Rooms(roomid);
  			if (!room) {
  				Monitor.warn(`while deallocating, room ${roomid} did not exist for ${this.userid} in rooms ${[...this.inRooms]} and games ${[...this.games]}`);
  				this.games.delete(roomid);
  				continue;
  			}
  			const game = room.game;
  			if (!game) {
  				Monitor.warn(`while deallocating, room ${roomid} did not have a game for ${this.userid} in rooms ${[...this.inRooms]} and games ${[...this.games]}`);
  				this.games.delete(roomid);
  				continue;
  			}
  			if (game.ended) continue;
  			// @ts-ignore
  			if (game.forfeit) game.forfeit(this);
  		}
  		this.clearChatQueue();
  		exports.Users.delete(this);
  	}
  	toString() {
  		return this.userid;
  	}
  } exports.User = User;
  
  /*********************************************************
   * Inactive user pruning
   *********************************************************/
  
  function pruneInactive(threshold) {
  	const now = Date.now();
  	for (const user of users.values()) {
  		const awayTimer = user.can('lock') ? STAFF_IDLE_TIMER : IDLE_TIMER;
  		const bypass = user.statusType !== 'online' ||
  			(!user.can('bypassall') &&
  				(user.can('bypassafktimer') ||
  				Array.from(user.inRooms).some(room => user.can('bypassafktimer', null, Rooms(room) ))));
  		if (!bypass && !user.connections.some(connection => now - connection.lastActiveTime < awayTimer)) {
  			user.setStatusType('idle');
  		}
  		if (user.connected) continue;
  		if ((now - user.lastConnected) > threshold) {
  			user.destroy();
  		}
  	}
  }
  
  /*********************************************************
   * Routing
   *********************************************************/
  
  function socketConnect(worker, workerid, socketid, ip, protocol) {
  	const id = '' + workerid + '-' + socketid;
  	const connection = new Connection(id, worker, socketid, null, ip, protocol);
  	connections.set(id, connection);
  
  	const banned = Punishments.checkIpBanned(connection);
  	if (banned) {
  		return connection.destroy();
  	}
  	// Emergency mode connections logging
  	if (Config.emergency) {
  		// tslint:disable-next-line:no-floating-promises
  		_fs.FS.call(void 0, 'logs/cons.emergency.log').append('[' + ip + ']\n');
  	}
  
  	const user = new User(connection);
  	connection.user = user;
  	Punishments.checkIp(user, connection);
  	// Generate 1024-bit challenge string.
  	require('crypto').randomBytes(128, (err, buffer) => {
  		if (err) {
  			// It's not clear what sort of condition could cause this.
  			// For now, we'll basically assume it can't happen.
  			Monitor.crashlog(err, 'randomBytes');
  			// This is pretty crude, but it's the easiest way to deal
  			// with this case, which should be impossible anyway.
  			user.disconnectAll();
  		} else if (connection.user) {	// if user is still connected
  			connection.challenge = buffer.toString('hex');
  			// console.log('JOIN: ' + connection.user.name + ' [' + connection.challenge.substr(0, 15) + '] [' + socket.id + ']');
  			const keyid = Config.loginserverpublickeyid || 0;
  			connection.sendTo(null, `|challstr|${keyid}|${connection.challenge}`);
  		}
  	});
  
  	user.joinRoom('global', connection);
  }
  function socketDisconnect(worker, workerid, socketid) {
  	const id = '' + workerid + '-' + socketid;
  
  	const connection = connections.get(id);
  	if (!connection) return;
  	connection.onDisconnect();
  }
  function socketReceive(worker, workerid, socketid, message) {
  	const id = `${workerid}-${socketid}`;
  
  	const connection = connections.get(id);
  	if (!connection) return;
  	connection.lastActiveTime = Date.now();
  
  	// Due to a bug in SockJS or Faye, if an exception propagates out of
  	// the `data` event handler, the user will be disconnected on the next
  	// `data` event. To prevent this, we log exceptions and prevent them
  	// from propagating out of this function.
  
  	// drop legacy JSON messages
  	if (message.charAt(0) === '{') return;
  
  	// drop invalid messages without a pipe character
  	const pipeIndex = message.indexOf('|');
  	if (pipeIndex < 0) return;
  
  	const user = connection.user;
  	if (!user) return;
  
  	// The client obviates the room id when sending messages to Lobby by default
  	const roomId = message.substr(0, pipeIndex) || (Rooms.lobby || Rooms.global).id;
  	message = message.slice(pipeIndex + 1);
  
  	const room = Rooms(roomId);
  	if (!room) return;
  	const multilineMessage = Chat.multiLinePattern.test(message);
  	if (multilineMessage) {
  		user.chat(multilineMessage, room, connection);
  		return;
  	}
  
  	const lines = message.split('\n');
  	if (!lines[lines.length - 1]) lines.pop();
  	if (lines.length > (user.isStaff ||
  		(room.auth && room.auth[user.userid] && room.auth[user.userid] !== '+') ? THROTTLE_MULTILINE_WARN_STAFF
  			: THROTTLE_MULTILINE_WARN)) {
  		connection.popup(`You're sending too many lines at once. Try using a paste service like [[Pastebin]].`);
  		return;
  	}
  	// Emergency logging
  	if (Config.emergency) {
  		// tslint:disable-next-line:no-floating-promises
  		_fs.FS.call(void 0, 'logs/emergency.log').append(`[${user} (${connection.ip})] ${roomId}|${message}\n`);
  	}
  
  	const startTime = Date.now();
  	for (const line of lines) {
  		if (user.chat(line, room, connection) === false) break;
  	}
  	const deltaTime = Date.now() - startTime;
  	if (deltaTime > 1000) {
  		Monitor.warn(`[slow] ${deltaTime}ms - ${user.name} <${connection.ip}>: ${roomId}|${message}`);
  	}
  }
  
  const users = new Map();
  const prevUsers = new Map();
  let numUsers = 0;
  
   const Users = Object.assign(getUser, {
  	delete: deleteUser,
  	move,
  	add,
  	merge,
  	users,
  	prevUsers,
  	get: getUser,
  	getExact: getExactUser,
  	findUsers,
  	usergroups,
  	setOfflineGroup,
  	isUsernameKnown,
  	isTrusted,
  	importUsergroups,
  	cacheGroupData,
  	PLAYER_SYMBOL,
  	HOST_SYMBOL,
  	connections,
  	User,
  	Connection,
  	socketDisconnect,
  	socketReceive,
  	pruneInactive,
  	pruneInactiveTimer: setInterval(() => {
  		pruneInactive(Config.inactiveuserthreshold || 60 * MINUTES);
  	}, 30 * MINUTES),
  	socketConnect,
  }); exports.Users = Users;
  