mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-09 04:23:45 -05:00
1159 lines
37 KiB
JavaScript
1159 lines
37 KiB
JavaScript
/**
|
|
* System commands
|
|
* Pokemon Showdown - http://pokemonshowdown.com/
|
|
*
|
|
* These are system commands - commands required for Pokemon Showdown
|
|
* to run. A lot of these are sent by the client.
|
|
*
|
|
* If you'd like to modify commands, please go to config/commands.js,
|
|
* which also teaches you how to use commands.
|
|
*
|
|
* @license MIT license
|
|
*/
|
|
|
|
var crypto = require('crypto');
|
|
|
|
var commands = exports.commands = {
|
|
|
|
version: function(target, room, user) {
|
|
if (!this.canBroadcast()) return;
|
|
this.sendReplyBox('Server version: <b>'+CommandParser.package.version+'</b> <small>(<a href="http://pokemonshowdown.com/versions#' + CommandParser.serverVersion + '" target="_blank">' + CommandParser.serverVersion.substr(0,10) + '</a>)</small>');
|
|
},
|
|
|
|
me: function(target, room, user, connection) {
|
|
target = this.canTalk(target);
|
|
if (!target) return;
|
|
|
|
return '/me ' + target;
|
|
},
|
|
|
|
mee: function(target, room, user, connection) {
|
|
target = this.canTalk(target);
|
|
if (!target) return;
|
|
|
|
return '/mee ' + target;
|
|
},
|
|
|
|
avatar: function(target, room, user) {
|
|
if (!target) return this.parse('/avatars');
|
|
var parts = target.split(',');
|
|
var avatar = parseInt(parts[0]);
|
|
if (!avatar || avatar > 294 || avatar < 1) {
|
|
if (!parts[1]) {
|
|
this.sendReply("Invalid avatar.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
user.avatar = avatar;
|
|
if (!parts[1]) {
|
|
this.sendReply("Avatar changed to:\n" +
|
|
'|raw|<img src="//play.pokemonshowdown.com/sprites/trainers/'+avatar+'.png" alt="" width="80" height="80" />');
|
|
}
|
|
},
|
|
|
|
logout: function(target, room, user) {
|
|
user.resetName();
|
|
},
|
|
|
|
r: 'reply',
|
|
reply: function(target, room, user) {
|
|
if (!target) return this.parse('/help reply');
|
|
if (!user.lastPM) {
|
|
return this.sendReply('No one has PMed you yet.');
|
|
}
|
|
return this.parse('/msg '+(user.lastPM||'')+', '+target);
|
|
},
|
|
|
|
pm: 'msg',
|
|
whisper: 'msg',
|
|
w: 'msg',
|
|
msg: function(target, room, user) {
|
|
if (!target) return this.parse('/help msg');
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!target) {
|
|
this.sendReply('You forgot the comma.');
|
|
return this.parse('/help msg');
|
|
}
|
|
if (!targetUser || !targetUser.connected) {
|
|
if (!target) {
|
|
this.sendReply('User '+this.targetUsername+' not found. Did you forget a comma?');
|
|
} else {
|
|
this.sendReply('User '+this.targetUsername+' not found. Did you misspell their name?');
|
|
}
|
|
return this.parse('/help msg');
|
|
}
|
|
|
|
if (user.locked && !targetUser.can('lock', user)) {
|
|
return this.popupReply('You can only private message members of the moderation team (users marked by %, @, &, or ~) when locked.');
|
|
}
|
|
if (targetUser.locked && !user.can('lock', targetUser)) {
|
|
return this.popupReply('This user is locked and cannot PM.');
|
|
}
|
|
|
|
target = this.canTalk(target, null);
|
|
if (!target) return false;
|
|
|
|
var message = '|pm|'+user.getIdentity()+'|'+targetUser.getIdentity()+'|'+target;
|
|
user.send(message);
|
|
if (targetUser !== user) targetUser.send(message);
|
|
targetUser.lastPM = user.userid;
|
|
user.lastPM = targetUser.userid;
|
|
},
|
|
|
|
makechatroom: function(target, room, user) {
|
|
if (!this.can('makeroom')) return;
|
|
var id = toId(target);
|
|
if (Rooms.rooms[id]) {
|
|
return this.sendReply("The room '"+target+"' already exists.");
|
|
}
|
|
Rooms.rooms[id] = new Rooms.ChatRoom(id, target);
|
|
return this.sendReply("The room '"+target+"' was created.");
|
|
},
|
|
|
|
privateroom: function(target, room, user) {
|
|
if (!this.can('makeroom')) return;
|
|
if (target === 'off') {
|
|
room.isPrivate = false;
|
|
this.addModCommand(user.name+' made the room public.');
|
|
} else {
|
|
room.isPrivate = true;
|
|
this.addModCommand(user.name+' made the room private.');
|
|
}
|
|
},
|
|
|
|
join: function(target, room, user, connection) {
|
|
var targetRoom = Rooms.get(target);
|
|
if (target && !targetRoom) {
|
|
return connection.sendTo(target, "|noinit|nonexistent|The room '"+target+"' does not exist.");
|
|
}
|
|
if (targetRoom && !targetRoom.battle && targetRoom !== Rooms.lobby && !user.named) {
|
|
return connection.sendTo(target, "|noinit|namerequired|You must have a name in order to join the room '"+target+"'.");
|
|
}
|
|
if (!user.joinRoom(targetRoom || room, connection)) {
|
|
// This condition appears to be impossible for now.
|
|
return connection.sendTo(target, "|noinit|joinfailed|The room '"+target+"' could not be joined.");
|
|
}
|
|
},
|
|
|
|
leave: 'part',
|
|
part: function(target, room, user, connection) {
|
|
if (room.id === 'global') return false;
|
|
var targetRoom = Rooms.get(target);
|
|
if (target && !targetRoom) {
|
|
return this.sendReply("The room '"+target+"' does not exist.");
|
|
}
|
|
user.leaveRoom(targetRoom || room, connection);
|
|
},
|
|
|
|
/*********************************************************
|
|
* Moderating: Punishments
|
|
*********************************************************/
|
|
|
|
kick: 'warn',
|
|
k: 'warn',
|
|
warn: function(target, room, user) {
|
|
if (!target) return this.parse('/help warn');
|
|
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser || !targetUser.connected) {
|
|
return this.sendReply('User '+this.targetUsername+' not found.');
|
|
}
|
|
if (!this.can('warn', targetUser)) return false;
|
|
|
|
this.addModCommand(''+targetUser.name+' was warned by '+user.name+'.' + (target ? " (" + target + ")" : ""));
|
|
targetUser.send('|c|~|/warn '+target);
|
|
},
|
|
|
|
m: 'mute',
|
|
mute: function(target, room, user) {
|
|
if (!target) return this.parse('/help mute');
|
|
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser) {
|
|
return this.sendReply('User '+this.targetUsername+' not found.');
|
|
}
|
|
if (!this.can('mute', targetUser)) return false;
|
|
if (targetUser.mutedRooms[room.id] || targetUser.locked || !targetUser.connected) {
|
|
var problem = ' but was already '+(!targetUser.connected ? 'offline' : targetUser.locked ? 'locked' : 'muted');
|
|
if (!target) {
|
|
return this.privateModCommand('('+targetUser.name+' would be muted by '+user.name+problem+'.)');
|
|
}
|
|
return this.addModCommand(''+targetUser.name+' would be muted by '+user.name+problem+'.' + (target ? " (" + target + ")" : ""));
|
|
}
|
|
|
|
targetUser.popup(user.name+' has muted you for 7 minutes. '+target);
|
|
this.addModCommand(''+targetUser.name+' was muted by '+user.name+' for 7 minutes.' + (target ? " (" + target + ")" : ""));
|
|
var alts = targetUser.getAlts();
|
|
if (alts.length) this.addModCommand(""+targetUser.name+"'s alts were also muted: "+alts.join(", "));
|
|
|
|
targetUser.mute(room.id, 7*60*1000);
|
|
},
|
|
|
|
hourmute: function(target, room, user) {
|
|
if (!target) return this.parse('/help hourmute');
|
|
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser) {
|
|
return this.sendReply('User '+this.targetUsername+' not found.');
|
|
}
|
|
if (!this.can('mute', targetUser)) return false;
|
|
|
|
if (((targetUser.mutedRooms[room.id] && (targetUser.muteDuration[room.id]||0) >= 50*60*1000) || targetUser.locked) && !target) {
|
|
var problem = ' but was already '+(!targetUser.connected ? 'offline' : targetUser.locked ? 'locked' : 'muted');
|
|
return this.privateModCommand('('+targetUser.name+' would be muted by '+user.name+problem+'.)');
|
|
}
|
|
|
|
targetUser.popup(user.name+' has muted you for 60 minutes. '+target);
|
|
this.addModCommand(''+targetUser.name+' was muted by '+user.name+' for 60 minutes.' + (target ? " (" + target + ")" : ""));
|
|
var alts = targetUser.getAlts();
|
|
if (alts.length) this.addModCommand(""+targetUser.name+"'s alts were also muted: "+alts.join(", "));
|
|
|
|
targetUser.mute(room.id, 60*60*1000, true);
|
|
},
|
|
|
|
um: 'unmute',
|
|
unmute: function(target, room, user) {
|
|
if (!target) return this.parse('/help something');
|
|
var targetid = toUserid(target);
|
|
var targetUser = Users.get(target);
|
|
if (!targetUser) {
|
|
return this.sendReply('User '+target+' not found.');
|
|
}
|
|
if (!this.can('mute', targetUser)) return false;
|
|
|
|
if (!targetUser.mutedRooms[room.id]) {
|
|
return this.sendReply(''+targetUser.name+' isn\'t muted.');
|
|
}
|
|
|
|
this.addModCommand(''+targetUser.name+' was unmuted by '+user.name+'.');
|
|
|
|
targetUser.unmute(room.id);
|
|
},
|
|
|
|
ipmute: 'lock',
|
|
lock: function(target, room, user) {
|
|
if (!target) return this.parse('/help lock');
|
|
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser) {
|
|
return this.sendReply('User '+this.targetUser+' not found.');
|
|
}
|
|
if (!user.can('lock', targetUser)) {
|
|
return this.sendReply('/lock - Access denied.');
|
|
}
|
|
|
|
if ((targetUser.locked || Users.checkBanned(targetUser.latestIp)) && !target) {
|
|
var problem = ' but was already '+(targetUser.locked ? 'locked' : 'banned');
|
|
return this.privateModCommand('('+targetUser.name+' would be locked by '+user.name+problem+'.)');
|
|
}
|
|
|
|
targetUser.popup(user.name+' has locked you from talking in chats, battles, and PMing regular users.\n\n'+target+'\n\nIf you feel that your lock was unjustified, you can still PM staff members (%, @, &, and ~) to discuss it.');
|
|
|
|
this.addModCommand(""+targetUser.name+" was locked from talking by "+user.name+"." + (target ? " (" + target + ")" : ""));
|
|
var alts = targetUser.getAlts();
|
|
if (alts.length) this.addModCommand(""+targetUser.name+"'s alts were also locked: "+alts.join(", "));
|
|
|
|
targetUser.lock();
|
|
},
|
|
|
|
unlock: function(target, room, user) {
|
|
if (!target) return this.parse('/help unlock');
|
|
if (!this.can('lock')) return false;
|
|
|
|
var unlocked = Users.unlock(target);
|
|
|
|
if (unlocked) {
|
|
var names = Object.keys(unlocked);
|
|
this.addModCommand('' + names.join(', ') + ' ' +
|
|
((names.length > 1) ? 'were' : 'was') +
|
|
' unlocked by ' + user.name + '.');
|
|
} else {
|
|
this.sendReply('User '+target+' is not locked.');
|
|
}
|
|
},
|
|
|
|
b: 'ban',
|
|
ban: function(target, room, user) {
|
|
if (!target) return this.parse('/help ban');
|
|
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser) {
|
|
return this.sendReply('User '+this.targetUsername+' not found.');
|
|
}
|
|
if (!this.can('ban', targetUser)) return false;
|
|
|
|
if (Users.checkBanned(targetUser.latestIp) && !target) {
|
|
var problem = ' but was already banned';
|
|
return this.privateModCommand('('+targetUser.name+' would be banned by '+user.name+problem+'.)');
|
|
}
|
|
|
|
targetUser.popup(user.name+" has banned you." + (config.appealurl ? (" If you feel that your banning was unjustified you can appeal the ban:\n" + config.appealurl) : "") + "\n\n"+target);
|
|
|
|
this.addModCommand(""+targetUser.name+" was banned by "+user.name+"." + (target ? " (" + target + ")" : ""));
|
|
var alts = targetUser.getAlts();
|
|
if (alts.length) {
|
|
this.addModCommand(""+targetUser.name+"'s alts were also banned: "+alts.join(", "));
|
|
for (var i = 0; i < alts.length; ++i) {
|
|
this.add('|unlink|' + toId(alts[i]));
|
|
}
|
|
}
|
|
|
|
this.add('|unlink|' + targetUser.userid);
|
|
targetUser.ban();
|
|
},
|
|
|
|
unban: function(target, room, user) {
|
|
if (!target) return this.parse('/help unban');
|
|
if (!user.can('ban')) {
|
|
return this.sendReply('/unban - Access denied.');
|
|
}
|
|
|
|
var name = Users.unban(target);
|
|
|
|
if (name) {
|
|
this.addModCommand(''+name+' was unbanned by '+user.name+'.');
|
|
} else {
|
|
this.sendReply('User '+target+' is not banned.');
|
|
}
|
|
},
|
|
|
|
unbanall: function(target, room, user) {
|
|
if (!user.can('ban')) {
|
|
return this.sendReply('/unbanall - Access denied.');
|
|
}
|
|
// we have to do this the hard way since it's no longer a global
|
|
for (var i in Users.bannedIps) {
|
|
delete Users.bannedIps[i];
|
|
}
|
|
for (var i in Users.lockedIps) {
|
|
delete Users.lockedIps[i];
|
|
}
|
|
this.addModCommand('All bans and locks have been lifted by '+user.name+'.');
|
|
},
|
|
|
|
banip: function(target, room, user) {
|
|
target = target.trim();
|
|
if (!target) {
|
|
return this.parse('/help banip');
|
|
}
|
|
if (!this.can('rangeban')) return false;
|
|
|
|
Users.bannedIps[target] = '#ipban';
|
|
this.addModCommand(user.name+' temporarily banned the '+(target.charAt(target.length-1)==='*'?'IP range':'IP')+': '+target);
|
|
},
|
|
|
|
unbanip: function(target, room, user) {
|
|
target = target.trim();
|
|
if (!target) {
|
|
return this.parse('/help unbanip');
|
|
}
|
|
if (!this.can('rangeban')) return false;
|
|
if (!Users.bannedIps[target]) {
|
|
return this.sendReply(''+target+' is not a banned IP or IP range.');
|
|
}
|
|
delete Users.bannedIps[target];
|
|
this.addModCommand(user.name+' unbanned the '+(target.charAt(target.length-1)==='*'?'IP range':'IP')+': '+target);
|
|
},
|
|
|
|
/*********************************************************
|
|
* Moderating: Other
|
|
*********************************************************/
|
|
|
|
demote: 'promote',
|
|
promote: function(target, room, user, connection, cmd) {
|
|
if (!target) return this.parse('/help promote');
|
|
var target = this.splitTarget(target, true);
|
|
var targetUser = this.targetUser;
|
|
var userid = toUserid(this.targetUsername);
|
|
var name = targetUser ? targetUser.name : this.targetUsername;
|
|
|
|
var currentGroup = ' ';
|
|
if (targetUser) {
|
|
currentGroup = targetUser.group;
|
|
} else if (Users.usergroups[userid]) {
|
|
currentGroup = Users.usergroups[userid].substr(0,1);
|
|
}
|
|
|
|
var nextGroup = target ? target : Users.getNextGroupSymbol(currentGroup, cmd === 'demote');
|
|
if (target === 'deauth') nextGroup = config.groupsranking[0];
|
|
if (!config.groups[nextGroup]) {
|
|
return this.sendReply('Group \'' + nextGroup + '\' does not exist.');
|
|
}
|
|
if (!user.checkPromotePermission(currentGroup, nextGroup)) {
|
|
return this.sendReply('/promote - Access denied.');
|
|
}
|
|
|
|
var isDemotion = (config.groups[nextGroup].rank < config.groups[currentGroup].rank);
|
|
if (!Users.setOfflineGroup(name, nextGroup)) {
|
|
return this.sendReply('/promote - WARNING: This user is offline and could be unregistered. Use /forcepromote if you\'re sure you want to risk it.');
|
|
}
|
|
var groupName = (config.groups[nextGroup].name || nextGroup || '').trim() || 'a regular user';
|
|
if (isDemotion) {
|
|
this.privateModCommand('('+name+' was demoted to ' + groupName + ' by '+user.name+'.)');
|
|
if (targetUser) {
|
|
targetUser.popup('You were demoted to ' + groupName + ' by ' + user.name + '.');
|
|
}
|
|
} else {
|
|
this.addModCommand(''+name+' was promoted to ' + groupName + ' by '+user.name+'.');
|
|
}
|
|
if (targetUser) {
|
|
targetUser.updateIdentity();
|
|
}
|
|
},
|
|
|
|
forcepromote: function(target, room, user) {
|
|
// warning: never document this command in /help
|
|
if (!this.can('forcepromote')) return false;
|
|
var target = this.splitTarget(target, true);
|
|
var name = this.targetUsername;
|
|
var nextGroup = target ? target : Users.getNextGroupSymbol(' ', false);
|
|
|
|
if (!Users.setOfflineGroup(name, nextGroup, true)) {
|
|
return this.sendReply('/forcepromote - Don\'t forcepromote unless you have to.');
|
|
}
|
|
var groupName = config.groups[nextGroup].name || nextGroup || '';
|
|
this.addModCommand(''+name+' was promoted to ' + (groupName.trim()) + ' by '+user.name+'.');
|
|
},
|
|
|
|
deauth: function(target, room, user) {
|
|
return this.parse('/demote '+target+', deauth');
|
|
},
|
|
|
|
modchat: function(target, room, user) {
|
|
if (!target) {
|
|
return this.sendReply('Moderated chat is currently set to: '+config.modchat);
|
|
}
|
|
if (!this.can('modchat') || !this.canTalk()) return false;
|
|
|
|
target = target.toLowerCase();
|
|
switch (target) {
|
|
case 'on':
|
|
case 'true':
|
|
case 'yes':
|
|
case 'registered':
|
|
this.sendReply("Modchat registered has been removed.");
|
|
this.sendReply("If you're dealing with a spammer, make sure to run /loadbanlist.");
|
|
return false;
|
|
break;
|
|
case 'off':
|
|
case 'false':
|
|
case 'no':
|
|
config.modchat = false;
|
|
break;
|
|
default:
|
|
if (!config.groups[target]) {
|
|
return this.parse('/help modchat');
|
|
}
|
|
if (config.groupsranking.indexOf(target) > 1 && !user.can('modchatall')) {
|
|
return this.sendReply('/modchat - Access denied for setting higher than ' + config.groupsranking[1] + '.');
|
|
}
|
|
config.modchat = target;
|
|
break;
|
|
}
|
|
if (config.modchat === true) {
|
|
this.add('|raw|<div class="broadcast-red"><b>Moderated chat was enabled!</b><br />Only registered users can talk.</div>');
|
|
} else if (!config.modchat) {
|
|
this.add('|raw|<div class="broadcast-blue"><b>Moderated chat was disabled!</b><br />Anyone may talk now.</div>');
|
|
} else {
|
|
var modchat = sanitize(config.modchat);
|
|
this.add('|raw|<div class="broadcast-red"><b>Moderated chat was set to '+modchat+'!</b><br />Only users of rank '+modchat+' and higher can talk.</div>');
|
|
}
|
|
this.logModCommand(user.name+' set modchat to '+config.modchat);
|
|
},
|
|
|
|
declare: function(target, room, user) {
|
|
if (!target) return this.parse('/help declare');
|
|
if (!this.can('declare')) return false;
|
|
|
|
if (!this.canTalk()) return;
|
|
|
|
this.add('|raw|<div class="broadcast-blue"><b>'+target+'</b></div>');
|
|
this.logModCommand(user.name+' declared '+target);
|
|
},
|
|
|
|
wall: 'announce',
|
|
announce: function(target, room, user) {
|
|
if (!target) return this.parse('/help announce');
|
|
if (!this.can('announce')) return false;
|
|
|
|
target = this.canTalk(target);
|
|
if (!target) return;
|
|
|
|
return '/announce '+target;
|
|
},
|
|
|
|
fr: 'forcerename',
|
|
forcerename: function(target, room, user) {
|
|
if (!target) return this.parse('/help forcerename');
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser) {
|
|
return this.sendReply('User '+this.targetUsername+' not found.');
|
|
}
|
|
if (!this.can('forcerename', targetUser)) return false;
|
|
|
|
if (targetUser.userid === toUserid(this.targetUser)) {
|
|
var entry = ''+targetUser.name+' was forced to choose a new name by '+user.name+'.' + (target ? " (" + target + ")" : "");
|
|
this.logModCommand(entry);
|
|
Rooms.lobby.sendAuth('(' + entry + ')');
|
|
if (room.id !== 'lobby') {
|
|
this.add(entry);
|
|
} else {
|
|
this.logEntry(entry);
|
|
}
|
|
targetUser.resetName();
|
|
targetUser.send('|nametaken||'+user.name+" has forced you to change your name. "+target);
|
|
} else {
|
|
this.sendReply("User "+targetUser.name+" is no longer using that name.");
|
|
}
|
|
},
|
|
|
|
frt: 'forcerenameto',
|
|
forcerenameto: function(target, room, user) {
|
|
if (!target) return this.parse('/help forcerenameto');
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser) {
|
|
return this.sendReply('User '+this.targetUsername+' not found.');
|
|
}
|
|
if (!target) {
|
|
return this.sendReply('No new name was specified.');
|
|
}
|
|
if (!this.can('forcerenameto', targetUser)) return false;
|
|
|
|
if (targetUser.userid === toUserid(this.targetUser)) {
|
|
var entry = ''+targetUser.name+' was forcibly renamed to '+target+' by '+user.name+'.';
|
|
this.logModCommand(entry);
|
|
Rooms.lobby.sendAuth('(' + entry + ')');
|
|
if (room.id !== 'lobby') {
|
|
room.add(entry);
|
|
} else {
|
|
room.logEntry(entry);
|
|
}
|
|
targetUser.forceRename(target, undefined, true);
|
|
} else {
|
|
this.sendReply("User "+targetUser.name+" is no longer using that name.");
|
|
}
|
|
},
|
|
|
|
modlog: function(target, room, user, connection) {
|
|
if (!this.can('modlog')) return false;
|
|
var lines = 0;
|
|
if (!target.match('[^0-9]')) {
|
|
lines = parseInt(target || 15, 10);
|
|
if (lines > 100) lines = 100;
|
|
}
|
|
var filename = 'logs/modlog.txt';
|
|
var command = 'tail -'+lines+' '+filename;
|
|
var grepLimit = 100;
|
|
if (!lines || lines < 0) { // searching for a word instead
|
|
if (target.match(/^["'].+["']$/)) target = target.substring(1,target.length-1);
|
|
command = "awk '{print NR,$0}' "+filename+" | sort -nr | cut -d' ' -f2- | grep -m"+grepLimit+" -i '"+target.replace(/\\/g,'\\\\\\\\').replace(/["'`]/g,'\'\\$&\'').replace(/[\{\}\[\]\(\)\$\^\.\?\+\-\*]/g,'[$&]')+"'";
|
|
}
|
|
|
|
require('child_process').exec(command, function(error, stdout, stderr) {
|
|
if (error && stderr) {
|
|
connection.popup('/modlog erred - modlog does not support Windows');
|
|
console.log('/modlog error: '+error);
|
|
return false;
|
|
}
|
|
if (lines) {
|
|
if (!stdout) {
|
|
connection.popup('The modlog is empty. (Weird.)');
|
|
} else {
|
|
connection.popup('Displaying the last '+lines+' lines of the Moderator Log:\n\n'+stdout);
|
|
}
|
|
} else {
|
|
if (!stdout) {
|
|
connection.popup('No moderator actions containing "'+target+'" were found.');
|
|
} else {
|
|
connection.popup('Displaying the last '+grepLimit+' logged actions containing "'+target+'":\n\n'+stdout);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
bw: 'banword',
|
|
banword: function(target, room, user) {
|
|
if (!this.can('declare')) return false;
|
|
target = toId(target);
|
|
if (!target) {
|
|
return this.sendReply('Specify a word or phrase to ban.');
|
|
}
|
|
Users.addBannedWord(target);
|
|
this.sendReply('Added \"'+target+'\" to the list of banned words.');
|
|
},
|
|
|
|
ubw: 'unbanword',
|
|
unbanword: function(target, room, user) {
|
|
if (!this.can('declare')) return false;
|
|
target = toId(target);
|
|
if (!target) {
|
|
return this.sendReply('Specify a word or phrase to unban.');
|
|
}
|
|
Users.removeBannedWord(target);
|
|
this.sendReply('Removed \"'+target+'\" from the list of banned words.');
|
|
},
|
|
|
|
/*********************************************************
|
|
* Server management commands
|
|
*********************************************************/
|
|
|
|
hotpatch: function(target, room, user) {
|
|
if (!target) return this.parse('/help hotpatch');
|
|
if (!this.can('hotpatch')) return false;
|
|
|
|
Rooms.lobby.logEntry(user.name + ' used /hotpatch ' + target);
|
|
|
|
if (target === 'chat') {
|
|
|
|
CommandParser.uncacheTree('./command-parser.js');
|
|
CommandParser = require('./command-parser.js');
|
|
return this.sendReply('Chat commands have been hot-patched.');
|
|
|
|
} else if (target === 'battles') {
|
|
|
|
Simulator.SimulatorProcess.respawn();
|
|
return this.sendReply('Battles have been hotpatched. Any battles started after now will use the new code; however, in-progress battles will continue to use the old code.');
|
|
|
|
} else if (target === 'formats') {
|
|
|
|
// uncache the tools.js dependency tree
|
|
CommandParser.uncacheTree('./tools.js');
|
|
// reload tools.js
|
|
Data = {};
|
|
Tools = require('./tools.js'); // note: this will lock up the server for a few seconds
|
|
// rebuild the formats list
|
|
Rooms.global.formatListText = Rooms.global.getFormatListText();
|
|
// respawn simulator processes
|
|
Simulator.SimulatorProcess.respawn();
|
|
// broadcast the new formats list to clients
|
|
Rooms.global.send(Rooms.global.formatListText);
|
|
|
|
return this.sendReply('Formats have been hotpatched.');
|
|
|
|
}
|
|
this.sendReply('Your hot-patch command was unrecognized.');
|
|
},
|
|
|
|
savelearnsets: function(target, room, user) {
|
|
if (this.can('hotpatch')) return false;
|
|
fs.writeFile('data/learnsets.js', 'exports.BattleLearnsets = '+JSON.stringify(BattleLearnsets)+";\n");
|
|
this.sendReply('learnsets.js saved.');
|
|
},
|
|
|
|
disableladder: function(target, room, user) {
|
|
if (!this.can('disableladder')) return false;
|
|
if (LoginServer.disabled) {
|
|
return this.sendReply('/disableladder - Ladder is already disabled.');
|
|
}
|
|
LoginServer.disabled = true;
|
|
this.logModCommand('The ladder was disabled by ' + user.name + '.');
|
|
this.add('|raw|<div class="broadcast-red"><b>Due to high server load, the ladder has been temporarily disabled</b><br />Rated games will no longer update the ladder. It will be back momentarily.</div>');
|
|
},
|
|
|
|
enableladder: function(target, room, user) {
|
|
if (!this.can('disableladder')) return false;
|
|
if (!LoginServer.disabled) {
|
|
return this.sendReply('/enable - Ladder is already enabled.');
|
|
}
|
|
LoginServer.disabled = false;
|
|
this.logModCommand('The ladder was enabled by ' + user.name + '.');
|
|
this.add('|raw|<div class="broadcast-green"><b>The ladder is now back.</b><br />Rated games will update the ladder now.</div>');
|
|
},
|
|
|
|
lockdown: function(target, room, user) {
|
|
if (!this.can('lockdown')) return false;
|
|
|
|
lockdown = true;
|
|
for (var id in Rooms.rooms) {
|
|
if (id !== 'global') Rooms.rooms[id].addRaw('<div class="broadcast-red"><b>The server is restarting soon.</b><br />Please finish your battles quickly. No new battles can be started until the server resets in a few minutes.</div>');
|
|
if (Rooms.rooms[id].requestKickInactive) Rooms.rooms[id].requestKickInactive(user, true);
|
|
}
|
|
|
|
Rooms.lobby.logEntry(user.name + ' used /lockdown');
|
|
|
|
},
|
|
|
|
endlockdown: function(target, room, user) {
|
|
if (!this.can('lockdown')) return false;
|
|
|
|
lockdown = false;
|
|
for (var id in Rooms.rooms) {
|
|
if (id !== 'global') Rooms.rooms[id].addRaw('<div class="broadcast-green"><b>The server shutdown was canceled.</b></div>');
|
|
}
|
|
|
|
Rooms.lobby.logEntry(user.name + ' used /endlockdown');
|
|
|
|
},
|
|
|
|
kill: function(target, room, user) {
|
|
if (!this.can('lockdown')) return false;
|
|
|
|
if (!lockdown) {
|
|
return this.sendReply('For safety reasons, /kill can only be used during lockdown.');
|
|
}
|
|
|
|
if (CommandParser.updateServerLock) {
|
|
return this.sendReply('Wait for /updateserver to finish before using /kill.');
|
|
}
|
|
|
|
Rooms.lobby.destroyLog(function() {
|
|
Rooms.lobby.logEntry(user.name + ' used /kill');
|
|
}, function() {
|
|
process.exit();
|
|
});
|
|
|
|
// Just in the case the above never terminates, kill the process
|
|
// after 10 seconds.
|
|
setTimeout(function() {
|
|
process.exit();
|
|
}, 10000);
|
|
},
|
|
|
|
loadbanlist: function(target, room, user, connection) {
|
|
if (!this.can('modchat')) return false;
|
|
|
|
connection.sendTo(room, 'Loading ipbans.txt...');
|
|
fs.readFile('config/ipbans.txt', function (err, data) {
|
|
if (err) return;
|
|
data = (''+data).split("\n");
|
|
var count = 0;
|
|
for (var i=0; i<data.length; i++) {
|
|
data[i] = data[i].split('#')[0].trim();
|
|
if (data[i] && !Users.bannedIps[data[i]]) {
|
|
Users.bannedIps[data[i]] = '#ipban';
|
|
count++;
|
|
}
|
|
}
|
|
if (!count) {
|
|
connection.sendTo(room, 'No IPs were banned; ipbans.txt has not been updated since the last time /loadbanlist was called.');
|
|
} else {
|
|
connection.sendTo(room, ''+count+' IPs were loaded from ipbans.txt and banned.');
|
|
}
|
|
});
|
|
},
|
|
|
|
refreshpage: function(target, room, user) {
|
|
if (!this.can('hotpatch')) return false;
|
|
Rooms.lobby.send('|refresh|');
|
|
Rooms.lobby.logEntry(user.name + ' used /refreshpage');
|
|
},
|
|
|
|
updateserver: function(target, room, user, connection) {
|
|
if (!user.checkConsolePermission(connection)) {
|
|
return this.sendReply('/updateserver - Access denied.');
|
|
}
|
|
|
|
if (CommandParser.updateServerLock) {
|
|
return this.sendReply('/updateserver - Another update is already in progress.');
|
|
}
|
|
|
|
CommandParser.updateServerLock = true;
|
|
|
|
var logQueue = [];
|
|
logQueue.push(user.name + ' used /updateserver');
|
|
|
|
connection.sendTo(room, 'updating...');
|
|
|
|
var exec = require('child_process').exec;
|
|
exec('git diff-index --quiet HEAD --', function(error) {
|
|
var cmd = 'git pull --rebase';
|
|
if (error) {
|
|
if (error.code === 1) {
|
|
// The working directory or index have local changes.
|
|
cmd = 'git stash;' + cmd + ';git stash pop';
|
|
} else {
|
|
// The most likely case here is that the user does not have
|
|
// `git` on the PATH (which would be error.code === 127).
|
|
connection.sendTo(room, '' + error);
|
|
logQueue.push('' + error);
|
|
logQueue.forEach(Rooms.lobby.logEntry.bind(Rooms.lobby));
|
|
CommandParser.updateServerLock = false;
|
|
return;
|
|
}
|
|
}
|
|
var entry = 'Running `' + cmd + '`';
|
|
connection.sendTo(room, entry);
|
|
logQueue.push(entry);
|
|
exec(cmd, function(error, stdout, stderr) {
|
|
('' + stdout + stderr).split('\n').forEach(function(s) {
|
|
connection.sendTo(room, s);
|
|
logQueue.push(s);
|
|
});
|
|
logQueue.forEach(Rooms.lobby.logEntry.bind(Rooms.lobby));
|
|
CommandParser.updateServerLock = false;
|
|
});
|
|
});
|
|
},
|
|
|
|
crashfixed: function(target, room, user) {
|
|
if (!lockdown) {
|
|
return this.sendReply('/crashfixed - There is no active crash.');
|
|
}
|
|
if (!this.can('hotpatch')) return false;
|
|
|
|
lockdown = false;
|
|
config.modchat = false;
|
|
Rooms.lobby.addRaw('<div class="broadcast-green"><b>We fixed the crash without restarting the server!</b><br />You may resume talking in the lobby and starting new battles.</div>');
|
|
Rooms.lobby.logEntry(user.name + ' used /crashfixed');
|
|
},
|
|
|
|
crashlogged: function(target, room, user) {
|
|
if (!lockdown) {
|
|
return this.sendReply('/crashlogged - There is no active crash.');
|
|
}
|
|
if (!this.can('declare')) return false;
|
|
|
|
lockdown = false;
|
|
config.modchat = false;
|
|
Rooms.lobby.addRaw('<div class="broadcast-green"><b>We have logged the crash and are working on fixing it!</b><br />You may resume talking in the lobby and starting new battles.</div>');
|
|
Rooms.lobby.logEntry(user.name + ' used /crashlogged');
|
|
},
|
|
|
|
eval: function(target, room, user, connection, cmd, message) {
|
|
if (!user.checkConsolePermission(connection)) {
|
|
return this.sendReply("/eval - Access denied.");
|
|
}
|
|
if (!this.canBroadcast()) return;
|
|
|
|
if (!this.broadcasting) this.sendReply('||>> '+target);
|
|
try {
|
|
var battle = room.battle;
|
|
var me = user;
|
|
this.sendReply('||<< '+eval(target));
|
|
} catch (e) {
|
|
this.sendReply('||<< error: '+e.message);
|
|
var stack = '||'+(''+e.stack).replace(/\n/g,'\n||');
|
|
connection.sendTo(room, stack);
|
|
}
|
|
},
|
|
|
|
evalbattle: function(target, room, user, connection, cmd, message) {
|
|
if (!user.checkConsolePermission(connection)) {
|
|
return this.sendReply("/evalbattle - Access denied.");
|
|
}
|
|
if (!this.canBroadcast()) return;
|
|
if (!room.battle) {
|
|
return this.sendReply("/evalbattle - This isn't a battle room.");
|
|
}
|
|
|
|
room.battle.send('eval', target.replace(/\n/g, '\f'));
|
|
},
|
|
|
|
/*********************************************************
|
|
* Battle commands
|
|
*********************************************************/
|
|
|
|
concede: 'forfeit',
|
|
surrender: 'forfeit',
|
|
forfeit: function(target, room, user) {
|
|
if (!room.battle) {
|
|
return this.sendReply("There's nothing to forfeit here.");
|
|
}
|
|
if (!room.forfeit(user)) {
|
|
return this.sendReply("You can't forfeit this battle.");
|
|
}
|
|
},
|
|
|
|
savereplay: function(target, room, user, connection) {
|
|
if (!room || !room.battle) return;
|
|
var logidx = 2; // spectator log (no exact HP)
|
|
if (room.battle.ended) {
|
|
// If the battle is finished when /savereplay is used, include
|
|
// exact HP in the replay log.
|
|
logidx = 3;
|
|
}
|
|
var data = room.getLog(logidx).join("\n");
|
|
var datahash = crypto.createHash('md5').update(data.replace(/[^(\x20-\x7F)]+/g,'')).digest('hex');
|
|
|
|
LoginServer.request('prepreplay', {
|
|
id: room.id.substr(7),
|
|
loghash: datahash,
|
|
p1: room.p1.name,
|
|
p2: room.p2.name,
|
|
format: room.format
|
|
}, function(success) {
|
|
connection.send('|queryresponse|savereplay|'+JSON.stringify({
|
|
log: data,
|
|
room: 'lobby',
|
|
id: room.id.substr(7)
|
|
}));
|
|
});
|
|
},
|
|
|
|
mv: 'move',
|
|
attack: 'move',
|
|
move: function(target, room, user) {
|
|
if (!room.decision) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
room.decision(user, 'choose', 'move '+target);
|
|
},
|
|
|
|
sw: 'switch',
|
|
switch: function(target, room, user) {
|
|
if (!room.decision) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
room.decision(user, 'choose', 'switch '+parseInt(target,10));
|
|
},
|
|
|
|
choose: function(target, room, user) {
|
|
if (!room.decision) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
room.decision(user, 'choose', target);
|
|
},
|
|
|
|
undo: function(target, room, user) {
|
|
if (!room.decision) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
room.decision(user, 'undo', target);
|
|
},
|
|
|
|
team: function(target, room, user) {
|
|
if (!room.decision) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
room.decision(user, 'choose', 'team '+target);
|
|
},
|
|
|
|
joinbattle: function(target, room, user) {
|
|
if (!room.joinBattle) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
room.joinBattle(user);
|
|
},
|
|
|
|
partbattle: 'leavebattle',
|
|
leavebattle: function(target, room, user) {
|
|
if (!room.leaveBattle) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
room.leaveBattle(user);
|
|
},
|
|
|
|
kickbattle: function(target, room, user) {
|
|
if (!room.leaveBattle) return this.sendReply('You can only do this in battle rooms.');
|
|
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser || !targetUser.connected) {
|
|
return this.sendReply('User '+this.targetUsername+' not found.');
|
|
}
|
|
if (!this.can('kick', targetUser)) return false;
|
|
|
|
if (room.leaveBattle(targetUser)) {
|
|
this.addModCommand(''+targetUser.name+' was kicked from a battle by '+user.name+'' + (target ? " (" + target + ")" : ""));
|
|
} else {
|
|
this.sendReply("/kickbattle - User isn\'t in battle.");
|
|
}
|
|
},
|
|
|
|
kickinactive: function(target, room, user) {
|
|
if (room.requestKickInactive) {
|
|
room.requestKickInactive(user);
|
|
} else {
|
|
this.sendReply('You can only kick inactive players from inside a room.');
|
|
}
|
|
},
|
|
|
|
timer: function(target, room, user) {
|
|
target = toId(target);
|
|
if (room.requestKickInactive) {
|
|
if (target === 'off' || target === 'stop') {
|
|
room.stopKickInactive(user, user.can('timer'));
|
|
} else if (target === 'on' || !target) {
|
|
room.requestKickInactive(user, user.can('timer'));
|
|
} else {
|
|
this.sendReply("'"+target+"' is not a recognized timer state.");
|
|
}
|
|
} else {
|
|
this.sendReply('You can only set the timer from inside a room.');
|
|
}
|
|
},
|
|
|
|
forcetie: 'forcewin',
|
|
forcewin: function(target, room, user) {
|
|
if (!this.can('forcewin')) return false;
|
|
if (!room.battle) {
|
|
this.sendReply('/forcewin - This is not a battle room.');
|
|
return false;
|
|
}
|
|
|
|
room.battle.endType = 'forced';
|
|
if (!target) {
|
|
room.battle.tie();
|
|
this.logModCommand(user.name+' forced a tie.');
|
|
return false;
|
|
}
|
|
target = Users.get(target);
|
|
if (target) target = target.userid;
|
|
else target = '';
|
|
|
|
if (target) {
|
|
room.battle.win(target);
|
|
this.logModCommand(user.name+' forced a win for '+target+'.');
|
|
}
|
|
|
|
},
|
|
|
|
/*********************************************************
|
|
* Challenging and searching commands
|
|
*********************************************************/
|
|
|
|
cancelsearch: 'search',
|
|
search: function(target, room, user) {
|
|
if (target) {
|
|
Rooms.global.searchBattle(user, target);
|
|
} else {
|
|
Rooms.global.cancelSearch(user);
|
|
}
|
|
},
|
|
|
|
chall: 'challenge',
|
|
challenge: function(target, room, user) {
|
|
target = this.splitTarget(target);
|
|
var targetUser = this.targetUser;
|
|
if (!targetUser || !targetUser.connected) {
|
|
return this.popupReply("The user '"+this.targetUsername+"' was not found.");
|
|
}
|
|
if (targetUser.blockChallenges && !user.can('bypassblocks', targetUser)) {
|
|
return this.popupReply("The user '"+this.targetUsername+"' is not accepting challenges right now.");
|
|
}
|
|
if (typeof target !== 'string') target = 'customgame';
|
|
var problems = Tools.validateTeam(user.team, target);
|
|
if (problems) {
|
|
return this.popupReply("Your team was rejected for the following reasons:\n\n- "+problems.join("\n- "));
|
|
}
|
|
user.makeChallenge(targetUser, target);
|
|
},
|
|
|
|
away: 'blockchallenges',
|
|
idle: 'blockchallenges',
|
|
blockchallenges: function(target, room, user) {
|
|
user.blockChallenges = true;
|
|
this.sendReply('You are now blocking all incoming challenge requests.');
|
|
},
|
|
|
|
back: 'allowchallenges',
|
|
allowchallenges: function(target, room, user) {
|
|
user.blockChallenges = false;
|
|
this.sendReply('You are available for challenges from now on.');
|
|
},
|
|
|
|
cchall: 'cancelChallenge',
|
|
cancelchallenge: function(target, room, user) {
|
|
user.cancelChallengeTo(target);
|
|
},
|
|
|
|
accept: function(target, room, user) {
|
|
var userid = toUserid(target);
|
|
var format = '';
|
|
if (user.challengesFrom[userid]) format = user.challengesFrom[userid].format;
|
|
if (!format) {
|
|
this.popupReply(target+" cancelled their challenge before you could accept it.");
|
|
return false;
|
|
}
|
|
var problems = Tools.validateTeam(user.team, format);
|
|
if (problems) {
|
|
this.popupReply("Your team was rejected for the following reasons:\n\n- "+problems.join("\n- "));
|
|
return false;
|
|
}
|
|
user.acceptChallengeFrom(userid);
|
|
},
|
|
|
|
reject: function(target, room, user) {
|
|
user.rejectChallengeFrom(toUserid(target));
|
|
},
|
|
|
|
saveteam: 'useteam',
|
|
utm: 'useteam',
|
|
useteam: function(target, room, user) {
|
|
try {
|
|
user.team = JSON.parse(target);
|
|
} catch (e) {
|
|
this.popupReply('Not a valid team.');
|
|
}
|
|
},
|
|
|
|
/*********************************************************
|
|
* Low-level
|
|
*********************************************************/
|
|
|
|
cmd: 'query',
|
|
query: function(target, room, user, connection) {
|
|
var spaceIndex = target.indexOf(' ');
|
|
var cmd = target;
|
|
if (spaceIndex > 0) {
|
|
cmd = target.substr(0, spaceIndex);
|
|
target = target.substr(spaceIndex+1);
|
|
} else {
|
|
target = '';
|
|
}
|
|
if (cmd === 'userdetails') {
|
|
var targetUser = Users.get(target);
|
|
if (!targetUser) {
|
|
connection.send('|queryresponse|userdetails|'+JSON.stringify({
|
|
userid: toId(target),
|
|
rooms: false
|
|
}));
|
|
return false;
|
|
}
|
|
var roomList = {};
|
|
for (var i in targetUser.roomCount) {
|
|
if (i==='global') continue;
|
|
var targetRoom = Rooms.get(i);
|
|
if (!targetRoom || targetRoom.isPrivate) continue;
|
|
var roomData = {};
|
|
if (targetRoom.battle) {
|
|
var battle = targetRoom.battle;
|
|
roomData.p1 = battle.p1?' '+battle.p1:'';
|
|
roomData.p2 = battle.p2?' '+battle.p2:'';
|
|
}
|
|
roomList[i] = roomData;
|
|
}
|
|
if (!targetUser.roomCount['global']) roomList = false;
|
|
var userdetails = {
|
|
userid: targetUser.userid,
|
|
avatar: targetUser.avatar,
|
|
rooms: roomList
|
|
};
|
|
if (user.can('ip', targetUser)) {
|
|
var ips = Object.keys(targetUser.ips);
|
|
if (ips.length === 1) {
|
|
userdetails.ip = ips[0];
|
|
} else {
|
|
userdetails.ips = ips;
|
|
}
|
|
}
|
|
connection.send('|queryresponse|userdetails|'+JSON.stringify(userdetails));
|
|
} else if (cmd === 'roomlist') {
|
|
connection.send('|queryresponse|roomlist|'+JSON.stringify({
|
|
rooms: Rooms.global.getRoomList(true)
|
|
}));
|
|
}
|
|
},
|
|
|
|
trn: function(target, room, user, connection) {
|
|
var commaIndex = target.indexOf(',');
|
|
var targetName = target;
|
|
var targetAuth = false;
|
|
var targetToken = '';
|
|
if (commaIndex >= 0) {
|
|
targetName = target.substr(0,commaIndex);
|
|
target = target.substr(commaIndex+1);
|
|
commaIndex = target.indexOf(',');
|
|
targetAuth = target;
|
|
if (commaIndex >= 0) {
|
|
targetAuth = !!parseInt(target.substr(0,commaIndex),10);
|
|
targetToken = target.substr(commaIndex+1);
|
|
}
|
|
}
|
|
user.rename(targetName, targetToken, targetAuth, connection);
|
|
},
|
|
|
|
};
|