pokemon-showdown/command-parser.js
The Immortal baa7df1d8d Save IP of banned users in the modlog
This adds a second parameter to addModCommand which is shown in the
modlog but not in the room log.
2013-09-23 21:27:03 +04:00

395 lines
12 KiB
JavaScript

/**
* 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
* config/commands.js - other commands that can be safely modified
*
* The command API is (mostly) documented in config/commands.js
*
* @license MIT license
*/
/*
To reload chat commands:
/hotpatch chat
*/
const MAX_MESSAGE_LENGTH = 300;
const BROADCAST_COOLDOWN = 20*1000;
const MESSAGE_COOLDOWN = 5*60*1000;
const MAX_PARSE_RECURSION = 10;
var crypto = require('crypto');
var modlog = exports.modlog = modlog || fs.createWriteStream('logs/modlog.txt', {flags:'a+'});
/**
* Command parser
*
* Usage:
* CommandParser.parse(message, room, user, connection)
*
* message - the message the user is trying to say
* room - the room the user is trying to say it in
* user - the user that sent the message
* connection - the connection the user sent the message from
*
* Returns the message the user should say, or a falsy value which
* means "don't say anything"
*
* Examples:
* CommandParser.parse("/join lobby", room, user, connection)
* will make the user join the lobby, and return false.
*
* CommandParser.parse("Hi, guys!", room, user, connection)
* will return "Hi, guys!" if the user isn't muted, or
* if he's muted, will warn him that he's muted, and
* return false.
*/
var parse = exports.parse = function(message, room, user, connection, levelsDeep) {
var cmd = '', target = '';
if (!message || !message.trim().length) return;
if (!levelsDeep) {
levelsDeep = 0;
// if (config.emergencylog && (connection.ip === '62.195.195.62' || connection.ip === '86.141.154.222' || connection.ip === '189.134.175.221' || message.length > 2048 || message.length > 256 && message.substr(0,5) !== '/utm ' && message.substr(0,5) !== '/trn ')) {
if (config.emergencylog && (user.userid === 'pindapinda' || connection.ip === '62.195.195.62' || connection.ip === '86.141.154.222' || connection.ip === '189.134.175.221')) {
config.emergencylog.write('<'+user.name+'@'+connection.ip+'> '+message+'\n');
}
}
if (message.substr(0,3) === '>> ') {
// multiline eval
message = '/eval '+message.substr(3);
} else if (message.substr(0,4) === '>>> ') {
// multiline eval
message = '/evalbattle '+message.substr(4);
}
if (message.substr(0,2) !== '//' && message.substr(0,1) === '/') {
var spaceIndex = message.indexOf(' ');
if (spaceIndex > 0) {
cmd = message.substr(1, spaceIndex-1);
target = message.substr(spaceIndex+1);
} else {
cmd = message.substr(1);
target = '';
}
} else if (message.substr(0,1) === '!') {
var spaceIndex = message.indexOf(' ');
if (spaceIndex > 0) {
cmd = message.substr(0, spaceIndex);
target = message.substr(spaceIndex+1);
} else {
cmd = message;
target = '';
}
}
cmd = cmd.toLowerCase();
var broadcast = false;
if (cmd.charAt(0) === '!') {
broadcast = true;
cmd = cmd.substr(1);
}
var commandHandler = commands[cmd];
if (typeof commandHandler === 'string') {
// in case someone messed up, don't loop
commandHandler = commands[commandHandler];
}
if (commandHandler) {
var context = {
sendReply: function(data) {
if (this.broadcasting) {
room.add(data, true);
} else {
connection.sendTo(room, data);
}
},
sendReplyBox: function(html) {
this.sendReply('|raw|<div class="infobox">'+html+'</div>');
},
popupReply: function(message) {
connection.popup(message);
},
add: function(data) {
room.add(data, true);
},
send: function(data) {
room.send(data);
},
privateModCommand: function(data) {
for (var i in room.users) {
if (room.users[i].isStaff) {
room.users[i].sendTo(room, data);
}
}
this.logEntry(data);
this.logModCommand(data);
},
logEntry: function(data) {
room.logEntry(data);
},
addModCommand: function(text, logOnlyText) {
this.add(text);
this.logModCommand(text+(logOnlyText||''));
},
logModCommand: function(result) {
modlog.write('['+(new Date().toJSON())+'] ('+room.id+') '+result+'\n');
},
can: function(permission, target, room) {
if (!user.can(permission, target, room)) {
this.sendReply('/'+cmd+' - Access denied.');
return false;
}
return true;
},
canBroadcast: function() {
if (broadcast) {
message = this.canTalk(message);
if (!message) return false;
if (!user.can('broadcast', null, room)) {
connection.sendTo(room, "You need to be voiced to broadcast this command's information.");
connection.sendTo(room, "To see it for yourself, use: /"+message.substr(1));
return false;
}
// broadcast cooldown
var normalized = toId(message);
if (room.lastBroadcast === normalized &&
room.lastBroadcastTime >= Date.now() - BROADCAST_COOLDOWN) {
connection.sendTo(room, "You can't broadcast this because it was just broadcast.")
return false;
}
this.add('|c|'+user.getIdentity(room.id)+'|'+message);
room.lastBroadcast = normalized;
room.lastBroadcastTime = Date.now();
this.broadcasting = true;
}
return true;
},
parse: function(message) {
if (levelsDeep > MAX_PARSE_RECURSION) {
return this.sendReply("Error: Too much recursion");
}
return parse(message, room, user, connection, levelsDeep+1);
},
canTalk: function(message, relevantRoom) {
var innerRoom = (relevantRoom !== undefined) ? relevantRoom : room;
return canTalk(user, innerRoom, connection, message);
},
targetUserOrSelf: function(target) {
if (!target) return user;
this.splitTarget(target);
return this.targetUser;
},
splitTarget: splitTarget
};
var result = commandHandler.call(context, target, room, user, connection, cmd, message);
if (result === undefined) result = false;
return result;
} else {
// Check for mod/demod/admin/deadmin/etc depending on the group ids
for (var g in config.groups) {
if (cmd === config.groups[g].id) {
return parse('/promote ' + toUserid(target) + ',' + g, room, user, connection);
} else if (cmd === 'de' + config.groups[g].id || cmd === 'un' + config.groups[g].id) {
var nextGroup = config.groupsranking[config.groupsranking.indexOf(g) - 1];
if (!nextGroup) nextGroup = config.groupsranking[0];
return parse('/demote ' + toUserid(target) + ',' + nextGroup, room, user, connection);
}
}
if (message.substr(0,1) === '/' && cmd) {
// To guard against command typos, we now emit an error message
return connection.sendTo(room.id, 'The command "/'+cmd+'" was unrecognized. To send a message starting with "/'+cmd+'", type "//'+cmd+'".');
}
}
message = canTalk(user, room, connection, message);
if (!message) return false;
return message;
};
function splitTarget(target, exactName) {
var commaIndex = target.indexOf(',');
if (commaIndex < 0) {
targetUser = Users.get(target, exactName)
this.targetUser = targetUser;
this.targetUsername = (targetUser?targetUser.name:target);
return '';
}
var targetUser = Users.get(target.substr(0, commaIndex), exactName);
if (!targetUser) {
targetUser = null;
}
this.targetUser = targetUser;
this.targetUsername = (targetUser?targetUser.name:target.substr(0, commaIndex));
return target.substr(commaIndex+1).trim();
}
/**
* Can this user talk?
* Shows an error message if not.
*/
function canTalk(user, room, connection, message) {
if (!user.named) {
connection.popup("You must choose a name before you can talk.");
return false;
}
if (room && user.locked) {
connection.sendTo(room, 'You are locked from talking in chat.');
return false;
}
if (room && user.mutedRooms[room.id]) {
connection.sendTo(room, 'You are muted and cannot talk in this room.');
return false;
}
if (room && room.modchat) {
if (room.modchat === 'crash') {
if (!user.can('ignorelimits')) {
connection.sendTo(room, 'Because the server has crashed, you cannot speak in lobby chat.');
return false;
}
} else {
var userGroup = user.group;
if (room.auth) {
if (room.auth[user.userid]) {
userGroup = room.auth[user.userid];
} else if (userGroup !== ' ') {
userGroup = '+';
}
}
if (!user.autoconfirmed && user.group === ' ' && room.modchat === 'autoconfirmed') {
connection.sendTo(room, 'Because moderated chat is set, your account must be at least one week old and you must have won at least one ladder game to speak in this chat.');
return false;
} else if (config.groupsranking.indexOf(userGroup) < config.groupsranking.indexOf(room.modchat)) {
var groupName = config.groups[room.modchat].name;
if (!groupName) groupName = room.modchat;
connection.sendTo(room, 'Because moderated chat is set, you must be of rank ' + groupName +' or higher to speak in lobby chat.');
return false;
}
}
}
if (room && !(user.userid in room.users)) {
connection.popup("You can't send a message to this room without being in it.");
return false;
}
if (typeof message === 'string') {
if (!message) {
connection.popup("Your message can't be blank.");
return false;
}
if (message.length > MAX_MESSAGE_LENGTH && !user.can('ignorelimits')) {
connection.popup("Your message is too long:\n\n"+message);
return false;
}
// hardcoded low quality website
if (/\bnimp\.org\b/i.test(message)) return false;
// remove zalgo
message = message.replace(/[\u0300-\u036f\u0E2F-\u0E4F]{3,}/g,'');
if (room && room.id === 'lobby') {
var normalized = message.trim();
if ((normalized === user.lastMessage) &&
((Date.now() - user.lastMessageTime) < MESSAGE_COOLDOWN)) {
connection.popup("You can't send the same message again so soon.");
return false;
}
user.lastMessage = message;
user.lastMessageTime = Date.now();
}
if (config.chatfilter) {
return config.chatfilter(user, room, connection.socket, message);
}
return message;
}
return true;
}
exports.package = {};
fs.readFile('package.json', function(err, data) {
if (err) return;
exports.package = JSON.parse(data);
});
exports.uncacheTree = function(root) {
var uncache = [require.resolve(root)];
do {
var newuncache = [];
for (var i = 0; i < uncache.length; ++i) {
if (require.cache[uncache[i]]) {
newuncache.push.apply(newuncache,
require.cache[uncache[i]].children.map(function(module) {
return module.filename;
})
);
delete require.cache[uncache[i]];
}
}
uncache = newuncache;
} while (uncache.length > 0);
};
// This function uses synchronous IO in order to keep it relatively simple.
// The function takes about 0.023 seconds to run on one tested computer,
// which is acceptable considering how long the server takes to start up
// anyway (several seconds).
exports.computeServerVersion = function() {
/**
* `filelist.txt` is a list of all the files in this project. It is used
* for computing a checksum of the project for the /version command. This
* information cannot be determined at runtime because the user may not be
* using a git repository (for example, the user may have downloaded an
* archive of the files).
*
* `filelist.txt` is generated by running `git ls-files > filelist.txt`.
*/
var filenames;
try {
var data = fs.readFileSync('filelist.txt', {encoding: 'utf8'});
filenames = data.split('\n');
} catch (e) {
return 0;
}
var hash = crypto.createHash('md5');
for (var i = 0; i < filenames.length; ++i) {
try {
hash.update(fs.readFileSync(filenames[i]));
} catch (e) {}
}
return hash.digest('hex');
};
exports.serverVersion = exports.computeServerVersion();
/*********************************************************
* Commands
*********************************************************/
var commands = exports.commands = require('./commands.js').commands;
var customCommands = require('./config/commands.js');
if (customCommands && customCommands.commands) {
Object.merge(commands, customCommands.commands);
}