<commit_before>/**
 * Moderation commands
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * These are commands for staff.
 *
 * For the API, see chat-plugins/COMMANDS.md
 *
 * @license MIT
 */
import {Utils} from '../../lib/utils';

/* eslint no-else-return: "error" */

const MAX_REASON_LENGTH = 300;
const MUTE_LENGTH = 7 * 60 * 1000;
const HOURMUTE_LENGTH = 60 * 60 * 1000;

/** Require reasons for punishment commands */
const REQUIRE_REASONS = true;

export const commands: ChatCommands = {

	roomowner(target, room, user) {
		if (!room) return this.requiresRoom();
		if (!room.persist) {
			return this.sendReply("/roomowner - This room isn't designed for per-room moderation to be added");
		}
		if (!target) return this.parse('/help roomowner');
		target = this.splitTarget(target, true);
		if (target) return this.errorReply(`This command does not support specifying a reason.`);
		const targetUser = this.targetUser;
		const name = this.targetUsername;
		const userid = toID(name);

		if (!Users.isUsernameKnown(userid)) {
			return this.errorReply(`User '${this.targetUsername}' is offline and unrecognized, and so can't be promoted.`);
		}

		if (!this.can('makeroom')) return false;
		if (room.auth.getDirect(userid) === '#') return this.errorReply(`${name} is already a room owner.`);

		room.auth.set(userid, '#');
		this.addModAction(`${name} was appointed Room Owner by ${user.name}.`);
		this.modlog('ROOMOWNER', userid);
		if (targetUser) {
			targetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.roomid}.`);
			room.onUpdateIdentity(targetUser);
			if (room.subRooms) {
				for (const subRoom of room.subRooms.values()) {
					subRoom.onUpdateIdentity(targetUser);
				}
			}
		}
		room.saveSettings();
	},
	roomownerhelp: [`/roomowner [username] - Appoints [username] as a room owner. Requires: &`],

	roomdemote: 'roompromote',
	forceroompromote: 'roompromote',
	forceroomdemote: 'roompromote',
	roompromote(target, room, user, connection, cmd) {
		if (!room) {
			// this command isn't marked as room-only because it's usable in PMs through /invite
			return this.errorReply("This command is only available in rooms");
		}
		if (!this.canTalk()) return;
		if (!target) return this.parse('/help roompromote');

		const force = cmd.startsWith('force');
		target = this.splitTarget(target, true);
		const targetUser = this.targetUser;
		const userid = toID(this.targetUsername);
		let name = targetUser ? targetUser.name : this.filter(this.targetUsername);
		if (!name) return;
		name = name.slice(0, 18);

		if (!userid) return this.parse('/help roompromote');
		if (!targetUser && !Users.isUsernameKnown(userid) && !force) {
			return this.errorReply(`User '${name}' is offline and unrecognized, and so can't be promoted.`);
		}
		if (targetUser && !targetUser.registered) {
			return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
		}

		let currentSymbol: GroupSymbol | 'whitelist' = room.auth.getDirect(userid);
		if (room.auth.has(userid) && currentSymbol === Users.Auth.defaultSymbol()) {
			currentSymbol = 'whitelist';
		}
		const currentGroup = Users.Auth.getGroup(currentSymbol);
		const nextSymbol = target === 'deauth' ? Users.Auth.defaultSymbol() : target as GroupSymbol;
		const nextGroup = Users.Auth.getGroup(nextSymbol);

		if (!nextSymbol) {
			return this.errorReply("Please specify a group such as /roomvoice or /roomdeauth");
		}
		if (!Config.groups[nextSymbol]) {
			if (!force || !user.can('bypassall')) {
				this.errorReply(`Group '${nextSymbol}' does not exist.`);
				if (user.can('bypassall')) {
					this.errorReply(`If you want to promote to a nonexistent group, use /forceroompromote`);
				}
				return;
			} else if (!Users.Auth.isValidSymbol(nextSymbol)) {
				// yes I know this excludes astral-plane characters and includes combining characters
				return this.errorReply(`Admins can forcepromote to nonexistent groups only if they are one character long`);
			}
		}

		if (!force && (nextGroup.globalonly || (nextGroup.battleonly && !room.battle))) {
			return this.errorReply(`Group 'room${nextGroup.id || nextSymbol}' does not exist as a room rank.`);
		}

		const currentGroupName = currentGroup.name || "regular user";
		const nextGroupName = nextGroup.name || "regular user";

		if (currentSymbol === nextSymbol) {
			return this.errorReply(`User '${name}' is already a ${nextGroupName} in this room.`);
		}
		if (!user.can('makeroom')) {
			if (currentGroup.id && !user.can(`room${currentGroup.id || 'voice'}` as 'roomvoice', null, room)) {
				return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${currentGroupName}.`);
			}
			if (nextSymbol !== ' ' && !user.can(`room${nextGroup.id || 'voice'}` as 'roomvoice', null, room)) {
				return this.errorReply(`/${cmd} - Access denied for promoting/demoting to ${nextGroupName}.`);
			}
		}
		if (targetUser?.locked && room.persist && room.settings.isPrivate !== true && nextGroup.rank > 2) {
			return this.errorReply("Locked users can't be promoted.");
		}

		if (nextSymbol === Users.Auth.defaultSymbol()) {
			room.auth.delete(userid);
		} else {
			room.auth.set(userid, nextSymbol);
		}

		// Only show popup if: user is online and in the room, the room is public, and not a groupchat or a battle.
		const shouldPopup = (
			targetUser && room.users[targetUser.id] && room.persist && room.settings.isPrivate !== true ?
				targetUser : null
		);

		if (this.pmTarget && targetUser) {
			const text = `${targetUser.name} was invited (and promoted to Room ${nextGroupName}) by ${user.name}.`;
			room.add(`|c|${user.getIdentity(room.roomid)}|/log ${text}`).update();
			this.modlog('INVITE', targetUser, null, {noip: 1, noalts: 1});
		} else if (
			nextSymbol in Config.groups && currentSymbol in Config.groups &&
			nextGroup.rank < currentGroup.rank
		) {
			if (targetUser && room.users[targetUser.id] && !nextGroup.modlog) {
				// if the user can't see the demotion message (i.e. rank < %), it is shown in the chat
				targetUser.send(`>${room.roomid}\n(You were demoted to Room ${nextGroupName} by ${user.name}.)`);
			}
			this.privateModAction(`${name} was demoted to Room ${nextGroupName} by ${user.name}.`);
			this.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid, '(demote)');
			shouldPopup?.popup(`You were demoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);
		} else if (nextSymbol === '#') {
			this.addModAction(`${'' + name} was promoted to ${nextGroupName} by ${user.name}.`);
			this.modlog('ROOM OWNER', userid);
			shouldPopup?.popup(`You were promoted to ${nextGroupName} by ${user.name} in ${room.roomid}.`);
		} else {
			this.addModAction(`${'' + name} was promoted to Room ${nextGroupName} by ${user.name}.`);
			this.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid);
			shouldPopup?.popup(`You were promoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);
		}

		if (targetUser) {
			targetUser.updateIdentity(room.roomid);
			if (room.subRooms) {
				for (const subRoom of room.subRooms.values()) {
					targetUser.updateIdentity(subRoom.roomid);
				}
			}
		}
		room.saveSettings();
	},
	roompromotehelp: [
		`/roompromote OR /roomdemote [username], [group symbol] - Promotes/demotes the user to the specified room rank. Requires: @ # &`,
		`/room[group] [username] - Promotes/demotes the user to the specified room rank. Requires: @ # &`,
		`/roomdeauth [username] - Removes all room rank from the user. Requires: @ # &`,
	],

	auth: 'authority',
	stafflist: 'authority',
	globalauth: 'authority',
	authlist: 'authority',
	authority(target, room, user, connection) {
		if (target && target !== '+') {
			const targetRoom = Rooms.search(target);
			const availableRoom = targetRoom?.checkModjoin(user);
			if (targetRoom && availableRoom) return this.parse(`/roomauth1 ${target}`);
			return this.parse(`/userauth ${target}`);
		}
		const showAll = !!target;
		const rankLists: {[k: string]: string[]} = {};
		for (const [id, symbol] of Users.globalAuth) {
			if (symbol === ' ' || (symbol === '+' && !showAll)) continue;
			if (!rankLists[symbol]) rankLists[symbol] = [];
			rankLists[symbol].push(Users.globalAuth.usernames.get(id) || id);
		}

		const buffer = Utils.sortBy(
			Object.entries(rankLists) as [GroupSymbol, string[]][],
			([symbol]) => -Users.Auth.getGroup(symbol).rank
		).map(
			([symbol, names]) => (
				`${(Config.groups[symbol] ? `**${Config.groups[symbol].name}s** (${symbol})` : symbol)}:\n` +
				Utils.sortBy(names, name => toID(name)).join(", ")
			)
		);
		if (!showAll) buffer.push(`(Use \`\`/auth +\`\` to show global voice users.)`);

		if (!buffer.length) return connection.popup("This server has no global authority.");
		connection.popup(buffer.join("\n\n"));
	},
	authhelp: [
		`/auth - Show global staff for the server.`,
		`/auth + - Show global staff for the server, including voices.`,
		`/auth [room] - Show what roomauth a room has.`,
		`/auth [user] - Show what global and roomauth a user has.`,
	],

	roomstaff: 'roomauth',
	roomauth1: 'roomauth',
	roomauth(target, room, user, connection, cmd) {
		let userLookup = '';
		if (cmd === 'roomauth1') userLookup = `\n\nTo look up auth for a user, use /userauth ${target}`;
		let targetRoom = room;
		if (target) targetRoom = Rooms.search(target)!;
		if (!targetRoom || !targetRoom.checkModjoin(user)) {
			return this.errorReply(`The room "${target}" does not exist.`);
		}
		const showAll = user.can('mute', null, targetRoom);

		const rankLists: {[groupSymbol: string]: ID[]} = {};
		for (const [id, rank] of targetRoom.auth) {
			if (rank === ' ' && !showAll) continue;
			if (!rankLists[rank]) rankLists[rank] = [];
			rankLists[rank].push(id);
		}

		const buffer = Utils.sortBy(
			Object.entries(rankLists) as [GroupSymbol, ID[]][],
			([symbol]) => -Users.Auth.getGroup(symbol).rank
		).map(([symbol, names]) => {
			let group = Config.groups[symbol] ? `${Config.groups[symbol].name}s (${symbol})` : symbol;
			if (symbol === ' ') group = 'Whitelisted (this list is only visible to staff)';
			return `${group}:\n` +
				Utils.sortBy(names).map(userid => {
					const isOnline = Users.get(userid)?.statusType === 'online';
					// targetRoom guaranteed to exist above
					return userid in targetRoom!.users && isOnline ? `**${userid}**` : userid;
				}).join(', ');
		});

		let curRoom = targetRoom;
		while (curRoom.parent) {
			const modjoinSetting = curRoom.settings.modjoin === true ? curRoom.settings.modchat : curRoom.settings.modjoin;
			const roomType = (modjoinSetting ? `modjoin ${modjoinSetting} ` : '');
			const inheritedUserType = (modjoinSetting ? ` of rank ${modjoinSetting} and above` : '');
			if (curRoom.parent) {
				const also = buffer.length === 0 ? `` : ` also`;
				buffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType}${also} have authority in this room.`);
			}
			curRoom = curRoom.parent;
		}
		if (!buffer.length) {
			connection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);
			return;
		}
		if (!curRoom.settings.isPrivate) {
			buffer.push(`${curRoom.title} is a public room, so global auth with no relevant roomauth will have authority in this room.`);
		} else if (curRoom.settings.isPrivate === 'hidden' || curRoom.settings.isPrivate === 'voice') {
			buffer.push(`${curRoom.title} is a hidden room, so global auth with no relevant roomauth will have authority in this room.`);
		}
		if (targetRoom !== room) buffer.unshift(`${targetRoom.title} room auth:`);
		connection.popup(`${buffer.join("\n\n")}${userLookup}`);
	},

	userauth(target, room, user, connection) {
		const targetId = toID(target) || user.id;
		const targetUser = Users.getExact(targetId);
		const targetUsername = targetUser?.name || target;

		const buffer = [];
		let innerBuffer = [];
		const group = Users.globalAuth.get(targetId);
		if (group !== ' ' || Users.isTrusted(targetId)) {
			buffer.push(`Global auth: ${group === ' ' ? 'trusted' : group}`);
		}
		for (const curRoom of Rooms.rooms.values()) {
			if (curRoom.settings.isPrivate) continue;
			if (!curRoom.auth.has(targetId)) continue;
			innerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);
		}
		if (innerBuffer.length) {
			buffer.push(`Room auth: ${innerBuffer.join(', ')}`);
		}
		if (targetId === user.id || user.can('lock')) {
			innerBuffer = [];
			for (const curRoom of Rooms.rooms.values()) {
				if (!curRoom.settings.isPrivate) continue;
				if (curRoom.settings.isPrivate === true) continue;
				if (!curRoom.auth.has(targetId)) continue;
				innerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);
			}
			if (innerBuffer.length) {
				buffer.push(`Hidden room auth: ${innerBuffer.join(', ')}`);
			}
		}
		if (targetId === user.id || user.can('makeroom')) {
			innerBuffer = [];
			for (const chatRoom of Rooms.global.chatRooms) {
				if (!chatRoom.settings.isPrivate) continue;
				if (chatRoom.settings.isPrivate !== true) continue;
				if (!chatRoom.auth.has(targetId)) continue;
				innerBuffer.push(chatRoom.auth.getDirect(targetId).trim() + chatRoom.roomid);
			}
			if (innerBuffer.length) {
				buffer.push(`Private room auth: ${innerBuffer.join(', ')}`);
			}
		}
		if (!buffer.length) {
			buffer.push("No global or room auth.");
		}

		buffer.unshift(`${targetUsername} user auth:`);
		connection.popup(buffer.join("\n\n"));
	},

	autojoin(target, room, user, connection) {
		const targets = target.split(',');
		if (targets.length > 16 || connection.inRooms.size > 1) {
			return connection.popup("To prevent DoS attacks, you can only use /autojoin for 16 or fewer rooms, when you haven't joined any rooms yet. Please use /join for each room separately.");
		}
		Rooms.global.autojoinRooms(user, connection);
		const autojoins: string[] = [];

		const promises = targets.map(
			roomid => user.tryJoinRoom(roomid as RoomID, connection).then(ret => {
				if (ret === Rooms.RETRY_AFTER_LOGIN) {
					autojoins.push(roomid);
				}
			})
		);

		return Promise.all(promises).then(() => {
			connection.autojoins = autojoins.join(',');
		});
	},

	joim: 'join',
	j: 'join',
	join(target, room, user, connection) {
		if (!target) return this.parse('/help join');
		if (target.startsWith('http://')) target = target.slice(7);
		if (target.startsWith('https://')) target = target.slice(8);
		if (target.startsWith(`${Config.routes.client}/`)) target = target.slice(Config.routes.client.length + 1);
		if (target.startsWith('psim.us/')) target = target.slice(8);
		return user.tryJoinRoom(target as RoomID, connection).then(ret => {
			if (ret === Rooms.RETRY_AFTER_LOGIN) {
				connection.sendTo(
					target as RoomID,
					`|noinit|namerequired|The room '${target}' does not exist or requires a login to join.`
				);
			}
		});
	},
	joinhelp: [`/join [roomname] - Attempt to join the room [roomname].`],

	leave: 'part',
	part(target, room, user, connection) {
		const targetRoom = target ? Rooms.search(target) : room;
		if (!targetRoom) {
			if (target.startsWith('view-')) return;
			return this.errorReply(`The room '${target}' does not exist.`);
		}
		user.leaveRoom(targetRoom, connection);
	},

	/*********************************************************
	 * Moderating: Punishments
	 *********************************************************/

	kick: 'warn',
	k: 'warn',
	warn(target, room, user) {
		if (!target) return this.parse('/help warn');
		if (!room) return this.requiresRoom();
		if (!this.canTalk()) return;
		if (room.settings.isPersonal && !user.can('warn' as any)) {
			return this.errorReply("Warning is unavailable in group chats.");
		}
		// If used in staff, help tickets or battles, log the warn to the global modlog.
		const globalWarn = room.roomid === 'staff' || room.roomid.startsWith('help-') || (room.battle && !room.parent);

		target = this.splitTarget(target);
		let privateReason = '';
		let publicReason = target;
		const targetLowercase = target.toLowerCase();
		if (target && (targetLowercase.includes('spoiler:') || targetLowercase.includes('spoilers:'))) {
			const privateIndex = targetLowercase.indexOf(targetLowercase.includes('spoilers:') ? 'spoilers:' : 'spoiler:');
			const bump = (targetLowercase.includes('spoilers:') ? 9 : 8);
			privateReason = `(PROOF: ${target.substr(privateIndex + bump, target.length).trim()}) `;
			publicReason = target.substr(0, privateIndex).trim();
		}

		const targetUser = this.targetUser;
		if (!targetUser || !targetUser.connected) {
			if (!targetUser || !globalWarn) return this.errorReply(`User '${this.targetUsername}' not found.`);
			if (!this.can('warn', null, room)) return false;

			this.addModAction(`${targetUser.name} would be warned by ${user.name} but is offline.${(publicReason ? ` (${publicReason})` : ``)}`);
			this.globalModlog('WARN OFFLINE', targetUser, ` by ${user.id}${(target ? `: ${publicReason} ${privateReason}` : ``)}`);
			return;
		}
		if (!(targetUser.id in room.users) && !globalWarn) {
			return this.errorReply(`User ${this.targetUsername} is not in the room ${room.roomid}.`);
		}
		if (publicReason.length > MAX_REASON_LENGTH) {
			return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
		}
		if (!this.can('warn', targetUser, room)) return false;
		if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to warn upper staff members.");

		const now = Date.now();
		const timeout = now - targetUser.lastWarnedAt;
		if (timeout < 15 * 1000) {
			const remainder = (15 - (timeout / 1000)).toFixed(2);
			return this.errorReply(`You must wait ${remainder} more seconds before you can warn ${targetUser.name} again.`);
		}

		this.addModAction(`${targetUser.name} was warned by ${user.name}.${(publicReason ? ` (${publicReason})` : ``)}`);
		if (globalWarn) {
			this.globalModlog('WARN', targetUser, ` by ${user.id}${(target ? `: ${publicReason} ${privateReason}` : ``)}`);
		} else {
			this.modlog('WARN', targetUser, target ? `${publicReason} ${privateReason}` : ``, {noalts: 1});
		}
		targetUser.send(`|c|~|/warn ${publicReason}`);

		const userid = targetUser.getLastId();

		this.add(`|hidelines|unlink|${userid}`);
		if (userid !== toID(this.inputUsername)) this.add(`|hidelines|unlink|${toID(this.inputUsername)}`);

		targetUser.lastWarnedAt = now;

		// Automatically upload replays as evidence/reference to the punishment
		if (globalWarn && room.battle) this.parse('/savereplay forpunishment');
	},
	warnhelp: [
		`/warn OR /k [username], [reason] - Warns a user showing them the site rules and [reason] in an overlay.`,
		`/warn OR /k [username], [reason] spoiler: [private reason] - Warns a user, marking [private reason] only in the modlog.`,
		`Requires: % @ # &`,
	],

	redirect: 'redir',
	redir(target, room, user, connection) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help redirect');
		if (room.settings.isPrivate || room.settings.isPersonal) {
			return this.errorReply("Users cannot be redirected from private or personal rooms.");
		}
		target = this.splitTarget(target);
		const targetUser = this.targetUser;
		const targetRoom = Rooms.search(target);
		if (!targetRoom || targetRoom.settings.modjoin || targetRoom.settings.staffRoom) {
			return this.errorReply(`The room "${target}" does not exist.`);
		}
		if (!this.can('warn', targetUser, room) || !this.can('warn', targetUser, targetRoom)) return false;

		if (!this.can('rangeban', targetUser)) {
			this.errorReply(`Redirects have been deprecated. Instead of /redirect, use <<room links>> or /invite to guide users to the correct room, and punish if users don't cooperate.`);
			return;
		}

		if (!targetUser || !targetUser.connected) {
			return this.errorReply(`User ${this.targetUsername} not found.`);
		}
		if (targetRoom.roomid === "global") return this.errorReply(`Users cannot be redirected to the global room.`);
		if (targetRoom.settings.isPrivate || targetRoom.settings.isPersonal) {
			return this.errorReply(`The room "${target}" is not public.`);
		}
		if (targetUser.inRooms.has(targetRoom.roomid)) {
			return this.errorReply(`User ${targetUser.name} is already in the room ${targetRoom.title}!`);
		}
		if (!targetUser.inRooms.has(room.roomid)) {
			return this.errorReply(`User ${this.targetUsername} is not in the room ${room.roomid}.`);
		}
		targetUser.leaveRoom(room.roomid);
		targetUser.popup(`You are in the wrong room; please go to <<${targetRoom.roomid}>> instead`);
		this.addModAction(`${targetUser.name} was redirected to room ${targetRoom.title} by ${user.name}.`);
		this.modlog('REDIRECT', targetUser, `to ${targetRoom.title}`, {noip: 1, noalts: 1});
		targetUser.leaveRoom(room);
	},
	redirhelp: [
		`/redirect OR /redir [username], [roomname] - [DEPRECATED]`,
		`Attempts to redirect the [username] to the [roomname]. Requires: &`,
	],

	m: 'mute',
	mute(target, room, user, connection, cmd) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help mute');
		if (!this.canTalk()) return;

		target = this.splitTarget(target);
		const targetUser = this.targetUser;
		if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
		if (target.length > MAX_REASON_LENGTH) {
			return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
		}

		const muteDuration = ((cmd === 'hm' || cmd === 'hourmute') ? HOURMUTE_LENGTH : MUTE_LENGTH);
		if (!this.can('mute', targetUser, room)) return false;
		if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to mute upper staff members.");
		const canBeMutedFurther = ((room.getMuteTime(targetUser) || 0) <= (muteDuration * 5 / 6));
		if (targetUser.locked ||
			(room.isMuted(targetUser) && !canBeMutedFurther) ||
			Punishments.isRoomBanned(targetUser, room.roomid)) {
			const alreadyPunishment = targetUser.locked ? "locked" : room.isMuted(targetUser) ? "muted" : "room banned";
			const problem = ` but was already ${alreadyPunishment}`;
			if (!target) {
				return this.privateModAction(`${targetUser.name} would be muted by ${user.name} ${problem}.`);
			}
			return this.addModAction(`${targetUser.name} would be muted by ${user.name} ${problem}. (${target})`);
		}

		if (targetUser.id in room.users) {
			targetUser.popup(`|modal|${user.name} has muted you in ${room.roomid} for ${Chat.toDurationString(muteDuration)}. ${target}`);
		}
		this.addModAction(`${targetUser.name} was muted by ${user.name} for ${Chat.toDurationString(muteDuration)}.${(target ? ` (${target})` : ``)}`);
		this.modlog(`${cmd.includes('h') ? 'HOUR' : ''}MUTE`, targetUser, target);
		if (targetUser.autoconfirmed && targetUser.autoconfirmed !== targetUser.id) {
			const displayMessage = `${targetUser.name}'s ac account: ${targetUser.autoconfirmed}`;
			this.privateModAction(displayMessage);
		}
		const userid = targetUser.getLastId();
		this.add(`|hidelines|unlink|${userid}`);
		if (userid !== toID(this.inputUsername)) this.add(`|hidelines|unlink|${toID(this.inputUsername)}`);

		room.mute(targetUser, muteDuration);
	},
	mutehelp: [`/mute OR /m [username], [reason] - Mutes a user with reason for 7 minutes. Requires: % @ # &`],

	hm: 'hourmute',
	hourmute(target) {
		if (!target) return this.parse('/help hourmute');
		this.run('mute');
	},
	hourmutehelp: [`/hourmute OR /hm [username], [reason] - Mutes a user with reason for an hour. Requires: % @ # &`],

	um: 'unmute',
	unmute(target, room, user) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help unmute');
		target = this.splitTarget(target);
		if (target) return this.errorReply(`This command does not support specifying a reason.`);
		if (!this.canTalk()) return;
		if (!this.can('mute', null, room)) return false;

		const targetUser = this.targetUser;
		const successfullyUnmuted = room.unmute(
			targetUser?.id || toID(this.targetUsername), `Your mute in '${room.title}' has been lifted.`
		);

		if (successfullyUnmuted) {
			this.addModAction(`${(targetUser ? targetUser.name : successfullyUnmuted)} was unmuted by ${user.name}.`);
			this.modlog('UNMUTE', (targetUser || successfullyUnmuted), null, {noip: 1, noalts: 1});
		} else {
			this.errorReply(`${(targetUser ? targetUser.name : this.targetUsername)} is not muted.`);
		}
	},
	unmutehelp: [`/unmute [username] - Removes mute from user. Requires: % @ # &`],

	rb: 'ban',
	weekban: 'ban',
	wrb: 'ban',
	forceroomban: 'ban',
	forceweekban: 'ban',
	forcerb: 'ban',
	roomban: 'ban',
	b: 'ban',
	ban(target, room, user, connection, cmd) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help ban');
		if (!this.canTalk()) return;
		const week = ['wrb', 'forceweekban', 'weekban'].includes(cmd);

		target = this.splitTarget(target);
		const targetUser = this.targetUser;
		if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
		if (target.length > MAX_REASON_LENGTH) {
			return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
		}
		if (!this.can('ban', targetUser, room)) return false;
		if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to ban upper staff members.");
		if (Punishments.getRoomPunishType(room, this.targetUsername) === 'BLACKLIST') {
			return this.errorReply(`This user is already blacklisted from ${room.roomid}.`);
		}
		const name = targetUser.getLastName();
		const userid = targetUser.getLastId();
		const force = ['forcerb', 'forceroomban', 'forceweekban'].includes(cmd);
		if (targetUser.trusted) {
			if (!force) {
				return this.sendReply(
					`${name} is a trusted user. If you are sure you would like to ban them, use /force${week ? 'week' : ''}roomban.`
				);
			}
		} else if (force) {
			return this.errorReply(`Use /roomban; ${name} is not a trusted user.`);
		}
		if (Punishments.isRoomBanned(targetUser, room.roomid) && !target) {
			const problem = " but was already banned";
			return this.privateModAction(`${name} would be banned by ${user.name} ${problem}.`);
		}

		if (targetUser.trusted && room.settings.isPrivate !== true && !room.settings.isPersonal) {
			Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} ${(targetUser.trusted !== targetUser.id ? ` (${targetUser.trusted})` : ``)} was roombanned from ${room.roomid} by ${user.name}, and should probably be demoted.`);
		}

		if (targetUser.id in room.users || user.can('lock')) {
			targetUser.popup(
				`|modal||html|<p>${Utils.escapeHTML(user.name)} has banned you from the room ${room.roomid} ` +
				`${(room.subRooms ? ` and its subrooms` : ``)}${week ? ' for a week' : ''}.` +
				`</p>${(target ? `<p>Reason: ${Utils.escapeHTML(target)}</p>` : ``)}` +
				`<p>To appeal the ban, PM the staff member that banned you${room.persist ? ` or a room owner. ` +
				`</p><p><button name="send" value="/roomauth ${room.roomid}">List Room Staff</button></p>` : `.</p>`}`
			);
		}

		const reason = (target ? ` (${target})` : ``);
		this.addModAction(`${name} was banned ${week ? ' for a week' : ''} from ${room.title} by ${user.name}.${reason}`);

		const time = week ? Date.now() + 7 * 24 * 60 * 60 * 1000 : null;
		const affected = Punishments.roomBan(room, targetUser, time, null, target);

		if (!room.settings.isPrivate && room.persist) {
			const acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
			let displayMessage = '';
			if (affected.length > 1) {
				displayMessage = `${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : ``)} banned alts: ${affected.slice(1).map(curUser => curUser.getLastName()).join(", ")}`;
				this.privateModAction(displayMessage);
			} else if (acAccount) {
				displayMessage = `${name}'s ac account: ${acAccount}`;
				this.privateModAction(displayMessage);
			}
		}
		room.hideText([userid, toID(this.inputUsername)]);

		if (room.settings.isPrivate !== true && room.persist) {
			this.globalModlog(`${week ? 'WEEK' : ''}ROOMBAN`, targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
		} else {
			this.modlog(`${week ? 'WEEK' : ''}ROOMBAN`, targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
		}
		return true;
	},
	banhelp: [
		`/ban [username], [reason] - Bans the user from the room you are in. Requires: @ # &`,
		`/weekban [username, reason] - Bans the user from the room you are in for a week. Requires: @ # &`,
	],

	unroomban: 'unban',
	roomunban: 'unban',
	unban(target, room, user, connection) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help unban');
		if (!this.can('ban', null, room)) return false;

		const name = Punishments.roomUnban(room, target);
<commit_msg>Weekban: Fix error message and add /wb alias (#7319)<commit_after>/**
 * Moderation commands
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * These are commands for staff.
 *
 * For the API, see chat-plugins/COMMANDS.md
 *
 * @license MIT
 */
import {Utils} from '../../lib/utils';

/* eslint no-else-return: "error" */

const MAX_REASON_LENGTH = 300;
const MUTE_LENGTH = 7 * 60 * 1000;
const HOURMUTE_LENGTH = 60 * 60 * 1000;

/** Require reasons for punishment commands */
const REQUIRE_REASONS = true;

export const commands: ChatCommands = {

	roomowner(target, room, user) {
		if (!room) return this.requiresRoom();
		if (!room.persist) {
			return this.sendReply("/roomowner - This room isn't designed for per-room moderation to be added");
		}
		if (!target) return this.parse('/help roomowner');
		target = this.splitTarget(target, true);
		if (target) return this.errorReply(`This command does not support specifying a reason.`);
		const targetUser = this.targetUser;
		const name = this.targetUsername;
		const userid = toID(name);

		if (!Users.isUsernameKnown(userid)) {
			return this.errorReply(`User '${this.targetUsername}' is offline and unrecognized, and so can't be promoted.`);
		}

		if (!this.can('makeroom')) return false;
		if (room.auth.getDirect(userid) === '#') return this.errorReply(`${name} is already a room owner.`);

		room.auth.set(userid, '#');
		this.addModAction(`${name} was appointed Room Owner by ${user.name}.`);
		this.modlog('ROOMOWNER', userid);
		if (targetUser) {
			targetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.roomid}.`);
			room.onUpdateIdentity(targetUser);
			if (room.subRooms) {
				for (const subRoom of room.subRooms.values()) {
					subRoom.onUpdateIdentity(targetUser);
				}
			}
		}
		room.saveSettings();
	},
	roomownerhelp: [`/roomowner [username] - Appoints [username] as a room owner. Requires: &`],

	roomdemote: 'roompromote',
	forceroompromote: 'roompromote',
	forceroomdemote: 'roompromote',
	roompromote(target, room, user, connection, cmd) {
		if (!room) {
			// this command isn't marked as room-only because it's usable in PMs through /invite
			return this.errorReply("This command is only available in rooms");
		}
		if (!this.canTalk()) return;
		if (!target) return this.parse('/help roompromote');

		const force = cmd.startsWith('force');
		target = this.splitTarget(target, true);
		const targetUser = this.targetUser;
		const userid = toID(this.targetUsername);
		let name = targetUser ? targetUser.name : this.filter(this.targetUsername);
		if (!name) return;
		name = name.slice(0, 18);

		if (!userid) return this.parse('/help roompromote');
		if (!targetUser && !Users.isUsernameKnown(userid) && !force) {
			return this.errorReply(`User '${name}' is offline and unrecognized, and so can't be promoted.`);
		}
		if (targetUser && !targetUser.registered) {
			return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
		}

		let currentSymbol: GroupSymbol | 'whitelist' = room.auth.getDirect(userid);
		if (room.auth.has(userid) && currentSymbol === Users.Auth.defaultSymbol()) {
			currentSymbol = 'whitelist';
		}
		const currentGroup = Users.Auth.getGroup(currentSymbol);
		const nextSymbol = target === 'deauth' ? Users.Auth.defaultSymbol() : target as GroupSymbol;
		const nextGroup = Users.Auth.getGroup(nextSymbol);

		if (!nextSymbol) {
			return this.errorReply("Please specify a group such as /roomvoice or /roomdeauth");
		}
		if (!Config.groups[nextSymbol]) {
			if (!force || !user.can('bypassall')) {
				this.errorReply(`Group '${nextSymbol}' does not exist.`);
				if (user.can('bypassall')) {
					this.errorReply(`If you want to promote to a nonexistent group, use /forceroompromote`);
				}
				return;
			} else if (!Users.Auth.isValidSymbol(nextSymbol)) {
				// yes I know this excludes astral-plane characters and includes combining characters
				return this.errorReply(`Admins can forcepromote to nonexistent groups only if they are one character long`);
			}
		}

		if (!force && (nextGroup.globalonly || (nextGroup.battleonly && !room.battle))) {
			return this.errorReply(`Group 'room${nextGroup.id || nextSymbol}' does not exist as a room rank.`);
		}

		const currentGroupName = currentGroup.name || "regular user";
		const nextGroupName = nextGroup.name || "regular user";

		if (currentSymbol === nextSymbol) {
			return this.errorReply(`User '${name}' is already a ${nextGroupName} in this room.`);
		}
		if (!user.can('makeroom')) {
			if (currentGroup.id && !user.can(`room${currentGroup.id || 'voice'}` as 'roomvoice', null, room)) {
				return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${currentGroupName}.`);
			}
			if (nextSymbol !== ' ' && !user.can(`room${nextGroup.id || 'voice'}` as 'roomvoice', null, room)) {
				return this.errorReply(`/${cmd} - Access denied for promoting/demoting to ${nextGroupName}.`);
			}
		}
		if (targetUser?.locked && room.persist && room.settings.isPrivate !== true && nextGroup.rank > 2) {
			return this.errorReply("Locked users can't be promoted.");
		}

		if (nextSymbol === Users.Auth.defaultSymbol()) {
			room.auth.delete(userid);
		} else {
			room.auth.set(userid, nextSymbol);
		}

		// Only show popup if: user is online and in the room, the room is public, and not a groupchat or a battle.
		const shouldPopup = (
			targetUser && room.users[targetUser.id] && room.persist && room.settings.isPrivate !== true ?
				targetUser : null
		);

		if (this.pmTarget && targetUser) {
			const text = `${targetUser.name} was invited (and promoted to Room ${nextGroupName}) by ${user.name}.`;
			room.add(`|c|${user.getIdentity(room.roomid)}|/log ${text}`).update();
			this.modlog('INVITE', targetUser, null, {noip: 1, noalts: 1});
		} else if (
			nextSymbol in Config.groups && currentSymbol in Config.groups &&
			nextGroup.rank < currentGroup.rank
		) {
			if (targetUser && room.users[targetUser.id] && !nextGroup.modlog) {
				// if the user can't see the demotion message (i.e. rank < %), it is shown in the chat
				targetUser.send(`>${room.roomid}\n(You were demoted to Room ${nextGroupName} by ${user.name}.)`);
			}
			this.privateModAction(`${name} was demoted to Room ${nextGroupName} by ${user.name}.`);
			this.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid, '(demote)');
			shouldPopup?.popup(`You were demoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);
		} else if (nextSymbol === '#') {
			this.addModAction(`${'' + name} was promoted to ${nextGroupName} by ${user.name}.`);
			this.modlog('ROOM OWNER', userid);
			shouldPopup?.popup(`You were promoted to ${nextGroupName} by ${user.name} in ${room.roomid}.`);
		} else {
			this.addModAction(`${'' + name} was promoted to Room ${nextGroupName} by ${user.name}.`);
			this.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid);
			shouldPopup?.popup(`You were promoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);
		}

		if (targetUser) {
			targetUser.updateIdentity(room.roomid);
			if (room.subRooms) {
				for (const subRoom of room.subRooms.values()) {
					targetUser.updateIdentity(subRoom.roomid);
				}
			}
		}
		room.saveSettings();
	},
	roompromotehelp: [
		`/roompromote OR /roomdemote [username], [group symbol] - Promotes/demotes the user to the specified room rank. Requires: @ # &`,
		`/room[group] [username] - Promotes/demotes the user to the specified room rank. Requires: @ # &`,
		`/roomdeauth [username] - Removes all room rank from the user. Requires: @ # &`,
	],

	auth: 'authority',
	stafflist: 'authority',
	globalauth: 'authority',
	authlist: 'authority',
	authority(target, room, user, connection) {
		if (target && target !== '+') {
			const targetRoom = Rooms.search(target);
			const availableRoom = targetRoom?.checkModjoin(user);
			if (targetRoom && availableRoom) return this.parse(`/roomauth1 ${target}`);
			return this.parse(`/userauth ${target}`);
		}
		const showAll = !!target;
		const rankLists: {[k: string]: string[]} = {};
		for (const [id, symbol] of Users.globalAuth) {
			if (symbol === ' ' || (symbol === '+' && !showAll)) continue;
			if (!rankLists[symbol]) rankLists[symbol] = [];
			rankLists[symbol].push(Users.globalAuth.usernames.get(id) || id);
		}

		const buffer = Utils.sortBy(
			Object.entries(rankLists) as [GroupSymbol, string[]][],
			([symbol]) => -Users.Auth.getGroup(symbol).rank
		).map(
			([symbol, names]) => (
				`${(Config.groups[symbol] ? `**${Config.groups[symbol].name}s** (${symbol})` : symbol)}:\n` +
				Utils.sortBy(names, name => toID(name)).join(", ")
			)
		);
		if (!showAll) buffer.push(`(Use \`\`/auth +\`\` to show global voice users.)`);

		if (!buffer.length) return connection.popup("This server has no global authority.");
		connection.popup(buffer.join("\n\n"));
	},
	authhelp: [
		`/auth - Show global staff for the server.`,
		`/auth + - Show global staff for the server, including voices.`,
		`/auth [room] - Show what roomauth a room has.`,
		`/auth [user] - Show what global and roomauth a user has.`,
	],

	roomstaff: 'roomauth',
	roomauth1: 'roomauth',
	roomauth(target, room, user, connection, cmd) {
		let userLookup = '';
		if (cmd === 'roomauth1') userLookup = `\n\nTo look up auth for a user, use /userauth ${target}`;
		let targetRoom = room;
		if (target) targetRoom = Rooms.search(target)!;
		if (!targetRoom || !targetRoom.checkModjoin(user)) {
			return this.errorReply(`The room "${target}" does not exist.`);
		}
		const showAll = user.can('mute', null, targetRoom);

		const rankLists: {[groupSymbol: string]: ID[]} = {};
		for (const [id, rank] of targetRoom.auth) {
			if (rank === ' ' && !showAll) continue;
			if (!rankLists[rank]) rankLists[rank] = [];
			rankLists[rank].push(id);
		}

		const buffer = Utils.sortBy(
			Object.entries(rankLists) as [GroupSymbol, ID[]][],
			([symbol]) => -Users.Auth.getGroup(symbol).rank
		).map(([symbol, names]) => {
			let group = Config.groups[symbol] ? `${Config.groups[symbol].name}s (${symbol})` : symbol;
			if (symbol === ' ') group = 'Whitelisted (this list is only visible to staff)';
			return `${group}:\n` +
				Utils.sortBy(names).map(userid => {
					const isOnline = Users.get(userid)?.statusType === 'online';
					// targetRoom guaranteed to exist above
					return userid in targetRoom!.users && isOnline ? `**${userid}**` : userid;
				}).join(', ');
		});

		let curRoom = targetRoom;
		while (curRoom.parent) {
			const modjoinSetting = curRoom.settings.modjoin === true ? curRoom.settings.modchat : curRoom.settings.modjoin;
			const roomType = (modjoinSetting ? `modjoin ${modjoinSetting} ` : '');
			const inheritedUserType = (modjoinSetting ? ` of rank ${modjoinSetting} and above` : '');
			if (curRoom.parent) {
				const also = buffer.length === 0 ? `` : ` also`;
				buffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType}${also} have authority in this room.`);
			}
			curRoom = curRoom.parent;
		}
		if (!buffer.length) {
			connection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);
			return;
		}
		if (!curRoom.settings.isPrivate) {
			buffer.push(`${curRoom.title} is a public room, so global auth with no relevant roomauth will have authority in this room.`);
		} else if (curRoom.settings.isPrivate === 'hidden' || curRoom.settings.isPrivate === 'voice') {
			buffer.push(`${curRoom.title} is a hidden room, so global auth with no relevant roomauth will have authority in this room.`);
		}
		if (targetRoom !== room) buffer.unshift(`${targetRoom.title} room auth:`);
		connection.popup(`${buffer.join("\n\n")}${userLookup}`);
	},

	userauth(target, room, user, connection) {
		const targetId = toID(target) || user.id;
		const targetUser = Users.getExact(targetId);
		const targetUsername = targetUser?.name || target;

		const buffer = [];
		let innerBuffer = [];
		const group = Users.globalAuth.get(targetId);
		if (group !== ' ' || Users.isTrusted(targetId)) {
			buffer.push(`Global auth: ${group === ' ' ? 'trusted' : group}`);
		}
		for (const curRoom of Rooms.rooms.values()) {
			if (curRoom.settings.isPrivate) continue;
			if (!curRoom.auth.has(targetId)) continue;
			innerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);
		}
		if (innerBuffer.length) {
			buffer.push(`Room auth: ${innerBuffer.join(', ')}`);
		}
		if (targetId === user.id || user.can('lock')) {
			innerBuffer = [];
			for (const curRoom of Rooms.rooms.values()) {
				if (!curRoom.settings.isPrivate) continue;
				if (curRoom.settings.isPrivate === true) continue;
				if (!curRoom.auth.has(targetId)) continue;
				innerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);
			}
			if (innerBuffer.length) {
				buffer.push(`Hidden room auth: ${innerBuffer.join(', ')}`);
			}
		}
		if (targetId === user.id || user.can('makeroom')) {
			innerBuffer = [];
			for (const chatRoom of Rooms.global.chatRooms) {
				if (!chatRoom.settings.isPrivate) continue;
				if (chatRoom.settings.isPrivate !== true) continue;
				if (!chatRoom.auth.has(targetId)) continue;
				innerBuffer.push(chatRoom.auth.getDirect(targetId).trim() + chatRoom.roomid);
			}
			if (innerBuffer.length) {
				buffer.push(`Private room auth: ${innerBuffer.join(', ')}`);
			}
		}
		if (!buffer.length) {
			buffer.push("No global or room auth.");
		}

		buffer.unshift(`${targetUsername} user auth:`);
		connection.popup(buffer.join("\n\n"));
	},

	autojoin(target, room, user, connection) {
		const targets = target.split(',');
		if (targets.length > 16 || connection.inRooms.size > 1) {
			return connection.popup("To prevent DoS attacks, you can only use /autojoin for 16 or fewer rooms, when you haven't joined any rooms yet. Please use /join for each room separately.");
		}
		Rooms.global.autojoinRooms(user, connection);
		const autojoins: string[] = [];

		const promises = targets.map(
			roomid => user.tryJoinRoom(roomid as RoomID, connection).then(ret => {
				if (ret === Rooms.RETRY_AFTER_LOGIN) {
					autojoins.push(roomid);
				}
			})
		);

		return Promise.all(promises).then(() => {
			connection.autojoins = autojoins.join(',');
		});
	},

	joim: 'join',
	j: 'join',
	join(target, room, user, connection) {
		if (!target) return this.parse('/help join');
		if (target.startsWith('http://')) target = target.slice(7);
		if (target.startsWith('https://')) target = target.slice(8);
		if (target.startsWith(`${Config.routes.client}/`)) target = target.slice(Config.routes.client.length + 1);
		if (target.startsWith('psim.us/')) target = target.slice(8);
		return user.tryJoinRoom(target as RoomID, connection).then(ret => {
			if (ret === Rooms.RETRY_AFTER_LOGIN) {
				connection.sendTo(
					target as RoomID,
					`|noinit|namerequired|The room '${target}' does not exist or requires a login to join.`
				);
			}
		});
	},
	joinhelp: [`/join [roomname] - Attempt to join the room [roomname].`],

	leave: 'part',
	part(target, room, user, connection) {
		const targetRoom = target ? Rooms.search(target) : room;
		if (!targetRoom) {
			if (target.startsWith('view-')) return;
			return this.errorReply(`The room '${target}' does not exist.`);
		}
		user.leaveRoom(targetRoom, connection);
	},

	/*********************************************************
	 * Moderating: Punishments
	 *********************************************************/

	kick: 'warn',
	k: 'warn',
	warn(target, room, user) {
		if (!target) return this.parse('/help warn');
		if (!room) return this.requiresRoom();
		if (!this.canTalk()) return;
		if (room.settings.isPersonal && !user.can('warn' as any)) {
			return this.errorReply("Warning is unavailable in group chats.");
		}
		// If used in staff, help tickets or battles, log the warn to the global modlog.
		const globalWarn = room.roomid === 'staff' || room.roomid.startsWith('help-') || (room.battle && !room.parent);

		target = this.splitTarget(target);
		let privateReason = '';
		let publicReason = target;
		const targetLowercase = target.toLowerCase();
		if (target && (targetLowercase.includes('spoiler:') || targetLowercase.includes('spoilers:'))) {
			const privateIndex = targetLowercase.indexOf(targetLowercase.includes('spoilers:') ? 'spoilers:' : 'spoiler:');
			const bump = (targetLowercase.includes('spoilers:') ? 9 : 8);
			privateReason = `(PROOF: ${target.substr(privateIndex + bump, target.length).trim()}) `;
			publicReason = target.substr(0, privateIndex).trim();
		}

		const targetUser = this.targetUser;
		if (!targetUser || !targetUser.connected) {
			if (!targetUser || !globalWarn) return this.errorReply(`User '${this.targetUsername}' not found.`);
			if (!this.can('warn', null, room)) return false;

			this.addModAction(`${targetUser.name} would be warned by ${user.name} but is offline.${(publicReason ? ` (${publicReason})` : ``)}`);
			this.globalModlog('WARN OFFLINE', targetUser, ` by ${user.id}${(target ? `: ${publicReason} ${privateReason}` : ``)}`);
			return;
		}
		if (!(targetUser.id in room.users) && !globalWarn) {
			return this.errorReply(`User ${this.targetUsername} is not in the room ${room.roomid}.`);
		}
		if (publicReason.length > MAX_REASON_LENGTH) {
			return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
		}
		if (!this.can('warn', targetUser, room)) return false;
		if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to warn upper staff members.");

		const now = Date.now();
		const timeout = now - targetUser.lastWarnedAt;
		if (timeout < 15 * 1000) {
			const remainder = (15 - (timeout / 1000)).toFixed(2);
			return this.errorReply(`You must wait ${remainder} more seconds before you can warn ${targetUser.name} again.`);
		}

		this.addModAction(`${targetUser.name} was warned by ${user.name}.${(publicReason ? ` (${publicReason})` : ``)}`);
		if (globalWarn) {
			this.globalModlog('WARN', targetUser, ` by ${user.id}${(target ? `: ${publicReason} ${privateReason}` : ``)}`);
		} else {
			this.modlog('WARN', targetUser, target ? `${publicReason} ${privateReason}` : ``, {noalts: 1});
		}
		targetUser.send(`|c|~|/warn ${publicReason}`);

		const userid = targetUser.getLastId();

		this.add(`|hidelines|unlink|${userid}`);
		if (userid !== toID(this.inputUsername)) this.add(`|hidelines|unlink|${toID(this.inputUsername)}`);

		targetUser.lastWarnedAt = now;

		// Automatically upload replays as evidence/reference to the punishment
		if (globalWarn && room.battle) this.parse('/savereplay forpunishment');
	},
	warnhelp: [
		`/warn OR /k [username], [reason] - Warns a user showing them the site rules and [reason] in an overlay.`,
		`/warn OR /k [username], [reason] spoiler: [private reason] - Warns a user, marking [private reason] only in the modlog.`,
		`Requires: % @ # &`,
	],

	redirect: 'redir',
	redir(target, room, user, connection) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help redirect');
		if (room.settings.isPrivate || room.settings.isPersonal) {
			return this.errorReply("Users cannot be redirected from private or personal rooms.");
		}
		target = this.splitTarget(target);
		const targetUser = this.targetUser;
		const targetRoom = Rooms.search(target);
		if (!targetRoom || targetRoom.settings.modjoin || targetRoom.settings.staffRoom) {
			return this.errorReply(`The room "${target}" does not exist.`);
		}
		if (!this.can('warn', targetUser, room) || !this.can('warn', targetUser, targetRoom)) return false;

		if (!this.can('rangeban', targetUser)) {
			this.errorReply(`Redirects have been deprecated. Instead of /redirect, use <<room links>> or /invite to guide users to the correct room, and punish if users don't cooperate.`);
			return;
		}

		if (!targetUser || !targetUser.connected) {
			return this.errorReply(`User ${this.targetUsername} not found.`);
		}
		if (targetRoom.roomid === "global") return this.errorReply(`Users cannot be redirected to the global room.`);
		if (targetRoom.settings.isPrivate || targetRoom.settings.isPersonal) {
			return this.errorReply(`The room "${target}" is not public.`);
		}
		if (targetUser.inRooms.has(targetRoom.roomid)) {
			return this.errorReply(`User ${targetUser.name} is already in the room ${targetRoom.title}!`);
		}
		if (!targetUser.inRooms.has(room.roomid)) {
			return this.errorReply(`User ${this.targetUsername} is not in the room ${room.roomid}.`);
		}
		targetUser.leaveRoom(room.roomid);
		targetUser.popup(`You are in the wrong room; please go to <<${targetRoom.roomid}>> instead`);
		this.addModAction(`${targetUser.name} was redirected to room ${targetRoom.title} by ${user.name}.`);
		this.modlog('REDIRECT', targetUser, `to ${targetRoom.title}`, {noip: 1, noalts: 1});
		targetUser.leaveRoom(room);
	},
	redirhelp: [
		`/redirect OR /redir [username], [roomname] - [DEPRECATED]`,
		`Attempts to redirect the [username] to the [roomname]. Requires: &`,
	],

	m: 'mute',
	mute(target, room, user, connection, cmd) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help mute');
		if (!this.canTalk()) return;

		target = this.splitTarget(target);
		const targetUser = this.targetUser;
		if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
		if (target.length > MAX_REASON_LENGTH) {
			return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
		}

		const muteDuration = ((cmd === 'hm' || cmd === 'hourmute') ? HOURMUTE_LENGTH : MUTE_LENGTH);
		if (!this.can('mute', targetUser, room)) return false;
		if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to mute upper staff members.");
		const canBeMutedFurther = ((room.getMuteTime(targetUser) || 0) <= (muteDuration * 5 / 6));
		if (targetUser.locked ||
			(room.isMuted(targetUser) && !canBeMutedFurther) ||
			Punishments.isRoomBanned(targetUser, room.roomid)) {
			const alreadyPunishment = targetUser.locked ? "locked" : room.isMuted(targetUser) ? "muted" : "room banned";
			const problem = ` but was already ${alreadyPunishment}`;
			if (!target) {
				return this.privateModAction(`${targetUser.name} would be muted by ${user.name} ${problem}.`);
			}
			return this.addModAction(`${targetUser.name} would be muted by ${user.name} ${problem}. (${target})`);
		}

		if (targetUser.id in room.users) {
			targetUser.popup(`|modal|${user.name} has muted you in ${room.roomid} for ${Chat.toDurationString(muteDuration)}. ${target}`);
		}
		this.addModAction(`${targetUser.name} was muted by ${user.name} for ${Chat.toDurationString(muteDuration)}.${(target ? ` (${target})` : ``)}`);
		this.modlog(`${cmd.includes('h') ? 'HOUR' : ''}MUTE`, targetUser, target);
		if (targetUser.autoconfirmed && targetUser.autoconfirmed !== targetUser.id) {
			const displayMessage = `${targetUser.name}'s ac account: ${targetUser.autoconfirmed}`;
			this.privateModAction(displayMessage);
		}
		const userid = targetUser.getLastId();
		this.add(`|hidelines|unlink|${userid}`);
		if (userid !== toID(this.inputUsername)) this.add(`|hidelines|unlink|${toID(this.inputUsername)}`);

		room.mute(targetUser, muteDuration);
	},
	mutehelp: [`/mute OR /m [username], [reason] - Mutes a user with reason for 7 minutes. Requires: % @ # &`],

	hm: 'hourmute',
	hourmute(target) {
		if (!target) return this.parse('/help hourmute');
		this.run('mute');
	},
	hourmutehelp: [`/hourmute OR /hm [username], [reason] - Mutes a user with reason for an hour. Requires: % @ # &`],

	um: 'unmute',
	unmute(target, room, user) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help unmute');
		target = this.splitTarget(target);
		if (target) return this.errorReply(`This command does not support specifying a reason.`);
		if (!this.canTalk()) return;
		if (!this.can('mute', null, room)) return false;

		const targetUser = this.targetUser;
		const successfullyUnmuted = room.unmute(
			targetUser?.id || toID(this.targetUsername), `Your mute in '${room.title}' has been lifted.`
		);

		if (successfullyUnmuted) {
			this.addModAction(`${(targetUser ? targetUser.name : successfullyUnmuted)} was unmuted by ${user.name}.`);
			this.modlog('UNMUTE', (targetUser || successfullyUnmuted), null, {noip: 1, noalts: 1});
		} else {
			this.errorReply(`${(targetUser ? targetUser.name : this.targetUsername)} is not muted.`);
		}
	},
	unmutehelp: [`/unmute [username] - Removes mute from user. Requires: % @ # &`],

	rb: 'ban',
	weekban: 'ban',
	wb: 'ban',
	wrb: 'ban',
	forceroomban: 'ban',
	forceweekban: 'ban',
	forcerb: 'ban',
	roomban: 'ban',
	b: 'ban',
	ban(target, room, user, connection, cmd) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help ban');
		if (!this.canTalk()) return;
		const week = ['wrb', 'wb', 'forceweekban', 'weekban'].includes(cmd);

		target = this.splitTarget(target);
		const targetUser = this.targetUser;
		if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
		if (target.length > MAX_REASON_LENGTH) {
			return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
		}
		if (!this.can('ban', targetUser, room)) return false;
		if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to ban upper staff members.");
		if (Punishments.getRoomPunishType(room, this.targetUsername) === 'BLACKLIST') {
			return this.errorReply(`This user is already blacklisted from ${room.roomid}.`);
		}
		const name = targetUser.getLastName();
		const userid = targetUser.getLastId();
		const force = cmd.startsWith('force');
		if (targetUser.trusted) {
			if (!force) {
				return this.sendReply(
					`${name} is a trusted user. If you are sure you would like to ban them, use /force${week ? 'week' : 'room'}ban.`
				);
			}
		} else if (force) {
			return this.errorReply(`Use /${week ? 'week' : 'room'}ban; ${name} is not a trusted user.`);
		}
		if (Punishments.isRoomBanned(targetUser, room.roomid) && !target) {
			const problem = " but was already banned";
			return this.privateModAction(`${name} would be banned by ${user.name} ${problem}.`);
		}

		if (targetUser.trusted && room.settings.isPrivate !== true && !room.settings.isPersonal) {
			Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} ${(targetUser.trusted !== targetUser.id ? ` (${targetUser.trusted})` : ``)} was roombanned from ${room.roomid} by ${user.name}, and should probably be demoted.`);
		}

		if (targetUser.id in room.users || user.can('lock')) {
			targetUser.popup(
				`|modal||html|<p>${Utils.escapeHTML(user.name)} has banned you from the room ${room.roomid} ` +
				`${(room.subRooms ? ` and its subrooms` : ``)}${week ? ' for a week' : ''}.` +
				`</p>${(target ? `<p>Reason: ${Utils.escapeHTML(target)}</p>` : ``)}` +
				`<p>To appeal the ban, PM the staff member that banned you${room.persist ? ` or a room owner. ` +
				`</p><p><button name="send" value="/roomauth ${room.roomid}">List Room Staff</button></p>` : `.</p>`}`
			);
		}

		const reason = (target ? ` (${target})` : ``);
		this.addModAction(`${name} was banned ${week ? ' for a week' : ''} from ${room.title} by ${user.name}.${reason}`);

		const time = week ? Date.now() + 7 * 24 * 60 * 60 * 1000 : null;
		const affected = Punishments.roomBan(room, targetUser, time, null, target);

		if (!room.settings.isPrivate && room.persist) {
			const acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
			let displayMessage = '';
			if (affected.length > 1) {
				displayMessage = `${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : ``)} banned alts: ${affected.slice(1).map(curUser => curUser.getLastName()).join(", ")}`;
				this.privateModAction(displayMessage);
			} else if (acAccount) {
				displayMessage = `${name}'s ac account: ${acAccount}`;
				this.privateModAction(displayMessage);
			}
		}
		room.hideText([userid, toID(this.inputUsername)]);

		if (room.settings.isPrivate !== true && room.persist) {
			this.globalModlog(`${week ? 'WEEK' : ''}ROOMBAN`, targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
		} else {
			this.modlog(`${week ? 'WEEK' : ''}ROOMBAN`, targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
		}
		return true;
	},
	banhelp: [
		`/ban [username], [reason] - Bans the user from the room you are in. Requires: @ # &`,
		`/weekban [username], [reason] - Bans the user from the room you are in for a week. Requires: @ # &`,
	],

	unroomban: 'unban',
	roomunban: 'unban',
	unban(target, room, user, connection) {
		if (!room) return this.requiresRoom();
		if (!target) return this.parse('/help unban');
		if (!this.can('ban', null, room)) return false;

		const name = Punishments.roomUnban(room, target);

{'fraction_non_alphanumeric': 0.1770717270194986, 'fraction_numerical': 0.002681058495821727, 'mean_word_length': 4.265951595159516, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 71, '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': '7538396', 'n_tokens_mistral': 20644, 'n_tokens_neox': 18822, 'n_words': 4817}