<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) {
		room = this.requireRoom();
		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.`);
		}

		this.checkCan('makeroom');
		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");
		}
		this.checkChat();
		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.)`);<commit_msg>Support promoting multiple users at once in rooms (#7344)<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) {
		room = this.requireRoom();
		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.`);
		}

		this.checkCan('makeroom');
		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");
		}
		this.checkChat();
		if (!target) return this.parse('/help roompromote');

		const force = cmd.startsWith('force');
		const users = target.split(',').map(part => part.trim());
		let nextSymbol = users.pop() as GroupSymbol | 'deauth';
		if (nextSymbol === 'deauth') nextSymbol = Users.Auth.defaultSymbol();
		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 nextGroupName = nextGroup.name || "regular user";

		for (const toPromote of users) {
			const targetUser = Users.get(toPromote);
			const userid = toID(toPromote);
			let name = targetUser ? targetUser.name : this.filter(this.targetUsername);
			if (!name) continue;
			name = name.slice(0, 18);

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

			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 currentGroupName = currentGroup.name || "regular user";

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

			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 [comma-separated usernames], [group symbol] - Promotes/demotes the user(s) to the specified room rank. Requires: @ # &`,
		`/room[group] [comma-separated usernames] - Promotes/demotes the user(s) to the specified room rank. Requires: @ # &`,
		`/roomdeauth [comma-separated usernames] - Removes all room rank from the user(s). 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.)`);
{'fraction_non_alphanumeric': 0.16458796059001082, 'fraction_numerical': 0.0030753459764223477, 'mean_word_length': 4.0157097972008, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 14, 'https://': 0, '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': '1538387', 'n_tokens_mistral': 6343, 'n_tokens_neox': 5645, 'n_words': 1485}