/** * 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('./fs'); const Matchmaker = require('./ladders-matchmaker').matchmaker; const MAX_REASON_LENGTH = 300; const MUTE_LENGTH = 7 * 60 * 1000; const HOURMUTE_LENGTH = 60 * 60 * 1000; exports.commands = { '!version': true, version: function (target, room, user) { if (!this.runBroadcast()) return; this.sendReplyBox("Server version: " + Chat.package.version + ""); }, '!authority': true, auth: 'authority', stafflist: 'authority', globalauth: 'authority', authlist: 'authority', authority: function (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: function (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.length, 'are', 'is')} ${userList.length} user${Chat.plural(userList.length)} in this room:
`; output += userList.join(`, `); this.sendReplyBox(output); }, userlisthelp: ["/userlist - Displays a list of users who are currently in the room."], '!me': true, mee: 'me', me: function (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: function (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); }, pi: function (target, room, user) { return this.sendReplyBox( 'Did you mean: 1. 3.1415926535897932384626... (Decimal)
' + '2. 3.184809493B91866... (Duodecimal)
' + '3. 3.243F6A8885A308D... (Hexadecimal)

' + 'How many digits of pi do YOU know? Test it out here!'); }, code: function (target, room, user) { if (!target) return this.parse('/help code'); if (!this.canTalk()) return; const separator = '\n'; if (target.includes(separator)) { const params = target.split(separator); let output = []; for (let i = 0; i < params.length; i++) { output.push(Chat.escapeHTML(params[i])); } let code = `
${output.join('
')}
`; if (output.length > 3) code = `
See code...${code}
`; if (!this.canBroadcast('!code')) return; if (this.broadcastMessage && !this.can('broadcast', null, room)) return false; if (!this.runBroadcast('!code')) return; this.sendReplyBox(code); } else { return this.errorReply("You can simply use ``[code]`` for code messages that are only one line."); } }, codehelp: [ "!code [code] - Broadcasts code to a room. Accepts multi-line arguments. Requires: + % @ & # ~", "/code [code] - Shows you code. Accepts multi-line arguments.", ], '!avatar': true, avatar: function (target, room, user) { if (!target) return this.parse('/avatars'); let parts = target.split(','); let avatarid = toId(parts[0]); let avatar = 0; let avatarTable = { lucas: 1, dawn: 2, youngster: 3, lass: 4, camper: 5, picnicker: 6, bugcatcher: 7, aromalady: 8, twins: 9, hiker: 10, battlegirl: 11, fisherman: 12, cyclist: 13, cyclistf: 14, blackbelt: 15, artist: 16, pokemonbreeder: 17, pokemonbreederf: 18, cowgirl: 19, jogger: 20, pokefan: 21, pokefanf: 22, pokekid: 23, youngcouple: 24, acetrainer: 25, acetrainerf: 26, waitress: 27, veteran: 28, ninjaboy: 29, dragontamer: 30, birdkeeper: 31, doubleteam: 32, richboy: 33, lady: 34, gentleman: 35, socialite: 36, madame: 36, beauty: 37, collector: 38, policeman: 39, pokemonranger: 40, pokemonrangerf: 41, scientist: 42, swimmer: 43, swimmerf: 44, tuber: 45, tuberf: 46, sailor: 47, sisandbro: 48, ruinmaniac: 49, psychic: 50, psychicf: 51, gambler: 52, dppguitarist: 53, acetrainersnow: 54, acetrainersnowf: 55, skier: 56, skierf: 57, roughneck: 58, clown: 59, worker: 60, schoolkid: 61, schoolkidf: 62, roark: 63, barry: 64, byron: 65, aaron: 66, bertha: 67, flint: 68, lucian: 69, dppcynthia: 70, bellepa: 71, rancher: 72, mars: 73, galacticgrunt: 74, gardenia: 75, crasherwake: 76, maylene: 77, fantina: 78, candice: 79, volkner: 80, parasollady: 81, waiter: 82, interviewers: 83, cameraman: 84, oli: 84, reporter: 85, roxy: 85, idol: 86, grace: 86, cyrus: 87, jupiter: 88, saturn: 89, galacticgruntf: 90, argenta: 91, palmer: 92, thorton: 93, buck: 94, darach: 95, marley: 96, mira: 97, cheryl: 98, riley: 99, dahlia: 100, ethan: 101, lyra: 102, archer: 132, ariana: 133, proton: 134, petrel: 135, mysteryman: 136, eusine: 136, ptlucas: 137, ptdawn: 138, falkner: 141, bugsy: 142, whitney: 143, morty: 144, chuck: 145, jasmine: 146, pryce: 147, clair: 148, will: 149, koga: 150, bruno: 151, karen: 152, lance: 153, brock: 154, misty: 155, ltsurge: 156, erika: 157, janine: 158, sabrina: 159, blaine: 160, blue: 161, red2: 162, red: 163, silver: 164, giovanni: 165, unknownf: 166, unknownm: 167, unknown: 168, hilbert: 169, hilda: 170, chili: 179, cilan: 180, cress: 181, lenora: 188, burgh: 189, elesa: 190, clay: 191, skyla: 192, cheren: 206, bianca: 207, n: 209, brycen: 222, iris: 223, drayden: 224, shauntal: 246, marshal: 247, grimsley: 248, caitlin: 249, ghetsis: 250, ingo: 256, alder: 257, cynthia: 260, emmet: 261, dueldiskhilbert: 262, dueldiskhilda: 263, hugh: 264, rosa: 265, nate: 266, colress: 267, bw2beauty: 268, bw2ghetsis: 269, bw2plasmagrunt: 270, bw2plasmagruntf: 271, bw2iris: 272, brycenman: 273, shadowtriad: 274, rood: 275, zinzolin: 276, bw2cheren: 277, marlon: 278, roxie: 279, roxanne: 280, brawly: 281, wattson: 282, flannery: 283, norman: 284, winona: 285, tate: 286, liza: 287, juan: 288, guitarist: 289, steven: 290, wallace: 291, magicqueen: 292, bellelba: 292, benga: 293, bw2elesa: '#bw2elesa', teamrocket: '#teamrocket', yellow: '#yellow', zinnia: '#zinnia', clemont: '#clemont', }; if (avatarTable.hasOwnProperty(avatarid)) { avatar = avatarTable[avatarid]; } else { avatar = parseInt(avatarid); } if (typeof avatar === 'number' && (!avatar || avatar > 294 || avatar < 1)) { if (!parts[1]) { this.errorReply("Invalid avatar."); } return false; } user.avatar = avatar; if (!parts[1]) { this.sendReply("Avatar changed to:\n" + '|raw|'); } }, avatarhelp: ["/avatar [avatar number 1 to 293] - Change your trainer sprite."], '!logout': true, signout: 'logout', logout: function (target, room, user) { user.resetName(); }, requesthelp: 'report', report: function (target, room, user) { if (room.id === 'help') { this.sendReply("Ask one of the Moderators (@) in the Help room."); } else { this.parse('/join help'); } }, r: 'reply', reply: function (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: function (target, room, user, connection) { if (!target) return this.parse('/help msg'); target = this.splitTarget(target); let targetUser = this.targetUser; if (!target) { this.errorReply("You forgot the comma."); return this.parse('/help msg'); } 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; } this.pmTarget = targetUser; this.room = undefined; if (!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: function (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)) { if (targetRoom.getAuth(targetUser) !== ' ') { return this.errorReply(`The user "${targetUser.name}" does not have permission to join "${targetRoom.title}".`); } this.room = targetRoom; this.parse(`/roomvoice ${targetUser.name}`); if (!targetRoom.checkModjoin(targetUser)) { if (targetRoom.getAuth(targetUser) !== ' ') { return this.errorReply(`The user "${targetUser.name}" does not have permission to join "${targetRoom.title}".`); } 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: function (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.ignorePMs && targetUser.ignorePMs !== user.group && !user.can('lock')) return this.errorReply("This user is currently ignoring 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
${target}
`; 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 * ~"], '!ignorepms': true, blockpm: 'ignorepms', blockpms: 'ignorepms', ignorepm: 'ignorepms', ignorepms: function (target, room, user) { if (user.ignorePMs === (target || true)) return this.errorReply("You are already blocking private messages! To unblock, use /unblockpms"); if (user.can('lock') && !user.can('bypassall')) return this.errorReply("You are not allowed to block private messages."); user.ignorePMs = true; if (target in Config.groups) { user.ignorePMs = target; return this.sendReply("You are now blocking private messages, except from staff and " + target + "."); } return this.sendReply("You are now blocking private messages, except from staff."); }, ignorepmshelp: ["/blockpms - Blocks private messages. Unblock them with /unignorepms."], '!unignorepms': true, unblockpm: 'unignorepms', unblockpms: 'unignorepms', unignorepm: 'unignorepms', unignorepms: function (target, room, user) { if (!user.ignorePMs) return this.errorReply("You are not blocking private messages! To block, use /blockpms"); user.ignorePMs = false; return this.sendReply("You are no longer blocking private messages."); }, unignorepmshelp: ["/unblockpms - Unblocks private messages. Block them with /blockpms."], '!away': true, idle: 'away', afk: 'away', away: function (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: function () { this.parse('/unblockpms'); this.parse('/unblockchallenges'); }, backhelp: ["/back - Unblocks challenges and/or private messages, if either are blocked."], '!rank': true, rank: function (target, room, user) { if (!target) target = user.name; Ladders.visualizeAll(target).then(values => { let buffer = '
'; buffer += ''; let ratings = values.join(''); if (!ratings) { buffer += ''; } else { buffer += ''; buffer += ratings; } buffer += '
User: ' + Chat.escapeHTML(target) + '
This user has not played any ladder games yet.
FormatEloWLTotal
'; this.sendReply('|raw|' + buffer); }); }, makeprivatechatroom: 'makechatroom', makechatroom: function (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'); // 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|
Private chat room created: ' + Chat.escapeHTML(target) + '
').update(); } this.sendReply("The private chat room '" + target + "' was created."); } else { if (Rooms.get('staff')) { Rooms.get('staff').add('|raw|
Public chat room created: ' + Chat.escapeHTML(target) + '
').update(); } if (Rooms.get('upperstaff')) { Rooms.get('upperstaff').add('|raw|
Public chat room created: ' + Chat.escapeHTML(target) + '
').update(); } this.sendReply("The chat room '" + target + "' was created."); } }, makechatroomhelp: ["/makechatroom [roomname] - Creates a new room named [roomname]. Requires: & ~"], makegroupchat: function (target, room, user, connection, cmd) { if (!user.autoconfirmed) { return this.errorReply("You must be autoconfirmed to make a groupchat."); } if (!user.trusted) { return this.errorReply("You must be global voice or roomdriver+ in some public room to make a groupchat."); } // if (!this.can('makegroupchat')) return false; if (target.length > 64) return this.errorReply("Title must be under 32 characters long."); let targets = target.split(',', 2); // Title defaults to a random 8-digit number. let title = targets[0].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-' + 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; } // Privacy settings, default to hidden. let privacy = (toId(targets[1]) === 'private') ? true : 'hidden'; let groupChatLink = '<<' + roomid + '>>'; let groupChatURL = ''; if (Config.serverid) { groupChatURL = 'http://' + (Config.serverid === 'showdown' ? 'psim.us' : Config.serverid + '.psim.us') + '/' + roomid; groupChatLink = '' + groupChatLink + ''; } let titleHTML = ''; if (/^[0-9]+$/.test(title)) { titleHTML = groupChatLink; } else { titleHTML = Chat.escapeHTML(title) + ' ' + groupChatLink + ''; } let targetRoom = Rooms.createChatRoom(roomid, '[G] ' + title, { isPersonal: true, isPrivate: privacy, auth: {}, introMessage: '

' + titleHTML + '

There are several ways to invite people:
- in this chat: /invite USERNAME
- anywhere in PS: link to <<' + roomid + '>>' + (groupChatURL ? '
- outside of PS: link to ' + groupChatURL + '' : '') + '

This room will expire after 40 minutes of inactivity or when the server is restarted.

', }); if (targetRoom) { // The creator is RO. targetRoom.auth[user.userid] = '#'; // Join after creating room. No other response is given. user.joinRoom(targetRoom.id); return; } return this.errorReply("An unknown error occurred while trying to create the room '" + title + "'."); }, makegroupchathelp: ["/makegroupchat [roomname], [hidden|private] - Creates a group chat named [roomname]. Leave off privacy to default to hidden. Requires global voice or roomdriver+ in a public room to make a groupchat."], deregisterchatroom: function (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; if (Rooms.global.deregisterChatRoom(id)) { this.sendReply("The room '" + target + "' was deregistered."); this.sendReply("It will be deleted as of the next server restart."); 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: function (target, room, user) { let roomid = target.trim(); if (!roomid) return this.parse('/help deleteroom'); let targetRoom = Rooms.search(roomid); if (!targetRoom) return this.errorReply("The room '" + target + "' doesn't exist."); if (room.isPersonal) { if (!this.can('editroom', null, targetRoom)) return; } else { if (!this.can('makeroom')) return; } target = targetRoom.title || targetRoom.id; if (targetRoom.id === 'global') { return this.errorReply("This room can't be deleted."); } if (targetRoom.chatRoomData) { if (targetRoom.isPrivate) { if (Rooms.get('upperstaff')) { Rooms.get('upperstaff').add(`|raw|

Private chat room deleted by ${user.userid}: ${Chat.escapeHTML(target)}
`).update(); } } else { if (Rooms.get('staff')) { Rooms.get('staff').add('|raw|
Public chat room deleted: ' + Chat.escapeHTML(target) + '
').update(); } if (Rooms.get('upperstaff')) { Rooms.get('upperstaff').add(`|raw|
Public chat room deleted by ${user.userid}: ${Chat.escapeHTML(target)}
`).update(); } } } targetRoom.add("|raw|
This room has been deleted.
"); targetRoom.update(); // |expire| needs to be its own message targetRoom.add("|expire|This room has been deleted."); this.sendReply("The room '" + target + "' was deleted."); targetRoom.update(); targetRoom.destroy(); }, deleteroomhelp: ["/deleteroom [roomname] - Deletes room [roomname]. Requires: & ~"], hideroom: 'privateroom', hiddenroom: 'privateroom', secretroom: 'privateroom', publicroom: 'privateroom', privateroom: function (target, room, user, connection, cmd) { if (room.battle || room.isPersonal) { if (!this.can('editroom', 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 (target === 'off' || !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 = Array.from(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 = Array.from(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.addModCommand(`${user.name} made this room public.`); if (room.chatRoomData) { delete room.chatRoomData.isPrivate; Rooms.global.writeChatRoomData(); } } else { const settingName = (setting === true ? 'secret' : setting); 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.addModCommand(`${user.name} made this room ${settingName}.`); 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: function (target, room, user) { if (!this.can('makeroom')) return; if (!room.chatRoomData) { return this.errorReply(`/officialroom - This room can't be made official`); } if (target === 'off') { if (!room.isOfficial) return this.errorReply(`This chat room is already unofficial.`); delete room.isOfficial; this.addModCommand(`${user.name} made this chat room unofficial.`); delete room.chatRoomData.isOfficial; Rooms.global.writeChatRoomData(); } else { if (room.isOfficial) return this.errorReply(`This chat room is already official.`); room.isOfficial = true; this.addModCommand(`${user.name} made this chat room official.`); room.chatRoomData.isOfficial = true; Rooms.global.writeChatRoomData(); } }, roomdesc: function (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-zA-Z0-9]+', ' ').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.privateModCommand(`(${user.name} changed the roomdesc to: "${target}".)`); if (room.chatRoomData) { room.chatRoomData.desc = room.desc; Rooms.global.writeChatRoomData(); } }, topic: 'roomintro', roomintro: function (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|
' + room.introMessage.replace(/\n/g, '') + '
'); if (!this.broadcasting && user.can('declare', null, room) && cmd !== 'topic') { this.sendReply('Source:'); this.sendReplyBox( '/roomintro ' + Chat.escapeHTML(room.introMessage).split('\n').map(line => { return line.replace(/^(\t+)/, (match, $1) => ' '.repeat(4 * $1.length)).replace(/^(\s+)/, (match, $1) => ' '.repeat($1.length)); }).join('
') + '
' ); } return; } if (!this.can('declare', null, room)) return false; if (target === 'off' || target === 'disable' || target === 'delete') return this.errorReply('Did you mean "/deleteroomintro"?'); target = this.canHTML(target); if (!target) return; if (!/$1'); } 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|
' + room.introMessage.replace(/\n/g, '') + '
'); this.privateModCommand(`(${user.name} changed the roomintro.)`); this.logEntry(room.introMessage.replace(/\n/g, '')); if (room.chatRoomData) { room.chatRoomData.introMessage = room.introMessage; Rooms.global.writeChatRoomData(); } }, deletetopic: 'deleteroomintro', deleteroomintro: function (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.privateModCommand(`(${user.name} deleted the roomintro.)`); this.logEntry(target); delete room.introMessage; if (room.chatRoomData) { delete room.chatRoomData.introMessage; Rooms.global.writeChatRoomData(); } }, stafftopic: 'staffintro', staffintro: function (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|
' + room.staffMessage.replace(/\n/g, '') + '
'); if (user.can('ban', null, room) && cmd !== 'stafftopic') { this.sendReply('Source:'); this.sendReplyBox( '/staffintro ' + Chat.escapeHTML(room.staffMessage).split('\n').map(line => { return line.replace(/^(\t+)/, (match, $1) => ' '.repeat(4 * $1.length)).replace(/^(\s+)/, (match, $1) => ' '.repeat($1.length)); }).join('
') + '
' ); } return; } if (!this.can('ban', null, room)) return false; if (!this.canTalk()) return; if (target === 'off' || target === 'disable' || target === 'delete') return this.errorReply('Did you mean "/deletestaffintro"?'); target = this.canHTML(target); if (!target) return; if (!/$1'); } 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|
' + target.replace(/\n/g, '') + '
'); this.privateModCommand(`(${user.name} changed the staffintro.)`); this.logEntry(room.staffMessage.replace(/\n/g, '')); if (room.chatRoomData) { room.chatRoomData.staffMessage = room.staffMessage; Rooms.global.writeChatRoomData(); } }, deletestafftopic: 'deletestaffintro', deletestaffintro: function (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.privateModCommand(`(${user.name} deleted the staffintro.)`); this.logEntry(target); delete room.staffMessage; if (room.chatRoomData) { delete room.chatRoomData.staffMessage; Rooms.global.writeChatRoomData(); } }, roomalias: function (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.privateModCommand(`(${user.name} added the room alias '${target.trim()}'.)`); 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: function (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.privateModCommand(`(${user.name} removed the room alias '${target}'.)`); 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: function (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); 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.addModCommand(`${name} was appointed Room Owner by ${user.name}.`); if (targetUser) { targetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.id}.`); room.onUpdateIdentity(targetUser); } Rooms.global.writeChatRoomData(); }, roomownerhelp: ["/roomowner [username] - Appoints [username] as a room owner. Requires: & ~"], '!roompromote': true, roomdemote: 'roompromote', roompromote: function (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'); 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)) { 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(); room.modlog(text); } 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.privateModCommand(`(${name} was demoted to Room ${groupName} by ${user.name}.)`); if (needsPopup) targetUser.popup(`You were demoted to Room ${groupName} by ${user.name} in ${room.id}.`); } else if (nextGroup === '#') { this.addModCommand(`${'' + name} was promoted to ${groupName} by ${user.name}.`); if (needsPopup) targetUser.popup(`You were promoted to ${groupName} by ${user.name} in ${room.id}.`); } else { this.addModCommand(`${'' + name} was promoted to Room ${groupName} by ${user.name}.`); if (needsPopup) targetUser.popup(`You were promoted to Room ${groupName} by ${user.name} in ${room.id}.`); } if (targetUser) targetUser.updateIdentity(room.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: function (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(", "); }); if (!buffer.length) { connection.popup("The room '" + targetRoom.title + "' has no auth." + userLookup); return; } if (targetRoom !== room) buffer.unshift("" + targetRoom.title + " room auth:"); connection.popup(buffer.join("\n\n") + userLookup); }, '!userauth': true, userauth: function (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)); } Rooms.rooms.forEach((curRoom, id) => { if (!curRoom.auth || curRoom.isPrivate) return; group = curRoom.auth[targetId]; if (!group) return; innerBuffer.push(group + id); }); if (innerBuffer.length) { buffer.push('Room auth: ' + innerBuffer.join(', ')); } if (targetId === user.userid || user.can('lock')) { innerBuffer = []; Rooms.rooms.forEach((curRoom, id) => { if (!curRoom.auth || !curRoom.isPrivate) return; if (curRoom.isPrivate === true) return; let auth = curRoom.auth[targetId]; if (!auth) return; innerBuffer.push(auth + id); }); if (innerBuffer.length) { buffer.push('Hidden room auth: ' + innerBuffer.join(', ')); } } if (targetId === user.userid || user.can('makeroom')) { innerBuffer = []; for (let i = 0; i < Rooms.global.chatRooms.length; i++) { let curRoom = Rooms.global.chatRooms[i]; 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('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: function (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; let name = targetUser.getLastName(); let userid = targetUser.getLastId(); if (Punishments.isRoomBanned(targetUser, room.id) && !target) { let problem = " but was already banned"; return this.privateModCommand("(" + 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|

" + Chat.escapeHTML(user.name) + " has banned you from the room " + room.id + ".

" + (target ? "

Reason: " + Chat.escapeHTML(target) + "

" : "") + "

To appeal the ban, PM the staff member that banned you" + (!room.battle && room.auth ? " or a room owner.

" : ".

") ); } const reason = (target ? ` (${target})` : ``); this.addModCommand(`${name} was banned from ${room.title} by ${user.name}.${reason}`, ` (${targetUser.latestIp})`); let affected = Punishments.roomBan(room, targetUser, null, null, target); if (!room.isPrivate && room.chatRoomData) { let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed); if (affected.length > 1) { this.privateModCommand("(" + name + "'s " + (acAccount ? " ac account: " + acAccount + ", " : "") + "banned alts: " + affected.slice(1).map(user => user.getLastName()).join(", ") + ")"); } else if (acAccount) { this.privateModCommand("(" + name + "'s ac account: " + acAccount + ")"); } } this.add('|unlink|hide|' + userid); if (userid !== toId(this.inputUsername)) this.add('|unlink|hide|' + toId(this.inputUsername)); if (!room.isPrivate && room.chatRoomData) { this.globalModlog("ROOMBAN", targetUser, " by " + user.name + (target ? ": " + target : "")); } return true; }, banhelp: ["/roomban [username], [reason] - Bans the user from the room you are in. Requires: @ # & ~"], unroomban: 'unban', roomunban: 'unban', unban: function (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.addModCommand("" + name + " was unbanned from " + room.title + " by " + user.name + "."); if (!room.isPrivate && room.chatRoomData) { this.globalModlog("UNROOMBAN", name, " by " + user.name); } } else { this.errorReply("User '" + target + "' is not banned."); } }, unbanhelp: ["/roomunban [username] - Unbans the user from the room you are in. Requires: @ # & ~"], '!autojoin': true, autojoin: function (target, room, user, connection) { let targets = target.split(','); if (targets.length > 11 || connection.inRooms.size > 1) return; Rooms.global.autojoinRooms(user, connection); let autojoins = []; for (let i = 0; i < targets.length; i++) { if (user.tryJoinRoom(targets[i], connection) === null) { autojoins.push(targets[i]); } } connection.autojoins = autojoins.join(','); }, '!join': true, joim: 'join', j: 'join', join: function (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); if (user.tryJoinRoom(target, connection) === null) { 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: function (target, room, user, connection) { let targetRoom = target ? Rooms.search(target) : room; if (!targetRoom || targetRoom === Rooms.global) { return this.errorReply("The room '" + target + "' does not exist."); } user.leaveRoom(targetRoom, connection); }, /********************************************************* * Moderating: Punishments *********************************************************/ kick: 'warn', k: 'warn', warn: function (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."); target = this.splitTarget(target); let targetUser = this.targetUser; if (!targetUser || !targetUser.connected) return this.errorReply("User '" + this.targetUsername + "' not found."); if (!(targetUser in room.users)) { 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; this.addModCommand("" + targetUser.name + " was warned by " + user.name + "." + (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: function (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) { return this.errorReply("The room '" + target + "' does not exist."); } if (!this.can('warn', targetUser, room) || !this.can('warn', targetUser, targetRoom)) return false; 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.parse('/msg ' + this.targetUsername + ', /invite ' + targetRoom.id); } if (targetRoom.users[targetUser.userid]) { return this.errorReply("User " + targetUser.name + " is already in the room " + targetRoom.title + "!"); } if (!room.users[targetUser.userid]) { return this.errorReply("User " + this.targetUsername + " is not in the room " + room.id + "."); } if (targetUser.joinRoom(targetRoom.id) === false) return this.errorReply("User " + targetUser.name + " could not be joined to room " + targetRoom.title + ". They could be banned from the room."); this.addModCommand("" + targetUser.name + " was redirected to room " + targetRoom.title + " by " + user.name + "."); targetUser.leaveRoom(room); }, redirhelp: ["/redirect OR /redir [username], [roomname] - Attempts to redirect the user [username] to the room [roomname]. Requires: % @ & ~"], m: 'mute', mute: function (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; 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.privateModCommand("(" + targetUser.name + " would be muted by " + user.name + problem + ".)"); } return this.addModCommand("" + targetUser.name + " would be muted by " + user.name + problem + "." + (target ? " (" + target + ")" : "")); } if (targetUser in room.users) targetUser.popup("|modal|" + user.name + " has muted you in " + room.id + " for " + Chat.toDurationString(muteDuration) + ". " + target); this.addModCommand("" + targetUser.name + " was muted by " + user.name + " for " + Chat.toDurationString(muteDuration) + "." + (target ? " (" + target + ")" : "")); if (targetUser.autoconfirmed && targetUser.autoconfirmed !== targetUser.userid) this.privateModCommand("(" + targetUser.name + "'s ac account: " + targetUser.autoconfirmed + ")"); 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: function (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: function (target, room, user) { if (!target) return this.parse('/help unmute'); target = this.splitTarget(target); if (!this.canTalk()) return; if (!this.can('mute', null, room)) return false; let targetUser = this.targetUser; let successfullyUnmuted = room.unmute(targetUser ? targetUser.userid : this.targetUsername, "Your mute in '" + room.title + "' has been lifted."); if (successfullyUnmuted) { this.addModCommand("" + (targetUser ? targetUser.name : successfullyUnmuted) + " was unmuted by " + user.name + "."); } 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: function (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))[0].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 && !target) { return this.privateModCommand(`(${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.`); } let roomauth = []; Rooms.rooms.forEach((curRoom, id) => { if (id === 'global' || !curRoom.auth) return; // Destroy personal rooms of the locked user. if (curRoom.isPersonal && curRoom.auth[userid] === '#') { curRoom.destroy(); } else { if (curRoom.isPrivate || curRoom.battle) return; 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.`); } 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(); } let weekMsg = week ? ' for a week' : ''; if (targetUser) { targetUser.popup(`|modal|${user.name} has locked you from talking in chats, battles, and PMing regular users${weekMsg}.` + (userReason ? `\n\nReason: ${userReason}` : "") + `\n\nIf you feel that your lock was unjustified, you can still PM staff members (%, @, &, and ~) to discuss it` + (Config.appealurl ? ` or you can appeal:\n${Config.appealurl}` : ".") + `\n\nYour lock will expire in a few days.`); } let lockMessage = `${name} was locked from talking${weekMsg} by ${user.name}.` + (userReason ? ` (${userReason})` : ""); this.addModCommand(lockMessage, ` ${proof}` + (targetUser ? `(${targetUser.latestIp})` : '')); // Notify staff room when a user is locked outside of it. if (room.id !== 'staff' && Rooms('staff')) { Rooms('staff').addLogMessage(user, `<<${room.id}>> ${lockMessage}`); } // 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); } let acAccount = (targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed); if (affected.length > 1) { this.privateModCommand(`(${name}'s ` + (acAccount ? ` ac account: ${acAccount}, ` : "") + `locked alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`); } else if (acAccount) { this.privateModCommand(`(${name}'s ac account: ${acAccount})`); } this.add(`|unlink|hide|${userid}`); if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`); const globalReason = (target ? `: ${userReason} ${proof}` : ``); this.globalModlog((week ? "WEEKLOCK" : "LOCK"), targetUser || userid, ` by ${user.name}${globalReason}`); 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: function (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.addModCommand(unlockMessage); // Notify staff room when a user is unlocked outside of it. if (!reason && room.id !== 'staff' && Rooms('staff')) { Rooms('staff').addLogMessage(user, "<<" + room.id + ">> " + unlockMessage); } if (!reason) this.globalModlog("UNLOCK", target, " by " + user.name); if (targetUser) targetUser.popup("" + user.name + " has unlocked you."); } else { this.errorReply("User '" + target + "' is not locked."); } }, unlockhelp: ["/unlock [username] - Unlocks the user. Requires: % @ * & ~"], forceglobalban: 'globalban', gban: 'globalban', globalban: function (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) { 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. targetUser.inRooms.forEach(roomid => { if (roomid === 'global') return; let targetRoom = Rooms.get(roomid); if (targetRoom.isPersonal && targetRoom.auth[userid] === '#') { 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.addModCommand(banMessage, ` ${proof}(${targetUser.latestIp})`); // Notify staff room when a user is banned outside of it. if (room.id !== 'staff' && Rooms('staff')) { Rooms('staff').addLogMessage(user, "<<" + room.id + ">> " + banMessage); } let affected = Punishments.ban(targetUser, null, null, userReason); let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed); 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; this.privateModCommand("(" + name + "'s " + (acAccount ? " ac account: " + acAccount + ", " : "") + "banned alts: " + affected.join(", ") + (guests ? " [" + guests + " guests]" : "") + ")"); for (let i = 0; i < affected.length; ++i) { this.add('|unlink|' + toId(affected[i])); } } else if (acAccount) { this.privateModCommand("(" + name + "'s ac account: " + acAccount + ")"); } this.add('|unlink|hide|' + userid); if (userid !== toId(this.inputUsername)) this.add('|unlink|hide|' + toId(this.inputUsername)); const globalReason = (target ? `: ${userReason} ${proof}` : ``); this.globalModlog("BAN", targetUser, ` by ${user.name}${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: function (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.addModCommand(unbanMessage); // Notify staff room when a user is unbanned outside of it. if (room.id !== 'staff' && Rooms('staff')) { Rooms('staff').addLogMessage(user, `<<${room.id}>> ${unbanMessage}`); } this.globalModlog("UNBAN", name, ` by ${user.name}`); } else { this.errorReply(`User '${target}' is not globally banned.`); } }, unglobalbanhelp: ["/unglobalban [username] - Unban a user. Requires: @ * & ~"], unbanall: function (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.addModCommand("All bans and locks have been lifted by " + user.name + "."); }, unbanallhelp: ["/unbanall - Unban all IP addresses. Requires: & ~"], deroomvoiceall: function (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.addModCommand("All " + count + " roomvoices have been cleared by " + user.name + "."); }, deroomvoiceallhelp: ["/deroomvoiceall - Devoice all roomvoiced users. Requires: # & ~"], rangeban: 'banip', banip: function (target, room, user) { target = this.splitTargetText(target); let targetIp = this.targetUsername.trim(); if (!targetIp || !/^[0-9.]+(?:\.\*)?$/.test(targetIp)) return this.parse('/help banip'); if (!target) return this.errorReply("/banip requires a ban reason"); if (!this.can('rangeban')) return false; const targetDesc = "IP " + (targetIp.endsWith('*') ? "range " : "") + targetIp; const curPunishment = Punishments.ipSearch(targetIp); if (curPunishment && curPunishment[0] === 'BAN') { return this.errorReply(`The ${targetDesc} is already temporarily banned.`); } Punishments.banRange(targetIp, target); this.addModCommand(`${user.name} hour-banned the ${targetDesc}: ${target}`); }, 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: function (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.addModCommand("" + user.name + " unbanned the " + (target.charAt(target.length - 1) === '*' ? "IP range" : "IP") + ": " + target); }, unbaniphelp: ["/unbanip [ip] - Unbans. Accepts wildcards to ban ranges. Requires: & ~"], rangelock: 'lockip', lockip: function (target, room, user) { target = this.splitTargetText(target); let targetIp = this.targetUsername.trim(); if (!targetIp || !/^[0-9.]+(?:\.\*)?$/.test(targetIp)) return this.parse('/help lockip'); if (!target) return this.errorReply("/lockip requires a lock reason"); if (!this.can('rangeban')) return false; const targetDesc = "IP " + (targetIp.endsWith('*') ? "range " : "") + targetIp; const curPunishment = Punishments.ipSearch(targetIp); if (curPunishment && (curPunishment[0] === 'BAN' || curPunishment[0] === 'LOCK')) { const punishDesc = curPunishment[0] === 'BAN' ? `temporarily banned` : `temporarily locked`; return this.errorReply(`The ${targetDesc} is already ${punishDesc}.`); } Punishments.lockRange(targetIp, target); this.addModCommand(`${user.name} hour-locked the ${targetDesc}: ${target}`); }, 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', unlockip: function (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.addModCommand("" + user.name + " unlocked the " + (target.charAt(target.length - 1) === '*' ? "IP range" : "IP") + ": " + target); }, /********************************************************* * Moderating: Other *********************************************************/ mn: 'modnote', modnote: function (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; return this.privateModCommand("(" + user.name + " notes: " + target + ")"); }, modnotehelp: ["/modnote [note] - Adds a moderator note that can be read through modlog. Requires: % @ * # & ~"], globalpromote: 'promote', promote: function (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.privateModCommand(`(${name} was demoted to ${groupName} by ${user.name}.)`); if (targetUser) targetUser.popup(`You were demoted to ${groupName} by ${user.name}.`); } else { this.addModCommand(`${name} was promoted to ${groupName} by ${user.name}.`); if (targetUser) targetUser.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: function (target) { if (!target) return this.parse('/help trustuser'); if (!this.can('promote')) return; 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 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."); }, trustuserhelp: ["/trustuser [username] - Trusts the user (makes them immune to locks). Requires: & ~"], globaldemote: 'demote', demote: function (target) { if (!target) return this.parse('/help demote'); this.run('promote'); }, demotehelp: ["/demote [username], [group] - Demotes the user to the specified group. Requires: & ~"], forcepromote: function (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.addModCommand("" + name + " was promoted to " + (Config.groups[nextGroup].name || "regular user") + " by " + user.name + "."); }, devoice: 'deauth', deauth: function (target, room, user) { return this.parse('/demote ' + target + ', deauth'); }, deglobalvoice: 'globaldeauth', deglobalauth: 'globaldeauth', globaldevoice: 'globaldeauth', globaldeauth: function (target, room, user) { return this.parse('/globaldemote ' + target + ', deauth'); }, deroomvoice: 'roomdeauth', roomdevoice: 'roomdeauth', deroomauth: 'roomdeauth', roomdeauth: function (target, room, user) { return this.parse('/roomdemote ' + target + ', deauth'); }, declare: function (target, room, user) { if (!target) return this.parse('/help declare'); if (!this.can('declare', null, room)) return false; if (!this.canTalk()) return; this.add(`|notify|${room.title} announcement!|${target}`); this.add(Chat.html`|raw|
${target}
`); this.logModCommand(`${user.name} declared: ${target}`); }, declarehelp: ["/declare [message] - Anonymously announces a message. Requires: # * & ~"], htmldeclare: function (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; this.add(`|notify|${room.title} announcement!|${Chat.stripHTML(target)}`); this.add(`|raw|
${target}
`); this.logModCommand(`${user.name} HTML-declared: ${target}`); }, htmldeclarehelp: ["/htmldeclare [message] - Anonymously announces a message using safe HTML. Requires: ~"], gdeclare: 'globaldeclare', globaldeclare: function (target, room, user) { if (!target) return this.parse('/help globaldeclare'); if (!this.can('gdeclare')) return false; target = this.canHTML(target); if (!target) return; Rooms.rooms.forEach((curRoom, id) => { if (id !== 'global') curRoom.addRaw(`
${target}
`).update(); }); Users.users.forEach(u => { if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw
${target}
`); }); this.logModCommand(`${user.name} globally declared: ${target}`); }, globaldeclarehelp: ["/globaldeclare [message] - Anonymously announces a message to every room on the server. Requires: ~"], cdeclare: 'chatdeclare', chatdeclare: function (target, room, user) { if (!target) return this.parse('/help chatdeclare'); if (!this.can('gdeclare')) return false; target = this.canHTML(target); if (!target) return; Rooms.rooms.forEach((curRoom, id) => { if (id !== 'global' && curRoom.type !== 'battle') curRoom.addRaw(`
${target}
`).update(); }); this.logModCommand(`${user.name} declared to all chat rooms: ${target}`); }, chatdeclarehelp: ["/cdeclare [message] - Anonymously announces a message to all chatrooms on the server. Requires: ~"], '!announce': true, wall: 'announce', announce: function (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: % @ * # & ~"], fr: 'forcerename', forcerename: function (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; let entry = targetUser.name + " was forced to choose a new name by " + user.name + (reason ? ": " + reason : ""); this.privateModCommand("(" + entry + ")"); Matchmaker.cancelSearch(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: function (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 '${target}' is already namelocked.`); let reasonText = reason ? ` (${reason})` : `.`; let lockMessage = `${targetUser.name} was namelocked by ${user.name}${reasonText}`; this.addModCommand(lockMessage, ` (${targetUser.latestIp})`); // Notify staff room when a user is locked outside of it. if (room.id !== 'staff' && Rooms('staff')) { Rooms('staff').addLogMessage(user, "<<" + room.id + ">> " + lockMessage); } this.globalModlog("NAMELOCK", targetUser, ` by ${user.name}${reasonText}`); Matchmaker.cancelSearch(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: function (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.addModCommand(unlocked + " was unnamelocked by " + user.name + "." + reason); if (!reason) this.globalModlog("UNNAMELOCK", target, " by " + user.name); 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', hidealtstext: 'hidetext', hidetext: function (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) return this.errorReply("User '" + name + "' not found."); let userid = targetUser.getLastId(); let hidetype = ''; if (!user.can('lock', targetUser) && !this.can('ban', targetUser, room)) return false; if (targetUser.locked || Punishments.isRoomBanned(targetUser, room.id) || user.can('rangeban')) { hidetype = 'hide|'; } else { return this.errorReply("User '" + name + "' is not banned from this room or locked."); } if (cmd === 'hidealtstext' || cmd === 'hidetextalts') { this.addModCommand(`${targetUser.name}'s alts' messages were cleared from ${room.title} by ${user.name}.`); this.add(`|unlink|${hidetype}${userid}`); const alts = targetUser.getAltUsers(true); for (let i = 0; i < alts.length; ++i) { this.add(`|unlink|${hidetype}${alts[i].name}`); } for (let i in targetUser.prevNames) { this.add(`|unlink|${hidetype}${targetUser.prevNames[i]}`); } } else { this.addModCommand("" + targetUser.name + "'s messages were cleared from " + room.title + " by " + user.name + "."); this.add('|unlink|' + hidetype + userid); if (userid !== toId(this.inputUsername)) this.add('|unlink|' + hidetype + toId(this.inputUsername)); } }, hidetexthelp: [ "/hidetext [username] - Removes a locked or banned user's messages from chat (includes users banned from the room). Requires: % (global only), @ * # & ~", "/hidealtstext OR /hidetextalts [username] - Removes a locked or banned user's messages, and their alternate account's messages from the chat (includes users banned from the room). Requires: % (global only), @ * # & ~", ], ab: 'blacklist', blacklist: function (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) return this.errorReply("User '" + this.targetUsername + "' not found."); 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) { 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|

" + Chat.escapeHTML(user.name) + " has blacklisted you from the room " + room.id + ".

" + (target ? "

Reason: " + Chat.escapeHTML(target) + "

" : "") + "

To appeal the ban, PM the staff member that blacklisted you" + (!room.battle && room.auth ? " or a room owner.

" : ".

") ); } this.addModCommand(`${name} was blacklisted from ${room.title} by ${user.name}. ${(target ? ` (${target})` : ``)}`, ` (${targetUser.latestIp})`); let affected = Punishments.roomBlacklist(room, targetUser, null, null, target); if (!room.isPrivate && room.chatRoomData) { let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed); if (affected.length > 1) { this.privateModCommand("(" + name + "'s " + (acAccount ? " ac account: " + acAccount + ", " : "") + "blacklisted alts: " + affected.slice(1).map(user => user.getLastName()).join(", ") + ")"); } else if (acAccount) { this.privateModCommand("(" + name + "'s ac account: " + acAccount + ")"); } } this.add('|unlink|hide|' + userid); if (userid !== toId(this.inputUsername)) this.add('|unlink|hide|' + toId(this.inputUsername)); if (!room.isPrivate && room.chatRoomData) { this.globalModlog("BLACKLIST", targetUser, " by " + user.name + (target ? ": " + target : "")); } return true; }, blacklisthelp: ["/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~"], nameblacklist: 'blacklistname', blacklistname: function (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)) 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.`); } for (let i = 0; i < targets.length; i++) { let userid = targets[i]; Punishments.roomBlacklist(room, null, null, userid, reason); let 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."); } } this.addModCommand(`${targets.join(', ')}${(targets.length > 1 ? " 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: function (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.addModCommand("" + name + " was unblacklisted by " + user.name + "."); if (!room.isPrivate && room.chatRoomData) { this.globalModlog("UNBLACKLIST", name, " by " + user.name); } } else { this.errorReply("User '" + target + "' is not blacklisted."); } }, unblacklisthelp: ["/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~"], unblacklistall: function (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.addModCommand(`All blacklists in this room have been lifted by ${user.name}.`); this.logEntry(`Unblacklisted users: ${unblacklisted.join(', ')}`); }, unblacklistallhelp: ["/unblacklistall - Unblacklists all blacklisted users in the current room. Requires #, &, ~"], expiringbls: 'showblacklist', expiringblacklists: 'showblacklist', blacklists: 'showblacklist', showbl: 'showblacklist', showblacklist: function (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 = ''; subMap.forEach((punishment, userid) => { 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'; subMap.forEach((punishment, ip) => { 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)` : ``}:
`; blMap.forEach((data, userid) => { const [expireTime, ...alts] = data; if (soonExpiring && expireTime > Date.now() + SOON_EXPIRING_TIME) return; const expiresIn = new Date(expireTime).getTime() - Date.now(); const expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24); buf += `- ${userid}, for ${expiresDays} day${Chat.plural(expiresDays)}`; if (alts.length) buf += `, alts${ips}: ${alts.join(', ')}`; buf += `
`; }); 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: function (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."); Punishments.addSharedIp(ip, note); if (note) note = ` (${note})`; return this.addModCommand(`The IP '${ip}' was marked as shared by ${user.name}.${note}`); }, marksharedhelp: ["/markshared [ip] - Marks an IP address as shared. Requires @, &, ~"], unmarkshared: function (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); return this.addModCommand(`The IP '${target}' was unmarked as shared by ${user.name}.`); }, unmarksharedhelp: ["/unmarkshared [ip] - Unmarks a shared IP address. Requires @, &, ~"], /********************************************************* * Server management commands *********************************************************/ hotpatch: function (target, room, user) { if (!target) return this.parse('/help hotpatch'); if (!this.can('hotpatch')) return false; if (Monitor.hotpatchLock) return this.errorReply("Hotpatch is currently been disabled. (" + Monitor.hotpatchLock + ")"); Rooms.global.notifyRooms(['development', 'staff', 'upperstaff'], `|c|${user.getIdentity()}|/log ${user.name} used /hotpatch ${target}`); try { if (target === 'chat' || target === 'commands') { if (Monitor.hotpatchLockChat) return this.errorReply("Hotpatch has been disabled for chat. (" + Monitor.hotpatchLockChat + ")"); const ProcessManagers = require('./process-manager').cache; for (let PM of ProcessManagers.keys()) { if (PM.isChatBased) { PM.unspawn(); ProcessManagers.delete(PM); } } Chat.uncacheTree('./chat'); delete require.cache[require.resolve('./chat-commands')]; delete require.cache[require.resolve('./chat-plugins/info')]; global.Chat = require('./chat'); let runningTournaments = Tournaments.tournaments; Chat.uncacheTree('./tournaments'); global.Tournaments = require('./tournaments'); Tournaments.tournaments = runningTournaments; return this.sendReply("Chat commands have been hot-patched."); } else if (target === 'tournaments') { let runningTournaments = Tournaments.tournaments; Chat.uncacheTree('./tournaments'); global.Tournaments = require('./tournaments'); Tournaments.tournaments = runningTournaments; return this.sendReply("Tournaments have been hot-patched."); } else if (target === 'battles') { Rooms.SimulatorProcess.respawn(); return this.sendReply("Battles have been hotpatched. Any battles started after now will use the new code; however, in-progress battles will continue to use the old code."); } else if (target === 'formats') { // store custom formats let customFormats = {}; for (let i in Dex.data.Formats) { if (!Dex.data.Formats[i].customBanlist) continue; customFormats[i] = {name: Dex.data.Formats[i].name, customBanlist: Dex.data.Formats[i].customBanlist}; } // uncache the sim/dex.js dependency tree Chat.uncacheTree('./sim/dex'); // reload sim/dex.js global.Dex = require('./sim/dex'); // note: this will lock up the server for a few seconds // rebuild custom formats for (let i in customFormats) { Dex.getFormat(customFormats[i].name, customFormats[i].customBanlist, i); } // rebuild the formats list delete Rooms.global.formatList; // respawn validator processes TeamValidator.PM.respawn(); // respawn simulator processes Rooms.SimulatorProcess.respawn(); // broadcast the new formats list to clients Rooms.global.send(Rooms.global.formatListText); return this.sendReply("Formats have been hotpatched."); } else if (target === 'loginserver') { FS('config/custom.css').unwatch(); Chat.uncacheTree('./loginserver'); global.LoginServer = require('./loginserver'); return this.sendReply("The login server has been hotpatched. New login server requests will use the new code."); } else if (target === 'learnsets' || target === 'validator') { TeamValidator.PM.respawn(); return this.sendReply("The team validator has been hotpatched. Any battles started after now will have teams be validated according to the new code."); } else if (target === 'punishments') { delete require.cache[require.resolve('./punishments')]; global.Punishments = require('./punishments'); return this.sendReply("Punishments have been hotpatched."); } else if (target === 'dnsbl' || target === 'datacenters') { Dnsbl.loadDatacenters(); return this.sendReply("Dnsbl has been hotpatched."); } else if (target.startsWith('disablechat')) { if (Monitor.hotpatchLockChat) return this.errorReply("Hotpatch is already disabled."); let reason = target.split(', ')[1]; if (!reason) return this.errorReply("Usage: /hotpatch disablechat, [reason]"); Monitor.hotpatchLockChat = reason; return this.sendReply("You have disabled hotpatch until the next server restart."); } else if (target.startsWith('disable')) { let reason = target.split(', ')[1]; if (!reason) return this.errorReply("Usage: /hotpatch disable, [reason]"); Monitor.hotpatchLock = reason; return this.sendReply("You have disabled hotpatch until the next server restart."); } } catch (e) { return this.errorReply("Something failed while trying to hotpatch " + target + ": \n" + e.stack); } this.errorReply("Your hot-patch command was unrecognized."); }, 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.", "/hotpatch chat - reload chat-commands.js and the chat-plugins", "/hotpatch battles - spawn new simulator processes", "/hotpatch validator - spawn new team validator processes", "/hotpatch formats - reload the sim/dex.js tree, rebuild and rebroad the formats list, and spawn new simulator and team validator processes", "/hotpatch dnsbl - reloads Dnsbl datacenters", "/hotpatch disable, [reason] - disables the use of hotpatch until the next server restart"], savelearnsets: function (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: function (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').readTextIfExists().then(data => { let datacenters = []; for (const row of data.split("\n")) { if (!row) continue; const rowSplit = row.split(','); const rowData = [ Dnsbl.ipToNumber(rowSplit[0]), Dnsbl.ipToNumber(rowSplit[1]), Dnsbl.urlToHost(rowSplit[3]), row, ]; datacenters.push(rowData); } data = String(target).split("\n"); let successes = 0; let identicals = 0; let widenSuccesses = 0; for (let row of data) { if (!row) continue; let rowSplit = row.split(','); let rowData = [ Dnsbl.ipToNumber(rowSplit[0]), Dnsbl.ipToNumber(rowSplit[1]), Dnsbl.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: function (target, room, user) { if (!this.can('disableladder')) return false; if (LoginServer.disabled) { return this.errorReply("/disableladder - Ladder is already disabled."); } LoginServer.disabled = true; this.logModCommand("The ladder was disabled by " + user.name + "."); this.add("|raw|
Due to high server load, the ladder has been temporarily disabled
Rated games will no longer update the ladder. It will be back momentarily.
"); }, enableladder: function (target, room, user) { if (!this.can('disableladder')) return false; if (!LoginServer.disabled) { return this.errorReply("/enable - Ladder is already enabled."); } LoginServer.disabled = false; this.logModCommand("The ladder was enabled by " + user.name + "."); this.add("|raw|
The ladder is now back.
Rated games will update the ladder now.
"); }, lockdown: function (target, room, user) { if (!this.can('lockdown')) return false; Rooms.global.startLockdown(); this.logEntry(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: function (target, room, user) { if (!this.can('lockdown')) return false; if (Config.autolockdown === undefined) Config.autolockdown = true; if (target === 'on' || target === 'enable') { 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."); this.logEntry(`${user.name} used /autolockdownkill on`); } else if (target === 'off' || target === 'disable') { 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."); this.logEntry(`${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: function (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."); this.logEntry(user.name + " used /prelockdown"); }, slowlockdown: function (target, room, user) { if (!this.can('lockdown')) return false; Rooms.global.startLockdown(undefined, true); this.logEntry(user.name + " used /slowlockdown"); }, endlockdown: function (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) { Rooms.rooms.forEach((curRoom, id) => { if (id !== 'global') curRoom.addRaw("
The server restart was canceled.
").update(); }); } else { this.sendReply("Preparation for the server shutdown was canceled."); } Rooms.global.lockdown = false; this.logEntry(user.name + " used /endlockdown"); }, emergency: function (target, room, user) { if (!this.can('lockdown')) return false; if (Config.emergency) { return this.errorReply("We're already in emergency mode."); } Config.emergency = true; Rooms.rooms.forEach((curRoom, id) => { if (id !== 'global') curRoom.addRaw("
The server has entered emergency mode. Some features might be disabled or limited.
").update(); }); this.logEntry(user.name + " used /emergency"); }, endemergency: function (target, room, user) { if (!this.can('lockdown')) return false; if (!Config.emergency) { return this.errorReply("We're not in emergency mode."); } Config.emergency = false; Rooms.rooms.forEach((curRoom, id) => { if (id !== 'global') curRoom.addRaw("
The server is no longer in emergency mode.
").update(); }); this.logEntry(user.name + " used /endemergency"); }, kill: function (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."); } Sockets.workers.forEach(worker => worker.kill()); if (!room.destroyLog) { process.exit(); return; } room.logEntry(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: function (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: function (target, room, user) { if (!this.can('hotpatch')) return false; Rooms.global.send('|refresh|'); this.logEntry(user.name + " used /refreshpage"); }, updateserver: function (target, room, user, connection) { if (!user.hasConsoleAccess(connection)) { return this.errorReply("/updateserver - Access denied."); } if (Chat.updateServerLock) { return this.errorReply("/updateserver - Another update is already in progress."); } Chat.updateServerLock = true; let logQueue = []; logQueue.push(user.name + " used /updateserver"); connection.sendTo(room, "updating..."); let exec = require('child_process').exec; exec(`git fetch && git rebase --autostash FETCH_HEAD`, (error, stdout, stderr) => { for (let s of ("" + stdout + stderr).split("\n")) { connection.sendTo(room, s); logQueue.push(s); } for (let line of logQueue) { room.logEntry(line); } Chat.updateServerLock = false; }); }, crashfixed: function (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("
We fixed the crash without restarting the server!
You may resume talking in the lobby and starting new battles.
").update(); } this.logEntry(user.name + " used /crashfixed"); }, crashfixedhelp: ["/crashfixed - Ends the active lockdown caused by a crash without the need of a restart. Requires: ~"], memusage: 'memoryusage', memoryusage: function (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: function (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"], eval: function (target, room, user, connection) { if (!user.hasConsoleAccess(connection)) { return this.errorReply("/eval - Access denied."); } if (!this.runBroadcast()) return; if (!this.broadcasting) this.sendReply('||>> ' + target); try { /* eslint-disable no-unused-vars */ let battle = room.battle; let me = user; this.sendReply('||<< ' + eval(target)); /* eslint-enable no-unused-vars */ } catch (e) { this.sendReply('|| << ' + ('' + e.stack).replace(/\n *at CommandContext\.exports\.commands(\.[a-z0-9]+)*\.eval [\s\S]*/m, '').replace(/\n/g, '\n||')); } }, evalbattle: function (target, room, user, connection) { if (!user.hasConsoleAccess(connection)) { return this.errorReply("/evalbattle - Access denied."); } if (!this.runBroadcast()) return; if (!room.battle) { return this.errorReply("/evalbattle - This isn't a battle room."); } room.battle.send('eval', target.replace(/\n/g, '\f')); }, ebat: 'editbattle', editbattle: function (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.players[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.send('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.send('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.send('eval', "let pl=" + getPlayer(targets[0]) + ";let p=pl" + getPokemon(targets[1]) + ";p.moveset[p.moves.indexOf('" + toId(targets[2]) + "')].pp = " + parseInt(targets[3])); break; case 'boost': case 'b': room.battle.send('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.send('eval', "let p=" + getPlayer(targets[0]) + getPokemon(targets[1]) + ";p.addVolatile('" + toId(targets[2]) + "')"); break; case 'sidecondition': case 'sc': room.battle.send('eval', "let p=" + getPlayer(targets[0]) + ".addSideCondition('" + toId(targets[1]) + "')"); break; case 'fieldcondition': case 'pseudoweather': case 'fc': room.battle.send('eval', "battle.addPseudoWeather('" + toId(targets[0]) + "')"); break; case 'weather': case 'w': room.battle.send('eval', "battle.setWeather('" + toId(targets[0]) + "')"); break; case 'terrain': case 't': room.battle.send('eval', "battle.setTerrain('" + toId(targets[0]) + "')"); 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"], /********************************************************* * Battle commands *********************************************************/ forfeit: function (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: function (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"); room.game.choose(user, target); }, mv: 'move', attack: 'move', move: function (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"); room.game.choose(user, 'move ' + target); }, sw: 'switch', switch: function (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"); room.game.choose(user, 'switch ' + parseInt(target)); }, team: function (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"); room.game.choose(user, 'team ' + target); }, undo: function (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', savereplay: function (target, room, user, connection) { if (!room || !room.battle) return; // retrieve spectator log (0) if there are privacy concerns let logidx = room.battle.ended ? 3 : 0; let data = room.getLog(logidx).join("\n"); let datahash = crypto.createHash('md5').update(data.replace(/[^(\x20-\x7F)]+/g, '')).digest('hex'); let players = room.battle.playerNames; LoginServer.request('prepreplay', { id: room.id.substr(7), loghash: datahash, p1: players[0], p2: players[1], format: room.format, hidden: room.isPrivate ? '1' : '', }, success => { 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: function (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); let targetUser = this.targetUser; let name = this.targetUsername; if (!targetUser) return this.errorReply("User " + name + " not found."); if (targetUser.can('joinbattle', null, room)) { return this.sendReply("" + name + " can already join battles as a Player."); } if (!this.can('joinbattle', null, room)) return; room.auth[targetUser.userid] = '\u2606'; this.addModCommand("" + name + " was promoted to Player by " + user.name + "."); }, addplayerhelp: ["/addplayer [username] - Allow the specified user to join the battle as a player."], joinbattle: 'joingame', joingame: function (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); }, leavebattle: 'leavegame', partbattle: 'leavegame', leavegame: function (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: function (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.addModCommand("" + targetUser.name + " was kicked from a battle by " + user.name + (target ? " (" + target + ")" : "")); } else { this.errorReply("/kickbattle - User isn't in battle."); } }, kickbattlehelp: ["/kickbattle [username], [reason] - Kicks a user from a battle with reason. Requires: % @ * & ~"], kickinactive: function (target, room, user) { this.parse(`/timer on`); }, timer: function (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.players[user]) { return this.errorReply(`Access denied`); } if (target === 'off' || target === 'false' || 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 (target === 'on' || target === 'true') { timer.start(user); } else { this.errorReply(`"${target}" is not a recognized timer state.`); } }, autotimer: 'forcetimer', forcetimer: function (target, room, user) { target = toId(target); if (!this.can('autotimer')) return; if (target === 'off' || target === 'false' || target === 'stop') { Config.forcetimer = false; this.addModCommand("Forcetimer is now OFF: The timer is now opt-in. (set by " + user.name + ")"); } else if (target === 'on' || target === 'true' || !target) { Config.forcetimer = true; this.addModCommand("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: function (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.logModCommand(user.name + " forced a tie."); return false; } let targetUser = Users.getExact(target); if (!targetUser) return this.errorReply("User '" + target + "' not found."); target = targetUser ? targetUser.userid : ''; if (target) { room.battle.win(targetUser); this.logModCommand(user.name + " forced a win for " + target + "."); } }, 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, cancelsearch: 'search', search: function (target, room, user) { if (target) { 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 search for a battle."); return false; } } Matchmaker.searchBattle(user, target); } else { Matchmaker.cancelSearch(user, target); } }, '!challenge': true, chall: 'challenge', challenge: function (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 (targetUser.blockChallenges && !user.can('bypassblocks', targetUser)) { return this.popupReply("The user '" + this.targetUsername + "' is not accepting challenges right now."); } if (user.challengeTo) { return this.popupReply("You're already challenging '" + user.challengeTo.to + "'. Cancel that challenge before challenging someone else."); } 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; } } user.prepBattle(Dex.getFormat(target).id, 'challenge', connection).then(result => { if (result) user.makeChallenge(targetUser, target); }); }, '!blockchallenges': true, bch: 'blockchallenges', blockchall: 'blockchallenges', blockchalls: 'blockchallenges', blockchallenges: function (target, room, user) { if (user.blockChallenges) return this.errorReply("You are already blocking challenges!"); user.blockChallenges = true; 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: function (target, room, user) { if (!user.blockChallenges) return this.errorReply("You are already available for challenges!"); user.blockChallenges = false; 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: function (target, room, user) { user.cancelChallengeTo(target); }, '!accept': true, accept: function (target, room, user, connection) { let userid = toId(target); if (!userid && this.pmTarget) userid = this.pmTarget.userid; let format = ''; if (user.challengesFrom[userid]) format = user.challengesFrom[userid].format; if (!format) { this.popupReply(target + " isn't challenging you - maybe they cancelled before you could accept?"); return false; } user.prepBattle(Dex.getFormat(format).id, 'challenge', connection).then(result => { if (result) user.acceptChallengeFrom(userid); }); }, '!reject': true, reject: function (target, room, user) { let userid = toId(target); if (!userid && this.pmTarget) userid = this.pmTarget.userid; user.rejectChallengeFrom(userid); }, '!useteam': true, saveteam: 'useteam', utm: 'useteam', useteam: function (target, room, user) { user.team = target; }, '!vtm': true, vtm: function (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."); TeamValidator(format.id).prepTeam(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: function (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 = {}; targetUser.inRooms.forEach(roomid => { if (roomid === 'global') return; let targetRoom = Rooms.get(roomid); if (!targetRoom) return; // shouldn't happen let roomData = {}; if (targetRoom.isPrivate) { if (!user.inRooms.has(roomid) && !user.games.has(roomid)) return; 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, 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.getRoomList(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(target).getTop().then(result => { connection.send('|queryresponse|laddertop|' + JSON.stringify(result)); }); } else { // default to sending null connection.send('|queryresponse|' + cmd + '|null'); } }, '!trn': true, trn: function (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: function (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: function (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"); this.sendReply("OPTION COMMANDS: /nick, /avatar, /ignore, /away, /back, /timestamps, /highlight"); this.sendReply("INFORMATIONAL COMMANDS: /data, /dexsearch, /movesearch, /itemsearch, /groups, /faq, /rules, /intro, /formatshelp, /othermetas, /learn, /analysis, /calc (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, /unlock, /announce, /redirect"); this.sendReply("MODERATOR COMMANDS: /ban, /unban, /ip, /modchat"); this.sendReply("LEADER COMMANDS: /declare, /forcetie, /forcewin, /promote, /demote, /banip, /host, /unbanall"); } 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[targets.length - 1] + 'help'; let namespace = allCommands[targets[0]]; for (let i = 1; i < targets.length - 1; i++) { if (!namespace[targets[i]]) return this.errorReply("Help for the command '" + target + "' was not found. Try /help for general help"); namespace = namespace[targets[i]]; } 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"); } } }, }; 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 ' ); });