<commit_before>/**
 * Command parser
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * This is the command parser. Call it with CommandParser.parse
 * (scroll down to its definition for details)
 *
 * Individual commands are put in:
 *   commands.js - "core" commands that shouldn't be modified
 *   chat-plugins/ - other commands that can be safely modified
 *
 * The command API is (mostly) documented in chat-plugins/COMMANDS.md
 *
 * @license MIT license
 */

/*

To reload chat commands:

/hotpatch chat

*/

'use strict';

const MAX_MESSAGE_LENGTH = 300;

const BROADCAST_COOLDOWN = 20 * 1000;

const MESSAGE_COOLDOWN = 5 * 60 * 1000;

const MAX_PARSE_RECURSION = 10;

const VALID_COMMAND_TOKENS = '/!';

const BROADCAST_TOKEN = '!';

const fs = require('fs');
const path = require('path');

/*********************************************************
 * Load command files
 *********************************************************/

let baseCommands = exports.baseCommands = require('./commands.js').commands;
let commands = exports.commands = Object.assign({}, baseCommands);

// Install plug-in commands

// info always goes first so other plugins can shadow it
Object.assign(commands, require('./chat-plugins/info.js').commands);

for (let file of fs.readdirSync(path.resolve(__dirname, 'chat-plugins'))) {
	if (file.substr(-3) !== '.js' || file === 'info.js') continue;
	Object.assign(commands, require('./chat-plugins/' + file).commands);
}

/*********************************************************
 * Modlog
 *********************************************************/

let modlog = exports.modlog = {
	lobby: fs.createWriteStream(path.resolve(__dirname, 'logs/modlog/modlog_lobby.txt'), {flags:'a+'}),
	battle: fs.createWriteStream(path.resolve(__dirname, 'logs/modlog/modlog_battle.txt'), {flags:'a+'}),
};

let writeModlog = exports.writeModlog = function (roomid, text) {
	if (!modlog[roomid]) {
		modlog[roomid] = fs.createWriteStream(path.resolve(__dirname, 'logs/modlog/modlog_' + roomid + '.txt'), {flags:'a+'});
	}
	modlog[roomid].write('[' + (new Date().toJSON()) + '] ' + text + '\n');
};

/*********************************************************
 * Parser
 *********************************************************/

class CommandContext {
	constructor(options) {
		this.cmd = options.cmd || '';
		this.cmdToken = options.cmdToken || '';

		this.target = options.target || '';
		this.message = options.message || '';

		this.levelsDeep = options.levelsDeep || 0;
		this.namespaces = options.namespaces || null;

		this.room = options.room || null;
		this.user = options.user || null;
		this.connection = options.connection || null;

		this.targetUserName = '';
		this.targetUser = null;
	}

	sendReply(data) {
		if (this.broadcasting) {
			this.room.add(data);
		} else {
			this.connection.sendTo(this.room, data);
		}
	}
	errorReply(message) {
		if (this.pmTarget) {
			let prefix = '|pm|' + this.user.getIdentity() + '|' + (this.pmTarget.getIdentity ? this.pmTarget.getIdentity() : ' ' + this.pmTarget) + '|/error ';
			this.connection.send(prefix + message.replace(/\n/g, prefix));
		} else {
			this.sendReply('|html|<div class="message-error">' + Tools.escapeHTML(message).replace(/\n/g, '<br />') + '</div>');
		}
	}
	sendReplyBox(html) {
		this.sendReply('|raw|<div class="infobox">' + html + '</div>');
	}
	popupReply(message) {
		this.connection.popup(message);
	}
	add(data) {
		this.room.add(data);
	}
	send(data) {
		this.room.send(data);
	}
	privateModCommand(data, noLog) {
		this.sendModCommand(data);
		this.logEntry(data);
		this.logModCommand(data);
	}
	sendModCommand(data) {
		let users = this.room.users;
		let auth = this.room.auth;

		for (let i in users) {
			let user = users[i];
			// hardcoded for performance reasons (this is an inner loop)
			if (user.isStaff || (auth && (auth[user.userid] || '+') !== '+')) {
				user.sendTo(this.room, data);
			}
		}
	}
	logEntry(data) {
		this.room.logEntry(data);
	}
	addModCommand(text, logOnlyText) {
		this.add(text);
		this.logModCommand(text + (logOnlyText || ""));
	}
	logModCommand(text) {
		let roomid = (this.room.battle ? 'battle' : this.room.id);
		if (this.room.isPersonal) roomid = 'groupchat';
		writeModlog(roomid, '(' + this.room.id + ') ' + text);
	}
	globalModlog(action, user, text) {
		let buf = "(" + this.room.id + ") " + action + ": ";
		if (typeof user === 'string') {
			buf += "[" + toId(user) + "]";
		} else {
			let userid = user.getLastId();
			buf += "[" + userid + "]";
			if (user.autoconfirmed && user.autoconfirmed !== userid) buf += " ac:[" + user.autoconfirmed + "]";
		}
		buf += text;
		writeModlog('global', buf);
	}
	can(permission, target, room) {
		if (!this.user.can(permission, target, room)) {
			this.errorReply(this.cmdToken + this.namespaces.concat(this.cmd).join(" ") + " - Access denied.");
			return false;
		}
		return true;
	}
	canBroadcast(checkOnly, suppressMessage) {
		if (!this.broadcasting && this.cmdToken === BROADCAST_TOKEN) {
			let message = this.canTalk(this.message);
			if (!message) return false;
			if (!this.user.can('broadcast', null, this.room)) {
				this.errorReply("You need to be voiced to broadcast this command's information.");
				this.errorReply("To see it for yourself, use: /" + message.substr(1));
				return false;
			}

			// broadcast cooldown
			let normalized = message.toLowerCase().replace(/[^a-z0-9\s!,]/g, '');
			if (this.room.lastBroadcast === normalized &&
					this.room.lastBroadcastTime >= Date.now() - BROADCAST_COOLDOWN) {
				this.errorReply("You can't broadcast this because it was just broadcast.");
				return false;
			}
			if (!checkOnly) {
				this.add('|c|' + this.user.getIdentity(this.room.id) + '|' + (suppressMessage || message));
				this.room.lastBroadcast = normalized;
				this.room.lastBroadcastTime = Date.now();
			}
			this.broadcasting = true;
		}
		return true;
	}
	parse(message, inNamespace, room) {
		if (inNamespace && this.cmdToken) {
			message = this.cmdToken + this.namespaces.concat(message.slice(1)).join(" ");
		}
		return CommandParser.parse(message, room || this.room, this.user, this.connection, this.levelsDeep + 1);
	}
	run(targetCmd, inNamespace) {
		if (targetCmd === 'constructor') return this.sendReply("Access denied.");
		let commandHandler;
		if (typeof targetCmd === 'function') {
			commandHandler = targetCmd;
		} else if (inNamespace) {
			commandHandler = commands;
			for (let i = 0; i < this.namespaces.length; i++) {
				commandHandler = commandHandler[this.namespaces[i]];
			}
			commandHandler = commandHandler[targetCmd];
		} else {
			commandHandler = commands[targetCmd];
		}

		let result;
		try {
			result = commandHandler.call(this, this.target, this.room, this.user, this.connection, this.cmd, this.message);
		} catch (err) {
			if (require('./crashlogger.js')(err, 'A chat command', {
				user: this.user.name,
				room: this.room.id,<commit_msg>Fix canBroadcast again Making it work when called with checkOnly first then without after<commit_after>/**
 * Command parser
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * This is the command parser. Call it with CommandParser.parse
 * (scroll down to its definition for details)
 *
 * Individual commands are put in:
 *   commands.js - "core" commands that shouldn't be modified
 *   chat-plugins/ - other commands that can be safely modified
 *
 * The command API is (mostly) documented in chat-plugins/COMMANDS.md
 *
 * @license MIT license
 */

/*

To reload chat commands:

/hotpatch chat

*/

'use strict';

const MAX_MESSAGE_LENGTH = 300;

const BROADCAST_COOLDOWN = 20 * 1000;

const MESSAGE_COOLDOWN = 5 * 60 * 1000;

const MAX_PARSE_RECURSION = 10;

const VALID_COMMAND_TOKENS = '/!';

const BROADCAST_TOKEN = '!';

const fs = require('fs');
const path = require('path');

/*********************************************************
 * Load command files
 *********************************************************/

let baseCommands = exports.baseCommands = require('./commands.js').commands;
let commands = exports.commands = Object.assign({}, baseCommands);

// Install plug-in commands

// info always goes first so other plugins can shadow it
Object.assign(commands, require('./chat-plugins/info.js').commands);

for (let file of fs.readdirSync(path.resolve(__dirname, 'chat-plugins'))) {
	if (file.substr(-3) !== '.js' || file === 'info.js') continue;
	Object.assign(commands, require('./chat-plugins/' + file).commands);
}

/*********************************************************
 * Modlog
 *********************************************************/

let modlog = exports.modlog = {
	lobby: fs.createWriteStream(path.resolve(__dirname, 'logs/modlog/modlog_lobby.txt'), {flags:'a+'}),
	battle: fs.createWriteStream(path.resolve(__dirname, 'logs/modlog/modlog_battle.txt'), {flags:'a+'}),
};

let writeModlog = exports.writeModlog = function (roomid, text) {
	if (!modlog[roomid]) {
		modlog[roomid] = fs.createWriteStream(path.resolve(__dirname, 'logs/modlog/modlog_' + roomid + '.txt'), {flags:'a+'});
	}
	modlog[roomid].write('[' + (new Date().toJSON()) + '] ' + text + '\n');
};

/*********************************************************
 * Parser
 *********************************************************/

class CommandContext {
	constructor(options) {
		this.cmd = options.cmd || '';
		this.cmdToken = options.cmdToken || '';

		this.target = options.target || '';
		this.message = options.message || '';

		this.levelsDeep = options.levelsDeep || 0;
		this.namespaces = options.namespaces || null;

		this.room = options.room || null;
		this.user = options.user || null;
		this.connection = options.connection || null;

		this.targetUserName = '';
		this.targetUser = null;
	}

	sendReply(data) {
		if (this.broadcasting) {
			this.room.add(data);
		} else {
			this.connection.sendTo(this.room, data);
		}
	}
	errorReply(message) {
		if (this.pmTarget) {
			let prefix = '|pm|' + this.user.getIdentity() + '|' + (this.pmTarget.getIdentity ? this.pmTarget.getIdentity() : ' ' + this.pmTarget) + '|/error ';
			this.connection.send(prefix + message.replace(/\n/g, prefix));
		} else {
			this.sendReply('|html|<div class="message-error">' + Tools.escapeHTML(message).replace(/\n/g, '<br />') + '</div>');
		}
	}
	sendReplyBox(html) {
		this.sendReply('|raw|<div class="infobox">' + html + '</div>');
	}
	popupReply(message) {
		this.connection.popup(message);
	}
	add(data) {
		this.room.add(data);
	}
	send(data) {
		this.room.send(data);
	}
	privateModCommand(data, noLog) {
		this.sendModCommand(data);
		this.logEntry(data);
		this.logModCommand(data);
	}
	sendModCommand(data) {
		let users = this.room.users;
		let auth = this.room.auth;

		for (let i in users) {
			let user = users[i];
			// hardcoded for performance reasons (this is an inner loop)
			if (user.isStaff || (auth && (auth[user.userid] || '+') !== '+')) {
				user.sendTo(this.room, data);
			}
		}
	}
	logEntry(data) {
		this.room.logEntry(data);
	}
	addModCommand(text, logOnlyText) {
		this.add(text);
		this.logModCommand(text + (logOnlyText || ""));
	}
	logModCommand(text) {
		let roomid = (this.room.battle ? 'battle' : this.room.id);
		if (this.room.isPersonal) roomid = 'groupchat';
		writeModlog(roomid, '(' + this.room.id + ') ' + text);
	}
	globalModlog(action, user, text) {
		let buf = "(" + this.room.id + ") " + action + ": ";
		if (typeof user === 'string') {
			buf += "[" + toId(user) + "]";
		} else {
			let userid = user.getLastId();
			buf += "[" + userid + "]";
			if (user.autoconfirmed && user.autoconfirmed !== userid) buf += " ac:[" + user.autoconfirmed + "]";
		}
		buf += text;
		writeModlog('global', buf);
	}
	can(permission, target, room) {
		if (!this.user.can(permission, target, room)) {
			this.errorReply(this.cmdToken + this.namespaces.concat(this.cmd).join(" ") + " - Access denied.");
			return false;
		}
		return true;
	}
	canBroadcast(checkOnly, suppressMessage) {
		let message = this.canTalk(this.message);
		let normalized = message.toLowerCase().replace(/[^a-z0-9\s!,]/g, '');

		if (!this.broadcasting && this.cmdToken === BROADCAST_TOKEN) {
			if (!message) return false;
			if (!this.user.can('broadcast', null, this.room)) {
				this.errorReply("You need to be voiced to broadcast this command's information.");
				this.errorReply("To see it for yourself, use: /" + message.substr(1));
				return false;
			}

			// broadcast cooldown
			if (this.room.lastBroadcast === normalized &&
					this.room.lastBroadcastTime >= Date.now() - BROADCAST_COOLDOWN) {
				this.errorReply("You can't broadcast this because it was just broadcast.");
				return false;
			}
			this.broadcasting = true;
		}
		if (this.broadcasting && !checkOnly) {
			this.add('|c|' + this.user.getIdentity(this.room.id) + '|' + (suppressMessage || message));
			this.room.lastBroadcast = normalized;
			this.room.lastBroadcastTime = Date.now();
		}
		return true;
	}
	parse(message, inNamespace, room) {
		if (inNamespace && this.cmdToken) {
			message = this.cmdToken + this.namespaces.concat(message.slice(1)).join(" ");
		}
		return CommandParser.parse(message, room || this.room, this.user, this.connection, this.levelsDeep + 1);
	}
	run(targetCmd, inNamespace) {
		if (targetCmd === 'constructor') return this.sendReply("Access denied.");
		let commandHandler;
		if (typeof targetCmd === 'function') {
			commandHandler = targetCmd;
		} else if (inNamespace) {
			commandHandler = commands;
			for (let i = 0; i < this.namespaces.length; i++) {
				commandHandler = commandHandler[this.namespaces[i]];
			}
			commandHandler = commandHandler[targetCmd];
		} else {
			commandHandler = commands[targetCmd];
		}

		let result;
		try {
			result = commandHandler.call(this, this.target, this.room, this.user, this.connection, this.cmd, this.message);
		} catch (err) {
			if (require('./crashlogger.js')(err, 'A chat command', {
				user: this.user.name,
				room: this.room.id,
{'fraction_non_alphanumeric': 0.21292639138240574, 'fraction_numerical': 0.0037342908438061043, 'mean_word_length': 4.051142546245919, 'pattern_counts': {'":': 2, '<': 15, '<?xml version=': 0, '>': 15, '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': '4072361', 'n_tokens_mistral': 5006, 'n_tokens_neox': 4568, 'n_words': 959}