PageRenderTime 158ms CodeModel.GetById 15ms app.highlight 127ms RepoModel.GetById 1ms app.codeStats 1ms

/chat-commands.js

https://github.com/CanBoy67/Pokemon-Showdown
JavaScript | 2134 lines | 1890 code | 181 blank | 63 comment | 424 complexity | b230d2c5713942189b1ead1f84151dd4 MD5 | raw file
   1/* to reload chat commands:
   2
   3>> for (var i in require.cache) delete require.cache[i];parseCommand = require('./chat-commands.js').parseCommand;''
   4
   5*/
   6
   7var crypto = require('crypto');
   8
   9/**
  10 * `parseCommand`. This is the function most of you are interested in,
  11 * apparently.
  12 *
  13 * `message` is exactly what the user typed in.
  14 * If the user typed in a command, `cmd` and `target` are the command (with "/"
  15 * omitted) and command target. Otherwise, they're both the empty string.
  16 *
  17 * For instance, say a user types in "/foo":
  18 * cmd === "/foo", target === "", message === "/foo bar baz"
  19 *
  20 * Or, say a user types in "/foo bar baz":
  21 * cmd === "foo", target === "bar baz", message === "/foo bar baz"
  22 *
  23 * Or, say a user types in "!foo bar baz":
  24 * cmd === "!foo", target === "bar baz", message === "!foo bar baz"
  25 *
  26 * Or, say a user types in "foo bar baz":
  27 * cmd === "", target === "", message === "foo bar baz"
  28 *
  29 * `user` and `socket` are the user and socket that sent the message,
  30 * and `room` is the room that sent the message.
  31 *
  32 * Deal with the message however you wish:
  33 *   return; will output the message normally: "user: message"
  34 *   return false; will supress the message output.
  35 *   returning a string will replace the message with that string,
  36 *     then output it normally.
  37 *
  38 */
  39
  40var modlog = modlog || fs.createWriteStream('logs/modlog.txt', {flags:'a+'});
  41var updateServerLock = false;
  42
  43function parseCommandLocal(user, cmd, target, room, socket, message) {
  44	if (!room) return;
  45	cmd = cmd.toLowerCase();
  46	switch (cmd) {
  47	case 'cmd':
  48		var spaceIndex = target.indexOf(' ');
  49		var cmd = target;
  50		if (spaceIndex > 0) {
  51			cmd = target.substr(0, spaceIndex);
  52			target = target.substr(spaceIndex+1);
  53		} else {
  54			target = '';
  55		}
  56		if (cmd === 'userdetails') {
  57			var targetUser = Users.get(target);
  58			if (!targetUser || !room) return false;
  59			var roomList = {};
  60			for (var i in targetUser.roomCount) {
  61				if (i==='lobby') continue;
  62				var targetRoom = Rooms.get(i);
  63				if (!targetRoom) continue;
  64				var roomData = {};
  65				if (targetRoom.battle) {
  66					var battle = targetRoom.battle;
  67					roomData.p1 = battle.p1?' '+battle.p1:'';
  68					roomData.p2 = battle.p2?' '+battle.p2:'';
  69				}
  70				roomList[i] = roomData;
  71			}
  72			var userdetails = {
  73				command: 'userdetails',
  74				userid: targetUser.userid,
  75				avatar: targetUser.avatar,
  76				rooms: roomList,
  77				room: room.id
  78			};
  79			if (user.can('ip', targetUser)) {
  80				userdetails.ip = targetUser.ip;
  81			}
  82			emit(socket, 'command', userdetails);
  83		} else if (cmd === 'roomlist') {
  84			if (!room || !room.getRoomList) return false;
  85			emit(socket, 'command', {
  86				command: 'roomlist',
  87				rooms: room.getRoomList(true),
  88				room: room.id
  89			});
  90		}
  91		return false;
  92		break;
  93
  94	case 'me':
  95	case 'mee':
  96		if (canTalk(user, room)) return true;
  97		break;
  98
  99	case '!birkal':
 100	case 'birkal':
 101		if (canTalk(user, room) && user.can('broadcast') && room.id === 'lobby') {
 102			if (cmd === '!birkal') {
 103				room.add('|c|'+user.getIdentity()+'|!birkal '+target, true);
 104			}
 105			room.logEntry(user.name + ' used /birkal ' + target);
 106			room.add('|c| Birkal|/me '+target, true);
 107			return false;
 108		}
 109		break;
 110
 111	case 'namelock':
 112	case 'nl':
 113		if(!target) {
 114			return false;
 115		}
 116		var targets = splitTarget(target);
 117		var targetUser = targets[0];
 118		var targetName = targets[1] || (targetUser && targetUser.name);
 119		if (!user.can('namelock', targetUser)) {
 120			emit(socket, 'console', '/namelock - access denied.');
 121			return false;
 122		} else if (targetUser && targetName) {
 123			var oldname = targetUser.name;
 124			var targetId = toUserid(targetName);
 125			var userOfName = Users.users[targetId];
 126			var isAlt = false;
 127			if (userOfName) {
 128				for(var altName in userOfName.getAlts()) {
 129					var altUser = Users.users[toUserid(altName)];
 130					if (!altUser) continue;
 131					if (targetId === altUser.userid) {
 132						isAlt = true;
 133						break;
 134					}
 135					for (var prevName in altUser.prevNames) {
 136						if (targetId === toUserid(prevName)) {
 137							isAlt = true;
 138							break;
 139						}
 140					}
 141					if (isAlt) break;
 142				}
 143			}
 144			if (!userOfName || oldname === targetName || isAlt) {
 145				targetUser.nameLock(targetName, true);
 146			}
 147			if (targetUser.nameLocked()) {
 148				logModCommand(room,user.name+" name-locked "+oldname+" to "+targetName+".");
 149				return false;
 150			}
 151			emit(socket, 'console', oldname+" can't be name-locked to "+targetName+".");
 152		} else {
 153			emit(socket, 'console', "User "+targets[2]+" not found.");
 154		}
 155		return false;
 156		break;
 157	case 'nameunlock':
 158	case 'unnamelock':
 159	case 'nul':
 160	case 'unl':
 161		if(!user.can('namelock') || !target) {
 162			return false;
 163		}
 164		var removed = false;
 165		for (var i in nameLockedIps) {
 166			if (nameLockedIps[i] === target) {
 167				delete nameLockedIps[i];
 168				removed = true;
 169			}
 170		}
 171		if (removed) {
 172			var targetUser = Users.get(target);
 173			if (targetUser) {
 174				rooms.lobby.sendIdentity(targetUser);
 175			}
 176			logModCommand(room,user.name+" unlocked the name of "+target+".");
 177		} else {
 178			emit(socket, 'console', target+" not found.");
 179		}
 180		return false;
 181		break;
 182
 183	case 'forfeit':
 184	case 'concede':
 185	case 'surrender':
 186		if (!room.battle) {
 187			emit(socket, 'console', "There's nothing to forfeit here.");
 188			return false;
 189		}
 190		if (!room.forfeit(user)) {
 191			emit(socket, 'console', "You can't forfeit this battle.");
 192		}
 193		return false;
 194		break;
 195
 196	case 'register':
 197		emit(socket, 'console', 'You must win a rated battle to register.');
 198		return false;
 199		break;
 200
 201	case 'avatar':
 202		if (!target) return parseCommand(user, 'avatars', '', room, socket);
 203		var parts = target.split(',');
 204		var avatar = parseInt(parts[0]);
 205		if (!avatar || avatar > 294 || avatar < 1) {
 206			if (!parts[1]) {
 207				emit(socket, 'console', 'Invalid avatar.');
 208			}
 209			return false;
 210		}
 211
 212		user.avatar = avatar;
 213		if (!parts[1]) {
 214			emit(socket, 'console', 'Avatar changed to:');
 215			emit(socket, 'console', {rawMessage: '<img src="/sprites/trainers/'+avatar+'.png" alt="" width="80" height="80" />'});
 216		}
 217
 218		return false;
 219		break;
 220
 221	case 'rooms':
 222		var targetUser = user;
 223		if (target) targetUser = Users.get(target);
 224		if (!targetUser) {
 225			emit(socket, 'console', 'User '+target+' not found.');
 226		} else {
 227			var output = "";
 228			var first = true;
 229			for (var i in targetUser.roomCount) {
 230				if (!first) output += ' | ';
 231				first = false;
 232
 233				output += '<a href="/'+i+'" room="'+i+'">'+i+'</a>';
 234			}
 235			if (!output) {
 236				emit(socket, 'console', ""+targetUser.name+" is offline.");
 237			} else {
 238				emit(socket, 'console', {rawMessage: ""+targetUser.name+" is in: "+output});
 239			}
 240		}
 241		return false;
 242		break;
 243
 244	case 'altcheck':
 245	case 'alt':
 246	case 'alts':
 247	case 'getalts':
 248		if (!target) return parseCommand(user, '?', cmd, room, socket);
 249		var targetUser = Users.get(target);
 250		if (!targetUser) {
 251			emit(socket, 'console', 'User '+target+' not found.');
 252			return false;
 253		}
 254		if (!user.can('alts', targetUser)) {
 255			emit(socket, 'console', '/alts - Access denied.');
 256			return false;
 257		}
 258
 259		var alts = targetUser.getAlts();
 260
 261		emit(socket, 'console', 'User: '+targetUser.name);
 262
 263		if (!user.can('alts', targetUser.getHighestRankedAlt())) {
 264			return false;
 265		}
 266
 267		var output = '';
 268		for (var i in targetUser.prevNames) {
 269			if (output) output += ", ";
 270			output += targetUser.prevNames[i];
 271		}
 272		if (output) emit(socket, 'console', 'Previous names: '+output);
 273
 274		for (var j=0; j<alts.length; j++) {
 275			var targetAlt = Users.get(alts[j]);
 276			if (!targetAlt.named && !targetAlt.connected) continue;
 277
 278			emit(socket, 'console', 'Alt: '+targetAlt.name);
 279			output = '';
 280			for (var i in targetAlt.prevNames) {
 281				if (output) output += ", ";
 282				output += targetAlt.prevNames[i];
 283			}
 284			if (output) emit(socket, 'console', 'Previous names: '+output);
 285		}
 286		return false;
 287		break;
 288
 289	case 'whois':
 290		var targetUser = user;
 291		if (target) {
 292			targetUser = Users.get(target);
 293		}
 294		if (!targetUser) {
 295			emit(socket, 'console', 'User '+target+' not found.');
 296		} else {
 297			emit(socket, 'console', 'User: '+targetUser.name);
 298			if (config.groups[targetUser.group] && config.groups[targetUser.group].name) {
 299				emit(socket, 'console', 'Group: ' + config.groups[targetUser.group].name + ' (' + targetUser.group + ')');
 300			}
 301			if (!targetUser.authenticated) {
 302				emit(socket, 'console', '(Unregistered)');
 303			}
 304			if (user.can('ip', targetUser)) {
 305				emit(socket, 'console', 'IP: '+targetUser.ip);
 306			}
 307			var output = 'In rooms: ';
 308			var first = true;
 309			for (var i in targetUser.roomCount) {
 310				if (!first) output += ' | ';
 311				first = false;
 312
 313				output += '<a href="/'+i+'" room="'+i+'">'+i+'</a>';
 314			}
 315			emit(socket, 'console', {rawMessage: output});
 316		}
 317		return false;
 318		break;
 319
 320	case 'ban':
 321	case 'b':
 322		if (!target) return parseCommand(user, '?', cmd, room, socket);
 323		var targets = splitTarget(target);
 324		var targetUser = targets[0];
 325		if (!targetUser) {
 326			emit(socket, 'console', 'User '+targets[2]+' not found.');
 327			return false;
 328		}
 329		if (!user.can('ban', targetUser)) {
 330			emit(socket, 'console', '/ban - Access denied.');
 331			return false;
 332		}
 333
 334		logModCommand(room,""+targetUser.name+" was banned by "+user.name+"." + (targets[1] ? " (" + targets[1] + ")" : ""));
 335		targetUser.emit('message', user.name+' has banned you.  If you feel that your banning was unjustified you can <a href="http://www.smogon.com/forums/announcement.php?f=126&a=204" target="_blank">appeal the ban</a>. '+targets[1]);
 336		var alts = targetUser.getAlts();
 337		if (alts.length) logModCommand(room,""+targetUser.name+"'s alts were also banned: "+alts.join(", "));
 338
 339		targetUser.ban();
 340		return false;
 341		break;
 342
 343	case 'banredirect':
 344	case 'br':
 345		emit(socket, 'console', '/banredirect - This command is obsolete and has been removed.');
 346		return false;
 347		break;
 348
 349	case 'redirect':
 350	case 'redir':
 351		emit(socket, 'console', '/redirect - This command is obsolete and has been removed.');
 352		return false;
 353		break;
 354
 355	case 'kick':
 356	case 'k':
 357		// TODO: /kick will be removed in due course.
 358		if (!target) return parseCommand(user, '?', cmd, room, socket);
 359		var targets = splitTarget(target);
 360		var targetUser = targets[0];
 361		if (!targetUser || !targetUser.connected) {
 362			emit(socket, 'console', 'User '+targets[2]+' not found.');
 363			return false;
 364		}
 365		if (!user.can('redirect', targetUser)) {
 366			emit(socket, 'console', '/redirect - Access denied.');
 367			return false;
 368		}
 369
 370		logModCommand(room,''+targetUser.name+' was kicked to the Rules page by '+user.name+'' + (targets[1] ? " (" + targets[1] + ")" : ""));
 371		targetUser.emit('console', {evalRulesRedirect: 1});
 372		return false;
 373		break;
 374
 375	case 'unban':
 376		if (!target) return parseCommand(user, '?', cmd, room, socket);
 377		if (!user.can('ban')) {
 378			emit(socket, 'console', '/unban - Access denied.');
 379			return false;
 380		}
 381
 382		var targetid = toUserid(target);
 383		var success = false;
 384
 385		for (var ip in bannedIps) {
 386			if (bannedIps[ip] === targetid) {
 387				delete bannedIps[ip];
 388				success = true;
 389			}
 390		}
 391		if (success) {
 392			logModCommand(room,''+target+' was unbanned by '+user.name+'.');
 393		} else {
 394			emit(socket, 'console', 'User '+target+' is not banned.');
 395		}
 396		return false;
 397		break;
 398
 399	case 'unbanall':
 400		if (!user.can('ban')) {
 401			emit(socket, 'console', '/unbanall - Access denied.');
 402			return false;
 403		}
 404		logModCommand(room,'All bans and ip mutes have been lifted by '+user.name+'.');
 405		bannedIps = {};
 406		mutedIps = {};
 407		return false;
 408		break;
 409
 410	case 'reply':
 411	case 'r':
 412		if (!target) return parseCommand(user, '?', cmd, room, socket);
 413		if (!user.lastPM) {
 414			emit(socket, 'console', 'No one has PMed you yet.');
 415			return false;
 416		}
 417		return parseCommand(user, 'msg', ''+(user.lastPM||'')+', '+target, room, socket);
 418		break;
 419
 420	case 'msg':
 421	case 'pm':
 422	case 'whisper':
 423	case 'w':
 424		if (!target) return parseCommand(user, '?', cmd, room, socket);
 425		var targets = splitTarget(target);
 426		var targetUser = targets[0];
 427		if (!targets[1]) {
 428			emit(socket, 'console', 'You forgot the comma.');
 429			return parseCommand(user, '?', cmd, room, socket);
 430		}
 431		if (!targets[0] || !targetUser.connected) {
 432			if (target.indexOf(' ')) {
 433				emit(socket, 'console', 'User '+targets[2]+' not found. Did you forget a comma?');
 434			} else {
 435				emit(socket, 'console', 'User '+targets[2]+' not found. Did you misspell their name?');
 436			}
 437			return parseCommand(user, '?', cmd, room, socket);
 438		}
 439		// temporarily disable this because blarajan
 440		/* if (user.muted && !targetUser.can('mute', user)) {
 441			emit(socket, 'console', 'You can only private message members of the Moderation Team (users marked by %, @, &, or ~) when muted.');
 442			return false;
 443		} */
 444
 445		if (!user.named) {
 446			emit(socket, 'console', 'You must choose a name before you can send private messages.');
 447			return false;
 448		}
 449
 450		var message = {
 451			name: user.getIdentity(),
 452			pm: targetUser.getIdentity(),
 453			message: targets[1]
 454		};
 455		user.emit('console', message);
 456		targets[0].emit('console', message);
 457		targets[0].lastPM = user.userid;
 458		user.lastPM = targets[0].userid;
 459		return false;
 460		break;
 461
 462	case 'ip':
 463	case 'getip':
 464		if (!target) {
 465			emit(socket, 'console', 'Your IP is: '+user.ip);
 466			return false;
 467		}
 468		var targetUser = Users.get(target);
 469		if (!targetUser) {
 470			emit(socket, 'console', 'User '+target+' not found.');
 471			return false;
 472		}
 473		if (!user.can('ip', targetUser)) {
 474			emit(socket, 'console', '/ip - Access denied.');
 475			return false;
 476		}
 477		emit(socket, 'console', 'User '+targetUser.name+' has IP: '+targetUser.ip);
 478		return false;
 479		break;
 480
 481	case 'mute':
 482	case 'm':
 483		if (!target) return parseCommand(user, '?', cmd, room, socket);
 484		var targets = splitTarget(target);
 485		var targetUser = targets[0];
 486		if (!targetUser) {
 487			emit(socket, 'console', 'User '+targets[2]+' not found.');
 488			return false;
 489		}
 490		if (!user.can('mute', targetUser)) {
 491			emit(socket, 'console', '/mute - Access denied.');
 492			return false;
 493		}
 494
 495		logModCommand(room,''+targetUser.name+' was muted by '+user.name+'.' + (targets[1] ? " (" + targets[1] + ")" : ""));
 496		targetUser.emit('message', user.name+' has muted you. '+targets[1]);
 497		var alts = targetUser.getAlts();
 498		if (alts.length) logModCommand(room,""+targetUser.name+"'s alts were also muted: "+alts.join(", "));
 499
 500		targetUser.muted = true;
 501		rooms.lobby.sendIdentity(targetUser);
 502		for (var i=0; i<alts.length; i++) {
 503			var targetAlt = Users.get(alts[i]);
 504			if (targetAlt) {
 505				targetAlt.muted = true;
 506				rooms.lobby.sendIdentity(targetAlt);
 507			}
 508		}
 509
 510		return false;
 511		break;
 512
 513	case 'ipmute':
 514		if (!target) return parseCommand(user, '?', cmd, room, socket);
 515		var targetUser = Users.get(target);
 516		if (!targetUser) {
 517			emit(socket, 'console', 'User '+target+' not found.');
 518			return false;
 519		}
 520		if (!user.can('mute', targetUser)) {
 521			emit(socket, 'console', '/ipmute - Access denied.');
 522			return false;
 523		}
 524
 525		logModCommand(room,''+targetUser.name+"'s IP was muted by "+user.name+'.');
 526		var alts = targetUser.getAlts();
 527		if (alts.length) logModCommand(room,""+targetUser.name+"'s alts were also muted: "+alts.join(", "));
 528
 529		targetUser.muted = true;
 530		rooms.lobby.sendIdentity(targetUser);
 531		mutedIps[targetUser.ip] = targetUser.userid;
 532		for (var i=0; i<alts.length; i++) {
 533			var targetAlt = Users.get(alts[i]);
 534			if (targetAlt) {
 535				targetAlt.muted = true;
 536				rooms.lobby.sendIdentity(targetAlt);
 537			}
 538		}
 539
 540		return false;
 541		break;
 542
 543	case 'unmute':
 544	case 'um':
 545		if (!target) return parseCommand(user, '?', cmd, room, socket);
 546		var targetid = toUserid(target);
 547		var targetUser = Users.get(target);
 548		if (!targetUser) {
 549			emit(socket, 'console', 'User '+target+' not found.');
 550			return false;
 551		}
 552		if (!user.can('mute', targetUser)) {
 553			emit(socket, 'console', '/unmute - Access denied.');
 554			return false;
 555		}
 556
 557		var success = false;
 558
 559		for (var ip in mutedIps) {
 560			if (mutedIps[ip] === targetid) {
 561				delete mutedIps[ip];
 562				success = true;
 563			}
 564		}
 565
 566		if (success) {
 567			logModCommand(room,''+(targetUser?targetUser.name:target)+"'s IP was unmuted by "+user.name+'.');
 568		}
 569
 570		targetUser.muted = false;
 571		rooms.lobby.sendIdentity(targetUser);
 572		logModCommand(room,''+targetUser.name+' was unmuted by '+user.name+'.');
 573		return false;
 574		break;
 575
 576	case 'promote':
 577	case 'demote':
 578		if (!target) return parseCommand(user, '?', cmd, room, socket);
 579		var targets = splitTarget(target, true);
 580		var targetUser = targets[0];
 581		var userid = toUserid(targets[2]);
 582
 583		var currentGroup = ' ';
 584		if (targetUser) {
 585			currentGroup = targetUser.group;
 586		} else if (Users.usergroups[userid]) {
 587			currentGroup = Users.usergroups[userid].substr(0,1);
 588		}
 589		var name = targetUser ? targetUser.name : targets[2];
 590
 591		var nextGroup = targets[1] ? targets[1] : Users.getNextGroupSymbol(currentGroup, cmd === 'demote');
 592		if (targets[1] === 'deauth') nextGroup = config.groupsranking[0];
 593		if (!config.groups[nextGroup]) {
 594			emit(socket, 'console', 'Group \'' + nextGroup + '\' does not exist.');
 595			return false;
 596		}
 597		if (!user.checkPromotePermission(currentGroup, nextGroup)) {
 598			emit(socket, 'console', '/promote - Access denied.');
 599			return false;
 600		}
 601
 602		var isDemotion = (config.groups[nextGroup].rank < config.groups[currentGroup].rank);
 603		if (!Users.setOfflineGroup(name, nextGroup)) {
 604			emit(socket, 'console', '/promote - WARNING: This user is offline and could be unregistered. Use /forcepromote if you\'re sure you want to risk it.');
 605			return false;
 606		}
 607		var groupName = (config.groups[nextGroup].name || nextGroup || '').trim() || 'a regular user';
 608		var entry = ''+name+' was '+(isDemotion?'demoted':'promoted')+' to ' + groupName + ' by '+user.name+'.';
 609		logModCommand(room, entry, isDemotion);
 610		if (isDemotion) {
 611			rooms.lobby.logEntry(entry);
 612			emit(socket, 'console', 'You demoted ' + name + ' to ' + groupName + '.');
 613			if (targetUser) {
 614				targetUser.emit('console', 'You were demoted to ' + groupName + ' by ' + user.name + '.');
 615			}
 616		}
 617		rooms.lobby.sendIdentity(targetUser);
 618		return false;
 619		break;
 620
 621	case 'forcepromote':
 622		// warning: never document this command in /help
 623		if (!user.can('forcepromote')) {
 624			emit(socket, 'console', '/forcepromote - Access denied.');
 625			return false;
 626		}
 627		var targets = splitTarget(target, true);
 628		var name = targets[2];
 629		var nextGroup = targets[1] ? targets[1] : Users.getNextGroupSymbol(' ', false);
 630
 631		if (!Users.setOfflineGroup(name, nextGroup, true)) {
 632			emit(socket, 'console', '/forcepromote - Don\'t forcepromote unless you have to.');
 633			return false;
 634		}
 635		var groupName = config.groups[nextGroup].name || nextGroup || '';
 636		logModCommand(room,''+name+' was promoted to ' + (groupName.trim()) + ' by '+user.name+'.');
 637		return false;
 638		break;
 639
 640	case 'deauth':
 641		return parseCommand(user, 'demote', target+', deauth', room, socket);
 642		break;
 643
 644	case 'modchat':
 645		if (!target) {
 646			emit(socket, 'console', 'Moderated chat is currently set to: '+config.modchat);
 647			return false;
 648		}
 649		if (!user.can('modchat')) {
 650			emit(socket, 'console', '/modchat - Access denied.');
 651			return false;
 652		}
 653
 654		target = target.toLowerCase();
 655		switch (target) {
 656		case 'on':
 657		case 'true':
 658		case 'yes':
 659			config.modchat = true;
 660			break;
 661		case 'off':
 662		case 'false':
 663		case 'no':
 664			config.modchat = false;
 665			break;
 666		default:
 667			if (!config.groups[target]) {
 668				emit(socket, 'console', 'That moderated chat setting is unrecognized.');
 669				return false;
 670			}
 671			if (config.groupsranking.indexOf(target) > 1 && !user.can('modchatall')) {
 672				emit(socket, 'console', '/modchat - Access denied for setting higher than ' + config.groupsranking[1] + '.');
 673				return false;
 674			}
 675			config.modchat = target;
 676			break;
 677		}
 678		if (config.modchat === true) {
 679			room.addRaw('<div class="broadcast-red"><b>Moderated chat was enabled!</b><br />Only registered users can talk.</div>');
 680		} else if (!config.modchat) {
 681			room.addRaw('<div class="broadcast-blue"><b>Moderated chat was disabled!</b><br />Anyone may talk now.</div>');
 682		} else {
 683			var modchat = sanitize(config.modchat);
 684			room.addRaw('<div class="broadcast-red"><b>Moderated chat was set to '+modchat+'!</b><br />Only users of rank '+modchat+' and higher can talk.</div>');
 685		}
 686		logModCommand(room,user.name+' set modchat to '+config.modchat,true);
 687		return false;
 688		break;
 689
 690	case 'declare':
 691		if (!target) return parseCommand(user, '?', cmd, room, socket);
 692		if (!user.can('declare')) {
 693			emit(socket, 'console', '/declare - Access denied.');
 694			return false;
 695		}
 696		room.addRaw('<div class="broadcast-blue"><b>'+target+'</b></div>');
 697		logModCommand(room,user.name+' declared '+target,true);
 698		return false;
 699		break;
 700
 701	case 'announce':
 702	case 'wall':
 703		if (!target) return parseCommand(user, '?', cmd, room, socket);
 704		if (!user.can('announce')) {
 705			emit(socket, 'console', '/announce - Access denied.');
 706			return false;
 707		}
 708		return '/announce '+target;
 709		break;
 710
 711	case 'hotpatch':
 712		if (!target) return parseCommand(user, '?', cmd, room, socket);
 713		if (!user.can('hotpatch')) {
 714			emit(socket, 'console', '/hotpatch - Access denied.');
 715			return false;
 716		}
 717
 718		if (target === 'all') {
 719			for (var i in require.cache) delete require.cache[i];
 720			Tools = require('./tools.js');
 721
 722			parseCommand = require('./chat-commands.js').parseCommand;
 723
 724			sim = require('./battles.js');
 725			BattlePokemon = sim.BattlePokemon;
 726			BattleSide = sim.BattleSide;
 727			Battle = sim.Battle;
 728			emit(socket, 'console', 'The game engine has been hot-patched.');
 729			return false;
 730		} else if (target === 'data') {
 731			for (var i in require.cache) delete require.cache[i];
 732			Tools = require('./tools.js');
 733			emit(socket, 'console', 'Game resources have been hot-patched.');
 734			return false;
 735		} else if (target === 'chat') {
 736			for (var i in require.cache) delete require.cache[i];
 737			parseCommand = require('./chat-commands.js').parseCommand;
 738			emit(socket, 'console', 'Chat commands have been hot-patched.');
 739			return false;
 740		}
 741		emit(socket, 'console', 'Your hot-patch command was unrecognized.');
 742		return false;
 743		break;
 744
 745	case 'savelearnsets':
 746		if (user.can('hotpatch')) {
 747			emit(socket, 'console', '/savelearnsets - Access denied.');
 748			return false;
 749		}
 750		fs.writeFile('data/learnsets.js', 'exports.BattleLearnsets = '+JSON.stringify(BattleLearnsets)+";\n");
 751		emit(socket, 'console', 'learnsets.js saved.');
 752		return false;
 753		break;
 754
 755	case 'rating':
 756	case 'ranking':
 757	case 'rank':
 758	case 'ladder':
 759		emit(socket, 'console', 'You are using an old version of Pokemon Showdown. Please reload the page.');
 760		return false;
 761		break;
 762
 763	case 'nick':
 764		if (!target) return parseCommand(user, '?', cmd, room, socket);
 765		user.rename(target);
 766		return false;
 767		break;
 768
 769	case 'disableladder':
 770		if (!user.can('disableladder')) {
 771			emit(socket, 'console', '/disableladder - Access denied.');
 772			return false;
 773		}
 774		if (LoginServer.disabled) {
 775			emit(socket, 'console', '/disableladder - Ladder is already disabled.');
 776			return false;
 777		}
 778		LoginServer.disabled = true;
 779		logModCommand(room, 'The ladder was disabled by ' + user.name + '.', true);
 780		room.addRaw('<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>');
 781		return false;
 782		break;
 783	case 'enableladder':
 784		if (!user.can('disableladder')) {
 785			emit(socket, 'console', '/enable - Access denied.');
 786			return false;
 787		}
 788		if (!LoginServer.disabled) {
 789			emit(socket, 'console', '/enable - Ladder is already enabled.');
 790			return false;
 791		}
 792		LoginServer.disabled = false;
 793		logModCommand(room, 'The ladder was enabled by ' + user.name + '.', true);
 794		room.addRaw('<div class="broadcast-green"><b>The ladder is now back.</b><br />Rated games will update the ladder now.</div>');
 795		return false;
 796		break;
 797
 798	case 'savereplay':
 799		if (!room || !room.battle) return false;
 800		var data = room.log.join("\n");
 801		var datahash = crypto.createHash('md5').update(data.replace(/[^(\x20-\x7F)]+/g,'')).digest('hex');
 802
 803		LoginServer.request('prepreplay', {
 804			id: room.id.substr(7),
 805			loghash: datahash,
 806			p1: room.p1.name,
 807			p2: room.p2.name,
 808			format: room.format
 809		}, function(success) {
 810			emit(socket, 'command', {
 811				command: 'savereplay',
 812				log: data,
 813				room: 'lobby',
 814				id: room.id.substr(7)
 815			});
 816		});
 817		return false;
 818		break;
 819
 820	case 'trn':
 821		var commaIndex = target.indexOf(',');
 822		var targetName = target;
 823		var targetAuth = false;
 824		var targetToken = '';
 825		if (commaIndex >= 0) {
 826			targetName = target.substr(0,commaIndex);
 827			target = target.substr(commaIndex+1);
 828			commaIndex = target.indexOf(',');
 829			targetAuth = target;
 830			if (commaIndex >= 0) {
 831				targetAuth = !!parseInt(target.substr(0,commaIndex),10);
 832				targetToken = target.substr(commaIndex+1);
 833			}
 834		}
 835		user.rename(targetName, targetToken, targetAuth, socket);
 836		return false;
 837		break;
 838
 839	case 'forcerename':
 840	case 'fr':
 841		if (!target) return parseCommand(user, '?', cmd, room, socket);
 842		var targets = splitTarget(target);
 843		var targetUser = targets[0];
 844		if (!targetUser) {
 845			emit(socket, 'console', 'User '+targets[2]+' not found.');
 846			return false;
 847		}
 848		if (!user.can('forcerename', targetUser)) {
 849			emit(socket, 'console', '/forcerename - Access denied.');
 850			return false;
 851		}
 852
 853		if (targetUser.userid === toUserid(targets[2])) {
 854			var entry = ''+targetUser.name+' was forced to choose a new name by '+user.name+'.' + (targets[1] ? " (" + targets[1] + ")" : "");
 855			logModCommand(room, entry, true);
 856			rooms.lobby.sendAuth(entry);
 857			if (room.id !== 'lobby') {
 858				room.add(entry);
 859			} else {
 860				room.logEntry(entry);
 861			}
 862			targetUser.resetName();
 863			targetUser.emit('nameTaken', {reason: user.name+" has forced you to change your name. "+targets[1]});
 864		} else {
 865			emit(socket, 'console', "User "+targetUser.name+" is no longer using that name.");
 866		}
 867		return false;
 868		break;
 869
 870	case 'forcerenameto':
 871	case 'frt':
 872		if (!target) return parseCommand(user, '?', cmd, room, socket);
 873		var targets = splitTarget(target);
 874		var targetUser = targets[0];
 875		if (!targetUser) {
 876			emit(socket, 'console', 'User '+targets[2]+' not found.');
 877			return false;
 878		}
 879		if (!targets[1]) {
 880			emit(socket, 'console', 'No new name was specified.');
 881			return false;
 882		}
 883		if (!user.can('forcerenameto', targetUser)) {
 884			emit(socket, 'console', '/forcerenameto - Access denied.');
 885			return false;
 886		}
 887
 888		if (targetUser.userid === toUserid(targets[2])) {
 889			var entry = ''+targetUser.name+' was forcibly renamed to '+targets[1]+' by '+user.name+'.';
 890			logModCommand(room, entry, true);
 891			rooms.lobby.sendAuth(entry);
 892			if (room.id !== 'lobby') {
 893				room.add(entry);
 894			} else {
 895				room.logEntry(entry);
 896			}
 897			targetUser.forceRename(targets[1]);
 898		} else {
 899			emit(socket, 'console', "User "+targetUser.name+" is no longer using that name.");
 900		}
 901		return false;
 902		break;
 903
 904	// INFORMATIONAL COMMANDS
 905
 906	case 'data':
 907	case '!data':
 908	case 'stats':
 909	case '!stats':
 910	case 'dex':
 911	case '!dex':
 912	case 'pokedex':
 913	case '!pokedex':
 914		showOrBroadcastStart(user, cmd, room, socket, message);
 915		var dataMessages = getDataMessage(target);
 916		for (var i=0; i<dataMessages.length; i++) {
 917			if (cmd.substr(0,1) !== '!') {
 918				sendData(socket, '>'+room.id+'\n'+dataMessages[i]);
 919			} else if (user.can('broadcast') && canTalk(user, room)) {
 920				room.add(dataMessages[i]);
 921			}
 922		}
 923		return false;
 924		break;
 925
 926	case 'learnset':
 927	case '!learnset':
 928	case 'learn':
 929	case '!learn':
 930	case 'learnall':
 931	case '!learnall':
 932	case 'learn5':
 933	case '!learn5':
 934		var lsetData = {set:{}};
 935		var targets = target.split(',');
 936		if (!targets[1]) return parseCommand(user, 'help', 'learn', room, socket);
 937		var template = Tools.getTemplate(targets[0]);
 938		var move = {};
 939		var problem;
 940		var all = (cmd.substr(cmd.length-3) === 'all');
 941
 942		if (cmd === 'learn5' || cmd === '!learn5') lsetData.set.level = 5;
 943
 944		showOrBroadcastStart(user, cmd, room, socket, message);
 945
 946		if (!template.exists) {
 947			showOrBroadcast(user, cmd, room, socket,
 948				'Pokemon "'+template.id+'" not found.');
 949			return false;
 950		}
 951
 952		for (var i=1, len=targets.length; i<len; i++) {
 953			move = Tools.getMove(targets[i]);
 954			if (!move.exists) {
 955				showOrBroadcast(user, cmd, room, socket,
 956					'Move "'+move.id+'" not found.');
 957				return false;
 958			}
 959			problem = Tools.checkLearnset(move, template, lsetData);
 960			if (problem) break;
 961		}
 962		var buffer = ''+template.name+(problem?" <span class=\"message-learn-cannotlearn\">can't</span> learn ":" <span class=\"message-learn-canlearn\">can</span> learn ")+(targets.length>2?"these moves":move.name);
 963		if (!problem) {
 964			var sourceNames = {E:"egg",S:"event",D:"dream world"};
 965			if (lsetData.sources || lsetData.sourcesBefore) buffer += " only when obtained from:<ul class=\"message-learn-list\">";
 966			if (lsetData.sources) {
 967				var sources = lsetData.sources.sort();
 968				var prevSource;
 969				var prevSourceType;
 970				for (var i=0, len=sources.length; i<len; i++) {
 971					var source = sources[i];
 972					if (source.substr(0,2) === prevSourceType) {
 973						if (prevSourceCount < 0) buffer += ": "+source.substr(2);
 974						else if (all || prevSourceCount < 3) buffer += ', '+source.substr(2);
 975						else if (prevSourceCount == 3) buffer += ', ...';
 976						prevSourceCount++;
 977						continue;
 978					}
 979					prevSourceType = source.substr(0,2);
 980					prevSourceCount = source.substr(2)?0:-1;
 981					buffer += "<li>gen "+source.substr(0,1)+" "+sourceNames[source.substr(1,1)];
 982					if (prevSourceType === '5E' && template.maleOnlyDreamWorld) buffer += " (cannot have DW ability)";
 983					if (source.substr(2)) buffer += ": "+source.substr(2);
 984				}
 985			}
 986			if (lsetData.sourcesBefore) buffer += "<li>any generation before "+(lsetData.sourcesBefore+1);
 987			buffer += "</ul>";
 988		}
 989		showOrBroadcast(user, cmd, room, socket,
 990			buffer);
 991		return false;
 992		break;
 993
 994	case 'uptime':
 995	case '!uptime':
 996		var uptime = process.uptime();
 997		var uptimeText;
 998		if (uptime > 24*60*60) {
 999			var uptimeDays = Math.floor(uptime/(24*60*60));
1000			uptimeText = ''+uptimeDays+' '+(uptimeDays == 1 ? 'day' : 'days');
1001			var uptimeHours = Math.floor(uptime/(60*60)) - uptimeDays*24;
1002			if (uptimeHours) uptimeText += ', '+uptimeHours+' '+(uptimeHours == 1 ? 'hour' : 'hours');
1003		} else {
1004			uptimeText = uptime.seconds().duration();
1005		}
1006		showOrBroadcastStart(user, cmd, room, socket, message);
1007		showOrBroadcast(user, cmd, room, socket,
1008			'<div class="infobox">' +
1009			'Uptime: <b>'+uptimeText+'</b>'+
1010			'</div>');
1011		return false;
1012		break;
1013
1014	case 'groups':
1015	case '!groups':
1016		showOrBroadcastStart(user, cmd, room, socket, message);
1017		showOrBroadcast(user, cmd, room, socket,
1018			'<div class="infobox">' +
1019			'+ <b>Voice</b> - They can use ! commands like !groups, and talk during moderated chat<br />' +
1020			'% <b>Driver</b> - The above, and they can also mute users and run tournaments<br />' +
1021			'@ <b>Moderator</b> - The above, and they can ban users and check for alts<br />' +
1022			'&amp; <b>Leader</b> - The above, and they can promote moderators and force ties<br />'+
1023			'~ <b>Administrator</b> - They can do anything, like change what this message says'+
1024			'</div>');
1025		return false;
1026		break;
1027
1028	case 'opensource':
1029	case '!opensource':
1030		showOrBroadcastStart(user, cmd, room, socket, message);
1031		showOrBroadcast(user, cmd, room, socket,
1032			'<div class="infobox">Pokemon Showdown is open source:<br />- Language: JavaScript<br />- <a href="https://github.com/Zarel/Pokemon-Showdown/commits/master" target="_blank">What\'s new?</a><br />- <a href="https://github.com/Zarel/Pokemon-Showdown" target="_blank">Server source code</a><br />- <a href="https://github.com/Zarel/Pokemon-Showdown-Client" target="_blank">Client source code</a></div>');
1033		return false;
1034		break;
1035
1036	case 'avatars':
1037	case '!avatars':
1038		showOrBroadcastStart(user, cmd, room, socket, message);
1039		showOrBroadcast(user, cmd, room, socket,
1040			'<div class="infobox">Want a custom avatar?<br />- <a href="/sprites/trainers/" target="_blank">How to change your avatar</a></div>');
1041		return false;
1042		break;
1043
1044	case 'intro':
1045	case 'introduction':
1046	case '!intro':
1047	case '!introduction':
1048		showOrBroadcastStart(user, cmd, room, socket, message);
1049		showOrBroadcast(user, cmd, room, socket,
1050			'<div class="infobox">New to competitive pokemon?<br />' +
1051			'- <a href="http://www.smogon.com/dp/articles/intro_comp_pokemon" target="_blank">An introduction to competitive pokemon</a><br />' +
1052			'- <a href="http://www.smogon.com/bw/articles/bw_tiers" target="_blank">What do "OU", "UU", etc mean?</a><br />' +
1053			'- <a href="http://www.smogon.com/bw/banlist/" target="_blank">What are the rules for each format? What is "Sleep Clause"?</a>' +
1054			'</div>');
1055		return false;
1056		break;
1057		
1058	case 'calc':
1059	case '!calc':
1060	case 'calculator':
1061	case '!calculator':
1062		showOrBroadcastStart(user, cmd, room, socket, message);
1063		showOrBroadcast(user, cmd , room , socket,
1064			'<div class="infobox">Pokemon Showdown! damage calculator. (Courtesy of Honko)<br />' +
1065			'- <a href="http://pokemonshowdown.com/damagecalc/" target="_blank">Damage Calculator</a><br />' +
1066			'</div>');
1067		return false;
1068		break;
1069	
1070	case 'cap':
1071	case '!cap':
1072		showOrBroadcastStart(user, cmd, room, socket, message);
1073		showOrBroadcast(user, cmd, room, socket,
1074			'<div class="infobox">An introduction to the Create-A-Pokemon project:<br />' +
1075			'- <a href="http://www.smogon.com/cap/" target="_blank">CAP project website and description</a><br />' +
1076			'- <a href="http://www.smogon.com/forums/showthread.php?t=48782" target="_blank">What Pokemon have been made?</a><br />' +
1077			'- <a href="http://www.smogon.com/forums/showthread.php?t=3464513" target="_blank">Talk about the metagame here</a><br />' +
1078			'- <a href="http://www.smogon.com/forums/showthread.php?t=3466826" target="_blank">Practice BW CAP teams</a>' +
1079			'</div>');
1080		return false;
1081		break;
1082
1083	case 'om':
1084	case 'othermetas':
1085	case '!om':
1086	case '!othermetas':
1087		showOrBroadcastStart(user, cmd, room, socket, message);
1088		showOrBroadcast(user, cmd, room, socket,
1089			'<div class="infobox">Information on the Other Metagames:<br />' +
1090			'- <a href="http://www.smogon.com/forums/showthread.php?t=3463764" target="_blank">Balanced Hackmons</a><br />' +
1091			'- <a href="http://www.smogon.com/forums/showthread.php?t=3471810" target="_blank">Dream World OU</a><br />' +
1092			'- <a href="http://www.smogon.com/forums/showthread.php?t=3467120" target="_blank">Glitchmons</a><br />' +
1093			'- <a href="http://www.smogon.com/sim/seasonal" target="_blank">Seasonal: Spring Forward</a><br />' +
1094			'- <a href="http://www.smogon.com/forums/showthread.php?t=3476469" target="_blank">Smogon Doubles</a><br />' +
1095			'- <a href="http://www.smogon.com/forums/showthread.php?t=3471161" target="_blank">VGC 2013</a>' +
1096			'</div>');
1097		return false;
1098		break;
1099
1100	case 'rules':
1101	case 'rule':
1102	case '!rules':
1103	case '!rule':
1104		showOrBroadcastStart(user, cmd, room, socket, message);
1105		showOrBroadcast(user, cmd, room, socket,
1106			'<div class="infobox">Please follow the rules:<br />' +
1107			'- <a href="http://pokemonshowdown.com/rules" target="_blank">Rules</a><br />' +
1108			'</div>');
1109		return false;
1110		break;
1111		
1112	case 'faq':
1113	case '!faq':
1114		target = target.toLowerCase();
1115		var buffer = '<div class="infobox">';
1116		var matched = false;
1117		if (!target || target === 'all') {
1118			matched = true;
1119			buffer += '<a href="http://www.smogon.com/sim/faq" target="_blank">Frequently Asked Questions</a><br />';
1120		}
1121		if (target === 'all' || target === 'deviation') {
1122			matched = true;
1123			buffer += '<a href="http://www.smogon.com/sim/faq#deviation" target="_blank">Why did this user gain or lose so many points?</a><br />';
1124		}
1125		if (target === 'all' || target === 'doubles' || target === 'triples' || target === 'rotation') {
1126			matched = true;
1127			buffer += '<a href="http://www.smogon.com/sim/faq#doubles" target="_blank">Can I play doubles/triples/rotation battles here?</a><br />';
1128		}
1129		if (target === 'all' || target === 'randomcap') {
1130			matched = true;
1131			buffer += '<a href="http://www.smogon.com/sim/faq#randomcap" target="_blank">What is this fakemon and what is it doing in my random battle?</a><br />';
1132		}
1133		if (target === 'all' || target === 'restarts') {
1134			matched = true;
1135			buffer += '<a href="http://www.smogon.com/sim/faq#restarts" target="_blank">Why is the server restarting?</a><br />';
1136		}
1137		if (target === 'all' || target === 'staff') {
1138			matched = true;
1139			buffer += '<a href="http://www.smogon.com/sim/staff_faq" target="_blank">Staff FAQ</a><br />';
1140		}
1141		if (!matched) {
1142			emit(socket, 'console', 'The FAQ entry "'+target+'" was not found. Try /faq for general help.');
1143			return false;
1144		}
1145		buffer += '</div>';
1146		showOrBroadcastStart(user, cmd, room, socket, message);
1147		showOrBroadcast(user, cmd, room, socket, buffer);
1148		return false;
1149		break;
1150
1151	case 'banlists':
1152	case 'tiers':
1153	case '!banlists':
1154	case '!tiers':
1155		showOrBroadcastStart(user, cmd, room, socket, message);
1156		showOrBroadcast(user, cmd, room, socket,
1157			'<div class="infobox">Smogon tiers:<br />' +
1158			'- <a href="http://www.smogon.com/bw/banlist/" target="_blank">The banlists for each tier</a><br />' +
1159			'- <a href="http://www.smogon.com/bw/tiers/uber" target="_blank">Uber Pokemon</a><br />' +
1160			'- <a href="http://www.smogon.com/bw/tiers/ou" target="_blank">Overused Pokemon</a><br />' +
1161			'- <a href="http://www.smogon.com/bw/tiers/uu" target="_blank">Underused Pokemon</a><br />' +
1162			'- <a href="http://www.smogon.com/bw/tiers/ru" target="_blank">Rarelyused Pokemon</a><br />' +
1163			'- <a href="http://www.smogon.com/bw/tiers/nu" target="_blank">Neverused Pokemon</a><br />' +
1164			'- <a href="http://www.smogon.com/bw/tiers/lc" target="_blank">Little Cup Pokemon</a><br />' +
1165			'</div>');
1166		return false;
1167		break;
1168
1169	case 'analysis':
1170	case '!analysis':
1171	case 'strategy':
1172	case '!strategy':
1173	case 'smogdex':
1174	case '!smogdex':
1175		var targets = target.split(',');
1176		var pokemon = Tools.getTemplate(targets[0]);
1177		var item = Tools.getItem(targets[0]);
1178		var move = Tools.getMove(targets[0]);
1179		var ability = Tools.getAbility(targets[0]);
1180		var atLeastOne = false;
1181		var generation = (targets[1] || "bw").trim().toLowerCase();
1182		var genNumber = 5;
1183
1184		showOrBroadcastStart(user, cmd, room, socket, message);
1185
1186		if (generation === "bw" || generation === "bw2" || generation === "5" || generation === "five") {
1187			generation = "bw";
1188		} else if (generation === "dp" || generation === "dpp" || generation === "4" || generation === "four") {
1189			generation = "dp";
1190			genNumber = 4;
1191		} else if (generation === "adv" || generation === "rse" || generation === "rs" || generation === "3" || generation === "three") {
1192			generation = "rs";
1193			genNumber = 3;
1194		} else if (generation === "gsc" || generation === "gs" || generation === "2" || generation === "two") {
1195			generation = "gs";
1196			genNumber = 2;
1197		} else if(generation === "rby" || generation === "rb" || generation === "1" || generation === "one") {
1198			generation = "rb";
1199			genNumber = 1;
1200		} else {
1201			generation = "bw";
1202		}
1203		
1204		// Pokemon
1205		if (pokemon.exists) {
1206			atLeastOne = true;
1207			if (genNumber < pokemon.gen) {
1208				showOrBroadcast(user, cmd, room, socket, pokemon.name+' did not exist in '+generation.toUpperCase()+'!');
1209				return false;
1210			}
1211			if (pokemon.tier === 'G4CAP' || pokemon.tier === 'G5CAP') {
1212				generation = "cap";
1213			}
1214	
1215			var poke = pokemon.name.toLowerCase();
1216			if (poke === 'nidoranm') poke = 'nidoran-m';
1217			if (poke === 'nidoranf') poke = 'nidoran-f';
1218			if (poke === 'farfetch\'d') poke = 'farfetchd';
1219			if (poke === 'mr. mime') poke = 'mr_mime';
1220			if (poke === 'mime jr.') poke = 'mime_jr';
1221			if (poke === 'deoxys-attack' || poke === 'deoxys-defense' || poke === 'deoxys-speed' || poke === 'kyurem-black' || poke === 'kyurem-white') poke = poke.substr(0,8);
1222			if (poke === 'wormadam-trash') poke = 'wormadam-s';
1223			if (poke === 'wormadam-sandy') poke = 'wormadam-g';
1224			if (poke === 'rotom-wash' || poke === 'rotom-frost' || poke === 'rotom-heat') poke = poke.substr(0,7);
1225			if (poke === 'rotom-mow') poke = 'rotom-c';
1226			if (poke === 'rotom-fan') poke = 'rotom-s';
1227			if (poke === 'giratina-origin' || poke === 'tornadus-therian' || poke === 'landorus-therian') poke = poke.substr(0,10);
1228			if (poke === 'shaymin-sky') poke = 'shaymin-s';
1229			if (poke === 'arceus') poke = 'arceus-normal';
1230			if (poke === 'thundurus-therian') poke = 'thundurus-t';
1231	
1232			showOrBroadcast(user, cmd, room, socket,
1233				'<a href="http://www.smogon.com/'+generation+'/pokemon/'+poke+'" target="_blank">'+generation.toUpperCase()+' '+pokemon.name+' analysis</a>, brought to you by <a href="http://www.smogon.com" target="_blank">Smogon University</a>');
1234		}
1235		
1236		// Item
1237		if (item.exists && genNumber > 1) {
1238			atLeastOne = true;
1239			var itemName = item.name.toLowerCase().replace(' ', '_');
1240			showOrBroadcast(user, cmd, room, socket,
1241					'<a href="http://www.smogon.com/'+generation+'/items/'+itemName+'" target="_blank">'+generation.toUpperCase()+' '+item.name+' item analysis</a>, brought to you by <a href="http://www.smogon.com" target="_blank">Smogon University</a>');
1242		}
1243		
1244		// Ability
1245		if (ability.exists && genNumber > 2) {
1246			atLeastOne = true;
1247			var abilityName = ability.name.toLowerCase().replace(' ', '_');
1248			showOrBroadcast(user, cmd, room, socket,
1249					'<a href="http://www.smogon.com/'+generation+'/abilities/'+abilityName+'" target="_blank">'+generation.toUpperCase()+' '+ability.name+' ability analysis</a>, brought to you by <a href="http://www.smogon.com" target="_blank">Smogon University</a>');
1250		}
1251		
1252		// Move
1253		if (move.exists) {
1254			atLeastOne = true;
1255			var moveName = move.name.toLowerCase().replace(' ', '_');
1256			showOrBroadcast(user, cmd, room, socket,
1257					'<a href="http://www.smogon.com/'+generation+'/moves/'+moveName+'" target="_blank">'+generation.toUpperCase()+' '+move.name+' move analysis</a>, brought to you by <a href="http://www.smogon.com" target="_blank">Smogon University</a>');
1258		}
1259		
1260		if (!atLeastOne) {
1261			showOrBroadcast(user, cmd, room, socket, 'Pokemon, item, move, or ability not found for generation ' + generation.toUpperCase() + '.');
1262			return false;
1263		}
1264		
1265		return false;
1266		break;
1267
1268	case 'join':
1269		var targetRoom = Rooms.get(target);
1270		if (!targetRoom) {
1271			emit(socket, 'console', "The room '"+target+"' does not exist.");
1272			return false;
1273		}
1274		if (!user.joinRoom(targetRoom, socket)) {
1275			emit(socket, 'console', "The room '"+target+"' could not be joined (most likely, you're already in it).");
1276			return false;
1277		}
1278		return false;
1279		break;
1280
1281	case 'leave':
1282	case 'part':
1283		if (room.id === 'lobby') return false;
1284
1285		user.leaveRoom(room, socket);
1286		return false;
1287		break;
1288
1289	// Battle commands
1290
1291	case 'reset':
1292	case 'restart':
1293		emit(socket, 'console', 'This functionality is no longer available.');
1294		return false;
1295		break;
1296
1297	case 'move':
1298	case 'attack':
1299	case 'mv':
1300		if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
1301
1302		room.decision(user, 'choose', 'move '+target);
1303		return false;
1304		break;
1305
1306	case 'switch':
1307	case 'sw':
1308		if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
1309
1310		room.decision(user, 'choose', 'switch '+parseInt(target,10));
1311		return false;
1312		break;
1313
1314	case 'choose':
1315		if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
1316
1317		room.decision(user, 'choose', target);
1318		return false;
1319		break;
1320
1321	case 'undo':
1322		if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
1323
1324		room.decision(user, 'undo', target);
1325		return false;
1326		break;
1327
1328	case 'team':
1329		if (!room.decision) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
1330
1331		room.decision(user, 'choose', 'team '+target);
1332		return false;
1333		break;
1334
1335	case 'search':
1336	case 'cancelsearch':
1337		if (!room.searchBattle) { emit(socket, 'console', 'You can only do this in lobby rooms.'); return false; }
1338
1339		if (target) {
1340			room.searchBattle(user, target);
1341		} else {
1342			room.cancelSearch(user);
1343		}
1344		return false;
1345		break;
1346
1347	case 'challenge':
1348	case 'chall':
1349		var targets = splitTarget(target);
1350		var targetUser = targets[0];
1351		target = targets[1];
1352		if (!targetUser || !targetUser.connected) {
1353			emit(socket, 'message', "The user '"+targets[2]+"' was not found.");
1354			return false;
1355		}
1356		if (targetUser.blockChallenges && !user.can('bypassblocks', targetUser)) {
1357			emit(socket, 'message', "The user '"+targets[2]+"' is not accepting challenges right now.");
1358			return false;
1359		}
1360		if (typeof target !== 'string') target = 'customgame';
1361		var problems = Tools.validateTeam(user.team, target);
1362		if (problems) {
1363			emit(socket, 'message', "Your team was rejected for the following reasons:\n\n- "+problems.join("\n- "));
1364			return false;
1365		}
1366		user.makeChallenge(targetUser, target);
1367		return false;
1368		break;
1369		
1370	case 'away':
1371	case 'idle':
1372	case 'blockchallenges':
1373		user.blockChallenges = true;
1374		emit(socket, 'console', 'You are now blocking all incoming challenge requests.');
1375		return false;
1376		break;
1377
1378	case 'back':
1379	case 'allowchallenges':
1380		user.blockChallenges = false;
1381		emit(socket, 'console', 'You are available for challenges from now on.');
1382		return false;
1383		break;
1384
1385	case 'cancelchallenge':
1386	case 'cchall':
1387		user.cancelChallengeTo(target);
1388		return false;
1389		break;
1390
1391	case 'accept':
1392		var userid = toUserid(target);
1393		var format = 'debugmode';
1394		if (user.challengesFrom[userid]) format = user.challengesFrom[userid].format;
1395		var problems = Tools.validateTeam(user.team, format);
1396		if (problems) {
1397			emit(socket, 'message', "Your team was rejected for the following reasons:\n\n- "+problems.join("\n- "));
1398			return false;
1399		}
1400		user.acceptChallengeFrom(userid);
1401		return false;
1402		break;
1403
1404	case 'reject':
1405		user.rejectChallengeFrom(toUserid(target));
1406		return false;
1407		break;
1408
1409	case 'saveteam':
1410	case 'utm':
1411		try {
1412			user.team = JSON.parse(target);
1413			user.emit('update', {team: 'saved', room: 'teambuilder'});
1414		} catch (e) {
1415			emit(socket, 'console', 'Not a valid team.');
1416		}
1417		return false;
1418		break;
1419
1420	case 'joinbattle':
1421	case 'partbattle':
1422		if (!room.joinBattle) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
1423
1424		room.joinBattle(user);
1425		return false;
1426		break;
1427
1428	case 'leavebattle':
1429		if (!room.leaveBattle) { emit(socket, 'console', 'You can only do this in battle rooms.'); return false; }
1430
1431		room.leaveBattle(user);
1432		return false;
1433		break;
1434
1435	case 'kickinactive':
1436		if (room.requestKickInactive) {
1437			room.requestKickInactive(user);
1438		} else {
1439			emit(socket, 'console', 'You can only kick inactive players from inside a room.');
1440		}
1441		return false;
1442		break;
1443
1444	case 'timer':
1445		target = toId(target);
1446		if (room.requestKickInactive) {
1447			if (target === 'off' || target === 'stop') {
1448				room.stopKickInactive(user, user.can('timer'));
1449			} else if (target === 'on' || !target) {
1450				room.requestKickInactive(user, user.can('timer'));
1451			} else {
1452				emit(socket, 'console', "'"+target+"' is not a recognized timer state.");
1453			}
1454		} else {
1455			emit(socket, 'console', 'You can only set the timer from inside a room.');
1456		}
1457		return false;
1458		break;
1459		break;
1460
1461	case 'lobbychat':
1462		target = toId(target);
1463		if (target === 'off') {
1464			user.blockLobbyChat = true;
1465			emit(socket, 'console', 'You are now blocking lobby chat.');
1466		} else {
1467			user.blockLobbyChat = false;
1468			emit(socket, 'console', 'You are now receiving lobby chat.');
1469		}
1470		return false;
1471		break;
1472		break;
1473
1474	case 'a':
1475		if (user.can('battlemessage')) {
1476			// secret sysop command
1477			room.battle.add(target);
1478			return false;
1479		}
1480		break;
1481
1482	// Admin commands
1483
1484	case 'forcewin':
1485	case 'forcetie':
1486		if (!user.can('forcewin') || !room.battle) {
1487			emit(socket, 'console', '/forcewin - Access denied.');
1488			return false;
1489		}
1490
1491		room.battle.endType = 'forced';
1492		if (!target) {
1493			room.battle.tie();
1494			logModCommand(room,user.name+' forced a tie.',true);
1495			return false;
1496		}
1497		target = Users.get(target);
1498		if (target) target = target.userid;
1499		else target = '';
1500
1501		if (target) {
1502			room.battle.win(target);
1503			logModCommand(room,user.name+' forced a win for '+target+'.',true);
1504		}
1505
1506		return false;
1507		break;
1508
1509	case 'potd':
1510		if (!user.can('potd')) {
1511			emit(socket, 'console', '/potd - Access denied.');
1512			return false;
1513		}
1514
1515		config.potd = target;
1516		Simulator.eval('config.potd = \''+toId(target)+'\'');
1517		if (target) {
1518			rooms.lobby.addRaw('<div class="broadcast-blue"><b>The Pokemon of the Day is now '+target+'!</b><br />This Pokemon will be guaranteed to show up in random battles.</div>');
1519			logModCommand(room, 'The Pokemon of the Day was changed to '+target+' by '+user.name+'.', true);
1520		} else {
1521			rooms.lobby.addRaw('<div class="broadcast-blue"><b>The Pokemon of the Day was removed!</b><br />No pokemon will be guaranteed in random battles.</div>');
1522			logModCommand(room, 'The Pokemon of the Day was removed by '+user.name+'.', true);
1523		}
1524		return false;
1525		break;
1526
1527	case 'lockdown':
1528		if (!user.can('lockdown')) {
1529			emit(socket, 'console', '/lockdown - Access denied.');
1530			return false;
1531		}
1532
1533		lockdown = true;
1534		for (var id in rooms) {
1535			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>');
1536			if (rooms[id].requestKickInactive) rooms[id].requestKickInactive(user, true);
1537		}
1538
1539		rooms.lobby.logEntry(user.name + ' used /lockdown');
1540
1541		return false;
1542		break;
1543
1544	case 'endlockdown':
1545		if (!user.can('lockdown')) {
1546			emit(socket, 'console', '/endlockdown - Access denied.');
1547			return false;
1548		}
1549
1550		lockdown = false;
1551		for (var id in rooms) {
1552			rooms[id].addRaw('<div class="broadcast-green"><b>The server shutdown was canceled.</b></div>');
1553		}
1554
1555		rooms.lobby.logEntry(user.name + ' used /endlockdown');
1556
1557		return false;
1558		break;
1559
1560	case 'kill':
1561		if (!user.can('lockdown')) {
1562			emit(socket, 'console', '/lockdown - Access denied.');
1563			return false;
1564		}
1565
1566		if (!lockdown) {
1567			emit(socket, 'console', 'For safety reasons, /kill can only be used during lockdown.');
1568			return false;
1569		}
1570
1571		if (updateServerLock) {
1572			emit(socket, 'console', 'Wait for /updateserver to finish before using /kill.');
1573			return false;
1574		}
1575
1576		rooms.lobby.destroyLog(function() {
1577			rooms.lobby.logEntry(user.name + ' used /kill');
1578		}, function() {
1579			process.exit();
1580		});
1581
1582		// Just in the case the above never terminates, kill the process
1583		// after 10 seconds.
1584		setTimeout(function() {
1585			process.exit();
1586		}, 10000);
1587		return false;
1588		break;
1589
1590	case 'loadbanlist':
1591		if (!user.can('declare')) {
1592			emit(socket, 'console', '/loadbanlist - Access denied.');
1593			return false;
1594		}
1595
1596		emit(socket, 'console', 'loading');
1597		fs.readFile('config/ipbans.txt', function (err, data) {
1598			if (err) return;
1599			data = (''+data).split("\n");
1600			for (var i=0; i<data.length; i++) {
1601				if (data[i]) bannedIps[data[i]] = '#ipban';
1602			}
1603			emit(socket, 'console', 'banned '+i+' ips');
1604		});
1605		return false;
1606		break;
1607
1608	case 'refreshpage':
1609		if (!user.can('hotpatch')) {
1610			emit(socket, 'console', '/refreshpage - Access denied.');
1611			return false;
1612		}
1613		rooms.lobby.send('|refresh|');
1614		rooms.lobby.logEntry(user.name + ' used /refreshpage');
1615		return false;
1616		break;
1617
1618	case 'updateserver':
1619		if (!user.can('console')) {
1620			emit(socket, 'console', '/updateserver - Access denied.');
1621			return false;
1622		}
1623
1624		if (updateServerLock) {
1625			emit(socket, 'console', '/updateserver - Another update is already in progress.');
1626			return false;
1627		}
1628
1629		updateServerLock = true;
1630
1631		var logQueue = [];
1632		logQueue.push(user.name + ' used /updateserver');
1633
1634		var exec = require('child_process').exec;
1635		exec('git diff-index --quiet HEAD --', function(error) {
1636			var cmd = 'git pull --rebase';
1637			if (error) {
1638				if (error.code === 1) {
1639					// The working directory or index have local changes.
1640					cmd = 'git stash;' + cmd + ';git stash pop';
1641				} else {
1642					// The most likely case here is that the user does not have
1643					// `git` on the PATH (which would be error.code === 127).
1644					user.emit('console', '' + error);
1645					logQueue.push('' + error);
1646					logQueue.forEach(rooms.lobby.logEntry);
1647					updateServerLock = false;
1648					return;
1649				}
1650			}
1651			var entry = 'Running `' + cmd + '`';
1652			user.emit('console', entry);
1653			logQueue.push(entry);
1654			exec(cmd, function(error, stdout, stderr) {
1655				('' + stdout + stderr).split('\n').forEach(function(s) {
1656					user.emit('console', s);
1657					logQueue.push(s);
1658				});
1659				logQueue.forEach(rooms.lobby.logEntry);
1660				updateServerLock = false;
1661			});
1662		});
1663		return false;
1664		break;
1665
1666	case 'crashfixed':
1667		if (!lockdown) {
1668			emit(socket, 'console', '/crashfixed - There is no active crash.');
1669			return false;
1670		}
1671		if (!user.can('hotpatch')) {
1672			emit(socket, 'console', '/crashfixed - Access denied.');
1673			return false;
1674		}
1675
1676		lockdown = false;
1677		config.modchat = false;
1678		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>');
1679		rooms.lobby.logEntry(user.name + ' used /crashfixed');
1680		return false;
1681		break;
1682	case 'crashnoted':
1683	case 'crashlogged':
1684		if (!lockdown) {
1685			emit(socket, 'console', '/crashnoted - There is no active crash.');
1686			return false;
1687		}
1688		if (!user.can('declare')) {
1689			emit(socket, 'console', '/crashnoted - Access denied.');
1690			return false;
1691		}
1692
1693		lockdown = false;
1694		config.modchat = false;
1695		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>');
1696		rooms.lobby.logEntry(user.name + ' used /crashnoted');
1697		return false;
1698		break;
1699	case 'modlog':
1700		if (!user.can('modlog')) {
1701			emit(socket, 'console', '/modlog - Access denied.');
1702			return false;
1703		}
1704		var lines = parseInt(target || 15, 10);
1705		if (lines > 100) lines = 100;
1706		var filename = 'logs/modlog.txt';
1707		var command = 'tail -'+lines+' '+filename;
1708		var grepLimit = 100;
1709		if (!lines || lines < 0) { // searching for a word instead
1710			if (target.match(/^["'].+["']$/)) target = target.substring(1,target.length-1);
1711			command = "awk '{print NR,$0}' "+filename+" | sort -nr | cut -d' ' -f2- | grep -m"+grepLimit+" -i '"+target.replace(/\\/g,'\\\\\\\\').replace(/["'`]/g,'\'\\$&\'').replace(/[\{\}\[\]\(\)\$\^\.\?\+\-\*]/g,'[$&]')+"'";
1712		}
1713
1714		require('child_process').exec(command, function(error, stdout, stderr) {
1715			if (error && stderr) {
1716				emit(socket, 'console', '/modlog errored, tell Zarel or bmelts.');
1717				console.log('/modlog error: '+error);
1718				return false;
1719			}
1720			if (lines) {
1721				if (!stdout) {
1722					emit(socket, 'console', 'The modlog is empty. (Weird.)');
1723				} else {
1724					emit(socket, 'message', 'Displaying the last '+lines+' lines of the Moderator Log:\n\n'+sanitize(stdout));
1725				}
1726			} else {
1727				if (!stdout) {
1728					emit(socket, 'console', 'No moderator actions containing "'+target+'" were found.');
1729				} else {
1730					emit(socket, 'message', 'Displaying the last '+grepLimit+' logged actions containing "'+target+'":\n\n'+sanitize(stdout));
1731				}
1732			}
1733		});
1734		return false;
1735		break;
1736	case 'banword':
1737	case 'bw':
1738		if (!user.can('declare')) {
1739			emit(socket, 'console', '/banword - Access denied.');
1740			return false;
1741		}
1742		target = toId(target);
1743		if (!target) {
1744			emit(socket, 'console', 'Specify a word or phrase to ban.');
1745			return false;
1746		}
1747		Users.addBannedWord(target);
1748		emit(socket, 'console', 'Added \"'+target+'\" to the list of banned words.');
1749		return false;
1750		break;
1751	case 'unbanword':
1752	case 'ubw':
1753		if (!user.can('declare')) {
1754			emit(socket, 'console', '/unbanword - Access denied.');
1755			return false;
1756		}
1757		target = toId(target);
1758		if (!target) {
1759			emit(socket, 'console', 'Specify a word or phrase to unban.');
1760			return false;
1761		}
1762		Users.removeBannedWord(target);
1763		emit(socket, 'console', 'Removed \"'+target+'\" from the list of banned words.');
1764		return false;
1765		break;
1766	case 'help':
1767	case 'commands':
1768	case 'h':
1769	case '?':
1770		target = target.toLowerCase();
1771		var matched = false;
1772		if (target === 'all' || target === 'msg' || target === 'pm' || cmd === 'whisper' || cmd === 'w') {
1773			matched = true;
1774			emit(socket, 'console', '/msg OR /whisper OR /w [username], [message] - Send a private message.');
1775		}
1776		if (target === 'all' || target === 'r' || target === 'reply') {
1777			matched = true;
1778			emit(socket, 'console', '/reply OR /r [message] - Send a private message to the last person you received a message from, or sent a message to.');
1779		}
1780		if (target === 'all' || target === 'getip' || target === 'ip') {
1781			matched = true;
1782			emit(socket, 'console', '/ip - Get your own IP address.');
1783			emit(socket, 'console', '/ip [username] - Get a user\'s IP address. Requires: @ & ~');
1784		}
1785		if (target === 'all' || target === 'rating' || target === 'ranking' || target === 'rank' || target === 'ladder') {
1786			matched = true;
1787			emit(socket, 'console', '/rating - Get your own rating.');
1788			emit(socket, 'console', '/rating [username] - Get user\'s rating.');
1789		}
1790		if (target === 'all' || target === 'nick') {
1791			matched = true;
1792			emit(socket, 'console', '/nick [new username] - Change your username.');
1793		}
1794		if (target === 'all' || target === 'avatar') {
1795			matched = true;
1796			emit(socket, 'console', '/avatar [new avatar number] - Change your trainer sprite.');
1797		}
1798		if (target === 'all' || target === 'rooms') {
1799			matched = true;
1800			emit(socket, 'console', '/rooms [username] - Show what rooms a user is in.');
1801		}
1802		if (target === 'all' || target === 'whois') {
1803			matched = true;
1804			emit(socket, 'console', '/whois [username] - Get details on a username: group, and rooms.');
1805		}
1806		if (target === 'all' || target === 'data') {
1807			matched = true;
1808			emit(socket, 'console', '/data [pokemon/item/move/ability] - Get details on this pokemon/item/move/ability.');
1809			emit(socket, 'console', '!data [pokemon/item/move/ability] - Show everyone these details. Requires: + % @ & ~');
1810		}
1811		if (target === "all" || target === 'analysis') {
1812			matched = true;
1813			emit(socket, 'console', '/analysis [pokemon], [generation] - Links to the Smogon University analysis for this Pokemon in the given generation.');
1814			emit(socket, 'console', '!analysis [pokemon], [generation] - Shows everyone this link. Requires: + % @ & ~');
1815		}
1816		if (target === 'all' || target === 'groups') {
1817			matched = true;
1818			emit(socket, 'console', '/groups - Explains what the + % @ & next to people\'s names mean.');
1819			emit(socket, 'console', '!groups - Show everyone that information. Requires: + % @ & ~');
1820		}
1821		if (target === 'all' || target === 'opensource') {
1822			matched = true;
1823			emit(socket, 'console', '/opensource - Links to PS\'s source code repository.');
1824			emit(socket, 'console', '!opensource - Show everyone that information. Requires: + % @ & ~');
1825		}
1826		if (target === 'all' || target === 'avatars') {
1827			matched = true;
1828			emit(socket, 'console', '/avatars - Explains how to change avatars.');
1829			emit(socket, 'console', '!avatars - Show everyone that information. Requires: + % @ & ~');
1830		}
1831		if (target === 'all' || target === 'intro') {
1832			matched = true;
1833			emit(socket, 'console', '/intro - Provides an introduction to competitive pokemon.');
1834			emit(socket, 'console', '!intro - Show everyone that information. Requires: + % @ & ~');
1835		}
1836		if (target === 'all' || target === 'cap') {
1837			matched = true;
1838			emit(socket, 'console', '/cap - Provides an introduction to the Create-A-Pokemon project.');
1839			emit(socket, 'console', '!cap - Show everyone that information. Requires: + % @ & ~');
1840		}
1841		if (target === 'all' || target === 'om') {
1842			matched = true;
1843			emit(socket, 'console', '/om - Provides links to information on the Other Metagames.');
1844			emit(socket, 'console', '!om - Show everyone that information. Requires: + % @ & ~');
1845		}
1846		if (target === 'all' || target === 'learn' || target === 'learnset' || target === 'learnall') {
1847			matched = true;
1848			emit(socket, 'console', '/learn [pokemon], [move, move, ...] - Displays how a Pokemon can learn the given moves, if it can at all.')
1849			emit(socket, 'console', '!learn [pokemon], [move, move, ...] - Show everyone that information. Requires: + % @ & ~')
1850		}
1851		if (target === 'all' || target === 'calc' || target === 'caclulator') {
1852			matched = true;
1853			emit(socket, 'console', '/calc - Provides a link to a damage calculator');
1854			emit(socket, 'console', '!calc - Shows everyone a link to a damage calculator. Requires: + % @ & ~');
1855		}
1856		if (target === 'all' || target === 'blockchallenges' || target === 'away' || target === 'idle') {
1857			matched = true;
1858			emit(socket, 'console', '/away - Blocks challenges so no one can challenge you.');
1859		}
1860		if (target === 'all' || target === 'allowchallenges' || target === 'back') {
1861			matched = true;
1862			emit(socket, 'console', '/back - Unlocks challenges so you can be challenged again.');
1863		}
1864		if (target === 'all' || target === 'faq') {
1865			matched = true;
1866			emit(socket, 'console', '/faq [theme] - Provides a link to the FAQ. Add deviation, doubles, randomcap, restart, or staff for a link to these questions. Add all for all of them.');
1867			emit(socket, 'console', '!faq [theme] - Shows everyone a link to the FAQ. Add deviation, doubles, randomcap, restart, or staff for a link to these questions. Add all for all of them. Requires: + % @ & ~');
1868		}
1869		if (target === 'all' || target === 'highlight') {
1870			matched = true;
1871			emit(socket, 'console', 'Set up highlights:');
1872			emit(socket, 'console', '/highlight add, word - add a new word to the highlight list.');
1873			emit(socket, 'console', '/highlight list - list all words that currently highlight you.');
1874			emit(socket, 'console', '/highlight delete, word - delete a word from the highlight list.');
1875			emit(socket, 'console', '/highlight delete - clear the highlight list');
1876		}
1877		if (target === 'timestamps') {
1878			matched = true;
1879			emit(socket, 'console', 'Set your timestamps preference:');
1880			emit(socket, 'console', '/timestamps [all|lobby|pms], [minutes|seconds|off]');
1881			emit(socket, 'console', 'all - change all timestamps preferences, lobby - change only lobby chat preferences, pms - change only PM preferences');
1882			emit(socket, 'console', 'off - set timestamps off, minutes - show timestamps of the form [hh:mm], seconds - show timestamps of the form [hh:mm:ss]');
1883		}
1884		if (target === '%' || target === 'altcheck' || target === 'alt' || target === 'alts' || target === 'getalts') {
1885			matched = true;
1886			emit(socket, 'console', '/alts OR /altcheck OR /alt OR /getalts [username] - Get a user\'s alts. Requires: @ & ~');
1887		}
1888		if (target === '%' || target === 'forcerename' || target === 'fr') {
1889			matched = true;
1890			emit(socket, 'console', '/forcerename OR /fr [username], [reason] - Forcibly change a user\'s name and shows them the [reason]. Requires: @ & ~');
1891		}
1892		if (target === '@' || target === 'ban' || target === 'b') {
1893			matched = true;
1894			emit(socket, 'console', '/ban OR /b [username], [reason] - Kick user from all rooms and ban user\'s IP address with reason. Requires: @ & ~');
1895		}
1896		if (target === "@" || target === 'kick' || target === 'k') {
1897			matched = true;
1898			emit(socket, 'console', '/kick OR /k [username] - Quickly kicks a user by redirecting them to the Pokemon Showdown Rules page. Requires: @ & ~');
1899		}
1900		if (target === '@' || target === 'unban') {
1901			matched = true;
1902			emit(socket, 'console', '/unban [username] - Unban a user. Requires: @ & ~');
1903		}
1904		if (target === '@' || target === 'unbanall') {
1905			matched = true;
1906			emit(socket, 'console', '/unbanall - Unban all IP addresses. Requires: @ & ~');
1907		}
1908		if (target === '@' || target === 'modlog') {
1909			matched = true;
1910			emit(socket, 'console', '/modlog [n] - If n is a number or omitted, display the last n lines of the moderator log. Defaults to 15. If n is not a number, search the moderator log for "n". Requires: @ & ~');
1911		}
1912		if (target === '%' || target === 'mute' || target === 'm') {
1913			matched = true;
1914			emit(socket, 'console', '/mute OR /m [username], [reason] - Mute user with reason. Requires: % @ & ~');
1915		}
1916		if (target === '%' || target === 'unmute') {
1917			matched = true;
1918			emit(socket, 'console', '/unmute [username] - Remove mute from user. Requires: % @ & ~');
1919		}
1920		if (target === '&' || target === 'promote') {
1921			matched = true;
1922			emit(socket, 'console', '/promote [username], [group] - Promotes the user to the specified group or next ranked group. Requires: & ~');
1923		}
1924		if (target === '&' || target === 'demote') {
1925			matched = true;
1926			emit(socket, 'console', '/demote [username], [group] - Demotes the user to the specified group or previous ranked group. Requires: & ~');
1927		}
1928		if (target === '&' || target === 'namelock' || target === 'nl') {
1929			matched === true;
1930			emit(socket, 'console', '/namelock OR /nl [username] - Disallowes the used from changing their names. Requires: & ~');
1931		}
1932		if (target === '&' || target === 'unnamelock') {
1933			matched === true;
1934			emit(socket, 'console', '/unnamelock - Removes name lock from user. Requres: & ~');
1935		}
1936		if (target === '&' || target === 'forcerenameto' || target === 'frt') {
1937			matched = true;
1938			emit(socket, 'console', '/forcerenameto OR /frt [username] - Force a user to choose a new name. Requires: & ~');
1939			emit(socket, 'console', '/forcerenameto OR /frt [username], [new name] - Forcibly change a user\'s name to [new name]. Requires: & ~');
1940		}
1941		if (target === '&' || target === 'forcetie') {
1942			matched === true;
1943			emit(socket, 'console', '/forcetie - Forces the current match to tie. Requires: & ~');
1944		}
1945		if (target === '&' || target === 'declare' ) {
1946			matched = true;
1947			emit(socket, 'console', '/declare [message] - Anonymously announces a message. Requires: & ~');
1948		}
1949		if (target === '&' || target === 'potd' ) {
1950			matched = true;
1951			emit(socket, 'console', '/potd [pokemon] - Sets the Random Battle Pokemon of the Day. Requires: & ~');
1952		}
1953		if (target === '%' || target === 'announce' || target === 'wall' ) {
1954			matched = true;
1955			emit(socket, 'console', '/announce OR /wall [message] - Makes an announcement. Requires: % @ & ~');
1956		}
1957		if (target === '@' || target === 'modchat') {
1958			matched = true;
1959			emit(socket, 'console', '/modchat [on/off/+/%/@/&/~] - Set the level of moderated chat. Requires: @ & ~');
1960		}
1961		if (target === '~' || target === 'hotpatch') {
1962			matched = true;
1963			emit(socket, 'console', 'Hot-patching the game engine allows you to update parts of Showdown without interrupting currently-running battles. Requires: ~');
1964			emit(socket, 'console', 'Hot-patching has greater memory requirements than restarting.');
1965			emit(socket, 'console', '/hotpatch all - reload the game engine, data, and chat commands');
1966			emit(socket, 'console', '/hotpatch data - reload the game data (abilities, moves...)');
1967			emit(socket, 'console', '/hotpatch chat - reload chat-commands.js');
1968		}
1969		if (target === '~' || target === 'lockdown') {
1970			matched = true;
1971			emit(socket, 'console', '/lockdown - locks down the server, which prevents new battles from starting so that the server can eventually be restarted. Requires: ~');
1972		}
1973		if (target === '~' || target === 'kill') {
1974			matched = true;
1975			emit(socket, 'console', '/kill - kills the server. Can\'t be done unless the server is in lockdown state. Requires: ~');
1976		}
1977		if (target === 'all' || target === 'help' || target === 'h' || target === '?' || target === 'commands') {
1978			matched = true;
1979			emit(socket, 'console', '/help OR /h OR /? - Gives you help.');
1980		}
1981		if (!target) {
1982			emit(socket, 'console', 'COMMANDS: /msg, /reply, /ip, /rating, /nick, /avatar, /rooms, /whois, /help, /away, /back, /timestamps');
1983			emit(socket, 'console', 'INFORMATIONAL COMMANDS: /data, /groups, /opensource, /avatars, /faq, /rules, /intro, /tiers, /othermetas, /learn, /analysis, /calc (replace / with ! to broadcast. (Requires: + % @ & ~))');
1984			emit(socket, 'console', 'For details on all commands, use /help all');
1985			if (user.group !== config.groupsranking[0]) {
1986				emit(socket, 'console', 'DRIVER COMMANDS: /mute, /unmute, /announce, /forcerename, /alts')
1987				emit(socket, 'console', 'MODERATOR COMMANDS: /ban, /unban, /unbanall, /ip, /modlog, /redirect, /kick');
1988				emit(socket, 'console', 'LEADER COMMANDS: /promote, /demote, /forcerenameto, /namelock, /nameunlock, /forcewin, /forcetie, /declare');
1989				emit(socket, 'console', 'For details on all moderator commands, use /help @');
1990			}
1991			emit(socket, 'console', 'For details of a specific command, use something like: /help data');
1992		} else if (!matched) {
1993			emit(socket, 'console', 'The command "/'+target+'" was not found. Try /help for general help');
1994		}
1995		return false;
1996		break;
1997
1998	default:
1999		// Check for mod/demod/admin/deadmin/etc depending on the group ids
2000		for (var g in config.groups) {
2001			if (cmd === config.groups[g].id) {
2002				return parseCommand(user, 'promote', toUserid(target) + ',' + g, room, socket);
2003			} else if (cmd === 'de' + config.groups[g].id || cmd === 'un' + config.groups[g].id) {
2004				var nextGroup = config.groupsranking[config.groupsranking.indexOf(g) - 1];
2005				if (!nextGroup) nextGroup = config.groupsranking[0];
2006				return parseCommand(user, 'demote', toUserid(target) + ',' + nextGroup, room, socket);
2007			}
2008		}
2009	}
2010
2011	if (message.substr(0,1) === '/' && cmd) {
2012		// To guard against command typos, we now emit an error message
2013		emit(socket, 'console', 'The command "/'+cmd+'" was unrecognized. To send a message starting with "/'+cmd+'", type "//'+cmd+'".');
2014		return false;
2015	}
2016
2017	// chat moderation
2018	if (!canTalk(user, room, socket)) {
2019		return false;
2020	}
2021
2022	if (message.match(/\bnimp\.org\b/)) {
2023		// spam site
2024		// todo: custom banlists
2025		return false;
2026	}
2027
2028	// remove zalgo
2029	message = message.replace(/[\u0300-\u036f]{3,}/g,'');
2030
2031	return message;
2032}
2033
2034/**
2035 * Can this user talk?
2036 * Pass the corresponding socket to give the user an error, if not
2037 */
2038function canTalk(user, room, socket) {
2039	if (!user.named) return false;
2040	if (user.muted) {
2041		if (socket) emit(socket, 'console', 'You are muted.');
2042		return false;
2043	}
2044	if (user.blockLobbyChat) {
2045		if (socket) emit(socket, 'console', "You can't send messages while blocking lobby chat.");
2046		return false;
2047	}
2048	if (config.modchat && room.id === 'lobby') {
2049		if (config.modchat === 'crash') {
2050			if (!user.can('ignorelimits')) {
2051				if (socket) emit(socket, 'console', 'Because the server has crashed, you cannot speak in lobby chat.');
2052				return false;
2053			}
2054		} else {
2055			if (!user.authenticated && config.modchat === true) {
2056				if (socket) emit(socket, 'console', 'Because moderated chat is set, you must be registered to speak in lobby chat. To register, simply win a rated battle by clicking the look for battle button');
2057				return false;
2058			} else if (config.groupsranking.indexOf(user.group) < config.groupsranking.indexOf(config.modchat)) {
2059				var groupName = config.groups[config.modchat].name;
2060				if (!groupName) groupName = config.modchat;
2061				if (socket) emit(socket, 'console', 'Because moderated chat is set, you must be of rank ' + groupName +' or higher to speak in lobby chat.');
2062				return false;
2063			}
2064		}
2065	}
2066	return true;
2067}
2068
2069function showOrBroadcastStart(user, cmd, room, socket, message) {
2070	if (cmd.substr(0,1) === '!') {
2071		if (!user.can('broadcast') || user.muted) {
2072			emit(socket, 'console', "You need to be voiced to broadcast this command's information.");
2073			emit(socket, 'console', "To see it for yourself, use: /"+message.substr(1));
2074		} else if (canTalk(user, room, socket)) {
2075			room.add('|c|'+user.getIdentity()+'|'+message);
2076		}
2077	}
2078}
2079
2080function showOrBroadcast(user, cmd, room, socket, rawMessage) {
2081	if (cmd.substr(0,1) !== '!') {
2082		sendData(socket, '>'+room.id+'\n|raw|'+rawMessage);
2083	} else if (user.can('broadcast') && canTalk(user, room)) {
2084		room.addRaw(rawMessage);
2085	}
2086}
2087
2088function getDataMessage(target) {
2089	var pokemon = Tools.getTemplate(target);
2090	var item = Tools.getItem(target);
2091	var move = Tools.getMove(target);
2092	var ability = Tools.getAbility(target);
2093	var atLeastOne = false;
2094	var response = [];
2095	if (pokemon.exists) {
2096		response.push('|c|~|/data-pokemon '+pokemon.name);
2097		atLeastOne = true;
2098	}
2099	if (ability.exists) {
2100		response.push('|c|~|/data-ability '+ability.name);
2101		atLeastOne = true;
2102	}
2103	if (item.exists) {
2104		response.push('|c|~|/data-item '+item.name);
2105		atLeastOne = true;
2106	}
2107	if (move.exists) {
2108		response.push('|c|~|/data-move '+move.name);
2109		atLeastOne = true;
2110	}
2111	if (!atLeastOne) {
2112		response.push("||No pokemon, item, move, or ability named '"+target+"' was found. (Check your spelling?)");
2113	}
2114	return response;
2115}
2116
2117function splitTarget(target, exactName) {
2118	var commaIndex = target.indexOf(',');
2119	if (commaIndex < 0) {
2120		return [Users.get(target, exactName), '', target];
2121	}
2122	var targetUser = Users.get(target.substr(0, commaIndex), exactName);
2123	if (!targetUser) {
2124		targetUser = null;
2125	}
2126	return [targetUser, target.substr(commaIndex+1).trim(), target.substr(0, commaIndex)];
2127}
2128
2129function logModCommand(room, result, noBroadcast) {
2130	if (!noBroadcast) room.add(result);
2131	modlog.write('['+(new Date().toJSON())+'] ('+room.id+') '+result+'\n');
2132}
2133
2134exports.parseCommand = parseCommandLocal;