pokemon-showdown/server/chat-commands.js
Guangcong Luo 87ef0cd5c4 TypeScript: export default
I'm currently pretty annoyed at TypeScript and TC39 for default exports
being a mess.

The goal here is to be able to type

    import Dex from './dex';

instead of any of

    import Dex = require('./dex');
    import {Dex} from './dex';
    import * as Dex from './dex';

This part involves a significant amount of struggle.

First, you can't automatically package up all your exports as your
default export. This leads to extremely un-DRY code, like in sim/index:

    export {
        Pokemon,
        Side,
        Battle,
        PRNG,
        Dex,
        TeamValidator,

        BattleStream,
    };

    export const Sim = {
        Pokemon,
        Side,
        Battle,
        PRNG,
        Dex,
        TeamValidator,

        BattleStream,
    };

(Both of these exports would be entirely unnecessary if you could just
automatically declare the file's exports as a default namespace.)

Second, a default export can't easily be a namespace. And TypeScript
doesn't allow types to exist in objects. Take the example from earlier:

    export const Sim = {
        Pokemon,
    };

If we were to try to use it:

    import Sim from './sim';
    let pokemon: Sim.Pokemon;

you'll get this error:

    Cannot find namespace 'Sim'. ts(2503)

You can, of course, fix this by making Sim a namespace:

    const PokemonT = Pokemon;
    type PokemonT = Pokemon;
    export namespace Sim {
        export const Pokemon = PokemonT;
        type Pokemon = PokemonT;
    }

But this quickly gets ridiculous the more classes you try to export.

You'd think there'd be a better way to do this. But I, at least,
haven't found one.
2019-05-14 20:33:33 +10:00

4369 lines
171 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.
*
* System commands should not be modified, added, or removed. If you'd
* like to modify or add commands, add or edit files in chat-plugins/
*
* For the API, see chat-plugins/COMMANDS.md
*
* @license MIT license
*/
'use strict';
/* eslint no-else-return: "error" */
const crypto = require('crypto');
const FS = require('../.lib-dist/fs').FS;
const MAX_REASON_LENGTH = 300;
const MUTE_LENGTH = 7 * 60 * 1000;
const HOURMUTE_LENGTH = 60 * 60 * 1000;
const MAX_CHATROOM_ID_LENGTH = 225;
/** Require reasons */
const REQUIRE_REASONS = true;
const avatarTable = new Set([
'aaron',
'acetrainercouple-gen3', 'acetrainercouple',
'acetrainerf-gen1', 'acetrainerf-gen1rb', 'acetrainerf-gen2', 'acetrainerf-gen3', 'acetrainerf-gen3rs', 'acetrainerf-gen4dp', 'acetrainerf-gen4', 'acetrainerf',
'acetrainer-gen1', 'acetrainer-gen1rb', 'acetrainer-gen2', 'acetrainer-gen3jp', 'acetrainer-gen3', 'acetrainer-gen3rs', 'acetrainer-gen4dp', 'acetrainer-gen4', 'acetrainer',
'acetrainersnowf',
'acetrainersnow',
'agatha-gen1', 'agatha-gen1rb', 'agatha-gen3',
'alder',
'anabel-gen3',
'archer',
'archie-gen3',
'argenta',
'ariana',
'aromalady-gen3', 'aromalady-gen3rs', 'aromalady',
'artist-gen4', 'artist',
'ash',
'backersf',
'backers',
'backpackerf',
'backpacker',
'baker',
'barry',
'battlegirl-gen3', 'battlegirl-gen4', 'battlegirl',
'beauty-gen1', 'beauty-gen1rb', 'beauty-gen2jp', 'beauty-gen2', 'beauty-gen3', 'beauty-gen3rs', 'beauty-gen4dp', 'beauty-gen5bw2', 'beauty',
'bellelba',
'bellepa',
'benga',
'bertha',
'bianca',
'biker-gen1', 'biker-gen1rb', 'biker-gen2', 'biker-gen3', 'biker-gen4', 'biker',
'bill-gen3',
'birch-gen3',
'birdkeeper-gen1', 'birdkeeper-gen1rb', 'birdkeeper-gen2', 'birdkeeper-gen3', 'birdkeeper-gen3rs', 'birdkeeper-gen4dp', 'birdkeeper',
'blackbelt-gen1', 'blackbelt-gen1rb', 'blackbelt-gen2', 'blackbelt-gen3', 'blackbelt-gen3rs', 'blackbelt-gen4dp', 'blackbelt-gen4', 'blackbelt',
'blaine-gen1', 'blaine-gen1rb', 'blaine-gen2', 'blaine-gen3', 'blaine',
'blue-gen1champion', 'blue-gen1', 'blue-gen1rbchampion', 'blue-gen1rb', 'blue-gen1rbtwo', 'blue-gen1two', 'blue-gen2', 'blue-gen3champion', 'blue-gen3', 'blue-gen3two', 'blue',
'boarder-gen2', 'boarder',
'brandon-gen3',
'brawly-gen3', 'brawly',
'brendan-gen3', 'brendan-gen3rs',
'brock-gen1', 'brock-gen1rb', 'brock-gen2', 'brock-gen3', 'brock',
'bruno-gen1', 'bruno-gen1rb', 'bruno-gen2', 'bruno-gen3', 'bruno',
'brycenman',
'brycen',
'buck',
'bugcatcher-gen1', 'bugcatcher-gen1rb', 'bugcatcher-gen2', 'bugcatcher-gen3', 'bugcatcher-gen3rs', 'bugcatcher',
'bugmaniac-gen3',
'bugsy-gen2', 'bugsy',
'burgh',
'burglar-gen1', 'burglar-gen1rb', 'burglar-gen2', 'burglar-gen3', 'burglar',
'byron',
'caitlin',
'cameraman',
'camper-gen2', 'camper-gen3', 'camper-gen3rs', 'camper',
'candice',
'channeler-gen1', 'channeler-gen1rb', 'channeler-gen3',
'cheren-gen5bw2', 'cheren',
'cheryl',
'chili',
'chuck-gen2', 'chuck',
'cilan',
'clair-gen2', 'clair',
'clay',
'clemont',
'clerkf',
'clerk-boss', 'clerk',
'clown',
'collector-gen3', 'collector',
'colress',
'courtney-gen3',
'cowgirl',
'crasherwake',
'cress',
'crushgirl-gen3',
'crushkin-gen3',
'cueball-gen1', 'cueball-gen1rb', 'cueball-gen3',
'cyclistf-gen4', 'cyclistf',
'cyclist-gen4', 'cyclist',
'cynthia-gen4', 'cynthia',
'cyrus',
'dahlia',
'dancer',
'darach',
'dawn-gen4pt', 'dawn',
'depotagent',
'doctor',
'doubleteam',
'dragontamer-gen3', 'dragontamer',
'drake-gen3',
'drayden',
'elesa-gen5bw2', 'elesa',
'emmet',
'engineer-gen1', 'engineer-gen1rb', 'engineer-gen3',
'erika-gen1', 'erika-gen1rb', 'erika-gen2', 'erika-gen3', 'erika',
'ethan-gen2c', 'ethan-gen2', 'ethan',
'eusine-gen2', 'eusine',
'expertf-gen3',
'expert-gen3',
'falkner-gen2',
'falkner',
'fantina',
'firebreather-gen2',
'firebreather',
'fisherman-gen1', 'fisherman-gen1rb', 'fisherman-gen2jp', 'fisherman-gen3', 'fisherman-gen3rs', 'fisherman-gen4', 'fisherman',
'flannery-gen3', 'flannery',
'flint',
'galacticgruntf',
'galacticgrunt',
'gambler-gen1', 'gambler-gen1rb', 'gambler',
'gamer-gen3',
'gardenia',
'gentleman-gen1', 'gentleman-gen1rb', 'gentleman-gen3', 'gentleman-gen3rs', 'gentleman-gen4dp', 'gentleman-gen4', 'gentleman',
'ghetsis-gen5bw', 'ghetsis',
'giovanni-gen1', 'giovanni-gen1rb', 'giovanni-gen3', 'giovanni',
'glacia-gen3',
'greta-gen3',
'grimsley',
'guitarist-gen3', 'guitarist-gen4', 'guitarist',
'harlequin',
'hexmaniac-gen3jp', 'hexmaniac-gen3',
'hiker-gen1', 'hiker-gen1rb', 'hiker-gen3', 'hiker-gen3rs', 'hiker-gen4', 'hiker',
'hilbert-dueldisk', 'hilbert',
'hilda-dueldisk', 'hilda',
'hooligans',
'hoopster',
'hugh',
'idol',
'infielder',
'ingo',
'interviewers-gen3',
'interviewers',
'iris-gen5bw2', 'iris',
'janine-gen2', 'janine',
'janitor',
'jasmine-gen2', 'jasmine',
'jessiejames-gen1',
'jogger',
'jrtrainerf-gen1', 'jrtrainerf-gen1rb',
'jrtrainer-gen1', 'jrtrainer-gen1rb',
'juan-gen3',
'juan',
'juggler-gen1', 'juggler-gen1rb', 'juggler-gen2', 'juggler-gen3', 'juggler',
'jupiter',
'karen-gen2', 'karen',
'kimonogirl',
'kindler-gen3',
'koga-gen1', 'koga-gen1rb', 'koga-gen3', 'koga',
'kris-gen2',
'lady-gen3', 'lady-gen3rs', 'lady-gen4', 'lady',
'lance-gen1', 'lance-gen1rb', 'lance-gen2', 'lance-gen3', 'lance',
'lass-gen1', 'lass-gen1rb', 'lass-gen2', 'lass-gen3', 'lass-gen3rs', 'lass-gen4dp', 'lass-gen4', 'lass',
'leaf-gen3',
'lenora',
'linebacker',
'li',
'liza',
'lorelei-gen1', 'lorelei-gen1rb', 'lorelei-gen3',
'ltsurge-gen1', 'ltsurge-gen1rb', 'ltsurge-gen2', 'ltsurge-gen3', 'ltsurge',
'lucas-gen4pt', 'lucas',
'lucian',
'lucy-gen3',
'lyra',
'madame-gen4dp', 'madame-gen4', 'madame',
'maid',
'marley',
'marlon',
'marshal',
'mars',
'matt-gen3',
'maxie-gen3',
'may-gen3', 'may-gen3rs',
'maylene',
'medium-gen2jp', 'medium',
'mira',
'misty-gen1', 'misty-gen1rb', 'misty-gen3', 'misty',
'morty-gen2', 'morty',
'mrfuji-gen3',
'musician',
'nate',
'ninjaboy-gen3', 'ninjaboy',
'noland-gen3',
'norman-gen3', 'norman',
'n',
'nurse',
'nurseryaide',
'oak-gen1', 'oak-gen1rb', 'oak-gen3',
'oldcouple-gen3',
'painter-gen3',
'palmer',
'parasollady-gen3', 'parasollady-gen4', 'parasollady',
'petrel',
'phoebe-gen3',
'picnicker-gen2', 'picnicker-gen3', 'picnicker-gen3rs', 'picnicker',
'pilot',
'plasmagruntf-gen5bw', 'plasmagruntf',
'plasmagrunt-gen5bw', 'plasmagrunt',
'pokefanf-gen2', 'pokefanf-gen3', 'pokefanf-gen4', 'pokefanf',
'pokefan-gen3', 'pokefan-gen4', 'pokefan',
'pokekid',
'pokemaniac-gen1', 'pokemaniac-gen1rb', 'pokemaniac-gen3', 'pokemaniac-gen3rs', 'pokemaniac',
'pokemonbreederf-gen3', 'pokemonbreederf-gen3frlg', 'pokemonbreederf-gen4', 'pokemonbreederf',
'pokemonbreeder-gen3', 'pokemonbreeder-gen4', 'pokemonbreeder',
'pokemonrangerf-gen3', 'pokemonrangerf-gen3rs', 'pokemonrangerf-gen4', 'pokemonrangerf',
'pokemonranger-gen3', 'pokemonranger-gen3rs', 'pokemonranger-gen4', 'pokemonranger',
'policeman-gen4', 'policeman',
'preschoolerf',
'preschooler',
'proton',
'pryce',
'psychicf-gen3', 'psychicf-gen3rs', 'psychicf-gen4', 'psychicfjp-gen3', 'psychicf',
'psychic-gen1', 'psychic-gen1rb', 'psychic-gen3', 'psychic-gen3rs', 'psychic-gen4', 'psychic',
'rancher',
'red-gen1main', 'red-gen1', 'red-gen1rb', 'red-gen1title', 'red-gen3', 'red',
'reporter',
'richboy-gen3', 'richboy-gen4', 'richboy',
'riley',
'roark',
'rocker-gen1', 'rocker-gen1rb', 'rocker-gen3',
'rocket-gen1', 'rocket-gen1rb',
'rocketgruntf-gen2', 'rocketgruntf',
'rocketgrunt-gen2', 'rocketgrunt',
'rood',
'rosa',
'roughneck-gen4', 'roughneck',
'roxanne-gen3', 'roxanne',
'roxie',
'ruinmaniac-gen3', 'ruinmaniac-gen3rs', 'ruinmaniac',
'sabrina-gen1', 'sabrina-gen1rb', 'sabrina-gen2', 'sabrina-gen3', 'sabrina',
'sage-gen2', 'sage-gen2jp', 'sage',
'sailor-gen1', 'sailor-gen1rb', 'sailor-gen2', 'sailor-gen3jp', 'sailor-gen3', 'sailor-gen3rs', 'sailor',
'saturn',
'schoolboy-gen2',
'schoolkidf-gen3', 'schoolkidf-gen4', 'schoolkidf',
'schoolkid-gen3', 'schoolkid-gen4dp', 'schoolkid-gen4', 'schoolkid',
'scientistf',
'scientist-gen1', 'scientist-gen1rb', 'scientist-gen2', 'scientist-gen3', 'scientist-gen4dp', 'scientist-gen4', 'scientist',
'shadowtriad',
'shauntal',
'shelly-gen3',
'sidney-gen3',
'silver-gen2kanto', 'silver-gen2', 'silver',
'sisandbro-gen3', 'sisandbro-gen3rs', 'sisandbro',
'skierf-gen4dp', 'skierf',
'skier-gen2', 'skier',
'skyla',
'smasher',
'spenser-gen3',
'srandjr-gen3',
'steven-gen3', 'steven',
'striker',
'supernerd-gen1', 'supernerd-gen1rb', 'supernerd-gen2', 'supernerd-gen3', 'supernerd',
'swimmerf-gen2', 'swimmerf-gen3', 'swimmerf-gen3rs', 'swimmerf-gen4dp', 'swimmerf-gen4', 'swimmerfjp-gen2', 'swimmerf',
'swimmer-gen1', 'swimmer-gen1rb', 'swimmer-gen4dp', 'swimmer-gen4', 'swimmerm-gen2', 'swimmerm-gen3', 'swimmerm-gen3rs', 'swimmer',
'tabitha-gen3',
'tamer-gen1', 'tamer-gen1rb', 'tamer-gen3',
'tateandliza-gen3',
'tate',
'teacher-gen2', 'teacher',
'teamaquabeta-gen3',
'teamaquagruntf-gen3',
'teamaquagruntm-gen3',
'teammagmagruntf-gen3',
'teammagmagruntm-gen3',
'teamrocketgruntf-gen3',
'teamrocketgruntm-gen3',
'teamrocket',
'thorton',
'triathletebikerf-gen3',
'triathletebikerm-gen3',
'triathleterunnerf-gen3',
'triathleterunnerm-gen3',
'triathleteswimmerf-gen3',
'triathleteswimmerm-gen3',
'tuberf-gen3', 'tuberf-gen3rs', 'tuberf',
'tuber-gen3', 'tuber',
'tucker-gen3',
'twins-gen2', 'twins-gen3', 'twins-gen3rs', 'twins-gen4dp', 'twins-gen4', 'twins',
'unknownf',
'unknown',
'veteranf',
'veteran-gen4', 'veteran',
'volkner',
'waiter-gen4dp', 'waiter-gen4', 'waiter',
'waitress-gen4', 'waitress',
'wallace-gen3', 'wallace-gen3rs', 'wallace',
'wally-gen3', 'wally',
'wattson-gen3', 'wattson',
'whitney-gen2', 'whitney',
'will-gen2', 'will',
'winona-gen3', 'winona',
'worker-gen4',
'workerice',
'worker',
'yellow',
'youngcouple-gen3', 'youngcouple-gen3rs', 'youngcouple-gen4dp', 'youngcouple',
'youngster-gen1', 'youngster-gen1rb', 'youngster-gen2', 'youngster-gen3', 'youngster-gen3rs', 'youngster-gen4', 'youngster',
'zinnia',
'zinzolin',
]);
/** @type {ChatCommands} */
const commands = {
'!version': true,
version(target, room, user) {
if (!this.runBroadcast()) return;
this.sendReplyBox(`Server version: <b>${Chat.package.version}</b>`);
},
'!authority': true,
auth: 'authority',
stafflist: 'authority',
globalauth: 'authority',
authlist: 'authority',
authority(target, room, user, connection) {
if (target) {
let targetRoom = Rooms.search(target);
let availableRoom = targetRoom && targetRoom.checkModjoin(user);
if (targetRoom && availableRoom) return this.parse(`/roomauth1 ${target}`);
return this.parse(`/userauth ${target}`);
}
let rankLists = {};
let ranks = Object.keys(Config.groups);
for (let u in Users.usergroups) {
let rank = Users.usergroups[u].charAt(0);
if (rank === ' ' || rank === '+') continue;
// In case the usergroups.csv file is not proper, we check for the server ranks.
if (ranks.includes(rank)) {
let name = Users.usergroups[u].substr(1);
if (!rankLists[rank]) rankLists[rank] = [];
if (name) rankLists[rank].push(name);
}
}
let buffer = Object.keys(rankLists).sort((a, b) =>
(Config.groups[b] || {rank: 0}).rank - (Config.groups[a] || {rank: 0}).rank
).map(r =>
`${(Config.groups[r] ? `**${Config.groups[r].name}s** (${r})` : r)}:\n${rankLists[r].sort((a, b) => toID(a).localeCompare(toID(b))).join(", ")}`
);
if (!buffer.length) return connection.popup("This server has no global authority.");
connection.popup(buffer.join("\n\n"));
},
authhelp: [
`/auth - Show global staff for the server.`,
`/auth [room] - Show what roomauth a room has.`,
`/auth [user] - Show what global and roomauth a user has.`,
],
userlist(target, room, user) {
let userList = [];
for (let i in room.users) {
let curUser = Users(room.users[i]);
if (!curUser || !curUser.named) continue;
userList.push(Chat.escapeHTML(curUser.getIdentity(room.id)));
}
let output = `There ${Chat.plural(userList, "are", "is")} <strong style="color:#24678d">${Chat.count(userList, "</strong> users")} in this room:<br />`;
output += userList.join(`, `);
this.sendReplyBox(output);
},
userlisthelp: [`/userlist - Displays a list of users who are currently in the room.`],
'!me': true,
mee: 'me',
me(target, room, user) {
if (this.cmd === 'mee' && /[A-Z-a-z0-9/]/.test(target.charAt(0))) {
return this.errorReply(`/mee - must not start with a letter or number`);
}
target = this.canTalk(`/${this.cmd} ${target || ''}`);
if (!target) return;
if (this.message.startsWith(`/ME`)) {
const uppercaseIdentity = user.getIdentity(room).toUpperCase();
if (room) {
this.add(`|c|${uppercaseIdentity}|${target}`);
} else {
let msg = `|pm|${uppercaseIdentity}|${this.pmTarget.getIdentity()}|${target}`;
user.send(msg);
if (this.pmTarget !== user) this.pmTarget.send(msg);
}
return;
}
return target;
},
'!battle': true,
'battle!': 'battle',
battle(target, room, user, connection, cmd) {
if (cmd === 'battle') return this.sendReply("What?! How are you not more excited to battle?! Try /battle! to show me you're ready.");
if (!target) target = "randombattle";
return this.parse(`/search ${target}`);
},
'!avatar': true,
avatar(target, room, user) {
if (!target) return this.parse(`${this.cmdToken}avatars`);
let parts = target.split(',');
let avatar = parts[0].toLowerCase().replace(/[^a-z0-9-]+/g, '');
if (!avatarTable.has(avatar)) {
let avatarNum = parseInt(avatar);
if (!avatarNum || avatarNum > 294 || avatarNum < 1) {
if (!parts[1]) {
this.errorReply("Invalid avatar.");
}
return false;
}
avatar = '' + avatarNum;
}
user.avatar = avatar;
let avatarUrl = avatar.startsWith('#') ? `trainers-custom/${avatar.slice(1)}.png` : `trainers/${avatar}.png`;
if (!parts[1]) {
this.sendReply(`Avatar changed to:\n|raw|<img src="//play.pokemonshowdown.com/sprites/${avatarUrl}" alt="${avatar}" width="80" height="80" />`);
}
},
avatarhelp: [`/avatar [avatar number 1 to 293] - Change your trainer sprite.`],
'!logout': true,
signout: 'logout',
logout(target, room, user) {
user.resetName();
},
r: 'reply',
reply(target, room, user) {
if (!target) return this.parse('/help reply');
if (!user.lastPM) {
return this.errorReply("No one has PMed you yet.");
}
return this.parse(`/msg ${user.lastPM || ''}, ${target}`);
},
replyhelp: [`/reply OR /r [message] - Send a private message to the last person you received a message from, or sent a message to.`],
'!msg': true,
pm: 'msg',
whisper: 'msg',
w: 'msg',
msg(target, room, user, connection) {
if (!target) return this.parse('/help msg');
if (!target.includes(',')) {
this.errorReply("You forgot the comma.");
return this.parse('/help msg');
}
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (this.targetUsername === '~') {
this.room = Rooms.global;
this.pmTarget = null;
} else if (!targetUser) {
let error = `User ${this.targetUsername} not found. Did you misspell their name?`;
error = `|pm|${this.user.getIdentity()}| ${this.targetUsername}|/error ${error}`;
connection.send(error);
return;
} else {
this.pmTarget = targetUser;
this.room = undefined;
}
if (targetUser && !targetUser.connected) {
return this.errorReply(`User ${this.targetUsername} is offline.`);
}
this.parse(target);
},
msghelp: [`/msg OR /whisper OR /w [username], [message] - Send a private message.`],
'!invite': true,
inv: 'invite',
invite(target, room, user) {
if (!target) return this.parse('/help invite');
if (!this.canTalk()) return;
if (room) target = this.splitTarget(target) || room.id;
let targetRoom = Rooms.search(target);
if (targetRoom && !targetRoom.checkModjoin(user)) {
targetRoom = undefined;
}
if (room) {
if (!this.targetUser) return this.errorReply(`The user "${this.targetUsername}" was not found.`);
if (!targetRoom) return this.errorReply(`The room "${target}" was not found.`);
return this.parse(`/pm ${this.targetUsername}, /invite ${targetRoom.id}`);
}
let targetUser = this.pmTarget;
if (!targetRoom || targetRoom === Rooms.global) return this.errorReply(`The room "${target}" was not found.`);
if (targetRoom.staffRoom && !targetUser.isStaff) return this.errorReply(`User "${targetUser.name}" requires global auth to join room "${targetRoom.id}".`);
if (!targetUser) return this.errorReply(`The user "${targetUser.name}" was not found.`);
if (!targetRoom.checkModjoin(targetUser)) {
this.room = targetRoom;
this.parse(`/roomvoice ${targetUser.name}`);
if (!targetRoom.checkModjoin(targetUser)) {
return this.errorReply(`You do not have permission to invite people into this room.`);
}
}
if (targetUser in targetRoom.users) return this.errorReply(`This user is already in "${targetRoom.title}".`);
return `/invite ${targetRoom.id}`;
},
invitehelp: [
`/invite [username] - Invites the player [username] to join the room you sent the command to.`,
`(in a PM) /invite [roomname] - Invites the player you're PMing to join the room [roomname].`,
],
pminfobox(target, room, user, connection) {
if (!this.canTalk()) return;
if (!this.can('addhtml', null, room)) return false;
if (!target) return this.parse("/help pminfobox");
target = this.canHTML(this.splitTarget(target));
if (!target) return;
let targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) return this.errorReply(`User ${this.targetUsername} is not currently online.`);
if (!(targetUser in room.users) && !user.can('addhtml')) return this.errorReply("You do not have permission to use this command to users who are not in this room.");
if (targetUser.blockPMs && targetUser.blockPMs !== user.group && !user.can('lock')) return this.errorReply("This user is currently blocking PMs.");
if (targetUser.locked && !user.can('lock')) return this.errorReply("This user is currently locked, so you cannot send them a pminfobox.");
// Apply the infobox to the message
target = `/raw <div class="infobox">${target}</div>`;
let message = `|pm|${user.getIdentity()}|${targetUser.getIdentity()}|${target}`;
user.send(message);
if (targetUser !== user) targetUser.send(message);
targetUser.lastPM = user.userid;
user.lastPM = targetUser.userid;
},
pminfoboxhelp: [`/pminfobox [user], [html]- PMs an [html] infobox to [user]. Requires * ~`],
pmuhtmlchange: 'pmuhtml',
pmuhtml(target, room, user, connection, cmd) {
if (!this.canTalk()) return;
if (!this.can('addhtml', null, room)) return false;
if (!target) return this.parse("/help " + cmd);
target = this.canHTML(this.splitTarget(target));
if (!target) return;
let targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) return this.errorReply(`User ${this.targetUsername} is not currently online.`);
if (!(targetUser in room.users) && !user.can('addhtml')) return this.errorReply("You do not have permission to use this command to users who are not in this room.");
if (targetUser.blockPMs && targetUser.blockPMs !== user.group && !user.can('lock')) return this.errorReply("This user is currently blocking PMs.");
if (targetUser.locked && !user.can('lock')) return this.errorReply("This user is currently locked, so you cannot send them UHTML.");
let message = `|pm|${user.getIdentity()}|${targetUser.getIdentity()}|/uhtml${(cmd === 'pmuhtmlchange' ? 'change' : '')} ${target}`;
user.send(message);
if (targetUser !== user) targetUser.send(message);
targetUser.lastPM = user.userid;
user.lastPM = targetUser.userid;
},
pmuhtmlhelp: [`/pmuhtml [user], [name], [html] - PMs [html] that can change to [user]. Requires * ~`],
pmuhtmlchangehelp: [`/pmuhtmlchange [user], [name], [html] - Changes html that was previously PMed to [user] to [html]. Requires * ~`],
'!blockpms': true,
blockpm: 'blockpms',
ignorepms: 'blockpms',
ignorepm: 'blockpms',
blockpms(target, room, user) {
if (user.blockPMs === (target || true)) return this.errorReply("You are already blocking private messages! To unblock, use /unblockpms");
user.blockPMs = true;
if (target in Config.groups) {
user.blockPMs = target;
user.update('blockPMs');
return this.sendReply(`You are now blocking private messages, except from staff and ${target}.`);
}
user.update();
return this.sendReply("You are now blocking private messages, except from staff.");
},
blockpmshelp: [`/blockpms - Blocks private messages. Unblock them with /unblockpms.`],
'!unblockpms': true,
unblockpm: 'unblockpms',
unignorepms: 'unblockpms',
unignorepm: 'unblockpms',
unblockpms(target, room, user) {
if (!user.blockPMs) return this.errorReply("You are not blocking private messages! To block, use /blockpms");
user.blockPMs = false;
user.update('blockPMs');
return this.sendReply("You are no longer blocking private messages.");
},
unblockpmshelp: [`/unblockpms - Unblocks private messages. Block them with /blockpms.`],
'!away': true,
idle: 'away',
afk: 'away',
away(target, room, user) {
this.parse('/blockchallenges');
this.parse(`/blockpms ${target}`);
},
awayhelp: [`/away - Blocks challenges and private messages. Unblock them with /back.`],
'!back': true,
unaway: 'back',
unafk: 'back',
back() {
this.parse('/unblockpms');
this.parse('/unblockchallenges');
},
backhelp: [`/back - Unblocks challenges and/or private messages, if either are blocked.`],
'!rank': true,
rank(target, room, user) {
if (!target) target = user.name;
Ladders.visualizeAll(target).then(values => {
let buffer = `<div class="ladder"><table>`;
buffer += `<tr><td colspan="8">User: <strong>${Chat.escapeHTML(target)}</strong></td></tr>`;
let ratings = values.join(``);
if (!ratings) {
buffer += `<tr><td colspan="8"><em>This user has not played any ladder games yet.</em></td></tr>`;
} else {
buffer += `<tr><th>Format</th><th><abbr title="Elo rating">Elo</abbr></th><th>W</th><th>L</th><th>Total</th>`;
buffer += ratings;
}
buffer += `</table></div>`;
this.sendReply(`|raw|${buffer}`);
});
},
makeprivatechatroom: 'makechatroom',
makechatroom(target, room, user, connection, cmd) {
if (!this.can('makeroom')) return;
// `,` is a delimiter used by a lot of /commands
// `|` and `[` are delimiters used by the protocol
// `-` has special meaning in roomids
if (target.includes(',') || target.includes('|') || target.includes('[') || target.includes('-')) {
return this.errorReply("Room titles can't contain any of: ,|[-");
}
let id = toID(target);
if (!id) return this.parse('/help makechatroom');
if (id.length > MAX_CHATROOM_ID_LENGTH) return this.errorReply("The given room title is too long.");
// Check if the name already exists as a room or alias
if (Rooms.search(id)) return this.errorReply(`The room '${target}' already exists.`);
if (!Rooms.global.addChatRoom(target)) return this.errorReply(`An error occurred while trying to create the room '${target}'.`);
if (cmd === 'makeprivatechatroom') {
let targetRoom = Rooms.search(target);
targetRoom.isPrivate = true;
targetRoom.chatRoomData.isPrivate = true;
Rooms.global.writeChatRoomData();
if (Rooms.get('upperstaff')) {
Rooms.get('upperstaff').add(`|raw|<div class="broadcast-green">Private chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
}
this.sendReply(`The private chat room '${target}' was created.`);
} else {
if (Rooms.get('staff')) {
Rooms.get('staff').add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
}
if (Rooms.get('upperstaff')) {
Rooms.get('upperstaff').add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
}
this.sendReply(`The chat room '${target}' was created.`);
}
},
makechatroomhelp: [`/makechatroom [roomname] - Creates a new room named [roomname]. Requires: & ~`],
subroomgroupchat: 'makegroupchat',
makegroupchat(target, room, user, connection, cmd) {
if (!this.canTalk()) return;
if (!user.autoconfirmed) {
return this.errorReply("You must be autoconfirmed to make a groupchat.");
}
if (cmd === 'subroomgroupchat') {
if (!user.can('mute', null, room)) return this.errorReply("You can only create subroom groupchats for rooms you're staff in.");
if (room.battle) return this.errorReply("You cannot create a subroom of a battle.");
if (room.isPersonal) return this.errorReply("You cannot create a subroom of a groupchat.");
}
let parent = cmd === 'subroomgroupchat' ? room.id : null;
// if (!this.can('makegroupchat')) return false;
// Title defaults to a random 8-digit number.
let title = target.trim();
if (title.length >= 32) {
return this.errorReply("Title must be under 32 characters long.");
} else if (!title) {
title = (`${Math.floor(Math.random() * 100000000)}`);
} else if (Config.chatfilter) {
let filterResult = Config.chatfilter.call(this, title, user, null, connection);
if (!filterResult) return;
if (title !== filterResult) {
return this.errorReply("Invalid title.");
}
}
// `,` is a delimiter used by a lot of /commands
// `|` and `[` are delimiters used by the protocol
// `-` has special meaning in roomids
if (title.includes(',') || title.includes('|') || title.includes('[') || title.includes('-')) {
return this.errorReply("Room titles can't contain any of: ,|[-");
}
// Even though they're different namespaces, to cut down on confusion, you
// can't share names with registered chatrooms.
let existingRoom = Rooms.search(toID(title));
if (existingRoom && !existingRoom.modjoin) return this.errorReply(`The room '${title}' already exists.`);
// Room IDs for groupchats are groupchat-TITLEID
let titleid = toID(title);
if (!titleid) {
titleid = `${Math.floor(Math.random() * 100000000)}`;
}
let roomid = `groupchat-${parent || user.userid}-${titleid}`;
// Titles must be unique.
if (Rooms.search(roomid)) return this.errorReply(`A group chat named '${title}' already exists.`);
// Tab title is prefixed with '[G]' to distinguish groupchats from
// registered chatrooms
if (Monitor.countGroupChat(connection.ip)) {
this.errorReply("Due to high load, you are limited to creating 4 group chats every hour.");
return;
}
let titleMsg = Chat.html `Welcome to ${parent ? room.title : user.name}'s${!/^[0-9]+$/.test(title) ? ` ${title}` : ''}${parent ? ' subroom' : ''} groupchat!`;
let targetRoom = Rooms.createChatRoom(roomid, `[G] ${title}`, {
isPersonal: true,
isPrivate: 'hidden',
modjoin: parent ? null : '+',
parentid: parent,
auth: {},
introMessage: `<div style="text-align: center"><table style="margin:auto;"><tr><td><img src="//play.pokemonshowdown.com/fx/groupchat.png" width=120 height=100></td><td><h2>${titleMsg}</h2><p>Follow the <a href="/rules">Pokémon Showdown Global Rules</a>!<br>Don't be disruptive to the rest of the site.</p></td></tr></table></div>`,
staffMessage: `<p>Groupchats are temporary rooms, and will expire if there hasn't been any activity in 40 minutes.</p><p>You can invite new users using <code>/invite</code>. Be careful with who you invite!</p><p>Commands: <button class="button" name="send" value="/roomhelp">Room Management</button> | <button class="button" name="send" value="/tournaments help">Tournaments</button></p><p>As creator of this groupchat, <u>you are entirely responsible for what occurs in this chatroom</u>. Global rules apply at all times.</p><p>If this room is used to break global rules or disrupt other areas of the server, <strong>you as the creator will be held accountable and punished</strong>.</p>`,
});
if (targetRoom) {
// The creator is a Room Owner in subroom groupchats and a Host otherwise..
targetRoom.auth[user.userid] = parent ? '#' : Users.HOST_SYMBOL;
// Join after creating room. No other response is given.
user.joinRoom(targetRoom.id);
user.popup(`You've just made a groupchat; it is now your responsibility, regardless of whether or not you actively partake in the room. For more info, read your groupchat's staff intro.`);
if (parent) this.modlog('SUBROOMGROUPCHAT', null, title);
return;
}
return this.errorReply(`An unknown error occurred while trying to create the room '${title}'.`);
},
makegroupchathelp: [
`/makegroupchat [roomname] - Creates an invite-only group chat named [roomname].`,
`/subroomgroupchat [roomname] - Creates a subroom groupchat of the current room. Can only be used in a public room you have staff in.`,
],
deregisterchatroom(target, room, user) {
if (!this.can('makeroom')) return;
this.errorReply("NOTE: You probably want to use `/deleteroom` now that it exists.");
let id = toID(target);
if (!id) return this.parse('/help deregisterchatroom');
let targetRoom = Rooms.search(id);
if (!targetRoom) return this.errorReply(`The room '${target}' doesn't exist.`);
target = targetRoom.title || targetRoom.id;
const isPrivate = targetRoom.isPrivate;
const staffRoom = Rooms('staff');
const upperStaffRoom = Rooms('upperstaff');
if (Rooms.global.deregisterChatRoom(id)) {
this.sendReply(`The room '${target}' was deregistered.`);
this.sendReply("It will be deleted as of the next server restart.");
target = Chat.escapeHTML(target);
if (isPrivate) {
if (upperStaffRoom) upperStaffRoom.add(`|raw|<div class="broadcast-red">Private chat room deregistered by ${user.userid}: <b>${target}</b></div>`).update();
} else {
if (staffRoom) staffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered: <b>${target}</b></div>`).update();
if (upperStaffRoom) upperStaffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered by ${user.userid}: <b>${target}</b></div>`).update();
}
return;
}
return this.errorReply(`The room "${target}" isn't registered.`);
},
deregisterchatroomhelp: [`/deregisterchatroom [roomname] - Deletes room [roomname] after the next server restart. Requires: & ~`],
deletechatroom: 'deleteroom',
deletegroupchat: 'deleteroom',
deleteroom(target, room, user, connection, cmd) {
let roomid = target.trim();
if (!roomid) {
// allow deleting personal rooms without typing out the room name
if (!room.isPersonal || cmd !== "deletegroupchat") {
return this.parse(`/help deleteroom`);
}
} else {
let targetRoom = Rooms.search(roomid);
if (targetRoom !== room) {
return this.parse(`/help deleteroom`);
}
}
if (room.isPersonal) {
if (!this.can('gamemanagement', null, room)) return;
} else {
if (!this.can('makeroom')) return;
}
const title = room.title || room.id;
if (room.id === 'global') {
return this.errorReply(`This room can't be deleted.`);
}
if (room.chatRoomData) {
if (room.isPrivate) {
if (Rooms.get('upperstaff')) {
Rooms.get('upperstaff').add(Chat.html`|raw|<div class="broadcast-red">Private chat room deleted by ${user.userid}: <b>${title}</b></div>`).update();
}
} else {
if (Rooms.get('staff')) {
Rooms.get('staff').add(Chat.html`|raw|<div class="broadcast-red">Public chat room deleted: <b>${title}</b></div>`).update();
}
if (Rooms.get('upperstaff')) {
Rooms.get('upperstaff').add(Chat.html`|raw|<div class="broadcast-red">Public chat room deleted by ${user.userid}: <b>${title}</b></div>`).update();
}
}
}
if (room.subRooms) {
for (const subRoom of room.subRooms.values()) subRoom.parent = null;
}
room.add(`|raw|<div class="broadcast-red"><b>This room has been deleted.</b></div>`);
room.update(); // |expire| needs to be its own message
room.add(`|expire|This room has been deleted.`);
this.sendReply(`The room "${title}" was deleted.`);
room.update();
room.destroy();
},
deleteroomhelp: [
`/deleteroom [roomname] - Deletes room [roomname]. Must be typed in the room to delete. Requires: & ~`,
`/deletegroupchat - Deletes the current room, if it's a groupchat. Requires: ★ # & ~`,
],
hideroom: 'privateroom',
hiddenroom: 'privateroom',
secretroom: 'privateroom',
publicroom: 'privateroom',
privateroom(target, room, user, connection, cmd) {
if (room.isPersonal) {
if (!this.can('editroom', null, room)) return;
} else if (room.battle) {
if (!this.can('editprivacy', null, room)) return;
} else {
// registered chatrooms show up on the room list and so require
// higher permissions to modify privacy settings
if (!this.can('makeroom')) return;
}
let setting;
switch (cmd) {
case 'privateroom':
return this.parse('/help privateroom');
case 'publicroom':
setting = false;
break;
case 'secretroom':
setting = true;
break;
default:
if (room.isPrivate === true && target !== 'force') {
return this.sendReply(`This room is a secret room. Use "/publicroom" to make it public, or "/hiddenroom force" to force it hidden.`);
}
setting = 'hidden';
break;
}
if ((setting === true || room.isPrivate === true) && !room.isPersonal) {
if (!this.can('makeroom')) return;
}
if (this.meansNo(target) || !setting) {
if (!room.isPrivate) {
return this.errorReply(`This room is already public.`);
}
if (room.isPersonal) return this.errorReply(`This room can't be made public.`);
if (room.privacySetter && user.can('nooverride', null, room) && !user.can('makeroom')) {
if (!room.privacySetter.has(user.userid)) {
const privacySetters = [...room.privacySetter].join(', ');
return this.errorReply(`You can't make the room public since you didn't make it private - only ${privacySetters} can.`);
}
room.privacySetter.delete(user.userid);
if (room.privacySetter.size) {
const privacySetters = [...room.privacySetter].join(', ');
return this.sendReply(`You are no longer forcing the room to stay private, but ${privacySetters} also need${Chat.plural(room.privacySetter, "", "s")} to use /publicroom to make the room public.`);
}
}
delete room.isPrivate;
room.privacySetter = null;
this.addModAction(`${user.name} made this room public.`);
this.modlog('PUBLICROOM');
if (room.chatRoomData) {
delete room.chatRoomData.isPrivate;
Rooms.global.writeChatRoomData();
}
} else {
const settingName = (setting === true ? 'secret' : setting);
if (room.subRooms) return this.errorReply("Private rooms cannot have subrooms.");
if (room.isPrivate === setting) {
if (room.privacySetter && !room.privacySetter.has(user.userid)) {
room.privacySetter.add(user.userid);
return this.sendReply(`This room is already ${settingName}, but is now forced to stay that way until you use /publicroom.`);
}
return this.errorReply(`This room is already ${settingName}.`);
}
room.isPrivate = setting;
this.addModAction(`${user.name} made this room ${settingName}.`);
this.modlog(`${settingName.toUpperCase()}ROOM`);
if (room.chatRoomData) {
room.chatRoomData.isPrivate = setting;
Rooms.global.writeChatRoomData();
}
room.privacySetter = new Set([user.userid]);
}
},
privateroomhelp: [
`/secretroom - Makes a room secret. Secret rooms are visible to & and up. Requires: & ~`,
`/hiddenroom [on/off] - Makes a room hidden. Hidden rooms are visible to % and up, and inherit global ranks. Requires: \u2606 & ~`,
`/publicroom - Makes a room public. Requires: \u2606 & ~`,
],
officialchatroom: 'officialroom',
officialroom(target, room, user) {
if (!this.can('makeroom')) return;
if (!room.chatRoomData) {
return this.errorReply(`/officialroom - This room can't be made official`);
}
if (this.meansNo(target)) {
if (!room.isOfficial) return this.errorReply(`This chat room is already unofficial.`);
delete room.isOfficial;
this.addModAction(`${user.name} made this chat room unofficial.`);
this.modlog('UNOFFICIALROOM');
delete room.chatRoomData.isOfficial;
Rooms.global.writeChatRoomData();
} else {
if (room.isOfficial) return this.errorReply(`This chat room is already official.`);
room.isOfficial = true;
this.addModAction(`${user.name} made this chat room official.`);
this.modlog('OFFICIALROOM');
room.chatRoomData.isOfficial = true;
Rooms.global.writeChatRoomData();
}
},
psplwinnerroom(target, room, user) {
if (!this.can('makeroom')) return;
if (!room.chatRoomData) {
return this.errorReply(`/psplwinnerroom - This room can't be marked as a PSPL Winner room`);
}
if (this.meansNo(target)) {
if (!room.pspl) return this.errorReply(`This chat room is already not a PSPL Winner room.`);
delete room.pspl;
this.addModAction(`${user.name} made this chat room no longer a PSPL Winner room.`);
this.modlog('PSPLROOM');
delete room.chatRoomData.pspl;
Rooms.global.writeChatRoomData();
} else {
if (room.pspl) return this.errorReply("This chat room is already a PSPL Winner room.");
room.pspl = true;
this.addModAction(`${user.name} made this chat room a PSPL Winner room.`);
this.modlog('UNPSPLROOM');
room.chatRoomData.pspl = true;
Rooms.global.writeChatRoomData();
}
},
setsubroom: 'subroom',
subroom(target, room, user) {
if (!user.can('makeroom')) return this.errorReply(`/subroom - Access denied. Did you mean /subrooms?`);
if (!target) return this.parse('/help subroom');
if (!room.chatRoomData) return this.errorReply(`Temporary rooms cannot be subrooms.`);
if (room.parent) return this.errorReply(`This room is already a subroom. To change which room this subroom belongs to, remove the subroom first.`);
if (room.subRooms) return this.errorReply(`This room is already a parent room, and a parent room cannot be made as a subroom.`);
const main = Rooms.search(target);
if (!main) return this.errorReply(`The room '${target}' does not exist.`);
if (main.isPrivate || !main.chatRoomData) return this.errorReply(`Only public rooms can have subrooms.`);
if (room === main) return this.errorReply(`You cannot set a room to be a subroom of itself.`);
room.parent = main;
if (!main.subRooms) main.subRooms = new Map();
main.subRooms.set(room.id, room);
const mainIdx = Rooms.global.chatRoomDataList.findIndex(r => r.title === main.title);
const subIdx = Rooms.global.chatRoomDataList.findIndex(r => r.title === room.title);
// This is needed to ensure that the main room gets loaded before the subroom.
if (mainIdx > subIdx) {
const tmp = Rooms.global.chatRoomDataList[mainIdx];
Rooms.global.chatRoomDataList[mainIdx] = Rooms.global.chatRoomDataList[subIdx];
Rooms.global.chatRoomDataList[subIdx] = tmp;
}
room.chatRoomData.parentid = main.id;
Rooms.global.writeChatRoomData();
for (let userid in room.users) {
room.users[userid].updateIdentity(room.id);
}
this.modlog('SUBROOM', null, `of ${main.title}`);
return this.addModAction(`This room was set as a subroom of ${main.title} by ${user.name}.`);
},
removesubroom: 'unsubroom',
desubroom: 'unsubroom',
unsubroom(target, room, user) {
if (!this.can('makeroom')) return;
if (!room.parent || !room.chatRoomData) return this.errorReply(`This room is not currently a subroom of a public room.`);
const parent = room.parent;
if (parent && parent.subRooms) {
parent.subRooms.delete(room.id);
if (!parent.subRooms.size) parent.subRooms = null;
}
room.parent = null;
delete room.chatRoomData.parentid;
Rooms.global.writeChatRoomData();
for (let userid in room.users) {
room.users[userid].updateIdentity(room.id);
}
this.modlog('UNSUBROOM');
return this.addModAction(`This room was unset as a subroom by ${user.name}.`);
},
parentroom: 'subrooms',
subrooms(target, room, user, connection, cmd) {
if (cmd === 'parentroom') {
if (!room.parent) return this.errorReply(`This room is not a subroom.`);
return this.sendReply(`This is a subroom of ${room.parent.title}.`);
}
if (!room.chatRoomData) return this.errorReply(`Temporary rooms cannot have subrooms.`);
if (!this.runBroadcast()) return;
const showSecret = !this.broadcasting && user.can('mute', null, room);
const subRooms = room.getSubRooms(showSecret);
if (!subRooms.length) return this.sendReply(`This room doesn't have any subrooms.`);
const subRoomText = subRooms.map(room => Chat.html`<a href="/${room.id}">${room.title}</a><br/><small>${room.desc}</small>`);
return this.sendReplyBox(`<p style="font-weight:bold;">${Chat.escapeHTML(room.title)}'s subroom${Chat.plural(subRooms)}:</p><ul><li>${subRoomText.join('</li><br/><li>')}</li></ul></strong>`);
},
subroomhelp: [
`/subroom [room] - Marks the current room as a subroom of [room]. Requires: & ~`,
`/unsubroom - Unmarks the current room as a subroom. Requires: & ~`,
`/subrooms - Displays the current room's subrooms.`,
`/parentroom - Displays the current room's parent room.`,
],
roomdesc(target, room, user) {
if (!target) {
if (!this.runBroadcast()) return;
if (!room.desc) return this.sendReply(`This room does not have a description set.`);
this.sendReplyBox(Chat.html`The room description is: ${room.desc}`);
return;
}
if (!this.can('declare')) return false;
if (target.length > 80) return this.errorReply(`Error: Room description is too long (must be at most 80 characters).`);
let normalizedTarget = ' ' + target.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + ' ';
if (normalizedTarget.includes(' welcome ')) {
return this.errorReply(`Error: Room description must not contain the word "welcome".`);
}
if (normalizedTarget.slice(0, 9) === ' discuss ') {
return this.errorReply(`Error: Room description must not start with the word "discuss".`);
}
if (normalizedTarget.slice(0, 12) === ' talk about ' || normalizedTarget.slice(0, 17) === ' talk here about ') {
return this.errorReply(`Error: Room description must not start with the phrase "talk about".`);
}
room.desc = target;
this.sendReply(`(The room description is now: ${target})`);
this.privateModAction(`(${user.name} changed the roomdesc to: "${target}".)`);
this.modlog('ROOMDESC', null, `to "${target}"`);
if (room.chatRoomData) {
room.chatRoomData.desc = room.desc;
Rooms.global.writeChatRoomData();
}
},
topic: 'roomintro',
roomintro(target, room, user, connection, cmd) {
if (!target) {
if (!this.runBroadcast()) return;
if (!room.introMessage) return this.sendReply("This room does not have an introduction set.");
this.sendReply('|raw|<div class="infobox infobox-limited">' + room.introMessage.replace(/\n/g, '') + '</div>');
if (!this.broadcasting && user.can('declare', null, room) && cmd !== 'topic') {
this.sendReply('Source:');
const code = Chat.escapeHTML(room.introMessage).replace(/\n/g, '<br />');
this.sendReplyBox(`<code style="white-space: pre-wrap">/roomintro ${code}</code>`);
}
return;
}
if (!this.can('editroom', null, room)) return false;
if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deleteroomintro"?');
target = this.canHTML(target);
if (!target) return;
if (!/</.test(target)) {
// not HTML, do some simple URL linking
let re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
target = target.replace(re, '<a href="$1">$1</a>');
}
if (target.substr(0, 11) === '/roomintro ') target = target.substr(11);
room.introMessage = target.replace(/\r/g, '');
this.sendReply("(The room introduction has been changed to:)");
this.sendReply(`|raw|<div class="infobox infobox-limited">${room.introMessage.replace(/\n/g, '')}</div>`);
this.privateModAction(`(${user.name} changed the roomintro.)`);
this.modlog('ROOMINTRO');
this.roomlog(room.introMessage.replace(/\n/g, ''));
if (room.chatRoomData) {
room.chatRoomData.introMessage = room.introMessage;
Rooms.global.writeChatRoomData();
}
},
deletetopic: 'deleteroomintro',
deleteroomintro(target, room, user) {
if (!this.can('declare', null, room)) return false;
if (!room.introMessage) return this.errorReply("This room does not have a introduction set.");
this.privateModAction(`(${user.name} deleted the roomintro.)`);
this.modlog('DELETEROOMINTRO');
this.roomlog(target);
delete room.introMessage;
if (room.chatRoomData) {
delete room.chatRoomData.introMessage;
Rooms.global.writeChatRoomData();
}
},
stafftopic: 'staffintro',
staffintro(target, room, user, connection, cmd) {
if (!target) {
if (!this.can('mute', null, room)) return false;
if (!room.staffMessage) return this.sendReply("This room does not have a staff introduction set.");
this.sendReply(`|raw|<div class="infobox">${room.staffMessage.replace(/\n/g, ``)}</div>`);
if (user.can('ban', null, room) && cmd !== 'stafftopic') {
this.sendReply('Source:');
const code = Chat.escapeHTML(room.staffMessage).replace(/\n/g, '<br />');
this.sendReplyBox(`<code style="white-space: pre-wrap">/staffintro ${code}</code>`);
}
return;
}
if (!this.can('ban', null, room)) return false;
if (!this.canTalk()) return;
if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deletestaffintro"?');
target = this.canHTML(target);
if (!target) return;
if (!/</.test(target)) {
// not HTML, do some simple URL linking
let re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
target = target.replace(re, '<a href="$1">$1</a>');
}
if (target.substr(0, 12) === '/staffintro ') target = target.substr(12);
room.staffMessage = target.replace(/\r/g, '');
this.sendReply("(The staff introduction has been changed to:)");
this.sendReply(`|raw|<div class="infobox">${target.replace(/\n/g, ``)}</div>`);
this.privateModAction(`(${user.name} changed the staffintro.)`);
this.modlog('STAFFINTRO');
this.roomlog(room.staffMessage.replace(/\n/g, ``));
if (room.chatRoomData) {
room.chatRoomData.staffMessage = room.staffMessage;
Rooms.global.writeChatRoomData();
}
},
deletestafftopic: 'deletestaffintro',
deletestaffintro(target, room, user) {
if (!this.can('ban', null, room)) return false;
if (!room.staffMessage) return this.errorReply("This room does not have a staff introduction set.");
this.privateModAction(`(${user.name} deleted the staffintro.)`);
this.modlog('DELETESTAFFINTRO');
this.roomlog(target);
delete room.staffMessage;
if (room.chatRoomData) {
delete room.chatRoomData.staffMessage;
Rooms.global.writeChatRoomData();
}
},
roomalias(target, room, user) {
if (!target) {
if (!this.runBroadcast()) return;
if (!room.aliases || !room.aliases.length) return this.sendReplyBox("This room does not have any aliases.");
return this.sendReplyBox(`This room has the following aliases: ${room.aliases.join(", ")}`);
}
if (!this.can('makeroom')) return false;
if (target.includes(',')) {
this.errorReply(`Invalid room alias: ${target.trim()}`);
return this.parse('/help roomalias');
}
let alias = toID(target);
if (!alias.length) return this.errorReply("Only alphanumeric characters are valid in an alias.");
if (Rooms(alias) || Rooms.aliases.has(alias)) return this.errorReply("You cannot set an alias to an existing room or alias.");
if (room.isPersonal) return this.errorReply("Personal rooms can't have aliases.");
Rooms.aliases.set(alias, room.id);
this.privateModAction(`(${user.name} added the room alias '${alias}'.)`);
this.modlog('ROOMALIAS', null, alias);
if (!room.aliases) room.aliases = [];
room.aliases.push(alias);
if (room.chatRoomData) {
room.chatRoomData.aliases = room.aliases;
Rooms.global.writeChatRoomData();
}
},
roomaliashelp: [
`/roomalias - displays a list of all room aliases of the room the command was entered in.`,
`/roomalias [alias] - adds the given room alias to the room the command was entered in. Requires: & ~`,
`/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: & ~`,
],
deleteroomalias: 'removeroomalias',
deroomalias: 'removeroomalias',
unroomalias: 'removeroomalias',
removeroomalias(target, room, user) {
if (!room.aliases) return this.errorReply("This room does not have any aliases.");
if (!this.can('makeroom')) return false;
if (target.includes(',')) {
this.errorReply(`Invalid room alias: ${target.trim()}`);
return this.parse('/help removeroomalias');
}
let alias = toID(target);
if (!alias || !Rooms.aliases.has(alias)) return this.errorReply("Please specify an existing alias.");
if (Rooms.aliases.get(alias) !== room.id) return this.errorReply("You may only remove an alias from the current room.");
this.privateModAction(`(${user.name} removed the room alias '${alias}'.)`);
this.modlog('REMOVEALIAS', null, alias);
let aliasIndex = room.aliases.indexOf(alias);
if (aliasIndex >= 0) {
room.aliases.splice(aliasIndex, 1);
Rooms.aliases.delete(alias);
Rooms.global.writeChatRoomData();
}
},
removeroomaliashelp: [`/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: & ~`],
roomowner(target, room, user) {
if (!room.chatRoomData) {
return this.sendReply("/roomowner - This room isn't designed for per-room moderation to be added");
}
if (!target) return this.parse('/help roomowner');
target = this.splitTarget(target, true);
if (target) return this.errorReply(`This command does not support specifying a reason.`);
let targetUser = this.targetUser;
let name = this.targetUsername;
let userid = toID(name);
if (!Users.isUsernameKnown(userid)) {
return this.errorReply(`User '${this.targetUsername}' is offline and unrecognized, and so can't be promoted.`);
}
if (!this.can('makeroom')) return false;
if (!room.auth) room.auth = room.chatRoomData.auth = {};
room.auth[userid] = '#';
this.addModAction(`${name} was appointed Room Owner by ${user.name}.`);
this.modlog('ROOMOWNER', userid);
if (targetUser) {
targetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.id}.`);
room.onUpdateIdentity(targetUser);
if (room.subRooms) {
for (const subRoom of room.subRooms.values()) {
subRoom.onUpdateIdentity(targetUser);
}
}
}
Rooms.global.writeChatRoomData();
},
roomownerhelp: [`/roomowner [username] - Appoints [username] as a room owner. Requires: & ~`],
'!roompromote': true,
roomdemote: 'roompromote',
roompromote(target, room, user, connection, cmd) {
if (!room) {
// this command isn't marked as room-only because it's usable in PMs through /invite
return this.errorReply("This command is only available in rooms");
}
if (!room.auth) {
this.sendReply("/roompromote - This room isn't designed for per-room moderation.");
return this.sendReply("Before setting room staff, you need to set a room owner with /roomowner");
}
if (!this.canTalk()) return;
if (!target) return this.parse('/help roompromote');
const force = target.startsWith('!!!');
if (force) target = target.slice(3);
target = this.splitTarget(target, true);
let targetUser = this.targetUser;
let userid = toID(this.targetUsername);
let name = targetUser ? targetUser.name : this.targetUsername;
if (!userid) return this.parse('/help roompromote');
if (!targetUser && !Users.isUsernameKnown(userid) && !force) {
return this.errorReply(`User '${name}' is offline and unrecognized, and so can't be promoted.`);
}
if (targetUser && !targetUser.registered) {
return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
}
let currentGroup = room.getAuth({userid, group: (Users.usergroups[userid] || ' ').charAt(0)});
let nextGroup = target;
if (target === 'deauth') nextGroup = Config.groupsranking[0];
if (!nextGroup) {
return this.errorReply("Please specify a group such as /roomvoice or /roomdeauth");
}
if (!Config.groups[nextGroup]) {
return this.errorReply(`Group '${nextGroup}' does not exist.`);
}
if (Config.groups[nextGroup].globalonly || (Config.groups[nextGroup].battleonly && !room.battle)) {
return this.errorReply(`Group 'room${Config.groups[nextGroup].id}' does not exist as a room rank.`);
}
let groupName = Config.groups[nextGroup].name || "regular user";
if ((room.auth[userid] || Config.groupsranking[0]) === nextGroup) {
return this.errorReply(`User '${name}' is already a ${groupName} in this room.`);
}
if (!user.can('makeroom')) {
if (currentGroup !== ' ' && !user.can('room' + (Config.groups[currentGroup] ? Config.groups[currentGroup].id : 'voice'), null, room)) {
return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${(Config.groups[currentGroup] ? Config.groups[currentGroup].name : "an undefined group")}.`);
}
if (nextGroup !== ' ' && !user.can('room' + Config.groups[nextGroup].id, null, room)) {
return this.errorReply(`/${cmd} - Access denied for promoting/demoting to ${Config.groups[nextGroup].name}.`);
}
}
let nextGroupIndex = Config.groupsranking.indexOf(nextGroup) || 1; // assume voice if not defined (although it should be by now)
if (targetUser && targetUser.locked && !room.isPrivate && !room.battle && !room.isPersonal && nextGroupIndex >= 2) {
return this.errorReply("Locked users can't be promoted.");
}
if (nextGroup === Config.groupsranking[0]) {
delete room.auth[userid];
} else {
room.auth[userid] = nextGroup;
}
// Only show popup if: user is online and in the room, the room is public, and not a groupchat or a battle.
let needsPopup = targetUser && room.users[targetUser.userid] && !room.isPrivate && !room.isPersonal && !room.battle;
if (this.pmTarget && targetUser) {
const text = `${targetUser.name} was invited (and promoted to Room ${groupName}) by ${user.name}.`;
room.add(`|c|${user.getIdentity(room)}|/log ${text}`).update();
this.modlog('INVITE', targetUser, null, {noip: 1, noalts: 1});
} else if (nextGroup in Config.groups && currentGroup in Config.groups && Config.groups[nextGroup].rank < Config.groups[currentGroup].rank) {
if (targetUser && room.users[targetUser.userid] && !Config.groups[nextGroup].modlog) {
// if the user can't see the demotion message (i.e. rank < %), it is shown in the chat
targetUser.send(`>${room.id}\n(You were demoted to Room ${groupName} by ${user.name}.)`);
}
this.privateModAction(`(${name} was demoted to Room ${groupName} by ${user.name}.)`);
this.modlog(`ROOM${groupName.toUpperCase()}`, userid, '(demote)');
if (needsPopup) targetUser.popup(`You were demoted to Room ${groupName} by ${user.name} in ${room.id}.`);
} else if (nextGroup === '#') {
this.addModAction(`${'' + name} was promoted to ${groupName} by ${user.name}.`);
this.modlog('ROOM OWNER', userid);
if (needsPopup) targetUser.popup(`You were promoted to ${groupName} by ${user.name} in ${room.id}.`);
} else {
this.addModAction(`${'' + name} was promoted to Room ${groupName} by ${user.name}.`);
this.modlog(`ROOM${groupName.toUpperCase()}`, userid);
if (needsPopup) targetUser.popup(`You were promoted to Room ${groupName} by ${user.name} in ${room.id}.`);
}
if (targetUser) {
targetUser.updateIdentity(room.id);
if (room.subRooms) {
for (const subRoom of room.subRooms.values()) {
targetUser.updateIdentity(subRoom.id);
}
}
}
if (room.chatRoomData) Rooms.global.writeChatRoomData();
},
roompromotehelp: [
`/roompromote OR /roomdemote [username], [group symbol] - Promotes/demotes the user to the specified room rank. Requires: @ # & ~`,
`/room[group] [username] - Promotes/demotes the user to the specified room rank. Requires: @ # & ~`,
`/roomdeauth [username] - Removes all room rank from the user. Requires: @ # & ~`,
],
'!roomauth': true,
roomstaff: 'roomauth',
roomauth1: 'roomauth',
roomauth(target, room, user, connection, cmd) {
let userLookup = '';
if (cmd === 'roomauth1') userLookup = `\n\nTo look up auth for a user, use /userauth ${target}`;
let targetRoom = room;
if (target) targetRoom = Rooms.search(target);
if (!targetRoom || targetRoom.id === 'global' || !targetRoom.checkModjoin(user)) return this.errorReply(`The room "${target}" does not exist.`);
if (!targetRoom.auth) return this.sendReply(`/roomauth - The room '${targetRoom.title || target}' isn't designed for per-room moderation and therefore has no auth list.${userLookup}`);
let rankLists = {};
for (let u in targetRoom.auth) {
if (!rankLists[targetRoom.auth[u]]) rankLists[targetRoom.auth[u]] = [];
rankLists[targetRoom.auth[u]].push(u);
}
let buffer = Object.keys(rankLists).sort((a, b) =>
(Config.groups[b] || {rank: 0}).rank - (Config.groups[a] || {rank: 0}).rank
).map(r => {
let roomRankList = rankLists[r].sort();
roomRankList = roomRankList.map(s => s in targetRoom.users ? `**${s}**` : s);
return `${Config.groups[r] ? `${Config.groups[r].name}s (${r})` : r}:\n${roomRankList.join(", ")}`;
});
let curRoom = targetRoom;
while (curRoom.parent) {
const modjoinSetting = curRoom.modjoin === true ? curRoom.modchat : curRoom.modjoin;
const roomType = (modjoinSetting ? `modjoin ${modjoinSetting} ` : '');
const inheritedUserType = (modjoinSetting ? ` of rank ${modjoinSetting} and above` : '');
if (curRoom.parent) {
const also = buffer.length === 0 ? `` : ` also`;
buffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType}${also} have authority in this room.`);
}
curRoom = curRoom.parent;
}
if (!buffer.length) {
connection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);
return;
}
if (!curRoom.isPrivate) {
buffer.push(`${curRoom.title} is a public room, so global auth with no relevant roomauth will have authority in this room.`);
} else if (curRoom.isPrivate === 'hidden' || curRoom.isPrivate === 'voice') {
buffer.push(`${curRoom.title} is a hidden room, so global auth with no relevant roomauth will have authority in this room.`);
}
if (targetRoom !== room) buffer.unshift(`${targetRoom.title} room auth:`);
connection.popup(`${buffer.join("\n\n")}${userLookup}`);
},
'!userauth': true,
userauth(target, room, user, connection) {
let targetId = toID(target) || user.userid;
let targetUser = Users.getExact(targetId);
let targetUsername = (targetUser ? targetUser.name : target);
let buffer = [];
let innerBuffer = [];
let group = Users.usergroups[targetId];
if (group) {
buffer.push(`Global auth: ${group.charAt(0)}`);
}
for (const curRoom of Rooms.rooms.values()) {
if (!curRoom.auth || curRoom.isPrivate) continue;
group = curRoom.auth[targetId];
if (!group) continue;
innerBuffer.push(group + curRoom.id);
}
if (innerBuffer.length) {
buffer.push(`Room auth: ${innerBuffer.join(', ')}`);
}
if (targetId === user.userid || user.can('lock')) {
innerBuffer = [];
for (const curRoom of Rooms.rooms.values()) {
if (!curRoom.auth || !curRoom.isPrivate) continue;
if (curRoom.isPrivate === true) continue;
let auth = curRoom.auth[targetId];
if (!auth) continue;
innerBuffer.push(auth + curRoom.id);
}
if (innerBuffer.length) {
buffer.push(`Hidden room auth: ${innerBuffer.join(', ')}`);
}
}
if (targetId === user.userid || user.can('makeroom')) {
innerBuffer = [];
for (const chatRoom of Rooms.global.chatRooms) {
if (!chatRoom.auth || !chatRoom.isPrivate) continue;
if (chatRoom.isPrivate !== true) continue;
let auth = chatRoom.auth[targetId];
if (!auth) continue;
innerBuffer.push(auth + chatRoom.id);
}
if (innerBuffer.length) {
buffer.push(`Private room auth: ${innerBuffer.join(', ')}`);
}
}
if (!buffer.length) {
buffer.push("No global or room auth.");
}
buffer.unshift(`${targetUsername} user auth:`);
connection.popup(buffer.join("\n\n"));
},
rb: 'ban',
roomban: 'ban',
b: 'ban',
ban(target, room, user, connection) {
if (!target) return this.parse('/help ban');
if (!this.canTalk()) return;
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!this.can('ban', targetUser, room)) return false;
if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to ban upper staff members.");
if (Punishments.getRoomPunishType(room, this.targetUsername) === 'BLACKLIST') return this.errorReply(`This user is already blacklisted from ${room.id}.`);
let name = targetUser.getLastName();
let userid = targetUser.getLastId();
if (Punishments.isRoomBanned(targetUser, room.id) && !target) {
let problem = " but was already banned";
return this.privateModAction(`(${name} would be banned by ${user.name} ${problem}.)`);
}
if (targetUser.trusted && room.isPrivate !== true && !room.isPersonal) {
Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} ${(targetUser.trusted !== targetUser.userid ? ` (${targetUser.trusted})` : ``)} was roombanned from ${room.id} by ${user.name}, and should probably be demoted.`);
}
if (targetUser in room.users || user.can('lock')) {
targetUser.popup(
`|modal||html|<p>${Chat.escapeHTML(user.name)} has banned you from the room ${room.id} ${(room.subRooms ? ` and its subrooms` : ``)}.</p>${(target ? `<p>Reason: ${Chat.escapeHTML(target)}</p>` : ``)}<p>To appeal the ban, PM the staff member that banned you${(!room.battle && room.auth ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.id}">List Room Staff</button></p>` : `.</p>`)}`
);
}
const reason = (target ? ` (${target})` : ``);
this.addModAction(`${name} was banned from ${room.title} by ${user.name}.${reason}`);
let affected = Punishments.roomBan(room, targetUser, null, null, target);
if (!room.isPrivate && room.chatRoomData) {
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : ``)} banned alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
this.privateModAction(displayMessage);
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
}
room.hideText([userid, toID(this.inputUsername)]);
if (room.isPrivate !== true && room.chatRoomData) {
this.globalModlog("ROOMBAN", targetUser, ` by ${user.userid}${(target ? `: ${target}` : ``)}`);
} else {
this.modlog("ROOMBAN", targetUser, ` by ${user.userid}${(target ? `: ${target}` : ``)}`);
}
return true;
},
banhelp: [`/ban [username], [reason] - Bans the user from the room you are in. Requires: @ # & ~`],
unroomban: 'unban',
roomunban: 'unban',
unban(target, room, user, connection) {
if (!target) return this.parse('/help unban');
if (!this.can('ban', null, room)) return false;
let name = Punishments.roomUnban(room, target);
if (name) {
this.addModAction(`${name} was unbanned from ${room.title} by ${user.name}.`);
if (room.isPrivate !== true && room.chatRoomData) {
this.globalModlog("UNROOMBAN", name, ` by ${user.userid}`);
}
} else {
this.errorReply(`User '${target}' is not banned from this room.`);
}
},
unbanhelp: [`/unban [username] - Unbans the user from the room you are in. Requires: @ # & ~`],
'!autojoin': true,
autojoin(target, room, user, connection) {
let targets = target.split(',');
if (targets.length > 11 || connection.inRooms.size > 1) return;
Rooms.global.autojoinRooms(user, connection);
let autojoins = [];
const promises = targets.map(target =>
user.tryJoinRoom(target, connection).then(ret => {
if (ret === Rooms.RETRY_AFTER_LOGIN) {
autojoins.push(target);
}
})
);
Promise.all(promises).then(() => {
connection.autojoins = autojoins.join(',');
});
},
'!join': true,
joim: 'join',
j: 'join',
join(target, room, user, connection) {
if (!target) return this.parse('/help join');
if (target.startsWith('http://')) target = target.slice(7);
if (target.startsWith('https://')) target = target.slice(8);
if (target.startsWith('play.pokemonshowdown.com/')) target = target.slice(25);
if (target.startsWith('psim.us/')) target = target.slice(8);
user.tryJoinRoom(target, connection).then(ret => {
if (ret === Rooms.RETRY_AFTER_LOGIN) {
connection.sendTo(target, `|noinit|namerequired|The room '${target}' does not exist or requires a login to join.`);
}
});
},
joinhelp: [`/join [roomname] - Attempt to join the room [roomname].`],
'!part': true,
leave: 'part',
part(target, room, user, connection) {
let targetRoom = target ? Rooms.search(target) : room;
if (!targetRoom || targetRoom === Rooms.global) {
if (target.startsWith('view-')) return;
return this.errorReply(`The room '${target}' does not exist.`);
}
user.leaveRoom(targetRoom, connection);
},
/*********************************************************
* Moderating: Punishments
*********************************************************/
kick: 'warn',
k: 'warn',
warn(target, room, user) {
if (!target) return this.parse('/help warn');
if (!this.canTalk()) return;
if (room.isPersonal && !user.can('warn')) return this.errorReply("Warning is unavailable in group chats.");
// If used in staff, help tickets or battles, log the warn to the global modlog.
const globalWarn = room.id === 'staff' || room.id.startsWith('help-') || (room.battle && !room.parent);
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) {
if (!targetUser || !globalWarn) return this.errorReply(`User '${this.targetUsername}' not found.`);
this.addModAction(`${targetUser.name} would be warned by ${user.name} but is offline.${(target ? ` (${target})` : ``)}`);
this.modlog('WARN', targetUser, target, {noalts: 1});
this.globalModlog('WARN', targetUser, ` by ${user.userid}${(target ? `: ${target}` : ``)}`);
return;
}
if (!(targetUser in room.users) && !globalWarn) {
return this.errorReply(`User ${this.targetUsername} is not in the room ${room.id}.`);
}
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!this.can('warn', targetUser, room)) return false;
if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to warn upper staff members.");
this.addModAction(`${targetUser.name} was warned by ${user.name}.${(target ? ` (${target})` : ``)}`);
this.modlog('WARN', targetUser, target, {noalts: 1});
if (globalWarn) {
this.globalModlog('WARN', targetUser, ` by ${user.userid}${(target ? `: ${target}` : ``)}`);
}
targetUser.send(`|c|~|/warn ${target}`);
let userid = targetUser.getLastId();
this.add(`|unlink|${userid}`);
if (userid !== toID(this.inputUsername)) this.add(`|unlink|${toID(this.inputUsername)}`);
},
warnhelp: [`/warn OR /k [username], [reason] - Warns a user showing them the Pok\u00e9mon Showdown Rules and [reason] in an overlay. Requires: % @ # & ~`],
redirect: 'redir',
redir(target, room, user, connection) {
if (!target) return this.parse('/help redirect');
if (room.isPrivate || room.isPersonal) return this.errorReply("Users cannot be redirected from private or personal rooms.");
target = this.splitTarget(target);
let targetUser = this.targetUser;
let targetRoom = Rooms.search(target);
if (!targetRoom || targetRoom.modjoin || targetRoom.staffRoom) {
return this.errorReply(`The room "${target}" does not exist.`);
}
if (!this.can('warn', targetUser, room) || !this.can('warn', targetUser, targetRoom)) return false;
if (!this.can('rangeban', targetUser)) {
this.errorReply(`Redirects have been deprecated. Instead of /redirect, use <<room links>> or /invite to guide users to the correct room, and punish if users don't cooperate.`);
return;
}
if (!targetUser || !targetUser.connected) {
return this.errorReply(`User ${this.targetUsername} not found.`);
}
if (targetRoom.id === "global") return this.errorReply(`Users cannot be redirected to the global room.`);
if (targetRoom.isPrivate || targetRoom.isPersonal) {
return this.errorReply(`The room "${target}" is not public.`);
}
if (targetUser.inRooms.has(targetRoom.id)) {
return this.errorReply(`User ${targetUser.name} is already in the room ${targetRoom.title}!`);
}
if (!targetUser.inRooms.has(room.id)) {
return this.errorReply(`User ${this.targetUsername} is not in the room ${room.id}.`);
}
targetUser.leaveRoom(room.id);
targetUser.popup(`You are in the wrong room; please go to <<${targetRoom.id}>> instead`);
this.addModAction(`${targetUser.name} was redirected to room ${targetRoom.title} by ${user.name}.`);
this.modlog('REDIRECT', targetUser, `to ${targetRoom.title}`, {noip: 1, noalts: 1});
targetUser.leaveRoom(room);
},
redirhelp: [`/redirect OR /redir [username], [roomname] - [DEPRECATED] Attempts to redirect the user [username] to the room [roomname]. Requires: & ~`],
m: 'mute',
mute(target, room, user, connection, cmd) {
if (!target) return this.parse('/help mute');
if (!this.canTalk()) return;
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
let muteDuration = ((cmd === 'hm' || cmd === 'hourmute') ? HOURMUTE_LENGTH : MUTE_LENGTH);
if (!this.can('mute', targetUser, room)) return false;
if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to mute upper staff members.");
let canBeMutedFurther = ((room.getMuteTime(targetUser) || 0) <= (muteDuration * 5 / 6));
if (targetUser.locked || (room.isMuted(targetUser) && !canBeMutedFurther) || Punishments.isRoomBanned(targetUser, room.id)) {
let problem = ` but was already ${(targetUser.locked ? "locked" : room.isMuted(targetUser) ? "muted" : "room banned")}`;
if (!target) {
return this.privateModAction(`(${targetUser.name} would be muted by ${user.name} ${problem}.)`);
}
return this.addModAction(`${targetUser.name} would be muted by ${user.name} ${problem}. (${target})`);
}
if (targetUser in room.users) targetUser.popup(`|modal|${user.name} has muted you in ${room.id} for ${Chat.toDurationString(muteDuration)}. ${target}`);
this.addModAction(`${targetUser.name} was muted by ${user.name} for ${Chat.toDurationString(muteDuration)}.${(target ? ` (${target})` : ``)}`);
this.modlog(`${cmd.includes('h') ? 'HOUR' : ''}MUTE`, targetUser, target);
if (targetUser.autoconfirmed && targetUser.autoconfirmed !== targetUser.userid) {
let displayMessage = `(${targetUser.name}'s ac account: ${targetUser.autoconfirmed})`;
this.privateModAction(displayMessage);
}
let userid = targetUser.getLastId();
this.add(`|unlink|${userid}`);
if (userid !== toID(this.inputUsername)) this.add(`|unlink|${toID(this.inputUsername)}`);
room.mute(targetUser, muteDuration, false);
},
mutehelp: [`/mute OR /m [username], [reason] - Mutes a user with reason for 7 minutes. Requires: % @ # & ~`],
hm: 'hourmute',
hourmute(target) {
if (!target) return this.parse('/help hourmute');
this.run('mute');
},
hourmutehelp: [`/hourmute OR /hm [username], [reason] - Mutes a user with reason for an hour. Requires: % @ # & ~`],
um: 'unmute',
unmute(target, room, user) {
if (!target) return this.parse('/help unmute');
target = this.splitTarget(target);
if (target) return this.errorReply(`This command does not support specifying a reason.`);
if (!this.canTalk()) return;
if (!this.can('mute', null, room)) return false;
let targetUser = this.targetUser;
let successfullyUnmuted = room.unmute(targetUser ? targetUser.userid : toID(this.targetUsername), `Your mute in '${room.title}' has been lifted.`);
if (successfullyUnmuted) {
this.addModAction(`${(targetUser ? targetUser.name : successfullyUnmuted)} was unmuted by ${user.name}.`);
this.modlog('UNMUTE', (targetUser || successfullyUnmuted), null, {noip: 1, noalts: 1});
} else {
this.errorReply(`${(targetUser ? targetUser.name : this.targetUsername)} is not muted.`);
}
},
unmutehelp: [`/unmute [username] - Removes mute from user. Requires: % @ # & ~`],
forcelock: 'lock',
l: 'lock',
ipmute: 'lock',
wl: 'lock',
weeklock: 'lock',
lock(target, room, user, connection, cmd) {
let week = cmd === 'wl' || cmd === 'weeklock';
if (!target) {
if (week) return this.parse('/help weeklock');
return this.parse('/help lock');
}
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser && !Punishments.search(toID(this.targetUsername)).length) {
return this.errorReply(`User '${this.targetUsername}' not found.`);
}
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!this.can('lock', targetUser)) return false;
let name, userid;
if (targetUser) {
name = targetUser.getLastName();
userid = targetUser.getLastId();
if (targetUser.locked && !week) {
return this.privateModAction(`(${name} would be locked by ${user.name} but was already locked.)`);
}
if (targetUser.trusted) {
if (cmd === 'forcelock') {
let from = targetUser.distrust();
Monitor.log(`[CrisisMonitor] ${name} was locked by ${user.name} and demoted from ${from.join(", ")}.`);
this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from.join(", ")}`);
} else {
return this.sendReply(`${name} is a trusted user. If you are sure you would like to lock them use /forcelock.`);
}
} else if (cmd === 'forcelock') {
return this.errorReply(`Use /lock; ${name} is not a trusted user.`);
}
} else {
name = this.targetUsername;
userid = toID(this.targetUsername);
}
let proof = '';
let userReason = target;
let targetLowercase = target.toLowerCase();
if (target && (targetLowercase.includes('spoiler:') || targetLowercase.includes('spoilers:'))) {
let proofIndex = (targetLowercase.includes('spoilers:') ? targetLowercase.indexOf('spoilers:') : targetLowercase.indexOf('spoiler:'));
let bump = (targetLowercase.includes('spoilers:') ? 9 : 8);
proof = `(PROOF: ${target.substr(proofIndex + bump, target.length).trim()}) `;
userReason = target.substr(0, proofIndex).trim();
}
// Use default time for locks.
let duration = week ? Date.now() + 7 * 24 * 60 * 60 * 1000 : null;
let affected = [];
if (targetUser) {
affected = Punishments.lock(targetUser, duration, null, userReason);
} else {
affected = Punishments.lock(null, duration, userid, userReason);
}
const globalReason = (target ? `: ${userReason} ${proof}` : '');
this.globalModlog((week ? "WEEKLOCK" : "LOCK"), targetUser || userid, ` by ${user.userid}${globalReason}`);
let weekMsg = week ? ' for a week' : '';
let lockMessage = `${name} was locked from talking${weekMsg} by ${user.name}.` + (userReason ? ` (${userReason})` : "");
this.addModAction(lockMessage);
// Notify staff room when a user is locked outside of it.
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addByUser(user, `<<${room.id}>> ${lockMessage}`);
}
room.hideText([userid, toID(this.inputUsername)]);
let acAccount = (targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : "")} locked alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
this.privateModAction(displayMessage);
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
if (targetUser) {
let message = `|popup||html|${user.name} has locked you from talking in chats, battles, and PMing regular users${weekMsg}`;
if (userReason) message += `\n\nReason: ${userReason}`;
let appeal = '';
if (Chat.pages.help) {
appeal += `<a href="view-help-request--appeal"><button class="button"><strong>Appeal your punishment</strong></button></a>`;
} else if (Config.appealurl) {
appeal += `appeal: <a href="${Config.appealurl}">${Config.appealurl}</a>`;
}
if (appeal) message += `\n\nIf you feel that your lock was unjustified, you can ${appeal}.`;
message += `\n\nYour lock will expire in a few days.`;
targetUser.send(message);
let roomauth = [];
for (const [id, curRoom] of Rooms.rooms) {
if (id === 'global' || !curRoom.auth) continue;
// Destroy personal rooms of the locked user.
if (curRoom.isPersonal && curRoom.auth[userid] === Users.HOST_SYMBOL) {
curRoom.destroy();
} else {
if (curRoom.isPrivate || curRoom.battle) continue;
let group = curRoom.auth[userid];
if (group) roomauth.push(`${group}${id}`);
}
}
if (roomauth.length) Monitor.log(`[CrisisMonitor] Locked user ${name} has public roomauth (${roomauth.join(', ')}), and should probably be demoted.`);
}
// Automatically upload replays as evidence/reference to the punishment
if (room.battle) this.parse('/savereplay');
return true;
},
lockhelp: [
`/lock OR /l [username], [reason] - Locks the user from talking in all chats. Requires: % @ & ~`,
`/weeklock OR /wl [username], [reason] - Same as /lock, but locks users for a week.`,
`/lock OR /l [username], [reason] spoiler: [proof] - Marks proof in modlog only.`,
],
unlock(target, room, user) {
if (!target) return this.parse('/help unlock');
if (!this.can('lock')) return false;
let targetUser = Users.get(target);
if (targetUser && targetUser.namelocked) {
return this.errorReply(`User ${targetUser.name} is namelocked, not locked. Use /unnamelock to unnamelock them.`);
}
let reason = '';
if (targetUser && targetUser.locked && targetUser.locked.charAt(0) === '#') {
reason = ` (${targetUser.locked})`;
}
let unlocked = Punishments.unlock(target);
if (unlocked) {
const unlockMessage = `${unlocked.join(", ")} ${((unlocked.length > 1) ? "were" : "was")} unlocked by ${user.name}.${reason}`;
this.addModAction(unlockMessage);
// Notify staff room when a user is unlocked outside of it.
if (!reason && room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addByUser(user, `<<${room.id}>> ${unlockMessage}`);
}
if (!reason) this.globalModlog("UNLOCK", toID(target), ` by ${user.userid}`);
if (targetUser) targetUser.popup(`${user.name} has unlocked you.`);
} else {
this.errorReply(`User '${target}' is not locked.`);
}
},
unlockname(target, room, user) {
if (!target) return this.parse('/help unlock');
if (!this.can('lock')) return false;
const userid = toID(target);
const punishment = Punishments.userids.get(userid);
if (!punishment) return this.errorReply("This name isn't locked.");
if (punishment[1] === userid) return this.errorReply(`"${userid}" was specifically locked by a staff member (check the global modlog). Use /unlock if you really want to unlock this name.`);
Punishments.userids.delete(userid);
Punishments.savePunishments();
for (const curUser of Users.findUsers([userid], [])) {
if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) {
curUser.locked = false;
curUser.namelocked = false;
curUser.updateIdentity();
}
}
this.globalModlog("UNLOCKNAME", userid, ` by ${user.name}`);
const unlockMessage = `The name '${target}' was unlocked by ${user.name}.`;
this.addModAction(unlockMessage);
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addByUser(user, `<<${room.id}>> ${unlockMessage}`);
}
},
unlockip(target, room, user) {
target = target.trim();
if (!target) return this.parse('/help unlock');
if (!this.can('ban')) return false;
const range = target.charAt(target.length - 1) === '*';
if (range && !this.can('rangeban')) return false;
if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address.");
const punishment = Punishments.ips.get(target);
if (!punishment) return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
Punishments.ips.delete(target);
Punishments.savePunishments();
for (const curUser of Users.findUsers([], [target])) {
if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) {
curUser.locked = false;
curUser.namelocked = false;
curUser.updateIdentity();
}
}
this.globalModlog(`UNLOCK${range ? 'RANGE' : 'IP'}`, target, ` by ${user.name}`);
const broadcastRoom = Rooms('staff') || room;
broadcastRoom.addByUser(user, `${user.name} unlocked the ${range ? "IP range" : "IP"}: ${target}`);
},
unlockiphelp: [`/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ & ~`],
unlocknamehelp: [`/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ & ~`],
unlockhelp: [
`/unlock [username] - Unlocks the user. Requires: % @ & ~`,
`/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ & ~`,
`/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ & ~`,
],
forceglobalban: 'globalban',
gban: 'globalban',
globalban(target, room, user, connection, cmd) {
if (!target) return this.parse('/help globalban');
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!target && REQUIRE_REASONS) {
return this.errorReply("Global bans require a reason.");
}
if (!this.can('ban', targetUser)) return false;
let name = targetUser.getLastName();
let userid = targetUser.getLastId();
if (targetUser.trusted) {
if (cmd === 'forceglobalban') {
let from = targetUser.distrust();
Monitor.log(`[CrisisMonitor] ${name} was globally banned by ${user.name} and demoted from ${from.join(", ")}.`);
this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from.join(", ")}`);
} else {
return this.sendReply(`${name} is a trusted user. If you are sure you would like to ban them use /forceglobalban.`);
}
} else if (cmd === 'forceglobalban') {
return this.errorReply(`Use /globalban; ${name} is not a trusted user.`);
}
// Destroy personal rooms of the banned user.
for (const roomid of targetUser.inRooms) {
if (roomid === 'global') continue;
let targetRoom = Rooms.get(roomid);
if (targetRoom.isPersonal && targetRoom.auth[userid] === Users.HOST_SYMBOL) {
targetRoom.destroy();
}
}
let proof = '';
let userReason = target;
let targetLowercase = target.toLowerCase();
if (target && (targetLowercase.includes('spoiler:') || targetLowercase.includes('spoilers:'))) {
let proofIndex = (targetLowercase.includes('spoilers:') ? targetLowercase.indexOf('spoilers:') : targetLowercase.indexOf('spoiler:'));
let bump = (targetLowercase.includes('spoilers:') ? 9 : 8);
proof = `(PROOF: ${target.substr(proofIndex + bump, target.length).trim()}) `;
userReason = target.substr(0, proofIndex).trim();
}
targetUser.popup(`|modal|${user.name} has globally banned you.${(userReason ? `\n\nReason: ${userReason}` : ``)} ${(Config.appealurl ? `\n\nIf you feel that your ban was unjustified, you can appeal:\n${Config.appealurl}` : ``)}\n\nYour ban will expire in a few days.`);
let banMessage = `${name} was globally banned by ${user.name}.${(userReason ? ` (${userReason})` : ``)}`;
this.addModAction(banMessage);
// Notify staff room when a user is banned outside of it.
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addByUser(user, `<<${room.id}>> ${banMessage}`);
}
let affected = Punishments.ban(targetUser, null, null, userReason);
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
let guests = affected.length - 1;
affected = affected.slice(1).map(user => user.getLastName()).filter(alt => alt.substr(0, 7) !== '[Guest ');
guests -= affected.length;
displayMessage = `(${name}'s ${(acAccount ? `ac account: ${acAccount}, ` : ``)} banned alts: ${affected.join(", ")} ${(guests ? ` [${guests} guests]` : ``)})`;
this.privateModAction(displayMessage);
for (const user of affected) {
this.add(`|unlink|${toID(user)}`);
}
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
room.hideText([userid, toID(this.inputUsername)]);
const globalReason = (target ? `: ${userReason} ${proof}` : '');
this.globalModlog("BAN", targetUser, ` by ${user.userid}${globalReason}`);
return true;
},
globalbanhelp: [
`/globalban OR /gban [username], [reason] - Kick user from all rooms and ban user's IP address with reason. Requires: @ & ~`,
`/globalban OR /gban [username], [reason] spoiler: [proof] - Marks proof in modlog only.`,
],
globalunban: 'unglobalban',
unglobalban(target, room, user) {
if (!target) return this.parse(`/help unglobalban`);
if (!this.can('ban')) return false;
let name = Punishments.unban(target);
let unbanMessage = `${name} was globally unbanned by ${user.name}.`;
if (name) {
this.addModAction(unbanMessage);
// Notify staff room when a user is unbanned outside of it.
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addByUser(user, `<<${room.id}>> ${unbanMessage}`);
}
this.globalModlog("UNBAN", name, ` by ${user.userid}`);
} else {
this.errorReply(`User '${target}' is not globally banned.`);
}
},
unglobalbanhelp: [`/unglobalban [username] - Unban a user. Requires: @ & ~`],
unbanall(target, room, user) {
if (!this.can('rangeban')) return false;
if (!target) {
user.lastCommand = '/unbanall';
this.errorReply("THIS WILL UNBAN AND UNLOCK ALL USERS.");
this.errorReply("To confirm, use: /unbanall confirm");
return;
}
if (user.lastCommand !== '/unbanall' || target !== 'confirm') {
return this.parse('/help unbanall');
}
user.lastCommand = '';
Punishments.userids.clear();
Punishments.ips.clear();
Punishments.savePunishments();
this.addModAction(`All bans and locks have been lifted by ${user.name}.`);
this.modlog('UNBANALL');
},
unbanallhelp: [`/unbanall - Unban all IP addresses. Requires: & ~`],
deroomvoiceall(target, room, user) {
if (!this.can('editroom', null, room)) return false;
if (!room.auth) return this.errorReply("Room does not have roomauth.");
if (!target) {
user.lastCommand = '/deroomvoiceall';
this.errorReply("THIS WILL DEROOMVOICE ALL ROOMVOICED USERS.");
this.errorReply("To confirm, use: /deroomvoiceall confirm");
return;
}
if (user.lastCommand !== '/deroomvoiceall' || target !== 'confirm') {
return this.parse('/help deroomvoiceall');
}
user.lastCommand = '';
let count = 0;
for (let userid in room.auth) {
if (room.auth[userid] === '+') {
delete room.auth[userid];
if (userid in room.users) room.users[userid].updateIdentity(room.id);
count++;
}
}
if (!count) {
return this.sendReply("(This room has zero roomvoices)");
}
if (room.chatRoomData) {
Rooms.global.writeChatRoomData();
}
this.addModAction(`All ${count} roomvoices have been cleared by ${user.name}.`);
this.modlog('DEROOMVOICEALL');
},
deroomvoiceallhelp: [`/deroomvoiceall - Devoice all roomvoiced users. Requires: # & ~`],
rangeban: 'banip',
banip(target, room, user) {
const [ip, reason] = this.splitOne(target);
if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip)) return this.parse('/help banip');
if (!reason) return this.errorReply("/banip requires a ban reason");
if (!this.can('rangeban')) return false;
const ipDesc = `IP ${(ip.endsWith('*') ? `range ` : ``)}${ip}`;
const curPunishment = Punishments.ipSearch(ip);
if (curPunishment && curPunishment[0] === 'BAN') {
return this.errorReply(`The ${ipDesc} is already temporarily banned.`);
}
Punishments.banRange(ip, reason);
this.addModAction(`${user.name} hour-banned the ${ipDesc}: ${reason}`);
this.modlog('RANGEBAN', null, reason);
},
baniphelp: [`/banip [ip] - Globally bans this IP or IP range for an hour. Accepts wildcards to ban ranges. Existing users on the IP will not be banned. Requires: & ~`],
unrangeban: 'unbanip',
unbanip(target, room, user) {
target = target.trim();
if (!target) {
return this.parse('/help unbanip');
}
if (!this.can('rangeban')) return false;
if (!Punishments.ips.has(target)) {
return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
}
Punishments.ips.delete(target);
this.addModAction(`${user.name} unbanned the ${(target.charAt(target.length - 1) === '*' ? "IP range" : "IP")}: ${target}`);
this.modlog('UNRANGEBAN', null, target);
},
unbaniphelp: [`/unbanip [ip] - Unbans. Accepts wildcards to ban ranges. Requires: & ~`],
rangelock: 'lockip',
lockip(target, room, user) {
const [ip, reason] = this.splitOne(target);
if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip)) return this.parse('/help lockip');
if (!reason) return this.errorReply("/lockip requires a lock reason");
if (!this.can('rangeban')) return false;
const ipDesc = `IP ${(ip.endsWith('*') ? `range ` : ``)}${ip}`;
const curPunishment = Punishments.ipSearch(ip);
if (curPunishment && (curPunishment[0] === 'BAN' || curPunishment[0] === 'LOCK')) {
const punishDesc = curPunishment[0] === 'BAN' ? `temporarily banned` : `temporarily locked`;
return this.errorReply(`The ${ipDesc} is already ${punishDesc}.`);
}
Punishments.lockRange(ip, reason);
this.addModAction(`${user.name} hour-locked the ${ipDesc}: ${reason}`);
this.modlog('RANGELOCK', null, reason);
},
lockiphelp: [`/lockip [ip] - Globally locks this IP or IP range for an hour. Accepts wildcards to ban ranges. Existing users on the IP will not be banned. Requires: & ~`],
unrangelock: 'unlockip',
rangeunlock: 'unlockip',
/*********************************************************
* Moderating: Other
*********************************************************/
mn: 'modnote',
modnote(target, room, user, connection) {
if (!target) return this.parse('/help modnote');
if (!this.canTalk()) return;
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The note is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!this.can('receiveauthmessages', null, room)) return false;
target = target.replace(/\n/g, "; ");
this.modlog('NOTE', null, target);
if (room.id === 'staff' || room.id === 'upperstaff') this.globalModlog('NOTE', null, ` by ${user.userid}: ${target}`);
return this.privateModAction(`(${user.name} notes: ${target})`);
},
modnotehelp: [`/modnote [note] - Adds a moderator note that can be read through modlog. Requires: % @ # & ~`],
globalpromote: 'promote',
promote(target, room, user, connection, cmd) {
if (!target) return this.parse('/help promote');
target = this.splitTarget(target, true);
let targetUser = this.targetUser;
let userid = toID(this.targetUsername);
let name = targetUser ? targetUser.name : this.targetUsername;
if (!userid) return this.parse('/help promote');
let currentGroup = ((targetUser && targetUser.group) || Users.usergroups[userid] || ' ')[0];
let nextGroup = target;
if (target === 'deauth') nextGroup = Config.groupsranking[0];
if (!nextGroup) {
return this.errorReply("Please specify a group such as /globalvoice or /globaldeauth");
}
if (!Config.groups[nextGroup]) {
return this.errorReply(`Group '${nextGroup}' does not exist.`);
}
if (!cmd.startsWith('global')) {
let groupid = Config.groups[nextGroup].id;
if (!groupid && nextGroup === Config.groupsranking[0]) groupid = 'deauth';
if (Config.groups[nextGroup].globalonly) return this.errorReply(`Did you mean "/global${groupid}"?`);
if (Config.groups[nextGroup].roomonly) return this.errorReply(`Did you mean "/room${groupid}"?`);
return this.errorReply(`Did you mean "/room${groupid}" or "/global${groupid}"?`);
}
if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) {
return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
}
let groupName = Config.groups[nextGroup].name || "regular user";
if (currentGroup === nextGroup) {
return this.errorReply(`User '${name}' is already a ${groupName}`);
}
if (!user.canPromote(currentGroup, nextGroup)) {
return this.errorReply(`/${cmd} - Access denied.`);
}
if (!Users.isUsernameKnown(userid)) {
return this.errorReply(`/globalpromote - WARNING: '${name}' is offline and unrecognized. The username might be misspelled (either by you or the person who told you) or unregistered. Use /forcepromote if you're sure you want to risk it.`);
}
if (targetUser && !targetUser.registered) {
return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
}
Users.setOfflineGroup(name, nextGroup);
if (Config.groups[nextGroup].rank < Config.groups[currentGroup].rank) {
this.privateModAction(`(${name} was demoted to ${groupName} by ${user.name}.)`);
this.modlog(`GLOBAL ${groupName.toUpperCase()}`, userid, '(demote)');
if (targetUser) targetUser.popup(`You were demoted to ${groupName} by ${user.name}.`);
} else {
this.addModAction(`${name} was promoted to ${groupName} by ${user.name}.`);
this.modlog(`GLOBAL ${groupName.toUpperCase()}`, userid);
if (targetUser) targetUser.popup(`You were promoted to ${groupName} by ${user.name}.`);
}
if (targetUser) targetUser.updateIdentity();
},
promotehelp: [`/promote [username], [group] - Promotes the user to the specified group. Requires: & ~`],
confirmuser: 'trustuser',
trustuser(target, room, user) {
if (!target) return this.parse('/help trustuser');
if (!this.can('promote')) return;
target = this.splitTarget(target, true);
if (target) return this.errorReply(`This command does not support specifying a reason.`);
let targetUser = this.targetUser;
let userid = toID(this.targetUsername);
let name = targetUser ? targetUser.name : this.targetUsername;
if (!userid) return this.parse('/help trustuser');
if (!targetUser) return this.errorReply(`User '${name}' is not online.`);
if (targetUser.trusted) return this.errorReply(`User '${name}' is already trusted.`);
targetUser.setGroup(Config.groupsranking[0], true);
this.sendReply(`User '${name}' is now trusted.`);
this.privateModAction(`${name} was set as a trusted user by ${user.name}.`);
this.modlog('TRUSTUSER', userid);
},
trustuserhelp: [`/trustuser [username] - Trusts the user (makes them immune to locks). Requires: & ~`],
globaldemote: 'demote',
demote(target) {
if (!target) return this.parse('/help demote');
this.run('promote');
},
demotehelp: [`/demote [username], [group] - Demotes the user to the specified group. Requires: & ~`],
forcepromote(target, room, user) {
// warning: never document this command in /help
if (!this.can('forcepromote')) return false;
target = this.splitTarget(target, true);
let name = this.targetUsername;
let nextGroup = target;
if (!Config.groups[nextGroup]) return this.errorReply(`Group '${nextGroup}' does not exist.`);
if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
if (Users.isUsernameKnown(name)) {
return this.errorReply("/forcepromote - Don't forcepromote unless you have to.");
}
Users.setOfflineGroup(name, nextGroup);
this.addModAction(`${name} was promoted to ${(Config.groups[nextGroup].name || "regular user")} by ${user.name}.`);
this.modlog(`GLOBAL${(Config.groups[nextGroup].name || "regular").toUpperCase()}`, toID(name));
},
devoice: 'deauth',
deauth(target, room, user) {
return this.parse(`/demote ${target}, deauth`);
},
deglobalvoice: 'globaldeauth',
deglobalauth: 'globaldeauth',
globaldevoice: 'globaldeauth',
globaldeauth(target, room, user) {
return this.parse(`/globaldemote ${target}, deauth`);
},
deroomvoice: 'roomdeauth',
roomdevoice: 'roomdeauth',
deroomauth: 'roomdeauth',
roomdeauth(target, room, user) {
return this.parse(`/roomdemote ${target}, deauth`);
},
declare(target, room, user) {
target = target.trim();
if (!target) return this.parse('/help declare');
if (!this.can('declare', null, room)) return false;
if (!this.canTalk()) return;
if (target.length > 2000) return this.errorReply("Declares should not exceed 2000 characters.");
for (let u in room.users) {
if (Users(u).connected) Users(u).sendTo(room, `|notify|${room.title} announcement!|${target}`);
}
this.add(Chat.html`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
this.modlog('DECLARE', null, target);
},
declarehelp: [`/declare [message] - Anonymously announces a message. Requires: # * & ~`],
htmldeclare(target, room, user) {
if (!target) return this.parse('/help htmldeclare');
if (!this.can('gdeclare', null, room)) return false;
if (!this.canTalk()) return;
target = this.canHTML(target);
if (!target) return;
for (let u in room.users) {
if (Users(u).connected) Users(u).sendTo(room, `|notify|${room.title} announcement!|${Chat.stripHTML(target)}`);
}
this.add(`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
this.modlog(`HTMLDECLARE`, null, target);
},
htmldeclarehelp: [`/htmldeclare [message] - Anonymously announces a message using safe HTML. Requires: ~`],
gdeclare: 'globaldeclare',
globaldeclare(target, room, user) {
if (!target) return this.parse('/help globaldeclare');
if (!this.can('gdeclare')) return false;
target = this.canHTML(target);
if (!target) return;
for (const u of Users.users.values()) {
if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw <div class="broadcast-blue"><b>${target}</b></div>`);
}
this.modlog(`GLOBALDECLARE`, null, target);
},
globaldeclarehelp: [`/globaldeclare [message] - Anonymously announces a message to every room on the server. Requires: ~`],
cdeclare: 'chatdeclare',
chatdeclare(target, room, user) {
if (!target) return this.parse('/help chatdeclare');
if (!this.can('gdeclare')) return false;
target = this.canHTML(target);
if (!target) return;
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.id !== 'global' && curRoom.type !== 'battle') {
curRoom.addRaw(`<div class="broadcast-blue"><b>${target}</b></div>`).update();
}
}
this.modlog(`CHATDECLARE`, null, target);
},
chatdeclarehelp: [`/cdeclare [message] - Anonymously announces a message to all chatrooms on the server. Requires: ~`],
'!announce': true,
wall: 'announce',
announce(target, room, user) {
if (!target) return this.parse('/help announce');
if (room && !this.can('announce', null, room)) return false;
target = this.canTalk(target);
if (!target) return;
return `/announce ${target}`;
},
announcehelp: [`/announce OR /wall [message] - Makes an announcement. Requires: % @ # & ~`],
notifyoffrank: 'notifyrank',
notifyrank(target, room, user, connection, cmd) {
if (!target) return this.parse(`/help notifyrank`);
if (!this.can('addhtml', null, room)) return false;
if (!this.canTalk()) return;
let [rank, titleNotification] = this.splitOne(target);
if (rank === 'all') rank = ` `;
if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`);
const id = `${room.id}-rank-${(Config.groups[rank].id || `all`)}`;
if (cmd === 'notifyoffrank') {
if (rank === ' ') {
room.send(`|tempnotifyoff|${id}`);
} else {
room.sendRankedUsers(`|tempnotifyoff|${id}`, rank);
}
} else {
let [title, notificationHighlight] = this.splitOne(titleNotification);
if (!title) title = `${room.title} ${(Config.groups[rank].name ? `${Config.groups[rank].name}+ ` : ``)}message!`;
if (!user.can('addhtml')) {
title += ` (notification from ${user.name})`;
}
const [notification, highlight] = this.splitOne(notificationHighlight);
if (notification.length > 300) return this.errorReply(`Notifications should not exceed 300 characters.`);
const message = `|tempnotify|${id}|${title}|${notification}${(highlight ? `|${highlight}` : ``)}`;
if (rank === ' ') {
room.send(message);
} else {
room.sendRankedUsers(message, rank);
}
this.modlog(`NOTIFYRANK`, null, target);
}
},
notifyrankhelp: [
`/notifyrank [rank], [title], [message], [highlight] - Sends a notification to users who are [rank] or higher (and highlight on [highlight], if specified). Requires: # * & ~`,
`/notifyoffrank [rank] - Closes the notification previously sent with /notifyrank [rank]. Requires: # * & ~`,
],
fr: 'forcerename',
forcerename(target, room, user) {
if (!target) return this.parse('/help forcerename');
let reason = this.splitTarget(target, true);
let targetUser = this.targetUser;
if (!targetUser) {
this.splitTarget(target);
if (this.targetUser) {
return this.errorReply(`User has already changed their name to '${this.targetUser.name}'.`);
}
return this.errorReply(`User '${target}' not found.`);
}
if (!this.can('forcerename', targetUser)) return false;
this.privateModAction(`(${targetUser.name} was forced to choose a new name by ${user.name}${(reason ? `: ${reason}` : ``)})`);
this.globalModlog('FORCERENAME', targetUser, ` by ${user.name}${(reason ? `: ${reason}` : ``)}`);
this.modlog('FORCERENAME', targetUser, reason, {noip: 1, noalts: 1});
Ladders.cancelSearches(targetUser);
targetUser.resetName(true);
targetUser.send(`|nametaken||${user.name} considers your name inappropriate${(reason ? `: ${reason}` : ".")}`);
return true;
},
forcerenamehelp: [`/forcerename OR /fr [username], [reason] - Forcibly change a user's name and shows them the [reason]. Requires: % @ & ~`],
nl: 'namelock',
namelock(target, room, user) {
if (!target) return this.parse('/help namelock');
let reason = this.splitTarget(target, true);
let targetUser = this.targetUser;
if (!targetUser) {
return this.errorReply(`User '${this.targetUsername}' not found.`);
}
if (!this.can('forcerename', targetUser)) return false;
if (targetUser.namelocked) return this.errorReply(`User '${targetUser.name}' is already namelocked.`);
const reasonText = reason ? ` (${reason})` : `.`;
const lockMessage = `${targetUser.name} was namelocked by ${user.name}${reasonText}`;
this.privateModAction(`(${lockMessage})`);
// Notify staff room when a user is locked outside of it.
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addByUser(user, `<<${room.id}>> ${lockMessage}`);
}
this.globalModlog("NAMELOCK", targetUser, ` by ${user.userid}${reasonText}`);
Ladders.cancelSearches(targetUser);
Punishments.namelock(targetUser, null, null, reason);
targetUser.popup(`|modal|${user.name} has locked your name and you can't change names anymore${reasonText}`);
return true;
},
namelockhelp: [`/namelock OR /nl [username], [reason] - Name locks a user and shows them the [reason]. Requires: % @ & ~`],
unl: 'unnamelock',
unnamelock(target, room, user) {
if (!target) return this.parse('/help unnamelock');
if (!this.can('forcerename')) return false;
let targetUser = Users.get(target);
let reason = '';
if (targetUser && targetUser.namelocked) {
reason = ` (${targetUser.namelocked})`;
}
let unlocked = Punishments.unnamelock(target);
if (unlocked) {
this.addModAction(`${unlocked} was unnamelocked by ${user.name}.${reason}`);
if (!reason) this.globalModlog("UNNAMELOCK", toID(target), ` by ${user.userid}`);
if (targetUser) targetUser.popup(`${user.name} has unnamelocked you.`);
} else {
this.errorReply(`User '${target}' is not namelocked.`);
}
},
unnamelockhelp: [`/unnamelock [username] - Unnamelocks the user. Requires: % @ & ~`],
hidetextalts: 'hidetext',
hidealttext: 'hidetext',
hidealtstext: 'hidetext',
htext: 'hidetext',
hidetext(target, room, user, connection, cmd) {
if (!target) return this.parse(`/help hidetext`);
this.splitTarget(target);
let targetUser = this.targetUser;
let name = this.targetUsername;
if (!targetUser && !room.log.hasUsername(target)) return this.errorReply(`User ${target} not found or has no roomlogs.`);
if (!targetUser && !user.can('lock')) return this.errorReply(`User ${name} not found.`);
let userid = toID(this.inputUsername);
if (!this.can('mute', null, room)) return;
if (targetUser && (cmd === 'hidealtstext' || cmd === 'hidetextalts' || cmd === 'hidealttext')) {
room.send(`|c|~|${name}'s alts messages were cleared from ${room.title} by ${user.name}.`);
this.modlog('HIDEALTSTEXT', targetUser, null, {noip: 1});
room.hideText([
userid,
...Object.keys(targetUser.prevNames),
...targetUser.getAltUsers(true).map(user => user.getLastId()),
]);
} else {
room.send(`|c|~|${name}'s messages were cleared from ${room.title} by ${user.name}.`);
this.modlog('HIDETEXT', targetUser || userid, null, {noip: 1, noalts: 1});
room.hideText([userid]);
}
},
hidetexthelp: [
`/hidetext [username] - Removes a user's messages from chat. Requires: % @ # & ~`,
`/hidealtstext [username] - Removes a user's messages, and their alternate account's messages from the chat. Requires: % @ # & ~`,
],
ab: 'blacklist',
blacklist(target, room, user) {
if (!target) return this.parse('/help blacklist');
if (!this.canTalk()) return;
if (toID(target) === 'show') return this.errorReply(`You're looking for /showbl`);
target = this.splitTarget(target);
const targetUser = this.targetUser;
if (!targetUser) {
this.errorReply(`User ${this.targetUsername} not found.`);
return this.errorReply(`If you want to blacklist an offline account by name (not IP), consider /blacklistname`);
}
if (!this.can('editroom', targetUser, room)) return false;
if (!room.chatRoomData) {
return this.errorReply(`This room is not going to last long enough for a blacklist to matter - just ban the user`);
}
let punishment = Punishments.isRoomBanned(targetUser, room.id);
if (punishment && punishment[0] === 'BLACKLIST') {
return this.errorReply(`This user is already blacklisted from this room.`);
}
if (!target && REQUIRE_REASONS) {
return this.errorReply(`Blacklists require a reason.`);
}
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
const name = targetUser.getLastName();
const userid = targetUser.getLastId();
if (targetUser.trusted && room.isPrivate !== true) {
Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name}${(targetUser.trusted !== targetUser.userid ? ` (${targetUser.trusted})` : '')} was blacklisted from ${room.id} by ${user.name}, and should probably be demoted.`);
}
if (targetUser in room.users || user.can('lock')) {
targetUser.popup(
`|modal||html|<p>${Chat.escapeHTML(user.name)} has blacklisted you from the room ${room.id}${(room.subRooms ? ` and its subrooms` : '')}. Reason: ${Chat.escapeHTML(target)}</p>` +
`<p>To appeal the ban, PM the staff member that blacklisted you${(!room.battle && room.auth ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.id}">List Room Staff</button></p>` : `.</p>`)}`
);
}
this.privateModAction(`(${name} was blacklisted from ${room.title} by ${user.name}. ${(target ? ` (${target})` : '')})`);
let affected = Punishments.roomBlacklist(room, targetUser, null, null, target);
if (!room.isPrivate && room.chatRoomData) {
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount},` : '')} blacklisted alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
this.privateModAction(displayMessage);
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
}
if (!room.isPrivate && room.chatRoomData) {
this.globalModlog("BLACKLIST", targetUser, ` by ${user.userid}${(target ? `: ${target}` : '')}`);
} else {
// Room modlog only
this.modlog("BLACKLIST", targetUser, ` by ${user.userid}${(target ? `: ${target}` : '')}`);
}
return true;
},
blacklisthelp: [
`/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~`,
`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`,
`/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`,
`/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`,
],
forcebattleban: 'battleban',
battleban(target, room, user, connection, cmd) {
if (!target) return this.parse(`/help battleban`);
const reason = this.splitTarget(target);
const targetUser = this.targetUser;
if (!targetUser) return this.errorReply(`User ${this.targetUsername} not found.`);
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!reason) {
return this.errorReply(`Battle bans require a reason.`);
}
const includesUrl = reason.includes('.pokemonshowdown.com/'); // lgtm [js/incomplete-url-substring-sanitization]
if (!room.battle && !includesUrl && cmd !== 'forcebattleban') {
return this.errorReply(`Battle bans require a battle replay if used outside of a battle; if the battle has expired, use /forcebattleban.`);
}
if (!this.can('rangeban', targetUser)) {
this.errorReply(`Battlebans have been deprecated. Alternatives:`);
this.errorReply(`- timerstalling and bragging about it: lock`);
this.errorReply(`- other timerstalling: they're not timerstalling, leave them alone`);
this.errorReply(`- bad nicknames: lock, locks prevent nicknames from appearing; you should always have been locking for this`);
this.errorReply(`- ladder cheating: gban, get a moderator if necessary`);
this.errorReply(`- serious ladder cheating: permaban, get a leader`);
this.errorReply(`- other: get a leader`);
return;
}
if (Punishments.isBattleBanned(targetUser)) return this.errorReply(`User '${targetUser.name}' is already banned from battling.`);
const reasonText = reason ? ` (${reason})` : `.`;
const battlebanMessage = `${targetUser.name} was banned from starting new battles by ${user.name}${reasonText}`;
this.privateModAction(`(${battlebanMessage})`);
// Notify staff room when a user is banned from battling outside of it.
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addByUser(user, `<<${room.id}>> ${battlebanMessage}`);
}
if (targetUser.trusted) {
Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} was banned from battling by ${user.name}, and should probably be demoted.`);
}
this.globalModlog("BATTLEBAN", targetUser, ` by ${user.userid}${reasonText}`);
Ladders.cancelSearches(targetUser);
Punishments.battleban(targetUser, null, null, reason);
targetUser.popup(`|modal|${user.name} has prevented you from starting new battles for 2 days${reasonText}`);
// Automatically upload replays as evidence/reference to the punishment
if (room.battle) this.parse('/savereplay');
return true;
},
battlebanhelp: [`/battleban [username], [reason] - [DEPRECATED] Prevents the user from starting new battles for 2 days and shows them the [reason]. Requires: & ~`],
unbattleban(target, room, user) {
if (!target) return this.parse('/help unbattleban');
if (!this.can('lock')) return;
const targetUser = Users.get(target);
const unbanned = Punishments.unbattleban(target);
if (unbanned) {
this.addModAction(`${unbanned} was allowed to battle again by ${user.name}.`);
this.globalModlog("UNBATTLEBAN", toID(target), `by ${user.name}`);
if (targetUser) targetUser.popup(`${user.name} has allowed you to battle again.`);
} else {
this.errorReply(`User ${target} is not banned from battling.`);
}
},
unbattlebanhelp: [`/unbattleban [username] - [DEPRECATED] Allows a user to battle again. Requires: % @ & ~`],
nameblacklist: 'blacklistname',
blacklistname(target, room, user) {
if (!target) return this.parse('/help blacklistname');
if (!this.canTalk()) return;
if (!this.can('editroom', null, room)) return false;
if (!room.chatRoomData) {
return this.errorReply("This room is not going to last long enough for a blacklist to matter - just ban the user");
}
let [targetStr, reason] = target.split('|').map(val => val.trim());
if (!targetStr || (!reason && REQUIRE_REASONS)) {
return this.errorReply("Usage: /blacklistname name1, name2, ... | reason");
}
let targets = targetStr.split(',').map(s => toID(s));
let duplicates = targets.filter(userid => {
let punishment = Punishments.roomUserids.nestedGet(room.id, userid);
return punishment && punishment[0] === 'BLACKLIST';
});
if (duplicates.length) {
return this.errorReply(`[${duplicates.join(', ')}] ${Chat.plural(duplicates, "are", "is")} already blacklisted.`);
}
const userRank = Config.groupsranking.indexOf(room.getAuth(user));
for (const userid of targets) {
if (!userid) return this.errorReply(`User '${userid}' is not a valid userid.`);
const targetRank = Config.groupsranking.indexOf(room.getAuth({userid}));
if (targetRank >= userRank) return this.errorReply(`/blacklistname - Access denied: ${userid} is of equal or higher authority than you.`);
Punishments.roomBlacklist(room, null, null, userid, reason);
const trusted = Users.isTrusted(userid);
if (trusted && room.isPrivate !== true) {
Monitor.log(`[CrisisMonitor] Trusted user ${userid}${(trusted !== userid ? ` (${trusted})` : ``)} was nameblacklisted from ${room.id} by ${user.name}, and should probably be demoted.`);
}
if (!room.isPrivate && room.chatRoomData) {
this.globalModlog("NAMEBLACKLIST", userid, ` by ${user.userid}${(reason ? `: ${reason}` : '')}`);
}
}
this.privateModAction(`(${targets.join(', ')}${Chat.plural(targets, " were", " was")} nameblacklisted from ${room.title} by ${user.name}.)`);
return true;
},
blacklistnamehelp: [`/blacklistname OR /nameblacklist [username1, username2, etc.] | reason - Blacklists the given username(s) from the room you are in for a year. Requires: # & ~`],
unab: 'unblacklist',
unblacklist(target, room, user) {
if (!target) return this.parse('/help unblacklist');
if (!this.can('editroom', null, room)) return false;
const name = Punishments.roomUnblacklist(room, target);
if (name) {
this.privateModAction(`(${name} was unblacklisted by ${user.name}.)`);
if (!room.isPrivate && room.chatRoomData) {
this.globalModlog("UNBLACKLIST", name, ` by ${user.userid}`);
}
} else {
this.errorReply(`User '${target}' is not blacklisted.`);
}
},
unblacklisthelp: [`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`],
unblacklistall(target, room, user) {
if (!this.can('editroom', null, room)) return false;
if (!target) {
user.lastCommand = '/unblacklistall';
this.errorReply("THIS WILL UNBLACKLIST ALL BLACKLISTED USERS IN THIS ROOM.");
this.errorReply("To confirm, use: /unblacklistall confirm");
return;
}
if (user.lastCommand !== '/unblacklistall' || target !== 'confirm') {
return this.parse('/help unblacklistall');
}
user.lastCommand = '';
let unblacklisted = Punishments.roomUnblacklistAll(room);
if (!unblacklisted) return this.errorReply("No users are currently blacklisted in this room to unblacklist.");
this.addModAction(`All blacklists in this room have been lifted by ${user.name}.`);
this.modlog('UNBLACKLISTALL');
this.roomlog(`Unblacklisted users: ${unblacklisted.join(', ')}`);
},
unblacklistallhelp: [`/unblacklistall - Unblacklists all blacklisted users in the current room. Requires #, &, ~`],
expiringbls: 'showblacklist',
expiringblacklists: 'showblacklist',
blacklists: 'showblacklist',
showbl: 'showblacklist',
showblacklist(target, room, user, connection, cmd) {
if (target) room = Rooms.search(target);
if (!room) return this.errorReply(`The room "${target}" was not found.`);
if (!this.can('mute', null, room)) return false;
const SOON_EXPIRING_TIME = 3 * 30 * 24 * 60 * 60 * 1000; // 3 months
if (!room.chatRoomData) return this.errorReply("This room does not support blacklists.");
const subMap = Punishments.roomUserids.get(room.id);
if (!subMap || subMap.size === 0) {
return this.sendReply("This room has no blacklisted users.");
}
let blMap = new Map();
let ips = '';
for (const [userid, punishment] of subMap) {
const [punishType, id, expireTime] = punishment;
if (punishType === 'BLACKLIST') {
if (!blMap.has(id)) blMap.set(id, [expireTime]);
if (id !== userid) blMap.get(id).push(userid);
}
}
if (user.can('ban')) {
const subMap = Punishments.roomIps.get(room.id);
if (subMap) {
ips = '/ips';
for (const [ip, punishment] of subMap) {
const [punishType, id] = punishment;
if (punishType === 'BLACKLIST') {
if (!blMap.has(id)) blMap.set(id, []);
blMap.get(id).push(ip);
}
}
}
}
let soonExpiring = (cmd === 'expiringblacklists' || cmd === 'expiringbls');
let buf = Chat.html`Blacklist for ${room.title}${soonExpiring ? ` (expiring within 3 months)` : ''}:<br />`;
for (const [userid, data] of blMap) {
const [expireTime, ...alts] = data;
if (soonExpiring && expireTime > Date.now() + SOON_EXPIRING_TIME) continue;
const expiresIn = new Date(expireTime).getTime() - Date.now();
const expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
buf += `- <strong>${userid}</strong>, for ${Chat.count(expiresDays, "days")}`;
if (alts.length) buf += `, alts${ips}: ${alts.join(', ')}`;
buf += `<br />`;
}
this.sendReplyBox(buf);
},
showblacklisthelp: [
`/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`,
`/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`,
],
markshared(target, room, user) {
if (!target) return this.parse('/help markshared');
if (!this.can('ban')) return false;
let [ip, note] = this.splitOne(target);
if (!/^[0-9.*]+$/.test(ip)) return this.errorReply("Please enter a valid IP address.");
if (Punishments.sharedIps.has(ip)) return this.errorReply("This IP is already marked as shared.");
if (!note) {
this.errorReply(`You must specify who owns this shared IP.`);
this.parse(`/help markshared`);
return;
}
Punishments.addSharedIp(ip, note);
note = ` (${note})`;
this.globalModlog('SHAREDIP', ip, ` by ${user.name}${note}`);
const message = `The IP '${ip}' was marked as shared by ${user.name}.${note}`;
const staffRoom = Rooms('staff');
if (staffRoom) return staffRoom.addByUser(user, message);
return this.addModAction(message);
},
marksharedhelp: [`/markshared [IP], [owner/organization of IP] - Marks an IP address as shared. Note: the owner/organization (i.e., University of Minnesota) of the shared IP is required. Requires @, &, ~`],
unmarkshared(target, room, user) {
if (!target) return this.parse('/help unmarkshared');
if (!this.can('ban')) return false;
if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address.");
if (!Punishments.sharedIps.has(target)) return this.errorReply("This IP isn't marked as shared.");
Punishments.removeSharedIp(target);
this.globalModlog('UNSHAREIP', target, ` by ${user.name}`);
return this.addModAction(`The IP '${target}' was unmarked as shared by ${user.name}.`);
},
unmarksharedhelp: [`/unmarkshared [ip] - Unmarks a shared IP address. Requires @, &, ~`],
/*********************************************************
* Server management commands
*********************************************************/
async hotpatch(target, room, user) {
if (!target) return this.parse('/help hotpatch');
if (!this.can('hotpatch')) return;
const lock = Monitor.hotpatchLock;
const hotpatches = ['chat', 'formats', 'loginserver', 'punishments', 'dnsbl'];
try {
if (target === 'all') {
if (lock['all']) return this.errorReply(`Hot-patching all has been disabled by ${lock['all'].by} (${lock['all'].reason})`);
if (Config.disablehotpatchall) return this.errorReply("This server does not allow for the use of /hotpatch all");
for (const hotpatch of hotpatches) {
this.parse(`/hotpatch ${hotpatch}`);
}
} else if (target === 'chat' || target === 'commands') {
if (lock['chat']) return this.errorReply(`Hot-patching chat has been disabled by ${lock['chat'].by} (${lock['chat'].reason})`);
if (lock['tournaments']) return this.errorReply(`Hot-patching tournaments has been disabled by ${lock['tournaments'].by} (${lock['tournaments'].reason})`);
Chat.destroy();
const processManagers = require('../.lib-dist/process-manager').processManagers;
for (let manager of processManagers.slice()) {
if (manager.filename.startsWith(FS('server/chat-plugins').path)) {
manager.destroy();
}
}
Chat.uncache('./server/chat');
Chat.uncache('./server/chat-commands');
Chat.uncacheDir('./server/chat-plugins');
Chat.uncacheDir('./translations');
global.Chat = require('./chat');
let runningTournaments = Tournaments.tournaments;
Chat.uncacheDir('./server/tournaments');
global.Tournaments = require('./tournaments');
Tournaments.tournaments = runningTournaments;
this.sendReply("Chat commands have been hot-patched.");
} else if (target === 'tournaments') {
if (lock['tournaments']) return this.errorReply(`Hot-patching tournaments has been disabled by ${lock['tournaments'].by} (${lock['tournaments'].reason})`);
let runningTournaments = Tournaments.tournaments;
Chat.uncacheDir('./server/tournaments');
global.Tournaments = require('./tournaments');
Tournaments.tournaments = runningTournaments;
this.sendReply("Tournaments have been hot-patched.");
} else if (target === 'formats' || target === 'battles') {
if (lock['formats']) return this.errorReply(`Hot-patching formats has been disabled by ${lock['formats'].by} (${lock['formats'].reason})`);
if (lock['battles']) return this.errorReply(`Hot-patching battles has been disabled by ${lock['battles'].by} (${lock['battles'].reason})`);
if (lock['validator']) return this.errorReply(`Hot-patching the validator has been disabled by ${lock['validator'].by} (${lock['validator'].reason})`);
// uncache the .sim-dist/dex.js dependency tree
Chat.uncacheDir('./.sim-dist');
Chat.uncacheDir('./data');
Chat.uncache('./config/formats');
// reload .sim-dist/dex.js
global.Dex = require('../.sim-dist/dex').Dex;
// rebuild the formats list
delete Rooms.global.formatList;
// respawn validator processes
TeamValidatorAsync.PM.respawn();
// respawn simulator processes
Rooms.PM.respawn();
// broadcast the new formats list to clients
Rooms.global.send(Rooms.global.formatListText);
this.sendReply("Formats have been hot-patched.");
} else if (target === 'loginserver') {
FS('config/custom.css').unwatch();
Chat.uncache('./server/loginserver');
global.LoginServer = require('./loginserver').LoginServer;
this.sendReply("The login server has been hot-patched. New login server requests will use the new code.");
} else if (target === 'learnsets' || target === 'validator') {
if (lock['validator']) return this.errorReply(`Hot-patching the validator has been disabled by ${lock['validator'].by} (${lock['validator'].reason})`);
if (lock['formats']) return this.errorReply(`Hot-patching formats has been disabled by ${lock['formats'].by} (${lock['formats'].reason})`);
TeamValidatorAsync.PM.respawn();
this.sendReply("The team validator has been hot-patched. Any battles started after now will have teams be validated according to the new code.");
} else if (target === 'punishments') {
if (lock['punishments']) return this.errorReply(`Hot-patching punishments has been disabled by ${lock['punishments'].by} (${lock['punishments'].reason})`);
Chat.uncache('./server/punishments');
global.Punishments = require('./punishments');
this.sendReply("Punishments have been hot-patched.");
} else if (target === 'dnsbl' || target === 'datacenters') {
IPTools.loadDatacenters();
this.sendReply("IPTools has been hot-patched.");
} else if (target.startsWith('disable')) {
this.sendReply("Disabling hot-patch has been moved to its own command:");
return this.parse('/help nohotpatch');
} else {
return this.errorReply("Your hot-patch command was unrecognized.");
}
} catch (e) {
Rooms.global.notifyRooms(['development', 'staff', 'upperstaff'], `|c|${user.getIdentity()}|/log ${user.name} used /hotpatch ${target} - but something failed while trying to hot-patch.`);
return this.errorReply(`Something failed while trying to hot-patch ${target}: \n${e.stack}`);
}
Rooms.global.notifyRooms(['development', 'staff', 'upperstaff'], `|c|${user.getIdentity()}|/log ${user.name} used /hotpatch ${target}`);
},
hotpatchhelp: [
`Hot-patching the game engine allows you to update parts of Showdown without interrupting currently-running battles. Requires: ~`,
`Hot-patching has greater memory requirements than restarting`,
`You can disable various hot-patches with /nohotpatch. For more information on this, see /help nohotpatch`,
`/hotpatch chat - reload chat-commands.js and the chat-plugins`,
`/hotpatch validator - spawn new team validator processes`,
`/hotpatch formats - reload the .sim-dist/dex.js tree, rebuild and rebroad the formats list, and spawn new simulator and team validator processes`,
`/hotpatch dnsbl - reloads IPTools datacenters`,
`/hotpatch punishments - reloads new punishments code`,
`/hotpatch tournaments - reloads new tournaments code`,
`/hotpatch all - hot-patches chat, tournaments, formats, login server, punishments, and dnsbl`,
],
hotpatchlock: 'nohotpatch',
nohotpatch(target, room, user) {
if (!this.can('declare')) return;
if (!target) return this.parse('/help nohotpatch');
const separator = ' ';
const hotpatch = toID(target.substr(0, target.indexOf(separator)));
const reason = target.substr(target.indexOf(separator), target.length).trim();
if (!reason || !target.includes(separator)) return this.parse('/help nohotpatch');
let lock = Monitor.hotpatchLock;
const validDisable = ['chat', 'battles', 'formats', 'validator', 'tournaments', 'punishments', 'all'];
if (validDisable.includes(hotpatch)) {
if (lock[hotpatch]) return this.errorReply(`Hot-patching ${hotpatch} has already been disabled by ${lock[hotpatch].by} (${lock[hotpatch].reason})`);
lock[hotpatch] = {
by: user.name,
reason: reason,
};
this.sendReply(`You have disabled hot-patching ${hotpatch}.`);
} else {
return this.errorReply("This hot-patch is not an option to disable.");
}
Rooms.global.notifyRooms(['development', 'staff', 'upperstaff'], `|c|${user.getIdentity()}|/log ${user.name} has disabled hot-patching ${hotpatch}. Reason: ${reason}`);
},
nohotpatchhelp: [`/nohotpatch [chat|formats|battles|validator|tournaments|punishments|all] [reason] - Disables hotpatching the specified part of the simulator. Requires: & ~`],
savelearnsets(target, room, user) {
if (!this.can('hotpatch')) return false;
this.sendReply("saving...");
FS('data/learnsets.js').write(`'use strict';\n\nexports.BattleLearnsets = {\n` +
Object.entries(Dex.data.Learnsets).map(([k, v]) => (
`\t${k}: {learnset: {\n` +
Object.entries(v.learnset).sort(
(a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
).map(([k, v]) => (
`\t\t${k}: ["` + v.join(`", "`) + `"],\n`
)).join('') +
`\t}},\n`
)).join('') +
`};\n`).then(() => {
this.sendReply("learnsets.js saved.");
});
},
widendatacenters: 'adddatacenters',
adddatacenters(target, room, user, connection, cmd) {
if (!this.can('hotpatch')) return false;
// should be in the format: IP, IP, name, URL
let widen = (cmd === 'widendatacenters');
FS('config/datacenters.csv').readIfExists().then(data => {
let datacenters = [];
for (const row of data.split("\n")) {
if (!row) continue;
const rowSplit = row.split(',');
const rowData = [
IPTools.ipToNumber(rowSplit[0]),
IPTools.ipToNumber(rowSplit[1]),
IPTools.urlToHost(rowSplit[3]),
row,
];
datacenters.push(rowData);
}
data = String(target).split("\n");
let successes = 0;
let identicals = 0;
let widenSuccesses = 0;
for (const row of data) {
if (!row) continue;
let rowSplit = row.split(',');
let rowData = [
IPTools.ipToNumber(rowSplit[0]),
IPTools.ipToNumber(rowSplit[1]),
IPTools.urlToHost(rowSplit[3]),
row,
];
if (rowData[1] < rowData[0]) {
this.errorReply(`Invalid range: ${row}`);
continue;
}
let iMin = 0;
let iMax = datacenters.length;
while (iMin < iMax) {
let i = Math.floor((iMax + iMin) / 2);
if (rowData[0] > datacenters[i][0]) {
iMin = i + 1;
} else {
iMax = i;
}
}
if (iMin < datacenters.length) {
let next = datacenters[iMin];
if (rowData[0] === next[0] && rowData[1] === next[1]) {
identicals++;
continue;
}
if (rowData[0] <= next[0] && rowData[1] >= next[1]) {
if (widen === true) {
widenSuccesses++;
datacenters.splice(iMin, 1, rowData);
continue;
}
this.errorReply(`Too wide: ${row}`);
this.errorReply(`Intersects with: ${next[3]}`);
continue;
}
if (rowData[1] >= next[0]) {
this.errorReply(`Could not insert: ${row}`);
this.errorReply(`Intersects with: ${next[3]}`);
continue;
}
}
if (iMin > 0) {
let prev = datacenters[iMin - 1];
if (rowData[0] >= prev[0] && rowData[1] <= prev[1]) {
this.errorReply(`Too narrow: ${row}`);
this.errorReply(`Intersects with: ${prev[3]}`);
continue;
}
if (rowData[0] <= prev[1]) {
this.errorReply(`Could not insert: ${row}`);
this.errorReply(`Intersects with: ${prev[3]}`);
continue;
}
}
successes++;
datacenters.splice(iMin, 0, rowData);
}
let output = datacenters.map(r => r[3]).join('\n') + '\n';
FS('config/datacenters.csv').write(output);
this.sendReply(`Done: ${successes} successes, ${identicals} unchanged.`);
if (widenSuccesses) this.sendReply(`${widenSuccesses} widens.`);
});
},
disableladder(target, room, user) {
if (!this.can('disableladder')) return false;
if (Ladders.disabled) {
return this.errorReply(`/disableladder - Ladder is already disabled.`);
}
Ladders.disabled = true;
this.modlog(`DISABLELADDER`);
Monitor.log(`The ladder was disabled by ${user.name}.`);
const innerHTML = (
`<b>Due to technical difficulties, the ladder has been temporarily disabled.</b><br />` +
`Rated games will no longer update the ladder. It will be back momentarily.`
);
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.type === 'battle') curRoom.rated = false;
if (curRoom.id !== 'global') curRoom.addRaw(`<div class="broadcast-red">${innerHTML}</div>`).update();
}
for (const u of Users.users.values()) {
if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw <div class="broadcast-red">${innerHTML}</div>`);
}
},
enableladder(target, room, user) {
if (!this.can('disableladder')) return false;
if (!Ladders.disabled) {
return this.errorReply(`/enable - Ladder is already enabled.`);
}
Ladders.disabled = false;
this.modlog('ENABLELADDER');
Monitor.log(`The ladder was enabled by ${user.name}.`);
const innerHTML = (
`<b>The ladder is now back.</b><br />` +
`Rated games will update the ladder now..`
);
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.id !== 'global') curRoom.addRaw(`<div class="broadcast-green">${innerHTML}</div>`).update();
}
for (const u of Users.users.values()) {
if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw <div class="broadcast-green">${innerHTML}</div>`);
}
},
lockdown(target, room, user) {
if (!this.can('lockdown')) return false;
Rooms.global.startLockdown();
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /lockdown`);
},
lockdownhelp: [`/lockdown - locks down the server, which prevents new battles from starting so that the server can eventually be restarted. Requires: ~`],
autolockdown: 'autolockdownkill',
autolockdownkill(target, room, user) {
if (!this.can('lockdown')) return false;
if (Config.autolockdown === undefined) Config.autolockdown = true;
if (this.meansYes(target)) {
if (Config.autolockdown) return this.errorReply("The server is already set to automatically kill itself upon the final battle finishing.");
Config.autolockdown = true;
this.sendReply("The server is now set to automatically kill itself upon the final battle finishing.");
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /autolockdownkill on`);
} else if (this.meansNo(target)) {
if (!Config.autolockdown) return this.errorReply("The server is already set to not automatically kill itself upon the final battle finishing.");
Config.autolockdown = false;
this.sendReply("The server is now set to not automatically kill itself upon the final battle finishing.");
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /autolockdownkill off`);
} else {
return this.parse('/help autolockdownkill');
}
},
autolockdownkillhelp: [
`/autolockdownkill on - Turns on the setting to enable the server to automatically kill itself upon the final battle finishing. Requires ~`,
`/autolockdownkill off - Turns off the setting to enable the server to automatically kill itself upon the final battle finishing. Requires ~`,
],
prelockdown(target, room, user) {
if (!this.can('lockdown')) return false;
Rooms.global.lockdown = 'pre';
this.sendReply("Tournaments have been disabled in preparation for the server restart.");
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /prelockdown`);
},
slowlockdown(target, room, user) {
if (!this.can('lockdown')) return false;
Rooms.global.startLockdown(undefined, true);
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /slowlockdown`);
},
endlockdown(target, room, user) {
if (!this.can('lockdown')) return false;
if (!Rooms.global.lockdown) {
return this.errorReply("We're not under lockdown right now.");
}
if (Rooms.global.lockdown === true) {
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.id !== 'global') curRoom.addRaw(`<div class="broadcast-green"><b>The server restart was canceled.</b></div>`).update();
}
} else {
this.sendReply("Preparation for the server shutdown was canceled.");
}
Rooms.global.lockdown = false;
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /endlockdown`);
},
emergency(target, room, user) {
if (!this.can('lockdown')) return false;
if (Config.emergency) {
return this.errorReply("We're already in emergency mode.");
}
Config.emergency = true;
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.id !== 'global') curRoom.addRaw(`<div class="broadcast-red">The server has entered emergency mode. Some features might be disabled or limited.</div>`).update();
}
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /emergency.`);
},
endemergency(target, room, user) {
if (!this.can('lockdown')) return false;
if (!Config.emergency) {
return this.errorReply("We're not in emergency mode.");
}
Config.emergency = false;
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.id !== 'global') curRoom.addRaw(`<div class="broadcast-green"><b>The server is no longer in emergency mode.</b></div>`).update();
}
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /endemergency.`);
},
kill(target, room, user) {
if (!this.can('lockdown')) return false;
if (Rooms.global.lockdown !== true) {
return this.errorReply("For safety reasons, /kill can only be used during lockdown.");
}
if (Chat.updateServerLock) {
return this.errorReply("Wait for /updateserver to finish before using /kill.");
}
for (const worker of Sockets.workers.values()) {
worker.kill();
}
if (!room.destroyLog) {
process.exit();
return;
}
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /kill`);
room.destroyLog(() => {
process.exit();
});
// Just in the case the above never terminates, kill the process
// after 10 seconds.
setTimeout(() => {
process.exit();
}, 10000);
},
killhelp: [`/kill - kills the server. Can't be done unless the server is in lockdown state. Requires: ~`],
loadbanlist(target, room, user, connection) {
if (!this.can('hotpatch')) return false;
connection.sendTo(room, "Loading ipbans.txt...");
Punishments.loadBanlist().then(
() => connection.sendTo(room, "ipbans.txt has been reloaded."),
error => connection.sendTo(room, `Something went wrong while loading ipbans.txt: ${error}`)
);
},
loadbanlisthelp: [`/loadbanlist - Loads the bans located at ipbans.txt. The command is executed automatically at startup. Requires: ~`],
refreshpage(target, room, user) {
if (!this.can('hotpatch')) return false;
Rooms.global.send('|refresh|');
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /refreshpage`);
},
async updateserver(target, room, user, connection) {
if (!user.can('hotpatch')) {
return this.errorReply(`/updateserver - Access denied.`);
}
if (Chat.updateServerLock) {
return this.errorReply(`/updateserver - Another update is already in progress (or a previous update crashed).`);
}
Chat.updateServerLock = true;
const logRoom = Rooms('staff') || room;
/** @return {Promise<[number, string, string]>} */
function exec(/** @type {string} */ command) {
logRoom.roomlog(`$ ${command}`);
return new Promise((resolve, reject) => {
require('child_process').exec(command, {
cwd: __dirname,
}, (error, stdout, stderr) => {
let log = `[o] ${stdout}[e] ${stderr}`;
if (error) log = `[c] ${error.code}\n${log}`;
logRoom.roomlog(log);
resolve([error && error.code || 0, stdout, stderr]);
});
});
}
this.sendReply(`Fetching newest version...`);
logRoom.roomlog(`${user.name} used /updateserver`);
let [code, stdout, stderr] = await exec(`git fetch`);
if (code) throw new Error(`updateserver: Crash while fetching - make sure this is a Git repository`);
if (!stdout && !stderr) {
Chat.updateServerLock = false;
this.sendReply(`There were no updates.`);
[code, stdout, stderr] = await exec('../build');
if (stderr) {
return this.errorReply(`Crash while rebuilding: ${stderr}`);
}
this.sendReply(`Rebuilt.`);
return;
}
[code, stdout, stderr] = await exec(`git rev-parse HEAD`);
if (code || stderr) throw new Error(`updateserver: Crash while grabbing hash`);
const oldHash = String(stdout).trim();
[code, stdout, stderr] = await exec(`git stash save --include-untracked "PS /updateserver autostash"`);
let stashedChanges = true;
if (code) throw new Error(`updateserver: Crash while stashing`);
if ((stdout + stderr).includes("No local changes")) {
stashedChanges = false;
} else if (stderr) {
throw new Error(`updateserver: Crash while stashing`);
} else {
this.sendReply(`Saving changes...`);
}
// errors can occur while rebasing or popping the stash; make sure to recover
try {
this.sendReply(`Rebasing...`);
[code] = await exec(`git rebase FETCH_HEAD`);
if (code) {
// conflict while rebasing
await exec(`git rebase --abort`);
throw new Error(`restore`);
}
if (stashedChanges) {
this.sendReply(`Restoring saved changes...`);
[code] = await exec(`git stash pop`);
if (code) {
// conflict while popping stash
await exec(`git reset HEAD .`);
await exec(`git checkout .`);
throw new Error(`restore`);
}
}
this.sendReply(`SUCCESSFUL, server updated.`);
} catch (e) {
// failed while rebasing or popping the stash
await exec(`git reset --hard ${oldHash}`);
await exec(`git stash pop`);
this.sendReply(`FAILED, old changes restored.`);
}
[code, stdout, stderr] = await exec('../build');
if (stderr) {
return this.errorReply(`Crash while rebuilding: ${stderr}`);
}
this.sendReply(`Rebuilt.`);
Chat.updateServerLock = false;
},
crashfixed(target, room, user) {
if (Rooms.global.lockdown !== true) {
return this.errorReply('/crashfixed - There is no active crash.');
}
if (!this.can('hotpatch')) return false;
Rooms.global.lockdown = false;
if (Rooms.lobby) {
Rooms.lobby.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>`).update();
}
const logRoom = Rooms('staff') || room;
logRoom.roomlog(`${user.name} used /crashfixed`);
},
crashfixedhelp: [`/crashfixed - Ends the active lockdown caused by a crash without the need of a restart. Requires: ~`],
memusage: 'memoryusage',
memoryusage(target) {
if (!this.can('hotpatch')) return false;
let memUsage = process.memoryUsage();
let results = [memUsage.rss, memUsage.heapUsed, memUsage.heapTotal];
let units = ["B", "KiB", "MiB", "GiB", "TiB"];
for (let i = 0; i < results.length; i++) {
let unitIndex = Math.floor(Math.log2(results[i]) / 10); // 2^10 base log
results[i] = `${(results[i] / Math.pow(2, 10 * unitIndex)).toFixed(2)} ${units[unitIndex]}`;
}
this.sendReply(`||[Main process] RSS: ${results[0]}, Heap: ${results[1]} / ${results[2]}`);
},
bash(target, room, user, connection) {
if (!user.hasConsoleAccess(connection)) {
return this.errorReply("/bash - Access denied.");
}
if (!target) return this.parse('/help bash');
connection.sendTo(room, `$ ${target}`);
require('child_process').exec(target, (error, stdout, stderr) => {
connection.sendTo(room, (`${stdout}${stderr}`));
});
},
bashhelp: [`/bash [command] - Executes a bash command on the server. Requires: ~ console access`],
async eval(target, room, user, connection) {
if (!user.hasConsoleAccess(connection)) {
return this.errorReply("/eval - Access denied.");
}
if (!this.runBroadcast(true)) return;
if (!this.broadcasting) this.sendReply(`||>> ${target}`);
try {
/* eslint-disable no-unused-vars */
let battle = room.battle;
let me = user;
let result = eval(target);
if (result && result.then) {
result = `Promise -> ${Chat.stringify(await result)}`;
} else {
result = Chat.stringify(result);
}
result = result.replace(/\n/g, '\n||');
this.sendReply('||<< ' + result);
/* eslint-enable no-unused-vars */
} catch (e) {
const message = ('' + e.stack).replace(/\n *at CommandContext\.eval [\s\S]*/m, '').replace(/\n/g, '\n||');
this.sendReply(`|| << ${message}`);
}
},
evalbattle(target, room, user, connection) {
if (!user.hasConsoleAccess(connection)) {
return this.errorReply("/evalbattle - Access denied.");
}
if (!this.runBroadcast(true)) return;
if (!room.battle) {
return this.errorReply("/evalbattle - This isn't a battle room.");
}
room.battle.stream.write(`>eval ${target.replace(/\n/g, '\f')}`);
},
ebat: 'editbattle',
editbattle(target, room, user) {
if (!this.can('forcewin')) return false;
if (!target) return this.parse('/help editbattle');
if (!room.battle) {
this.errorReply("/editbattle - This is not a battle room.");
return false;
}
let cmd;
let spaceIndex = target.indexOf(' ');
if (spaceIndex > 0) {
cmd = target.substr(0, spaceIndex).toLowerCase();
target = target.substr(spaceIndex + 1);
} else {
cmd = target.toLowerCase();
target = '';
}
if (cmd.charAt(cmd.length - 1) === ',') cmd = cmd.slice(0, -1);
let targets = target.split(',');
function getPlayer(input) {
let player = room.battle.playerTable[toID(input)];
if (player) return player.slot;
if (input.includes('1')) return 'p1';
if (input.includes('2')) return 'p2';
return 'p3';
}
function getPokemon(input) {
if (/^[0-9]+$/.test(input)) {
return `.pokemon[${(parseInt(input) - 1)}]`;
}
return `.pokemon.find(p => p.speciesid==='${toID(targets[1])}')`;
}
switch (cmd) {
case 'hp':
case 'h':
room.battle.stream.write(`>eval let p=${getPlayer(targets[0]) + getPokemon(targets[1])};p.sethp(${parseInt(targets[2])});if (p.isActive)battle.add('-damage',p,p.getHealth);`);
break;
case 'status':
case 's':
room.battle.stream.write(`>eval let pl=${getPlayer(targets[0])};let p=pl${getPokemon(targets[1])};p.setStatus('${toID(targets[2])}');if (!p.isActive){battle.add('','please ignore the above');battle.add('-status',pl.active[0],pl.active[0].status,'[silent]');}`);
break;
case 'pp':
room.battle.stream.write(`>eval let pl=${getPlayer(targets[0])};let p=pl${getPokemon(targets[1])};p.moveSlots[p.moves.indexOf('${toID(targets[2])}')].pp = ${parseInt(targets[3])};`);
break;
case 'boost':
case 'b':
room.battle.stream.write(`>eval let p=${getPlayer(targets[0]) + getPokemon(targets[1])};battle.boost({${toID(targets[2])}:${parseInt(targets[3])}},p)`);
break;
case 'volatile':
case 'v':
room.battle.stream.write(`>eval let p=${getPlayer(targets[0]) + getPokemon(targets[1])};p.addVolatile('${toID(targets[2])}')`);
break;
case 'sidecondition':
case 'sc':
room.battle.stream.write(`>eval let p=${getPlayer(targets[0])}.addSideCondition('${toID(targets[1])}', 'debug')`);
break;
case 'fieldcondition': case 'pseudoweather':
case 'fc':
room.battle.stream.write(`>eval battle.addPseudoWeather('${toID(targets[0])}', 'debug')`);
break;
case 'weather':
case 'w':
room.battle.stream.write(`>eval battle.setWeather('${toID(targets[0])}', 'debug')`);
break;
case 'terrain':
case 't':
room.battle.stream.write(`>eval battle.setTerrain('${toID(targets[0])}', 'debug')`);
break;
default:
this.errorReply(`Unknown editbattle command: ${cmd}`);
break;
}
},
editbattlehelp: [
`/editbattle hp [player], [pokemon], [hp]`,
`/editbattle status [player], [pokemon], [status]`,
`/editbattle pp [player], [pokemon], [move], [pp]`,
`/editbattle boost [player], [pokemon], [stat], [amount]`,
`/editbattle volatile [player], [pokemon], [volatile]`,
`/editbattle sidecondition [player], [sidecondition]`,
`/editbattle fieldcondition [fieldcondition]`,
`/editbattle weather [weather]`,
`/editbattle terrain [terrain]`,
`Short forms: /ebat h OR s OR pp OR b OR v OR sc OR fc OR w OR t`,
`[player] must be a username or number, [pokemon] must be species name or number (not nickname), [move] must be move name.`,
],
allowexportinputlog(/** @type {string} */ target, /** @type {Room?} */ room, /** @type {User} */ user) {
const battle = room.battle;
if (!battle) return this.errorReply(`Must be in a battle.`);
if (!battle.allowExtraction) return this.errorReply(`Someone must have requested extraction.`);
const targetUser = Users.getExact(target);
if (toID(battle.p1.name) === user.userid) {
battle.allowExtraction[0] = targetUser.userid;
} else if (toID(battle.p2.name) === user.userid) {
battle.allowExtraction[1] = targetUser.userid;
} else {
return this.errorReply(`Must be a player in the battle.`);
}
this.addModAction(`${user.userid} consents to sharing battle team and choices with ${targetUser.userid}.`);
if (battle.allowExtraction.join(',') !== `${targetUser.userid},${targetUser.userid}`) return;
this.addModAction(`${targetUser.name} has extracted the battle input log.`);
const inputLog = battle.inputLog.map(Chat.escapeHTML).join(`<br />`);
targetUser.sendTo(room, `|html|<div class="chat"><code style="white-space: pre-wrap; overflow-wrap: break-word; display: block">${inputLog}</code></div>`);
},
requestinputlog: 'exportinputlog',
exportinputlog(target, room, user) {
const battle = room.battle;
if (!battle) return this.errorReply(`This command only works in battle rooms.`);
if (!battle.inputLog) {
this.errorReply(`This command only works when the battle has ended - if the battle has stalled, ask players to forfeit.`);
if (user.can('forcewin')) this.errorReply(`Alternatively, you can end the battle with /forcetie.`);
return;
}
if (!this.can('exportinputlog', null, room)) return;
if (!battle.allowExtraction) {
battle.allowExtraction = ['', ''];
}
if (battle.allowExtraction[0] !== user.userid) {
const p1 = Users(battle.p1.name);
if (p1) p1.sendTo(room, Chat.html`|html|${user.name} wants to extract the battle input log. <button name="send" value="/allowexportinputlog ${user.userid}">Share your team and choices with "${user.name}"</button>`);
}
if (battle.allowExtraction[1] !== user.userid) {
const p2 = Users(battle.p2.name);
if (p2) p2.sendTo(room, Chat.html`|html|${user.name} wants to extract the battle input log. <button name="send" value="/allowexportinputlog ${user.userid}">Share your team and choices with "${user.name}"</button>`);
}
if (battle.allowExtraction.join(',') !== `${user.userid},${user.userid}`) {
this.addModAction(`${user.name} wants to extract the battle input log.`);
return;
}
this.addModAction(`${user.name} has extracted the battle input log.`);
const inputLog = battle.inputLog.map(Chat.escapeHTML).join(`<br />`);
user.sendTo(room, `|html|<div class="chat"><code style="white-space: pre-wrap; overflow-wrap: break-word; display: block">${inputLog}</code></div>`);
},
exportinputloghelp: [`/exportinputlog - Asks players in a battle for permission to export an inputlog. Requires: & ~`],
importinputlog(target, room, user, connection) {
if (!this.can('broadcast')) return;
const formatIndex = target.indexOf(`"formatid":"`);
const nextQuoteIndex = target.indexOf(`"`, formatIndex + 12);
if (formatIndex < 0 || nextQuoteIndex < 0) return this.errorReply(`Invalid input log.`);
target = target.replace(/\r/g, '');
if ((`\n` + target).includes(`\n>eval `) && !user.hasConsoleAccess(connection)) {
return this.errorReply(`Your input log contains untrusted code - you must have console access to use it.`);
}
const formatid = target.slice(formatIndex + 12, nextQuoteIndex);
const battleRoom = Rooms.createBattle(formatid, {inputLog: target});
const nameIndex1 = target.indexOf(`"name":"`);
const nameNextQuoteIndex1 = target.indexOf(`"`, nameIndex1 + 8);
const nameIndex2 = target.indexOf(`"name":"`, nameNextQuoteIndex1 + 1);
const nameNextQuoteIndex2 = target.indexOf(`"`, nameIndex2 + 8);
if (nameIndex1 >= 0 && nameNextQuoteIndex1 >= 0 && nameIndex2 >= 0 && nameNextQuoteIndex2 >= 0) {
const battle = battleRoom.battle;
battle.p1.name = target.slice(nameIndex1 + 8, nameNextQuoteIndex1);
battle.p2.name = target.slice(nameIndex2 + 8, nameNextQuoteIndex2);
}
battleRoom.auth[user.userid] = Users.HOST_SYMBOL;
this.parse(`/join ${battleRoom.id}`);
setTimeout(() => {
// timer to make sure this goes under the battle
battleRoom.add(`|html|<div class="broadcast broadcast-blue"><strong>This is an imported replay</strong><br />Players will need to be manually added with <code>/addplayer</code> or <code>/restoreplayers</code></div>`);
}, 500);
},
importinputloghelp: [`/importinputlog [inputlog] - Starts a battle with a given inputlog. Requires: + % @ & ~`],
inputlog() {
this.parse(`/help exportinputlog`);
this.parse(`/help importinputlog`);
},
/*********************************************************
* Battle commands
*********************************************************/
forfeit(target, room, user) {
if (!room.game) return this.errorReply("This room doesn't have an active game.");
if (!room.game.forfeit) {
return this.errorReply("This kind of game can't be forfeited.");
}
if (!room.game.forfeit(user)) {
return this.errorReply("Forfeit failed.");
}
},
choose(target, room, user) {
if (!room.game) return this.errorReply("This room doesn't have an active game.");
if (!room.game.choose) return this.errorReply("This game doesn't support /choose");
if (room.game.choose(user, target) === false) {
return this.errorReply("This game doesn't support /choose");
}
},
mv: 'move',
attack: 'move',
move(target, room, user) {
this.parse(`/choose move ${target}`);
},
sw: 'switch',
switch(target, room, user) {
this.parse(`/choose switch ${target}`);
},
team(target, room, user) {
this.parse(`/choose team ${target}`);
},
undo(target, room, user) {
if (!room.game) return this.errorReply("This room doesn't have an active game.");
if (!room.game.undo) return this.errorReply("This game doesn't support /undo");
room.game.undo(user, target);
},
uploadreplay: 'savereplay',
async savereplay(target, room, user, connection) {
if (!room || !room.battle) return;
const battle = room.battle;
// retrieve spectator log (0) if there are privacy concerns
const format = Dex.getFormat(room.format, true);
let hideDetails = !format.id.includes('customgame');
if (!format.team && battle.ended) hideDetails = false;
const data = room.getLog(hideDetails ? 0 : -1);
const datahash = crypto.createHash('md5').update(data.replace(/[^(\x20-\x7F)]+/g, '')).digest('hex');
let rating = 0;
if (battle.ended && room.rated) rating = room.rated;
const [success] = await LoginServer.request('prepreplay', {
id: room.id.substr(7),
loghash: datahash,
p1: battle.p1.name,
p2: battle.p2.name,
format: format.id,
rating: rating,
hidden: room.isPrivate || room.hideReplay ? '1' : '',
inputlog: battle.inputLog ? battle.inputLog.join('\n') : null,
});
if (success && success.errorip) {
connection.popup(`This server's request IP ${success.errorip} is not a registered server.`);
return;
}
connection.send('|queryresponse|savereplay|' + JSON.stringify({
log: data,
id: room.id.substr(7),
}));
},
addplayer(target, room, user) {
if (!target) return this.parse('/help addplayer');
if (!room.battle) return this.errorReply("You can only do this in battle rooms.");
if (room.rated) return this.errorReply("You can only add a Player to unrated battles.");
target = this.splitTarget(target, true).trim();
if (target !== 'p1' && target !== 'p2') {
this.errorReply(`Player must be set to "p1" or "p2", not "${target}".`);
return this.parse('/help addplayer');
}
let targetUser = this.targetUser;
let name = this.targetUsername;
if (!targetUser) return this.errorReply(`User ${name} not found.`);
if (!targetUser.inRooms.has(room.id)) {
return this.errorReply(`User ${name} must be in the battle room already.`);
}
if (!this.can('joinbattle', null, room)) return;
if (room.battle[target]) {
return this.errorReply(`This room already has a player in slot ${target}.`);
}
room.battle.addPlayer(targetUser, target);
this.addModAction(`${name} was added to the battle as Player ${target.slice(1)} by ${user.name}.`);
this.modlog('ROOMPLAYER', targetUser.getLastId());
},
addplayerhelp: [
`/addplayer [username], p1 - Allow the specified user to join the battle as Player 1.`,
`/addplayer [username], p2 - Allow the specified user to join the battle as Player 2.`,
],
restoreplayers(target, room, user) {
if (!room.battle) return this.errorReply("You can only do this in battle rooms.");
if (room.rated) return this.errorReply("You can only add a Player to unrated battles.");
let didSomething = false;
if (!room.battle.p1.userid && room.battle.p1.name !== 'Player 1') {
this.parse(`/addplayer ${room.battle.p1.name}, p1`);
didSomething = true;
}
if (!room.battle.p2.userid && room.battle.p2.name !== 'Player 2') {
this.parse(`/addplayer ${room.battle.p2.name}, p2`);
didSomething = true;
}
if (!didSomething) return this.errorReply(`Players could not be restored (maybe this battle already has two players?).`);
},
restoreplayershelp: [
`/restoreplayers - Restore previous players in an imported input log.`,
],
joinbattle: 'joingame',
joingame(target, room, user) {
if (!room.game) return this.errorReply("This room doesn't have an active game.");
if (!room.game.joinGame) return this.errorReply("This game doesn't support /joingame");
room.game.joinGame(user, target);
},
leavebattle: 'leavegame',
partbattle: 'leavegame',
leavegame(target, room, user) {
if (!room.game) return this.errorReply("This room doesn't have an active game.");
if (!room.game.leaveGame) return this.errorReply("This game doesn't support /leavegame");
room.game.leaveGame(user);
},
kickbattle: 'kickgame',
kickgame(target, room, user) {
if (!room.battle) return this.errorReply("You can only do this in battle rooms.");
if (room.battle.tour || room.battle.rated) return this.errorReply("You can only do this in unrated non-tour battles.");
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) {
return this.errorReply(`User ${this.targetUsername} not found.`);
}
if (!this.can('kick', targetUser)) return false;
if (room.game.leaveGame(targetUser)) {
this.addModAction(`${targetUser.name} was kicked from a battle by ${user.name} ${(target ? ` (${target})` : ``)}`);
this.modlog('KICKBATTLE', targetUser, target, {noip: 1, noalts: 1});
} else {
this.errorReply("/kickbattle - User isn't in battle.");
}
},
kickbattlehelp: [`/kickbattle [username], [reason] - Kicks a user from a battle with reason. Requires: % @ & ~`],
kickinactive(target, room, user) {
this.parse(`/timer on`);
},
timer(target, room, user) {
target = toID(target);
if (!room.game || !room.game.timer) {
return this.errorReply(`You can only set the timer from inside a battle room.`);
}
const timer = room.game.timer;
if (!timer.timerRequesters) {
return this.sendReply(`This game's timer is managed by a different command.`);
}
if (!target) {
if (!timer.timerRequesters.size) {
return this.sendReply(`The game timer is OFF.`);
}
return this.sendReply(`The game timer is ON (requested by ${[...timer.timerRequesters].join(', ')})`);
}
const force = user.can('timer', null, room);
if (!force && !room.game.playerTable[user]) {
return this.errorReply(`Access denied.`);
}
if (this.meansNo(target) || target === 'stop') {
if (timer.timerRequesters.size) {
timer.stop(force ? undefined : user);
if (force) room.send(`|inactiveoff|Timer was turned off by staff. Please do not turn it back on until our staff say it's okay.`);
} else {
this.errorReply(`The timer is already off.`);
}
} else if (this.meansYes(target) || target === 'start') {
timer.start(user);
} else {
this.errorReply(`"${target}" is not a recognized timer state.`);
}
},
autotimer: 'forcetimer',
forcetimer(target, room, user) {
target = toID(target);
if (!this.can('autotimer')) return;
if (this.meansNo(target) || target === 'stop') {
Config.forcetimer = false;
this.addModAction(`Forcetimer is now OFF: The timer is now opt-in. (set by ${user.name})`);
} else if (this.meansYes(target) || target === 'start' || !target) {
Config.forcetimer = true;
this.addModAction(`Forcetimer is now ON: All battles will be timed. (set by ${user.name})`);
} else {
this.errorReply(`'${target}' is not a recognized forcetimer setting.`);
}
},
forcetie: 'forcewin',
forcewin(target, room, user) {
if (!this.can('forcewin')) return false;
if (!room.battle) {
this.errorReply("/forcewin - This is not a battle room.");
return false;
}
room.battle.endType = 'forced';
if (!target) {
room.battle.tie();
this.modlog('FORCETIE');
return false;
}
let targetUser = Users.getExact(target);
if (!targetUser) return this.errorReply(`User '${target}' not found.`);
room.battle.win(targetUser);
this.modlog('FORCEWIN', targetUser.userid);
},
forcewinhelp: [
`/forcetie - Forces the current match to end in a tie. Requires: & ~`,
`/forcewin [user] - Forces the current match to end in a win for a user. Requires: & ~`,
],
/*********************************************************
* Challenging and searching commands
*********************************************************/
'!search': true,
search(target, room, user, connection) {
if (target) {
if (Config.laddermodchat) {
let userGroup = user.group;
if (Config.groupsranking.indexOf(userGroup) < Config.groupsranking.indexOf(Config.laddermodchat)) {
let groupName = Config.groups[Config.laddermodchat].name || Config.laddermodchat;
this.popupReply(`On this server, you must be of rank ${groupName} or higher to search for a battle.`);
return false;
}
}
Ladders(target).searchBattle(user, connection);
} else {
Ladders.cancelSearches(user);
}
},
'!cancelsearch': true,
cancelsearch(target, room, user) {
if (target) {
Ladders(toID(target)).cancelSearch(user);
} else {
Ladders.cancelSearches(user);
}
},
'!challenge': true,
chall: 'challenge',
challenge(target, room, user, connection) {
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) {
return this.popupReply(`The user '${this.targetUsername}' was not found.`);
}
if (user.locked && !targetUser.locked) {
return this.popupReply(`You are locked and cannot challenge unlocked users.`);
}
if (Punishments.isBattleBanned(user)) {
return this.popupReply(`You are banned from battling and cannot challenge users.`);
}
if (Config.pmmodchat) {
let userGroup = user.group;
if (Config.groupsranking.indexOf(userGroup) < Config.groupsranking.indexOf(Config.pmmodchat)) {
let groupName = Config.groups[Config.pmmodchat].name || Config.pmmodchat;
this.popupReply(`Because moderated chat is set, you must be of rank ${groupName} or higher to challenge users.`);
return false;
}
}
Ladders(target).makeChallenge(connection, targetUser);
},
'!blockchallenges': true,
bch: 'blockchallenges',
blockchall: 'blockchallenges',
blockchalls: 'blockchallenges',
blockchallenges(target, room, user) {
if (user.blockChallenges) return this.errorReply("You are already blocking challenges!");
user.blockChallenges = true;
user.update('blockChallenges');
this.sendReply("You are now blocking all incoming challenge requests.");
},
blockchallengeshelp: [`/blockchallenges - Blocks challenges so no one can challenge you. Unblock them with /unblockchallenges.`],
'!allowchallenges': true,
unbch: 'allowchallenges',
unblockchall: 'allowchallenges',
unblockchalls: 'allowchallenges',
unblockchallenges: 'allowchallenges',
allowchallenges(target, room, user) {
if (!user.blockChallenges) return this.errorReply("You are already available for challenges!");
user.blockChallenges = false;
user.update('blockChallenges');
this.sendReply("You are available for challenges from now on.");
},
allowchallengeshelp: [`/unblockchallenges - Unblocks challenges so you can be challenged again. Block them with /blockchallenges.`],
'!cancelchallenge': true,
cchall: 'cancelChallenge',
cancelchallenge(target, room, user) {
Ladders.cancelChallenging(user);
},
'!accept': true,
accept(target, room, user, connection) {
target = this.splitTarget(target);
if (target) return this.popupReply(`This command does not support specifying multiple users`);
const targetUser = this.targetUser || this.pmTarget;
if (!targetUser) return this.popupReply(`User "${this.targetUsername}" not found.`);
Ladders.acceptChallenge(connection, targetUser);
},
'!reject': true,
reject(target, room, user) {
target = toID(target);
if (!target && this.pmTarget) target = this.pmTarget.userid;
Ladders.rejectChallenge(user, target);
},
'!useteam': true,
saveteam: 'useteam',
utm: 'useteam',
useteam(target, room, user) {
user.team = target;
},
'!vtm': true,
vtm(target, room, user, connection) {
if (Monitor.countPrepBattle(connection.ip, connection)) {
return;
}
if (!target) return this.errorReply("Provide a valid format.");
let originalFormat = Dex.getFormat(target);
// Note: The default here of [Gen 7] Pokebank Anything Goes isn't normally hit; since the web client will send a default format
let format = originalFormat.effectType === 'Format' ? originalFormat : Dex.getFormat('[Gen 7] Pokebank Anything Goes');
if (format.effectType !== 'Format') return this.popupReply("Please provide a valid format.");
TeamValidatorAsync(format.id).validateTeam(user.team).then(result => {
let matchMessage = (originalFormat === format ? "" : `The format '${originalFormat.name}' was not found.`);
if (result.charAt(0) === '1') {
connection.popup(`${(matchMessage ? matchMessage + "\n\n" : "")}Your team is valid for ${format.name}.`);
} else {
connection.popup(`${(matchMessage ? matchMessage + "\n\n" : "")}Your team was rejected for the following reasons:\n\n- ${result.slice(1).replace(/\n/g, '\n- ')}`);
}
});
},
/*********************************************************
* Low-level
*********************************************************/
'!crq': true,
cmd: 'crq',
query: 'crq',
crq(target, room, user, connection) {
// In emergency mode, clamp down on data returned from crq's
let trustable = (!Config.emergency || (user.named && user.registered));
let spaceIndex = target.indexOf(' ');
let cmd = target;
if (spaceIndex > 0) {
cmd = target.substr(0, spaceIndex);
target = target.substr(spaceIndex + 1);
} else {
target = '';
}
if (cmd === 'userdetails') {
let targetUser = Users.get(target);
if (!trustable || !targetUser) {
connection.send('|queryresponse|userdetails|' + JSON.stringify({
userid: toID(target),
rooms: false,
}));
return false;
}
let roomList = {};
for (let roomid of targetUser.inRooms) {
if (roomid === 'global') continue;
let targetRoom = Rooms.get(roomid);
if (!targetRoom) continue; // shouldn't happen
let roomData = {};
if (targetRoom.isPrivate) {
if (!user.inRooms.has(roomid) && !user.games.has(roomid)) continue;
roomData.isPrivate = true;
}
if (targetRoom.battle) {
let battle = targetRoom.battle;
roomData.p1 = battle.p1 ? ' ' + battle.p1.name : '';
roomData.p2 = battle.p2 ? ' ' + battle.p2.name : '';
}
if (targetRoom.auth && targetUser.userid in targetRoom.auth) {
roomid = targetRoom.auth[targetUser.userid] + roomid;
}
roomList[roomid] = roomData;
}
if (!targetUser.connected) roomList = false;
let userdetails = {
userid: targetUser.userid,
avatar: targetUser.avatar,
group: targetUser.group,
autoconfirmed: !!targetUser.autoconfirmed,
rooms: roomList,
};
connection.send('|queryresponse|userdetails|' + JSON.stringify(userdetails));
} else if (cmd === 'roomlist') {
if (!trustable) return false;
connection.send('|queryresponse|roomlist|' + JSON.stringify({
rooms: Rooms.global.getBattles(target),
}));
} else if (cmd === 'rooms') {
if (!trustable) return false;
connection.send('|queryresponse|rooms|' + JSON.stringify(
Rooms.global.getRooms(user)
));
} else if (cmd === 'laddertop') {
if (!trustable) return false;
Ladders(toID(target)).getTop().then(result => {
connection.send('|queryresponse|laddertop|' + JSON.stringify(result));
});
} else if (cmd === 'roominfo') {
if (!trustable) return false;
let targetRoom = Rooms.get(target);
if (!targetRoom || targetRoom === Rooms.global) return false;
if (targetRoom.isPrivate && !user.inRooms.has(targetRoom.id) && !user.games.has(targetRoom.id)) {
return false;
}
let visibility;
if (targetRoom.isPrivate) {
visibility = (targetRoom.isPrivate === 'hidden') ? 'hidden' : 'secret';
} else {
visibility = 'public';
}
let roominfo = {
id: targetRoom.id,
title: targetRoom.title,
type: targetRoom.type,
visibility: visibility,
modchat: targetRoom.modchat,
modjoin: targetRoom.modjoin,
auth: {},
users: [],
};
if (targetRoom.auth) {
for (let userid in targetRoom.auth) {
let rank = targetRoom.auth[userid];
if (!roominfo.auth[rank]) roominfo.auth[rank] = [];
roominfo.auth[rank].push(userid);
}
}
for (let userid in targetRoom.users) {
let user = targetRoom.users[userid];
let userinfo = user.getIdentity(room.id);
roominfo.users.push(userinfo);
}
connection.send(`|queryresponse|roominfo|${JSON.stringify(roominfo)}`);
} else {
// default to sending null
connection.send(`|queryresponse|${cmd}|null`);
}
},
'!trn': true,
trn(target, room, user, connection) {
if (target === user.name) return false;
let commaIndex = target.indexOf(',');
let targetName = target;
let targetRegistered = false;
let targetToken = '';
if (commaIndex >= 0) {
targetName = target.substr(0, commaIndex);
target = target.substr(commaIndex + 1);
commaIndex = target.indexOf(',');
targetRegistered = target;
if (commaIndex >= 0) {
targetRegistered = !!parseInt(target.substr(0, commaIndex));
targetToken = target.substr(commaIndex + 1);
}
}
user.rename(targetName, targetToken, targetRegistered, connection);
},
a(target, room, user) {
if (!this.can('rawpacket')) return false;
// secret sysop command
room.add(target);
},
/*********************************************************
* Help commands
*********************************************************/
'!help': true,
commands: 'help',
h: 'help',
'?': 'help',
man: 'help',
help(target, room, user) {
if (!this.runBroadcast()) return;
target = target.toLowerCase();
// overall
if (target === 'help' || target === 'h' || target === '?' || target === 'commands') {
this.sendReply("/help OR /h OR /? - Gives you help.");
} else if (!target) {
this.sendReply("COMMANDS: /msg, /reply, /logout, /challenge, /search, /rating, /whois, /user, /report, /join, /leave, /makegroupchat, /userauth, /roomauth");
this.sendReply("BATTLE ROOM COMMANDS: /savereplay, /hideroom, /inviteonly, /invite, /timer, /forfeit");
this.sendReply("OPTION COMMANDS: /nick, /avatar, /ignore, /away, /back, /timestamps, /highlight, /showjoins, /hidejoins, /blockchallenges, /blockpms");
this.sendReply("INFORMATIONAL/RESOURCE COMMANDS: /groups, /faq, /rules, /intro, /formatshelp, /othermetas, /analysis, /punishments, /calc, /git, /cap, /roomhelp, /roomfaq (replace / with ! to broadcast. Broadcasting requires: + % @ # & ~)");
this.sendReply("DATA COMMANDS: /data, /dexsearch, /movesearch, /itemsearch, /learn, /statcalc, /effectiveness, /weakness, /coverage, /randommove, /randompokemon (replace / with ! to broadcast. Broadcasting requires: + % @ # & ~)");
if (user.group !== Config.groupsranking[0]) {
this.sendReply("DRIVER COMMANDS: /warn, /mute, /hourmute, /unmute, /alts, /forcerename, /modlog, /modnote, /lock, /weeklock, /unlock, /announce");
this.sendReply("MODERATOR COMMANDS: /globalban, /unglobalban, /ip, /modchat, /markshared, /unlockip");
this.sendReply("LEADER COMMANDS: /declare, /forcetie, /forcewin, /promote, /demote, /banip, /host, /unbanall, /ipsearch");
}
this.sendReply("For an overview of room commands, use /roomhelp");
this.sendReply("For details of a specific command, use something like: /help data");
} else {
let altCommandHelp;
let helpCmd;
let targets = target.split(' ');
let allCommands = Chat.commands;
if (typeof allCommands[target] === 'string') {
// If a function changes with command name, help for that command name will be searched first.
altCommandHelp = `${target}help`;
if (altCommandHelp in allCommands) {
helpCmd = altCommandHelp;
} else {
helpCmd = `${allCommands[target]}help`;
}
} else if (targets.length > 1 && typeof allCommands[targets[0]] === 'object') {
// Handle internal namespace commands
let helpCmd = `${targets.pop()}help`;
let namespace = allCommands[targets.shift()];
for (const t of targets) {
if (!namespace[t]) return this.errorReply(`Help for the command '${target}' was not found. Try /help for general help.`);
namespace = namespace[t];
}
if (typeof namespace[helpCmd] === 'object') return this.sendReply(namespace[helpCmd].join('\n'));
if (typeof namespace[helpCmd] === 'function') return this.run(namespace[helpCmd]);
return this.errorReply(`Help for the command '${target}' was not found. Try /help for general help.`);
} else {
helpCmd = `${target}help`;
}
if (helpCmd in allCommands) {
if (typeof allCommands[helpCmd] === 'function') {
// If the help command is a function, parse it instead
this.run(allCommands[helpCmd]);
} else if (Array.isArray(allCommands[helpCmd])) {
this.sendReply(allCommands[helpCmd].join('\n'));
}
} else {
this.errorReply(`Help for the command '${target}' was not found. Try /help for general help.`);
}
}
},
};
exports.commands = commands;
process.nextTick(() => {
// We might want to migrate most of this to a JSON schema of command attributes.
Chat.multiLinePattern.register(
'>>>? ', '/(?:room|staff)intro ', '/(?:staff)?topic ', '/(?:add|widen)datacenters ', '/bash ', '!code ', '/code ', '/modnote ', '/mn ',
'/importinputlog '
);
});