<commit_before>/**
 * Informational Commands
 * Pokemon Showdown - https://pokemonshowdown.com/
 *
 * These are informational commands. For instance, you can define the command
 * 'whois' here, then use it by typing /whois into Pokemon Showdown.
 *
 * For the API, see chat-plugins/COMMANDS.md
 *
 * @license MIT license
 */

'use strict';

/** @type {ChatCommands} */
const commands = {

	'!whois': true,
	ip: 'whois',
	rooms: 'whois',
	alt: 'whois',
	alts: 'whois',
	whoare: 'whois',
	whois: function (target, room, user, connection, cmd) {
		if (room && room.id === 'staff' && !this.runBroadcast()) return;
		if (!room) room = Rooms.global;
		let targetUser = this.targetUserOrSelf(target, user.group === ' ');
		let showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts');
		if (!targetUser) {
			if (showAll) return this.parse('/offlinewhois ' + target);
			return this.errorReply("User " + this.targetUsername + " not found.");
		}
		if (showAll && !user.trusted && targetUser !== user) {
			return this.errorReply(`/${cmd} - Access denied.`);
		}

		let buf = Chat.html`<strong class="username"><small style="display:none">${targetUser.group}</small>${targetUser.name}</strong> `;
		const ac = targetUser.autoconfirmed;
		if (ac && showAll) buf += ` <small style="color:gray">(ac${targetUser.userid === ac ? `` : `: ${ac}`})</small>`;
		if (!targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;
		let roomauth = '';
		if (room.auth && targetUser.userid in room.auth) roomauth = room.auth[targetUser.userid];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) {
			buf += `<br />Global ${Config.groups[targetUser.group].name} (${targetUser.group})`;
		}
		if (targetUser.isSysop) {
			buf += `<br />(Pok&eacute;mon Showdown System Operator)`;
		}
		if (!targetUser.registered) {
			buf += `<br />(Unregistered)`;
		}
		let publicrooms = "";
		let hiddenrooms = "";
		let privaterooms = "";
		for (const roomid of targetUser.inRooms) {
			if (roomid === 'global') continue;
			let targetRoom = Rooms.get(roomid);

			let authSymbol = (targetRoom.auth && targetRoom.auth[targetUser.userid] ? targetRoom.auth[targetUser.userid] : '');
			let battleTitle = (roomid.battle ? ` title="${roomid.title}"` : '');
			let output = `${authSymbol}<a href="/${roomid}"${battleTitle}>${roomid}</a>`;
			if (targetRoom.isPrivate === true) {
				if (targetRoom.modjoin === '~') continue;
				if (privaterooms) privaterooms += " | ";
				privaterooms += output;
			} else if (targetRoom.isPrivate) {
				if (hiddenrooms) hiddenrooms += " | ";
				hiddenrooms += output;
			} else {
				if (publicrooms) publicrooms += " | ";
				publicrooms += output;
			}
		}
		buf += '<br />Rooms: ' + (publicrooms || '<em>(no public rooms)</em>');

		if (!showAll) {
			return this.sendReplyBox(buf);
		}
		buf += '<br />';
		if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) {
			let prevNames = Object.keys(targetUser.prevNames).map(userid => {
				const punishment = Punishments.userids.get(userid);
				return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetUser.userid ? ` as ${punishment[1]}` : ''})` : '');
			}).join(", ");
			if (prevNames) buf += Chat.html`<br />Previous names: ${prevNames}`;

			for (const targetAlt of targetUser.getAltUsers(true)) {
				if (!targetAlt.named && !targetAlt.connected) continue;
				if (targetAlt.group === '~' && user.group !== '~') continue;

				const punishment = Punishments.userids.get(targetAlt.userid);
				const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : '';
				buf += Chat.html`<br />Alt: <span class="username">${targetAlt.name}</span>${punishMsg}`;
				if (!targetAlt.connected) buf += ` <em style="color:gray">(offline)</em>`;
				prevNames = Object.keys(targetAlt.prevNames).map(userid => {
					const punishment = Punishments.userids.get(userid);
					return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : '');
				}).join(", ");
				if (prevNames) buf += `<br />Previous names: ${prevNames}`;
			}
			if (targetUser.namelocked) {
				buf += `<br />NAMELOCKED: ${targetUser.namelocked}`;
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			} else if (targetUser.locked) {
				buf += `<br />LOCKED: ${targetUser.locked}`;
				switch (targetUser.locked) {
				case '#dnsbl':
					buf += ` - IP is in a DNS-based blacklist`;
					break;
				case '#range':
					buf += ` - IP or host is in a temporary range-lock`;
					break;
				case '#hostfilter':
					buf += ` - host is permanently locked for being a proxy`;
					break;
				}
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			}
			let battlebanned = Punishments.isBattleBanned(targetUser);
			if (battlebanned) {
				buf += `<br />BATTLEBANNED: ${battlebanned[1]}`;
				let expiresIn = new Date(battlebanned[2]).getTime() - Date.now();
				let expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
				let expiresText = '';
				if (expiresDays >= 1) {
					expiresText = `in around ${Chat.count(expiresDays, "days")}`;
				} else {
					expiresText = `soon`;
				}
				if (expiresIn > 1) buf += ` (expires ${expiresText})`;
				if (battlebanned[3]) buf += Chat.html` (reason: ${battlebanned[3]})`;
			}
			if (targetUser.semilocked) {
				buf += `<br />Semilocked: ${targetUser.semilocked}`;
			}
		}
		if ((user.can('ip', targetUser) || user === targetUser)) {
			let ips = Object.keys(targetUser.ips);
			ips = ips.map(ip => {
				let status = [];
				let punishment = Punishments.ips.get(ip);
				if (user.can('ip') && punishment) {
					let [punishType, userid] = punishment;
					let punishMsg = Punishments.punishmentTypes.get(punishType) || 'punished';
					if (userid !== targetUser.userid) punishMsg += ` as ${userid}`;
					status.push(punishMsg);
				}
				if (Punishments.sharedIps.has(ip)) {
					let sharedStr = 'shared';
					if (Punishments.sharedIps.get(ip)) {
						sharedStr += `: ${Punishments.sharedIps.get(ip)}`;
					}
					status.push(sharedStr);
				}
				return ip + (status.length ? ` (${status.join('; ')})` : '');
			});
			buf += `<br /> IP${Chat.plural(ips)}: ${ips.join(", ")}`;
			if (user.group !== ' ' && targetUser.latestHost) {
				buf += Chat.html`<br />Host: ${targetUser.latestHost}`;
			}
		}
		if ((user === targetUser || user.can('alts', targetUser)) && hiddenrooms) {
			buf += `<br />Hidden rooms: ${hiddenrooms}`;
		}
		const staffViewingLocked = user.can('alts', targetUser) && targetUser.locked;
		if ((user === targetUser || user.can('makeroom') || staffViewingLocked) && privaterooms) {
			buf += `<br />Private rooms: ${privaterooms}`;
		}

		if (user.can('alts', targetUser) || (room.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.userid in room.users)) {
			let punishments = Punishments.getRoomPunishments(targetUser, {checkIps: true});

			if (punishments.length) {
				buf += `<br />Room punishments: `;

				buf += punishments.map(([room, punishment]) => {
					const [punishType, punishUserid, expireTime, reason] = punishment;
					let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
					if (!punishDesc) punishDesc = `punished`;
					if (punishUserid !== targetUser.userid) punishDesc += ` as ${punishUserid}`;
					let expiresIn = new Date(expireTime).getTime() - Date.now();
					let expireString = Chat.toDurationString(expiresIn, {precision: 1});
					punishDesc += ` for ${expireString}`;

					if (reason) punishDesc += `: ${reason}`;
					return `<a href="/${room}">${room}</a> (${punishDesc})`;
				}).join(', ');
			}
		}
		this.sendReplyBox(buf);
	},
	whoishelp: [
		`/whois - Get details on yourself: alts, group, IP address, and rooms.`,
		`/whois [username] - Get details on a username: alts (Requires: % @ * & ~), group, IP address (Requires: @ * & ~), and rooms.`,
	],

	'!offlinewhois': true,
	checkpunishment: 'offlinewhois',
	offlinewhois: function (target, room, user) {
		if (!user.trusted) {
			return this.errorReply("/offlinewhois - Access denied.");
		}
		let userid = toId(target);
		if (!userid) return this.errorReply("Please enter a valid username.");
		let targetUser = Users(userid);
		let buf = Chat.html`<strong class="username">${target}</strong>`;
		if (!targetUser || !targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;

		let roomauth = '';
		if (room && room.auth && userid in room.auth) roomauth = room.auth[userid];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		let group = (Users.usergroups[userid] || '').charAt(0);
		if (Config.groups[group] && Config.groups[group].name) {
			buf += `<br />Global ${Config.groups[group].name} (${group})`;
		}

		buf += `<br /><br />`;
		let atLeastOne = false;

		let punishment = Punishments.userids.get(userid);
		if (punishment) {
			const [punishType, punishUserid, , reason] = punishment;
			const punishName = (Punishments.punishmentTypes.get(punishType) || punishType).toUpperCase();
			buf += `${punishName}: ${punishUserid}`;
			let expiresIn = Punishments.checkLockExpiration(userid);
			if (expiresIn) buf += expiresIn;
			if (reason) buf += Chat.html` (reason: ${reason})`;
			buf += '<br />';
			atLeastOne = true;
		}

		if (!user.can('alts') && !atLeastOne) {
			let hasJurisdiction = room && user.can('mute', null, room) && Punishments.roomUserids.nestedHas(room.id, userid);
			if (!hasJurisdiction) {
				return this.errorReply("/checkpunishment - User not found.");
			}
		}

		let punishments = Punishments.getRoomPunishments(targetUser || {userid});

		if (punishments && punishments.length) {
			buf += `<br />Room punishments: `;

			buf += punishments.map(([room, punishment]) => {
				const [punishType, punishUserid, expireTime, reason] = punishment;
				let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
				if (!punishDesc) punishDesc = `punished`;
				if (punishUserid !== userid) punishDesc += ` as ${punishUserid}`;
				let expiresIn = new Date(expireTime).getTime() - Date.now();
				let expireString = Chat.toDurationString(expiresIn, {precision: 1});
				punishDesc += ` for ${expireString}`;

				if (reason) punishDesc += `: ${reason}`;
				return `<a href="/${room}">${room}</a> (${punishDesc})`;
			}).join(', ');
			atLeastOne = true;
		}
		if (!atLeastOne) {
			buf += `This username has no punishments associated with it.`;
		}
		this.sendReplyBox(buf);
	},

	'!host': true,
	host: function (target, room, user, connection, cmd) {
		if (!target) return this.parse('/help host');
		if (!this.can('rangeban')) return;
		target = target.trim();
		if (!/^[0-9.]+$/.test(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.');
		Dnsbl.reverse(target).then(host => {
			this.sendReply('IP ' + target + ': ' + (host || "ERROR"));
		});
	},
	hosthelp: [`/host [ip] - Gets the host for a given IP. Requires: & ~`],

	'!ipsearch': true,
	searchip: 'ipsearch',
	ipsearchall: 'ipsearch',
	hostsearch: 'ipsearch',
	ipsearch: function (target, room, user, connection, cmd) {
		if (!target.trim()) return this.parse(`/help ipsearch`);
		if (!this.can('rangeban')) return;

		let [ip, roomid] = this.splitOne(target);
		let targetRoom = roomid ? Rooms(roomid) : null;
		if (!targetRoom && targetRoom !== null) return this.errorReply(`The room "${roomid}" does not exist.`);
		let results = /** @type {string[]} */ ([]);
		let isAll = (cmd === 'ipsearchall');

		if (/[a-z]/.test(ip)) {
			// host
			this.sendReply(`Users with host ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			Users.users.forEach(curUser => {
				if (results.length > 100 && !isAll) return;
				if (!curUser.latestHost || !curUser.latestHost.endsWith(ip)) return;
				if (targetRoom && !curUser.inRooms.has(targetRoom.id)) return;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			});
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else if (ip.slice(-1) === '*') {
			// IP range
			this.sendReply(`Users in IP range ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			ip = ip.slice(0, -1);
			Users.users.forEach(curUser => {
				if (results.length > 100 && !isAll) return;
				if (!curUser.latestIp.startsWith(ip)) return;
				if (targetRoom && !curUser.inRooms.has(targetRoom.id)) return;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			});
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else {
			this.sendReply(`Users with IP ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			Users.users.forEach(curUser => {
				if (curUser.latestIp !== ip) return;
				if (targetRoom && !curUser.inRooms.has(targetRoom.id)) return;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			});
		}
		if (!results.length) {
			if (!ip.includes('.')) return this.errorReply(`${ip} is not a valid IP or host.`);
			return this.sendReply(`No results found.`);
		}
		return this.sendReply(results.join('; '));
	},
	ipsearchhelp: [`/ipsearch [ip|range|host], (room) - Find all users with specified IP, IP range, or host. If a room is provided only users in the room will be shown. Requires: & ~`],

	checkchallenges: function (target, room, user) {
		if (!this.can('ban', null, room)) return false;
		if (!this.runBroadcast(true)) return;
		if (!this.broadcasting) {
			this.errorReply(`This command must be broadcast:`);
			return this.parse(`/help checkchallenges`);
		}
		target = this.splitTarget(target);
		const user1 = this.targetUser;
		const user2 = Users.get(target);
		if (!user1 || !user2 || user1 === user2) return this.parse(`/help checkchallenges`);
		if (!(user1 in room.users) || !(user2 in room.users)) {
			return this.errorReply(`Both users must be in this room.`);
		}
		let challenges = [];
		const user1Challs = Ladders.challenges.get(user1.userid);
		if (user1Challs) {
			for (const chall of user1Challs) {
				if (chall.from === user1.userid && Users.get(chall.to) === user2) {
					challenges.push(Chat.html`${user1.name} is challenging ${user2.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		const user2Challs = Ladders.challenges.get(user2.userid);
		if (user2Challs) {
			for (const chall of user2Challs) {
				if (chall.from === user2.userid && Users.get(chall.to) === user1) {
					challenges.push(Chat.html`${user2.name} is challenging ${user1.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		if (!challenges.length) {
			return this.sendReplyBox(Chat.html`${user1.name} and ${user2.name} are not challenging each other.`);
		}
		this.sendReplyBox(challenges.join(`<br />`));
	},
	checkchallengeshelp: [`!checkchallenges [user1], [user2] - Check if the specified users are challenging each other. Requires: @ * # & ~`],

	/*********************************************************
	 * Client fallback
	 *********************************************************/

	unignore: 'ignore',
	ignore: function (target, room, user) {
		if (!room) this.errorReply(`In PMs, this command can only be used by itself to ignore the person you're talking to: "/${this.cmd}", not "/${this.cmd} ${target}"`);
		this.errorReply(`You're using a custom client that doesn't support the ignore command.`);
	},

	/*********************************************************
	 * Data Search Dex
	 *********************************************************/

	'!data': true,
	pstats: 'data',
	stats: 'data',
	dex: 'data',
	pokedex: 'data',
	data: function (target, room, user, connection, cmd) {
		if (!this.runBroadcast()) return;
		target = Chat.filter.call(this, target, user, room, connection);
		if (!target) return;
		let buffer = '';
		let sep = target.split(',');
		if (sep.length !== 2) sep = [target];
		target = sep[0].trim();
		let targetId = toId(target);
		if (!targetId) return this.parse('/help data');
		let targetNum = parseInt(target);
		if (!isNaN(targetNum) && '' + targetNum === target) {
			for (let p in Dex.data.Pokedex) {
				let pokemon = Dex.getTemplate(p);<commit_msg>Properly fix the /data thing<commit_after>/**
 * Informational Commands
 * Pokemon Showdown - https://pokemonshowdown.com/
 *
 * These are informational commands. For instance, you can define the command
 * 'whois' here, then use it by typing /whois into Pokemon Showdown.
 *
 * For the API, see chat-plugins/COMMANDS.md
 *
 * @license MIT license
 */

'use strict';

/** @type {ChatCommands} */
const commands = {

	'!whois': true,
	ip: 'whois',
	rooms: 'whois',
	alt: 'whois',
	alts: 'whois',
	whoare: 'whois',
	whois: function (target, room, user, connection, cmd) {
		if (room && room.id === 'staff' && !this.runBroadcast()) return;
		if (!room) room = Rooms.global;
		let targetUser = this.targetUserOrSelf(target, user.group === ' ');
		let showAll = (cmd === 'ip' || cmd === 'whoare' || cmd === 'alt' || cmd === 'alts');
		if (!targetUser) {
			if (showAll) return this.parse('/offlinewhois ' + target);
			return this.errorReply("User " + this.targetUsername + " not found.");
		}
		if (showAll && !user.trusted && targetUser !== user) {
			return this.errorReply(`/${cmd} - Access denied.`);
		}

		let buf = Chat.html`<strong class="username"><small style="display:none">${targetUser.group}</small>${targetUser.name}</strong> `;
		const ac = targetUser.autoconfirmed;
		if (ac && showAll) buf += ` <small style="color:gray">(ac${targetUser.userid === ac ? `` : `: ${ac}`})</small>`;
		if (!targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;
		let roomauth = '';
		if (room.auth && targetUser.userid in room.auth) roomauth = room.auth[targetUser.userid];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		if (Config.groups[targetUser.group] && Config.groups[targetUser.group].name) {
			buf += `<br />Global ${Config.groups[targetUser.group].name} (${targetUser.group})`;
		}
		if (targetUser.isSysop) {
			buf += `<br />(Pok&eacute;mon Showdown System Operator)`;
		}
		if (!targetUser.registered) {
			buf += `<br />(Unregistered)`;
		}
		let publicrooms = "";
		let hiddenrooms = "";
		let privaterooms = "";
		for (const roomid of targetUser.inRooms) {
			if (roomid === 'global') continue;
			let targetRoom = Rooms.get(roomid);

			let authSymbol = (targetRoom.auth && targetRoom.auth[targetUser.userid] ? targetRoom.auth[targetUser.userid] : '');
			let battleTitle = (roomid.battle ? ` title="${roomid.title}"` : '');
			let output = `${authSymbol}<a href="/${roomid}"${battleTitle}>${roomid}</a>`;
			if (targetRoom.isPrivate === true) {
				if (targetRoom.modjoin === '~') continue;
				if (privaterooms) privaterooms += " | ";
				privaterooms += output;
			} else if (targetRoom.isPrivate) {
				if (hiddenrooms) hiddenrooms += " | ";
				hiddenrooms += output;
			} else {
				if (publicrooms) publicrooms += " | ";
				publicrooms += output;
			}
		}
		buf += '<br />Rooms: ' + (publicrooms || '<em>(no public rooms)</em>');

		if (!showAll) {
			return this.sendReplyBox(buf);
		}
		buf += '<br />';
		if (user.can('alts', targetUser) || user.can('alts') && user === targetUser) {
			let prevNames = Object.keys(targetUser.prevNames).map(userid => {
				const punishment = Punishments.userids.get(userid);
				return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetUser.userid ? ` as ${punishment[1]}` : ''})` : '');
			}).join(", ");
			if (prevNames) buf += Chat.html`<br />Previous names: ${prevNames}`;

			for (const targetAlt of targetUser.getAltUsers(true)) {
				if (!targetAlt.named && !targetAlt.connected) continue;
				if (targetAlt.group === '~' && user.group !== '~') continue;

				const punishment = Punishments.userids.get(targetAlt.userid);
				const punishMsg = punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : '';
				buf += Chat.html`<br />Alt: <span class="username">${targetAlt.name}</span>${punishMsg}`;
				if (!targetAlt.connected) buf += ` <em style="color:gray">(offline)</em>`;
				prevNames = Object.keys(targetAlt.prevNames).map(userid => {
					const punishment = Punishments.userids.get(userid);
					return userid + (punishment ? ` (${Punishments.punishmentTypes.get(punishment[0]) || 'punished'}${punishment[1] !== targetAlt.userid ? ` as ${punishment[1]}` : ''})` : '');
				}).join(", ");
				if (prevNames) buf += `<br />Previous names: ${prevNames}`;
			}
			if (targetUser.namelocked) {
				buf += `<br />NAMELOCKED: ${targetUser.namelocked}`;
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			} else if (targetUser.locked) {
				buf += `<br />LOCKED: ${targetUser.locked}`;
				switch (targetUser.locked) {
				case '#dnsbl':
					buf += ` - IP is in a DNS-based blacklist`;
					break;
				case '#range':
					buf += ` - IP or host is in a temporary range-lock`;
					break;
				case '#hostfilter':
					buf += ` - host is permanently locked for being a proxy`;
					break;
				}
				let punishment = Punishments.userids.get(targetUser.locked);
				if (punishment) {
					let expiresIn = Punishments.checkLockExpiration(targetUser.locked);
					if (expiresIn) buf += expiresIn;
					if (punishment[3]) buf += Chat.html` (reason: ${punishment[3]})`;
				}
			}
			let battlebanned = Punishments.isBattleBanned(targetUser);
			if (battlebanned) {
				buf += `<br />BATTLEBANNED: ${battlebanned[1]}`;
				let expiresIn = new Date(battlebanned[2]).getTime() - Date.now();
				let expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
				let expiresText = '';
				if (expiresDays >= 1) {
					expiresText = `in around ${Chat.count(expiresDays, "days")}`;
				} else {
					expiresText = `soon`;
				}
				if (expiresIn > 1) buf += ` (expires ${expiresText})`;
				if (battlebanned[3]) buf += Chat.html` (reason: ${battlebanned[3]})`;
			}
			if (targetUser.semilocked) {
				buf += `<br />Semilocked: ${targetUser.semilocked}`;
			}
		}
		if ((user.can('ip', targetUser) || user === targetUser)) {
			let ips = Object.keys(targetUser.ips);
			ips = ips.map(ip => {
				let status = [];
				let punishment = Punishments.ips.get(ip);
				if (user.can('ip') && punishment) {
					let [punishType, userid] = punishment;
					let punishMsg = Punishments.punishmentTypes.get(punishType) || 'punished';
					if (userid !== targetUser.userid) punishMsg += ` as ${userid}`;
					status.push(punishMsg);
				}
				if (Punishments.sharedIps.has(ip)) {
					let sharedStr = 'shared';
					if (Punishments.sharedIps.get(ip)) {
						sharedStr += `: ${Punishments.sharedIps.get(ip)}`;
					}
					status.push(sharedStr);
				}
				return ip + (status.length ? ` (${status.join('; ')})` : '');
			});
			buf += `<br /> IP${Chat.plural(ips)}: ${ips.join(", ")}`;
			if (user.group !== ' ' && targetUser.latestHost) {
				buf += Chat.html`<br />Host: ${targetUser.latestHost}`;
			}
		}
		if ((user === targetUser || user.can('alts', targetUser)) && hiddenrooms) {
			buf += `<br />Hidden rooms: ${hiddenrooms}`;
		}
		const staffViewingLocked = user.can('alts', targetUser) && targetUser.locked;
		if ((user === targetUser || user.can('makeroom') || staffViewingLocked) && privaterooms) {
			buf += `<br />Private rooms: ${privaterooms}`;
		}

		if (user.can('alts', targetUser) || (room.isPrivate !== true && user.can('mute', targetUser, room) && targetUser.userid in room.users)) {
			let punishments = Punishments.getRoomPunishments(targetUser, {checkIps: true});

			if (punishments.length) {
				buf += `<br />Room punishments: `;

				buf += punishments.map(([room, punishment]) => {
					const [punishType, punishUserid, expireTime, reason] = punishment;
					let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
					if (!punishDesc) punishDesc = `punished`;
					if (punishUserid !== targetUser.userid) punishDesc += ` as ${punishUserid}`;
					let expiresIn = new Date(expireTime).getTime() - Date.now();
					let expireString = Chat.toDurationString(expiresIn, {precision: 1});
					punishDesc += ` for ${expireString}`;

					if (reason) punishDesc += `: ${reason}`;
					return `<a href="/${room}">${room}</a> (${punishDesc})`;
				}).join(', ');
			}
		}
		this.sendReplyBox(buf);
	},
	whoishelp: [
		`/whois - Get details on yourself: alts, group, IP address, and rooms.`,
		`/whois [username] - Get details on a username: alts (Requires: % @ * & ~), group, IP address (Requires: @ * & ~), and rooms.`,
	],

	'!offlinewhois': true,
	checkpunishment: 'offlinewhois',
	offlinewhois: function (target, room, user) {
		if (!user.trusted) {
			return this.errorReply("/offlinewhois - Access denied.");
		}
		let userid = toId(target);
		if (!userid) return this.errorReply("Please enter a valid username.");
		let targetUser = Users(userid);
		let buf = Chat.html`<strong class="username">${target}</strong>`;
		if (!targetUser || !targetUser.connected) buf += ` <em style="color:gray">(offline)</em>`;

		let roomauth = '';
		if (room && room.auth && userid in room.auth) roomauth = room.auth[userid];
		if (Config.groups[roomauth] && Config.groups[roomauth].name) {
			buf += `<br />${Config.groups[roomauth].name} (${roomauth})`;
		}
		let group = (Users.usergroups[userid] || '').charAt(0);
		if (Config.groups[group] && Config.groups[group].name) {
			buf += `<br />Global ${Config.groups[group].name} (${group})`;
		}

		buf += `<br /><br />`;
		let atLeastOne = false;

		let punishment = Punishments.userids.get(userid);
		if (punishment) {
			const [punishType, punishUserid, , reason] = punishment;
			const punishName = (Punishments.punishmentTypes.get(punishType) || punishType).toUpperCase();
			buf += `${punishName}: ${punishUserid}`;
			let expiresIn = Punishments.checkLockExpiration(userid);
			if (expiresIn) buf += expiresIn;
			if (reason) buf += Chat.html` (reason: ${reason})`;
			buf += '<br />';
			atLeastOne = true;
		}

		if (!user.can('alts') && !atLeastOne) {
			let hasJurisdiction = room && user.can('mute', null, room) && Punishments.roomUserids.nestedHas(room.id, userid);
			if (!hasJurisdiction) {
				return this.errorReply("/checkpunishment - User not found.");
			}
		}

		let punishments = Punishments.getRoomPunishments(targetUser || {userid});

		if (punishments && punishments.length) {
			buf += `<br />Room punishments: `;

			buf += punishments.map(([room, punishment]) => {
				const [punishType, punishUserid, expireTime, reason] = punishment;
				let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
				if (!punishDesc) punishDesc = `punished`;
				if (punishUserid !== userid) punishDesc += ` as ${punishUserid}`;
				let expiresIn = new Date(expireTime).getTime() - Date.now();
				let expireString = Chat.toDurationString(expiresIn, {precision: 1});
				punishDesc += ` for ${expireString}`;

				if (reason) punishDesc += `: ${reason}`;
				return `<a href="/${room}">${room}</a> (${punishDesc})`;
			}).join(', ');
			atLeastOne = true;
		}
		if (!atLeastOne) {
			buf += `This username has no punishments associated with it.`;
		}
		this.sendReplyBox(buf);
	},

	'!host': true,
	host: function (target, room, user, connection, cmd) {
		if (!target) return this.parse('/help host');
		if (!this.can('rangeban')) return;
		target = target.trim();
		if (!/^[0-9.]+$/.test(target)) return this.errorReply('You must pass a valid IPv4 IP to /host.');
		Dnsbl.reverse(target).then(host => {
			this.sendReply('IP ' + target + ': ' + (host || "ERROR"));
		});
	},
	hosthelp: [`/host [ip] - Gets the host for a given IP. Requires: & ~`],

	'!ipsearch': true,
	searchip: 'ipsearch',
	ipsearchall: 'ipsearch',
	hostsearch: 'ipsearch',
	ipsearch: function (target, room, user, connection, cmd) {
		if (!target.trim()) return this.parse(`/help ipsearch`);
		if (!this.can('rangeban')) return;

		let [ip, roomid] = this.splitOne(target);
		let targetRoom = roomid ? Rooms(roomid) : null;
		if (!targetRoom && targetRoom !== null) return this.errorReply(`The room "${roomid}" does not exist.`);
		let results = /** @type {string[]} */ ([]);
		let isAll = (cmd === 'ipsearchall');

		if (/[a-z]/.test(ip)) {
			// host
			this.sendReply(`Users with host ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			Users.users.forEach(curUser => {
				if (results.length > 100 && !isAll) return;
				if (!curUser.latestHost || !curUser.latestHost.endsWith(ip)) return;
				if (targetRoom && !curUser.inRooms.has(targetRoom.id)) return;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			});
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else if (ip.slice(-1) === '*') {
			// IP range
			this.sendReply(`Users in IP range ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			ip = ip.slice(0, -1);
			Users.users.forEach(curUser => {
				if (results.length > 100 && !isAll) return;
				if (!curUser.latestIp.startsWith(ip)) return;
				if (targetRoom && !curUser.inRooms.has(targetRoom.id)) return;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			});
			if (results.length > 100 && !isAll) {
				return this.sendReply(`More than 100 users match the specified IP range. Use /ipsearchall to retrieve the full list.`);
			}
		} else {
			this.sendReply(`Users with IP ${ip}${targetRoom ? ` in the room ${targetRoom.title}` : ``}:`);
			Users.users.forEach(curUser => {
				if (curUser.latestIp !== ip) return;
				if (targetRoom && !curUser.inRooms.has(targetRoom.id)) return;
				results.push((curUser.connected ? " \u25C9 " : " \u25CC ") + " " + curUser.name);
			});
		}
		if (!results.length) {
			if (!ip.includes('.')) return this.errorReply(`${ip} is not a valid IP or host.`);
			return this.sendReply(`No results found.`);
		}
		return this.sendReply(results.join('; '));
	},
	ipsearchhelp: [`/ipsearch [ip|range|host], (room) - Find all users with specified IP, IP range, or host. If a room is provided only users in the room will be shown. Requires: & ~`],

	checkchallenges: function (target, room, user) {
		if (!this.can('ban', null, room)) return false;
		if (!this.runBroadcast(true)) return;
		if (!this.broadcasting) {
			this.errorReply(`This command must be broadcast:`);
			return this.parse(`/help checkchallenges`);
		}
		target = this.splitTarget(target);
		const user1 = this.targetUser;
		const user2 = Users.get(target);
		if (!user1 || !user2 || user1 === user2) return this.parse(`/help checkchallenges`);
		if (!(user1 in room.users) || !(user2 in room.users)) {
			return this.errorReply(`Both users must be in this room.`);
		}
		let challenges = [];
		const user1Challs = Ladders.challenges.get(user1.userid);
		if (user1Challs) {
			for (const chall of user1Challs) {
				if (chall.from === user1.userid && Users.get(chall.to) === user2) {
					challenges.push(Chat.html`${user1.name} is challenging ${user2.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		const user2Challs = Ladders.challenges.get(user2.userid);
		if (user2Challs) {
			for (const chall of user2Challs) {
				if (chall.from === user2.userid && Users.get(chall.to) === user1) {
					challenges.push(Chat.html`${user2.name} is challenging ${user1.name} in ${Dex.getFormat(chall.formatid).name}.`);
					break;
				}
			}
		}
		if (!challenges.length) {
			return this.sendReplyBox(Chat.html`${user1.name} and ${user2.name} are not challenging each other.`);
		}
		this.sendReplyBox(challenges.join(`<br />`));
	},
	checkchallengeshelp: [`!checkchallenges [user1], [user2] - Check if the specified users are challenging each other. Requires: @ * # & ~`],

	/*********************************************************
	 * Client fallback
	 *********************************************************/

	unignore: 'ignore',
	ignore: function (target, room, user) {
		if (!room) this.errorReply(`In PMs, this command can only be used by itself to ignore the person you're talking to: "/${this.cmd}", not "/${this.cmd} ${target}"`);
		this.errorReply(`You're using a custom client that doesn't support the ignore command.`);
	},

	/*********************************************************
	 * Data Search Dex
	 *********************************************************/

	'!data': true,
	pstats: 'data',
	stats: 'data',
	dex: 'data',
	pokedex: 'data',
	data: function (target, room, user, connection, cmd) {
		if (!this.runBroadcast()) return;
		target = Chat.filter.call(this, target, user, room, connection);
		if (target === false) return;
		let buffer = '';
		let sep = target.split(',');
		if (sep.length !== 2) sep = [target];
		target = sep[0].trim();
		let targetId = toId(target);
		if (!targetId) return this.parse('/help data');
		let targetNum = parseInt(target);
		if (!isNaN(targetNum) && '' + targetNum === target) {
			for (let p in Dex.data.Pokedex) {
				let pokemon = Dex.getTemplate(p);
{'fraction_non_alphanumeric': 0.19962154294032022, 'fraction_numerical': 0.0058806404657933044, 'mean_word_length': 3.8628255945639864, 'pattern_counts': {'":': 0, '<': 101, '<?xml version=': 0, '>': 131, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2807135', 'n_tokens_mistral': 13496, 'n_tokens_neox': 11745, 'n_words': 2676}