/** * Room settings commands * Pokemon Showdown - http://pokemonshowdown.com/ * * Commands for settings relating to room setting filtering. * * @license MIT */ import { Utils } from '../../lib'; import type { EffectiveGroupSymbol, RoomPermission } from '../user-groups'; const RANKS = Config.groupsranking; const SLOWCHAT_MINIMUM = 2; const SLOWCHAT_MAXIMUM = 60; const SLOWCHAT_USER_REQUIREMENT = 10; export const sections = [ 'official', 'battleformats', 'languages', 'entertainment', 'gaming', 'lifehobbies', 'onsitegames', ] as const; export type RoomSection = typeof sections[number]; export const RoomSections: { sectionNames: { [k in RoomSection]: string }, sections: readonly RoomSection[] } = { sectionNames: { official: 'Official', battleformats: 'Battle formats', languages: 'Languages', entertainment: 'Entertainment', gaming: 'Gaming', lifehobbies: 'Life & hobbies', onsitegames: 'On-site games', }, sections, }; export const commands: Chat.ChatCommands = { roomsetting: 'roomsettings', roomsettings(target, room, user, connection) { room = this.requireRoom(); if (room.battle) return this.errorReply("This command cannot be used in battle rooms."); let uhtml = 'uhtml'; if (!target) { room.update(); } else { void this.parse(`/${target}`); uhtml = 'uhtmlchange'; } let output = Utils.html`
Room Settings for ${room.title}
`; for (const handler of Chat.roomSettings) { const setting = handler(room, user, connection); if (typeof setting.permission === 'string') setting.permission = user.can(setting.permission, null, room); output += `${setting.label}:
`; for (const option of setting.options) { // disabled button if (option[1] === true) { output += Utils.html` `; } else { // only show proper buttons if we have the permissions to use them if (!setting.permission) continue; output += Utils.html` `; } } output += `
`; } output += '
'; user.sendTo(room, `|${uhtml}|roomsettings|${output}`); }, roomsettingshelp: [`/roomsettings - Shows current room settings with buttons to change them (if you can).`], modchat(target, room, user) { room = this.requireRoom(); if (!target) { const modchatSetting = (room.settings.modchat || "OFF"); return this.sendReply(`Moderated chat is currently set to: ${modchatSetting}`); } if (user.locked) { // would put this below but it behaves weird if there's no modchat set return this.errorReply(`/modchat - Access denied.`); } else { this.checkCan('modchat', null, room); } if ( room.settings.modchat && room.settings.modchat.length <= 1 && !room.auth.atLeast(user, room.settings.modchat) && // Upper Staff should probably be able to set /modchat ~ in secret rooms !user.can('bypassall') ) { return this.errorReply(`/modchat - Access denied for changing a setting currently at ${room.settings.modchat}.`); } if ((room as any).requestModchat) { const error = (room as GameRoom).requestModchat(user); if (error) return this.errorReply(error); } // only admins can force modchat on a forced public battle if (room.battle?.forcedSettings.modchat && !user.can('rangeban')) { return this.errorReply( `This battle is required to have modchat on due to one of the players having a username that starts with ` + `${room.battle.forcedSettings.modchat}.` ); } target = target.toLowerCase().trim(); const currentModchat = room.settings.modchat; switch (target) { case 'off': case 'false': case 'no': case 'disable': room.settings.modchat = null; break; case 'ac': case 'autoconfirmed': room.settings.modchat = 'autoconfirmed'; break; case 'trusted': room.settings.modchat = 'trusted'; break; case 'player': target = Users.PLAYER_SYMBOL; /* falls through */ default: if (!Users.Auth.isAuthLevel(target) || ['‽', '!'].includes(target)) { this.errorReply(`The rank '${target}' was unrecognized as a modchat level.`); return this.parse('/help modchat'); } // Users shouldn't be able to set modchat above their own rank (except for ROs who are also Upper Staff) const modchatLevelHigherThanUserRank = !room.auth.atLeast(user, target) && !user.can('bypassall'); if (modchatLevelHigherThanUserRank || !Users.Auth.hasPermission(user, 'modchat', target as GroupSymbol, room)) { return this.errorReply(`/modchat - Access denied for setting to ${target}.`); } room.settings.modchat = target; break; } if (currentModchat === room.settings.modchat) { return this.errorReply(`Modchat is already set to ${currentModchat || 'off'}.`); } if (!room.settings.modchat) { this.add("|raw|
Moderated chat was disabled!
Anyone may talk now.
"); } else { const modchatSetting = Utils.escapeHTML(room.settings.modchat); this.add(`|raw|
Moderated chat was set to ${modchatSetting}!
Only users of rank ${modchatSetting} and higher can talk.
`); } if ((room as GameRoom).requestModchat && !room.settings.modchat) (room as GameRoom).requestModchat(null); this.privateModAction(`${user.name} set modchat to ${room.settings.modchat || "off"}`); this.modlog('MODCHAT', null, `to ${room.settings.modchat || "false"}`); room.saveSettings(); }, modchathelp: [ `/modchat [off/autoconfirmed/trusted/+/%/@/*/player/#/~] - Set the level of moderated chat. Requires: % \u2606 for off/autoconfirmed/+/player options, * @ # ~ for all the options`, ], automodchat(target, room, user) { room = this.requireRoom(); if (!target) { if (!room.settings.autoModchat) return this.sendReply(`This room has automodchat OFF.`); const { rank: curRank, time: curTime } = room.settings.autoModchat; return this.sendReply(`Automodchat is currently set to set modchat to ${curRank} after ${curTime} minutes.`); } this.checkCan('declare', null, room); if (this.meansNo(toID(target))) { if (!room.settings.autoModchat) return this.errorReply(`Auto modchat is not set.`); delete room.settings.autoModchat; room.saveSettings(); if (room.modchatTimer) clearTimeout(room.modchatTimer); // fallback just in case (should never happen) this.privateModAction(`${user.name} turned off automatic modchat.`); return this.modlog(`AUTOMODCHAT`, null, 'OFF'); } let [rawTime, rank] = Utils.splitFirst(target, ',').map(i => i.trim()) as [string, GroupSymbol]; if (!rawTime) { return this.parse(`/help automodchat`); } if (!rank) { if (room.settings.autoModchat) { rank = room.settings.autoModchat.rank; } else { return this.parse(`/help automodchat`); } } const validGroups = [...Config.groupsranking as string[], 'trusted', 'autoconfirmed']; if (!validGroups.includes(rank)) { return this.errorReply(`Invalid rank.`); } const time = parseInt(rawTime); if (isNaN(time) || time > 480 || time < 5) { return this.errorReply("Invalid duration. Choose a number under 480 (in minutes) and over 5 minutes."); } room.settings.autoModchat = { rank, time, active: false, }; this.privateModAction(`${user.name} set automodchat to rank ${rank} and timeout ${time} minutes.`); this.modlog(`AUTOMODCHAT`, null, `${rank}: ${time} minutes`); room.saveSettings(); }, automodchathelp: [ `/automodchat [number], [rank] - Sets modchat [rank] to automatically turn on after [number] minutes with no staff.`, `[number] must be between 5 and 480. Requires: # ~`, `/automodchat off - Turns off automodchat.`, ], ionext() { this.errorReply(`"ionext" is an outdated feature. Hidden battles now have password-protected URLs, making them fully secure against eavesdroppers.`); this.errorReply(`You probably want to switch from /ionext to /hidenext, and from /ioo to /hideroom`); }, ioo() { this.errorReply(`"ioo" is an outdated feature. Hidden battles now have password-protected URLs, making them fully secure against eavesdroppers.`); this.errorReply(`You probably want to switch from /ioo to /hideroom`); }, inviteonlynext(target, room, user) { const groupConfig = Config.groups[Users.PLAYER_SYMBOL]; if (!groupConfig?.editprivacy) return this.errorReply(`/ionext - Access denied.`); if (this.meansNo(target)) { user.battleSettings.inviteOnly = false; user.update(); this.sendReply("Your next battle will be publicly visible."); } else { user.battleSettings.inviteOnly = true; user.update(); this.sendReply(`Your next battle will be invite-only${Rooms.RoomBattle.battleForcedSetting(user, 'privacy') ? `, unless it is rated` : ``}.`); } }, inviteonlynexthelp: [ `/inviteonlynext - Sets your next battle to be invite-only.`, `/inviteonlynext off - Sets your next battle to be publicly visible.`, ], inviteonly(target, room, user, connection, cmd) { room = this.requireRoom(); if (!target) return this.parse('/help inviteonly'); if (this.meansYes(target)) { return this.parse("/modjoin %"); } else { return this.parse(`/modjoin ${target}`); } }, inviteonlyhelp: [ `/inviteonly [on|off] - Sets modjoin %. Users can't join unless invited with /invite. Requires: # ~`, `/ioo - Shortcut for /inviteonly on`, `/inviteonlynext OR /ionext - Sets your next battle to be invite-only.`, `/ionext off - Sets your next battle to be publicly visible.`, ], modjoin(target, room, user) { room = this.requireRoom(); if (!target) { const modjoinSetting = room.settings.modjoin === true ? "SYNC" : room.settings.modjoin || "OFF"; return this.sendReply(`Modjoin is currently set to: ${modjoinSetting}`); } if (room.battle) { this.checkCan('editprivacy', null, room); if (room.battle.forcedSettings.privacy) { return this.errorReply( `This battle is required to be public due to a player having a name prefixed by '${room.battle.forcedSettings.privacy}'.` ); } if (room.battle.inviteOnlySetter && !user.can('mute', null, room) && room.battle.inviteOnlySetter !== user.id) { return this.errorReply(`Only the person who set this battle to be invite-only can turn it off.`); } room.battle.inviteOnlySetter = user.id; } else if (room.settings.isPersonal) { this.checkCan('editroom', null, room); } else { this.checkCan('makeroom'); } if (room.tour && !room.tour.allowModjoin) { return this.errorReply(`You can't do this in tournaments where modjoin is prohibited.`); } if (target === 'player') target = Users.PLAYER_SYMBOL; if (this.meansNo(target)) { if (!room.settings.modjoin) return this.errorReply(`Modjoin is already turned off in this room.`); room.settings.modjoin = null; this.add(`|raw|
This room is no longer invite only!
Anyone may now join.
`); this.addModAction(`${user.name} turned off modjoin.`); this.modlog('MODJOIN', null, 'OFF'); if (room.battle) room.battle.inviteOnlySetter = null; room.saveSettings(); return; } else if (target === 'sync') { if (room.settings.modjoin === true) return this.errorReply(`Modjoin is already set to sync modchat in this room.`); room.settings.modjoin = true; this.add(`|raw|
Moderated join is set to sync with modchat!
Only users who can speak in modchat can join.
`); this.addModAction(`${user.name} set modjoin to sync with modchat.`); this.modlog('MODJOIN SYNC'); } else if (target === 'ac' || target === 'autoconfirmed') { if (room.settings.modjoin === 'autoconfirmed') return this.errorReply(`Modjoin is already set to autoconfirmed.`); room.settings.modjoin = 'autoconfirmed'; this.add(`|raw|
Moderated join is set to autoconfirmed!
Users must be rank autoconfirmed or invited with /invite to join
`); this.addModAction(`${user.name} set modjoin to autoconfirmed.`); this.modlog('MODJOIN', null, 'autoconfirmed'); } else if (Users.Auth.isAuthLevel(target) && !['‽', '!'].includes(target)) { if (room.battle && !user.can('makeroom') && !'+%'.includes(target)) { return this.errorReply(`/modjoin - Access denied from setting modjoin past % in battles.`); } if (room.settings.isPersonal && !user.can('makeroom') && !'+%'.includes(target)) { return this.errorReply(`/modjoin - Access denied from setting modjoin past % in group chats.`); } if (room.settings.modjoin === target) return this.errorReply(`Modjoin is already set to ${target} in this room.`); room.settings.modjoin = target; this.add(`|raw|
This room is now invite only!
Users must be rank ${target} or invited with /invite to join
`); this.addModAction(`${user.name} set modjoin to ${target}.`); this.modlog('MODJOIN', null, target); } else { this.errorReply(`Unrecognized modjoin setting.`); void this.parse('/help modjoin'); return false; } room.saveSettings(); if (target === 'sync' && !room.settings.modchat) { const lowestGroup = Config.groupsranking.find(group => { const groupInfo = Users.Auth.getGroup(group); return ( groupInfo.symbol !== Users.Auth.defaultSymbol() && room.auth.atLeast(user, group) && Users.Auth.isValidSymbol(groupInfo.symbol) ); }); if (lowestGroup) void this.parse(`/modchat ${lowestGroup}`); } if (!room.settings.isPrivate) return this.parse('/hiddenroom'); }, modjoinhelp: [ `/modjoin [+|%|@|*|player|~|#|off] - Sets modjoin. Users lower than the specified rank can't join this room unless they have a room rank. Requires: \u2606 # ~`, `/modjoin [sync|off] - Sets modjoin. Only users who can speak in modchat can join this room. Requires: \u2606 # ~`, ], roomlanguage(target, room, user) { room = this.requireRoom(); if (!target) { return this.sendReply(`This room's primary language is ${Chat.languages.get(room.settings.language || '') || 'English'}`); } this.checkCan('editroom', null, room); const targetLanguage = toID(target); if (!Chat.languages.has(targetLanguage)) return this.errorReply(`"${target}" is not a supported language.`); room.settings.language = targetLanguage === 'english' ? false : targetLanguage; room.saveSettings(); this.modlog(`LANGUAGE`, null, Chat.languages.get(targetLanguage)); this.sendReply(`The room's language has been set to ${Chat.languages.get(targetLanguage)}`); }, roomlanguagehelp: [ `/roomlanguage [language] - Sets the the language for the room, which changes language of a few commands. Requires # ~`, `Supported Languages: English, Spanish, Italian, French, Simplified Chinese, Traditional Chinese, Japanese, Hindi, Turkish, Dutch, German.`, ], slowchat(target, room, user) { room = this.requireRoom(); if (!target) { const slowchatSetting = (room.settings.slowchat || "OFF"); return this.sendReply(`Slow chat is currently set to: ${slowchatSetting}`); } this.checkChat(); this.checkCan('modchat', null, room); let targetInt = parseInt(target); if (this.meansNo(target)) { if (!room.settings.slowchat) return this.errorReply(`Slow chat is already disabled in this room.`); room.settings.slowchat = false; } else if (targetInt) { if (!user.can('bypassall') && room.userCount < SLOWCHAT_USER_REQUIREMENT) { return this.errorReply(`This room must have at least ${SLOWCHAT_USER_REQUIREMENT} users to set slowchat; it only has ${room.userCount} right now.`); } if (room.settings.slowchat === targetInt) { return this.errorReply(`Slow chat is already set to ${room.settings.slowchat} seconds in this room.`); } if (targetInt < SLOWCHAT_MINIMUM) targetInt = SLOWCHAT_MINIMUM; if (targetInt > SLOWCHAT_MAXIMUM) targetInt = SLOWCHAT_MAXIMUM; room.settings.slowchat = targetInt; } else { return this.parse("/help slowchat"); } const slowchatSetting = (room.settings.slowchat || "OFF"); this.privateModAction(`${user.name} set slowchat to ${slowchatSetting}`); this.modlog('SLOWCHAT', null, `${slowchatSetting}`); room.saveSettings(); }, slowchathelp: [ `/slowchat [number] - Sets a limit on how often users in the room can send messages, between 2 and 60 seconds. Requires % @ # ~`, `/slowchat off - Disables slowchat in the room. Requires % @ # ~`, ], permission: 'permissions', permissions: { clear: 'set', set(target, room, user) { const [perm, displayRank] = this.splitOne(target); room = this.requireRoom(); let rank = displayRank; if (rank === 'default') rank = ''; if (rank === 'all users') rank = Users.Auth.defaultSymbol(); if (!room.persist) return this.errorReply(`This room does not allow customizing permissions.`); if (!target || !perm) return this.parse(`/permissions help`); if (rank && rank !== 'whitelist' && !Config.groupsranking.includes(rank as EffectiveGroupSymbol)) { return this.errorReply(`${rank} is not a valid rank.`); } const validPerms = Users.Auth.supportedRoomPermissions(room); const sanitizedPerm = perm.replace('!', '/'); // handles ! commands so we don't have to add commands to the array twice if (!validPerms.some(p => ( // we need to check the raw permissions also because broadcast permissions are listed with the ! p === sanitizedPerm || p === perm || p.startsWith(`${sanitizedPerm} `) || p.startsWith(`${perm} `) ))) { return this.errorReply(`${perm} is not a valid room permission.`); } if (!room.auth.atLeast(user, '#')) { return this.errorReply(`/permissions set - You must be at least a Room Owner to set permissions.`); } if ( Users.Auth.ROOM_PERMISSIONS.includes(perm as RoomPermission) && !Users.Auth.hasPermission(user, perm, null, room) ) { return this.errorReply(`/permissions set - You can't set the permission "${perm}" because you don't have it.`); } const currentPermissions = room.settings.permissions || {}; if (currentPermissions[perm] === (rank || undefined)) { return this.errorReply(`${perm} is already set to ${displayRank || 'default'}.`); } if (rank) { currentPermissions[perm] = rank as GroupSymbol; room.settings.permissions = currentPermissions; } else { delete currentPermissions[perm]; if (!Object.keys(currentPermissions).length) delete room.settings.permissions; } room.saveSettings(); this.modlog(`SETPERMISSION`, null, `${perm}: ${displayRank}`); this.refreshPage(`permissions-${room.roomid}`); return this.privateModAction(`${user.name} set the required rank for ${perm} to ${displayRank}.`); }, sethelp: [ `/permissions set [command], [rank symbol] - sets the required permission to use the command [command] to [rank]. Requires: # ~`, `/permissions clear [command] - resets the required permission to use the command [command] to the default. Requires: # ~`, ], view(target, room, user) { room = this.requireRoom(); return this.parse(`/join view-permissions-${room.roomid}`); }, help: '', ''(target, room, user) { room = this.requireRoom(); const allPermissions = Users.Auth.supportedRoomPermissions(room); const permissionGroups = allPermissions.filter(perm => !perm.startsWith('/') && !perm.startsWith('!')); const permissions = allPermissions.filter(perm => { const handler = Chat.parseCommand(perm)?.handler; if (handler?.isPrivate && !user.can('lock')) return false; return (perm.startsWith('/') || perm.startsWith('!')) && !perm.includes(' '); }); const subPermissions = allPermissions .filter(perm => (perm.startsWith('/') || perm.startsWith('!')) && perm.includes(' ')) .filter(perm => { const handler = Chat.parseCommand(perm)?.handler; if (handler?.isPrivate && !user.can('lock')) return false; return (perm.startsWith('/') || perm.startsWith('!')) && perm.includes(' '); }); const subPermissionsByNamespace: { [k: string]: string[] } = {}; for (const perm of subPermissions) { const [namespace] = perm.split(' ', 1); if (!subPermissionsByNamespace[namespace]) subPermissionsByNamespace[namespace] = []; subPermissionsByNamespace[namespace].push(perm); } let buffer = `Room permissions help:
`; buffer += `

Usage:
`; buffer += `/permissions set [permission], [rank symbol]
`; buffer += `/permissions clear [permission]
`; buffer += `/permissions view

`; buffer += `

Group permissions: (will affect multiple commands or part of one command)
`; buffer += `` + permissionGroups.join(` `) + `

`; buffer += `

Single-command permissions: (will affect one command)`; buffer += `Permissions starting with ! are for broadcasting the command, not using it.
`; buffer += `` + permissions.join(` `) + `

`; buffer += `

Sub-commands: (will affect one sub-command, like /roomevents view)`; for (const subPerms of Object.values(subPermissionsByNamespace)) { buffer += `
` + subPerms.join(` `) + `
`; } buffer += `

`; return this.sendReplyBox(buffer); }, }, stretching: 'stretchfilter', stretchingfilter: 'stretchfilter', stretchfilter(target, room, user) { room = this.requireRoom(); if (!target) { const stretchSetting = (room.settings.filterStretching ? "ON" : "OFF"); return this.sendReply(`This room's stretch filter is currently: ${stretchSetting}`); } this.checkChat(); this.checkCan('editroom', null, room); if (this.meansYes(target)) { if (room.settings.filterStretching) return this.errorReply(`This room's stretch filter is already ON`); room.settings.filterStretching = true; } else if (this.meansNo(target)) { if (!room.settings.filterStretching) return this.errorReply(`This room's stretch filter is already OFF`); room.settings.filterStretching = false; } else { return this.parse("/help stretchfilter"); } const stretchSetting = (room.settings.filterStretching ? "ON" : "OFF"); this.privateModAction(`${user.name} turned the stretch filter ${stretchSetting}`); this.modlog('STRETCH FILTER', null, stretchSetting); room.saveSettings(); }, stretchfilterhelp: [ `/stretchfilter [on/off] - Toggles filtering messages in the room for stretchingggggggg. Requires # ~`, ], capitals: 'capsfilter', capitalsfilter: 'capsfilter', capsfilter(target, room, user) { room = this.requireRoom(); if (!target) { const capsSetting = (room.settings.filterCaps ? "ON" : "OFF"); return this.sendReply(`This room's caps filter is currently: ${capsSetting}`); } this.checkChat(); this.checkCan('editroom', null, room); if (this.meansYes(target)) { if (room.settings.filterCaps) return this.errorReply(`This room's caps filter is already ON`); room.settings.filterCaps = true; } else if (this.meansNo(target)) { if (!room.settings.filterCaps) return this.errorReply(`This room's caps filter is already OFF`); room.settings.filterCaps = false; } else { return this.parse("/help capsfilter"); } const capsSetting = (room.settings.filterCaps ? "ON" : "OFF"); this.privateModAction(`${user.name} turned the caps filter ${capsSetting}`); this.modlog('CAPS FILTER', null, capsSetting); room.saveSettings(); }, capsfilterhelp: [`/capsfilter [on/off] - Toggles filtering messages in the room for EXCESSIVE CAPS. Requires # ~`], emojis: 'emojifilter', emoji: 'emojifilter', emojifilter(target, room, user) { room = this.requireRoom(); if (!target) { const emojiSetting = (room.settings.filterEmojis ? "ON" : "OFF"); return this.sendReply(`This room's emoji filter is currently: ${emojiSetting}`); } this.checkChat(); this.checkCan('editroom', null, room); if (this.meansYes(target)) { if (room.settings.filterEmojis) return this.errorReply(`This room's emoji filter is already ON`); room.settings.filterEmojis = true; } else if (this.meansNo(target)) { if (!room.settings.filterEmojis) return this.errorReply(`This room's emoji filter is already OFF`); room.settings.filterEmojis = false; } else { return this.parse("/help emojifilter"); } const emojiSetting = (room.settings.filterEmojis ? "ON" : "OFF"); this.privateModAction(`${user.name} turned the emoji filter ${emojiSetting}`); this.modlog('EMOJI FILTER', null, emojiSetting); room.saveSettings(); }, emojifilterhelp: [`/emojifilter [on/off] - Toggles filtering messages in the room for emojis. Requires # ~`], linkfilter(target, room, user) { room = this.requireRoom(); if (!target) { return this.sendReply( `This room's link filter is currently: ${room.settings.filterEmojis ? "ON" : "OFF"}` ); } this.checkChat(); this.checkCan('editroom', null, room); if (this.meansYes(target)) { if (room.settings.filterLinks) return this.errorReply(`This room's link filter is already ON`); room.settings.filterLinks = true; } else if (this.meansNo(target)) { if (!room.settings.filterLinks) return this.errorReply(`This room's link filter is already OFF`); room.settings.filterLinks = false; } else { return this.parse("/help linkfilter"); } const setting = (room.settings.filterLinks ? "ON" : "OFF"); this.privateModAction(`${user.name} turned the link filter ${setting}`); this.modlog('LINK FILTER', null, setting); room.saveSettings(); }, linkfilterhelp: [`/linkfilter [on/off] - Toggles filtering messages in the room for links. Requires # ~`], banwords: 'banword', banword: { regexadd: 'add', addregex: 'add', add(target, room, user, connection, cmd) { room = this.requireRoom(); if (!target || target === ' ') return this.parse('/help banword'); this.checkCan('declare', null, room); const regex = cmd.includes('regex'); if (regex && !user.can('makeroom')) return this.errorReply("Regex banwords are only allowed for administrators."); if (!room.settings.banwords) room.settings.banwords = []; // Most of the regex code is copied from the client. TODO: unify them? // Regex banwords can have commas in the {1,5} pattern let words = (regex ? target.match(/[^,]+(,\d*}[^,]*)?/g)! : target.split(',')) .map(word => word.replace(/\n/g, '').trim()).filter(word => word.length > 0); if (!words || words.length === 0) return this.parse('/help banword'); // Escape any character with a special meaning in regex if (!regex) { words = words.map(word => { if (/[\\^$*+?()|{}[\]]/.test(word) && user.can('rangeban')) { this.errorReply(`"${word}" might be a regular expression, did you mean "/banword addregex"?`); } return Utils.escapeRegex(word); }); } // PS adds a preamble to the banword regex that's 32 chars long let banwordRegexLen = (room.banwordRegex instanceof RegExp) ? room.banwordRegex.source.length : 32; for (const word of words) { Chat.validateRegex(word); if (room.settings.banwords.includes(word)) return this.errorReply(`${word} is already a banned phrase.`); // Banword strings are joined, so account for the first string not having the prefix banwordRegexLen += (banwordRegexLen === 32) ? word.length : `|${word}`.length; // RegExp instances whose source is greater than or equal to // v8's RegExpMacroAssembler::kMaxRegister in length will crash // the server on compile. In this case, that would happen each // time a chat message gets tested for any banned phrases. if (banwordRegexLen >= (1 << 16 - 1)) { return this.errorReply("This room has too many banned phrases to add the ones given."); } } for (const word of words) { room.settings.banwords.push(word); } room.banwordRegex = null; if (words.length > 1) { this.privateModAction(`The banwords ${words.map(w => `'${w}'`).join(', ')} were added by ${user.name}.`); this.modlog('BANWORD', null, words.map(w => `'${w}'`).join(', ')); this.sendReply(`Banned phrases successfully added.`); } else { this.privateModAction(`The banword '${words[0]}' was added by ${user.name}.`); this.modlog('BANWORD', null, words[0]); this.sendReply(`Banned phrase successfully added.`); } this.sendReply(`The list is currently: ${room.settings.banwords.join(', ')}`); room.saveSettings(); }, delete(target, room, user) { room = this.requireRoom(); if (!target) return this.parse('/help banword'); this.checkCan('declare', null, room); if (!room.settings.banwords) return this.errorReply("This room has no banned phrases."); const regexMatch = target.match(/[^,]+(,\d*}[^,]*)?/g); if (!regexMatch) return this.parse('/help banword'); const words = regexMatch.map(word => word.replace(/\n/g, '').trim()).filter(word => word.length > 0); for (const word of words) { if (!room.settings.banwords.includes(word)) return this.errorReply(`${word} is not a banned phrase in this room.`); } room.settings.banwords = room.settings.banwords.filter(w => !words.includes(w)); room.banwordRegex = null; if (words.length > 1) { this.privateModAction(`The banwords ${words.map(w => `'${w}'`).join(', ')} were removed by ${user.name}.`); this.modlog('UNBANWORD', null, words.map(w => `'${w}'`).join(', ')); this.sendReply(`Banned phrases successfully deleted.`); } else { this.privateModAction(`The banword '${words[0]}' was removed by ${user.name}.`); this.modlog('UNBANWORD', null, words[0]); this.sendReply(`Banned phrase successfully deleted.`); } if (!room.settings.banwords.length) room.settings.banwords = undefined; this.sendReply( room.settings.banwords ? `The list is now: ${room.settings.banwords.join(', ')}` : `The list is now empty.` ); room.saveSettings(); }, list(target, room, user) { room = this.requireRoom(); this.checkCan('mute', null, room); if (!room.settings.banwords?.length) { return this.sendReply("This room has no banned phrases."); } return this.sendReply(`Banned phrases in room ${room.roomid}: ${room.settings.banwords.join(', ')}`); }, ""(target, room, user) { return this.parse("/help banword"); }, }, banwordhelp: [ `/banword add [words] - Adds the comma-separated list of phrases to the banword list of the current room. Requires: # ~`, `/banword addregex [words] - Adds the comma-separated list of regular expressions to the banword list of the current room. Requires ~`, `/banword delete [words] - Removes the comma-separated list of phrases from the banword list. Requires: # ~`, `/banword list - Shows the list of banned words in the current room. Requires: % @ # ~`, ], showapprovals(target, room, user) { room = this.requireRoom(); this.checkCan('declare', null, room); target = toID(target); if (!target) { return this.sendReply(`Approvals are currently ${room.settings.requestShowEnabled ? `ENABLED` : `DISABLED`} for ${room}.`); } if (this.meansNo(target)) { if (!room.settings.requestShowEnabled) return this.errorReply(`Approvals are already disabled.`); room.settings.requestShowEnabled = undefined; this.privateModAction(`${user.name} disabled approvals in this room.`); } else if (this.meansYes(target)) { if (room.settings.requestShowEnabled) return this.errorReply(`Approvals are already enabled.`); room.settings.requestShowEnabled = true; this.privateModAction(`${user.name} enabled the use of media approvals in this room.`); if (!room.settings.permissions || room.settings.permissions['/show'] === '@') { this.privateModAction( `Note: Due to this room's settings, Drivers aren't allowed to use /show directly, ` + `but will be able to request and approve each other's /requestshow` ); } } else { return this.errorReply(`Unrecognized setting for approvals. Use 'on' or 'off'.`); } room.saveSettings(); return this.modlog(`SHOWAPPROVALS`, null, `${this.meansYes(target) ? `ON` : `OFF`}`); }, showapprovalshelp: [ `/showapprovals [setting] - Enable or disable the use of media approvals in the current room.`, `Requires: # ~`, ], showmedia(target, room, user) { this.errorReply(`/showmedia has been deprecated. Use /permissions instead.`); return this.parse(`/help permissions`); }, hightraffic(target, room, user) { room = this.requireRoom(); if (!target) { return this.sendReply(`This room is: ${room.settings.highTraffic ? 'high' : 'low'} traffic`); } this.checkCan('makeroom'); if (this.meansYes(target)) { room.settings.highTraffic = true; } else if (this.meansNo(target)) { room.settings.highTraffic = undefined; } else { return this.parse('/help hightraffic'); } room.saveSettings(); this.modlog(`HIGHTRAFFIC`, null, `${!!room.settings.highTraffic}`); this.addModAction(`This room was marked as ${room.settings.highTraffic ? 'high' : 'low'} traffic by ${user.name}.`); }, hightraffichelp: [ `/hightraffic [on|off] - (Un)marks a room as a high traffic room. Requires ~`, `When a room is marked as high-traffic, PS requires all messages sent to that room to contain at least 2 letters.`, ], /********************************************************* * Room management *********************************************************/ makeprivatechatroom: 'makechatroom', makepublicchatroom: 'makechatroom', makechatroom(target, room, user, connection, cmd) { room = this.requireRoom(); this.checkCan('makeroom'); const id = toID(target); if (!id || this.cmd === 'makechatroom') return this.parse('/help makechatroom'); if (!Rooms.global.addChatRoom(target)) { return this.errorReply(`The room '${target}' already exists or it is using an invalid title.`); } const targetRoom = Rooms.search(target); if (!targetRoom) throw new Error(`Error in room creation.`); if (cmd === 'makeprivatechatroom') { if (!targetRoom.persist) throw new Error(`Private chat room created without settings.`); targetRoom.setPrivate(true); const upperStaffRoom = Rooms.get('upperstaff'); if (upperStaffRoom) { upperStaffRoom.add(`|raw|
Private chat room created: ${Utils.escapeHTML(target)}
`).update(); } this.sendReply(`The private chat room '${target}' was created.`); } else { const staffRoom = Rooms.get('staff'); if (staffRoom) { staffRoom.add(`|raw|
Public chat room created: ${Utils.escapeHTML(target)}
`).update(); } const upperStaffRoom = Rooms.get('upperstaff'); if (upperStaffRoom) { upperStaffRoom.add(`|raw|
Public chat room created: ${Utils.escapeHTML(target)}
`).update(); } this.sendReply(`The chat room '${target}' was created.`); } }, makechatroomhelp: [ `/makeprivatechatroom [roomname] - Creates a new private room named [roomname]. Requires: ~`, `/makepublicchatroom [roomname] - Creates a new public room named [roomname]. Requires: ~`, ], subroomgroupchat: 'makegroupchat', srgc: 'makegroupchat', mgc: 'makegroupchat', makegroupchat(target, room, user, connection, cmd) { room = this.requireRoom(); this.checkChat(); if (!user.trusted) { return this.errorReply("You must be trusted (public room driver or global voice) to make a groupchat."); } const groupchatbanned = Punishments.isGroupchatBanned(user); if (groupchatbanned) { const expireText = Punishments.checkPunishmentExpiration(groupchatbanned); return this.errorReply(`You are banned from using groupchats ${expireText}.`); } if (cmd === 'subroomgroupchat' || cmd === 'srgc') { if (!user.can('mute', null, room)) { return this.errorReply("You can only create subroom groupchats for rooms you're staff in."); } if (room.battle) return this.errorReply("You cannot create a subroom of a battle."); if (room.settings.isPersonal) return this.errorReply("You cannot create a subroom of a groupchat."); } const parent = cmd === 'subroomgroupchat' || cmd === 'srgc' ? room.roomid : null; // this.checkCan('makegroupchat'); // Title defaults to a random 8-digit number. let title = target.trim(); if (title.length >= 32) { return this.errorReply("Title must be under 32 characters long."); } else if (!title) { title = (`${Math.floor(Math.random() * 100000000)}`); } else if (this.filter(title) !== title) { 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. const existingRoom = Rooms.search(toID(title)); if (existingRoom && !existingRoom.settings.modjoin) { return this.errorReply(`Your group chat name is too similar to existing chat room '${title}'.`); } // Room IDs for groupchats are groupchat-TITLEID let titleid = toID(title); if (!titleid) { titleid = `${Math.floor(Math.random() * 100000000)}` as ID; } const roomid = `groupchat-${parent || user.id}-${titleid}` as RoomID; // 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; } const titleMsg = Utils.html`Welcome to ${parent ? room.title : user.name}'s` + Utils.html`${!/^[0-9]+$/.test(title) ? ` ${title}` : ''}${parent ? ' subroom' : ''} groupchat!`; const targetRoom = Rooms.createChatRoom(roomid, `[G] ${title}`, { isPersonal: true, isPrivate: 'hidden', creationTime: Date.now(), modjoin: parent ? null : '+', parentid: parent, auth: {}, introMessage: `` + `

${titleMsg}

Follow the Pokémon Showdown Global Rules!
Don't be disruptive to the rest of the site.

`, staffMessage: `` + `

Groupchats are temporary rooms, and will expire if there hasn't been any activity in 40 minutes.

You can invite new users using /invite. Be careful with who you invite!

Commands: | |

As creator of this groupchat, you are entirely responsible for what occurs in this chatroom. Global rules apply at all times.

If this room is used to break global rules or disrupt other areas of the server, you as the creator will be held accountable and punished.

`, }); if (!targetRoom) { return this.errorReply(`An unknown error occurred while trying to create the room '${title}'.`); } // The creator is a Room Owner in subroom groupchats and a Host otherwise.. targetRoom.auth.set(user.id, parent ? '#' : Users.HOST_SYMBOL); // Join after creating room. No other response is given. user.joinRoom(targetRoom.roomid); user.popup(`You've just made a groupchat; it is now your responsibility, regardless of whether or not you actively partake in the room. For more info, read your groupchat's staff intro.`); if (parent) this.modlog('SUBROOMGROUPCHAT', null, title); }, makegroupchathelp: [ `/makegroupchat [roomname] - Creates an invite-only group chat named [roomname].`, `/subroomgroupchat [roomname] - Creates a subroom groupchat of the current room. Can only be used in a public room you have staff in.`, `Only users who are staff in a public room or global auth can make groupchats.`, ], groupchatuptime: 'roomuptime', roomuptime(target, room, user, connection, cmd) { if (!this.runBroadcast()) return; if (!room) return this.errorReply(`Can only be used in a room.`); // for hotpatching if (!room.settings.creationTime) room.settings.creationTime = Date.now(); const uptime = Chat.toDurationString(Date.now() - room.settings.creationTime); this.sendReplyBox(`Room uptime: ${uptime}`); }, roomuptimehelp: [`/roomuptime - Displays the uptime of the room.`], deregisterchatroom(target, room, user) { this.checkCan('makeroom'); this.errorReply("NOTE: You probably want to use `/deleteroom` now that it exists."); const id = toID(target); if (!id) return this.parse('/help deregisterchatroom'); const targetRoom = Rooms.search(id); if (!targetRoom) return this.errorReply(`The room '${target}' doesn't exist.`); target = targetRoom.title || targetRoom.roomid; const isPrivate = targetRoom.settings.isPrivate; const staffRoom = Rooms.get('staff'); const upperStaffRoom = Rooms.get('upperstaff'); if (Rooms.global.deregisterChatRoom(id)) { this.sendReply(`The room '${target}' was deregistered.`); this.sendReply("It will be deleted as of the next server restart."); target = Utils.escapeHTML(target); if (isPrivate) { if (upperStaffRoom) { upperStaffRoom.add(`|raw|
Private chat room deregistered by ${user.id}: ${target}
`).update(); } } else { if (staffRoom) { staffRoom.add(`|raw|
Public chat room deregistered: ${target}
`).update(); } if (upperStaffRoom) { upperStaffRoom.add(`|raw|
Public chat room deregistered by ${user.id}: ${target}
`).update(); } } return; } return this.errorReply(`The room "${target}" isn't registered.`); }, deregisterchatroomhelp: [ `/deregisterchatroom [roomname] - Deletes room [roomname] after the next server restart. Requires: ~`, ], deletechatroom: 'deleteroom', deletegroupchat: 'deleteroom', dgc: 'deleteroom', deleteroom(target, room, user, connection, cmd) { room = this.requireRoom(); const roomid = target.trim(); if (!roomid) { // allow deleting personal rooms without typing out the room name if (!room.settings.isPersonal || !['deletegroupchat', 'dgc'].includes(cmd)) { return this.parse(`/help deleteroom`); } } else { const targetRoom = Rooms.search(roomid); if (targetRoom !== room) { return this.parse(`/help deleteroom`); } } if (room.roomid.startsWith('groupchat-')) { this.checkCan('gamemanagement', null, room); } else { this.checkCan('makeroom'); } const title = room.title || room.roomid; if (room.persist) { if (room.settings.isPrivate) { const upperStaffRoom = Rooms.get('upperstaff'); if (upperStaffRoom) { upperStaffRoom.add( Utils.html`|raw|
Private chat room ` + `deleted by ${user.id}: ${title}
` ).update(); } } else { const staffRoom = Rooms.get('staff'); if (staffRoom) { staffRoom.add(Utils.html`|raw|
Public chat room deleted: ${title}
`).update(); } const upperStaffRoom = Rooms.get('upperstaff'); if (upperStaffRoom) { upperStaffRoom.add( Utils.html`|raw|
Public chat ` + `room deleted by ${user.id}: ${title}
` ).update(); } } } room.add(`|raw|
This room has been deleted.
`); room.update(); room.send(`|expire|This room has been deleted.`); room.destroy(); }, deleteroomhelp: [ `/deleteroom [roomname] - Deletes room [roomname]. Must be typed in the room to delete. Requires: ~`, `/deletegroupchat - Deletes the current room, if it's a groupchat. Requires: ★ # ~`, ], rename() { this.errorReply("Did you mean /renameroom?"); }, renamegroupchat: 'renameroom', renameroom(target, room, user, connection, cmd) { room = this.requireRoom(); if (room.game || room.minorActivity || room.tour) { return this.errorReply("Cannot rename room while a tour/poll/game is running."); } if (room.battle) { return this.errorReply("Cannot rename battle rooms."); } const oldTitle = room.title; const isGroupchat = cmd === 'renamegroupchat'; if (!toID(target)) return this.parse("/help renameroom"); if (room.persist && isGroupchat) return this.errorReply(`This isn't a groupchat.`); if (!room.persist && !isGroupchat) return this.errorReply(`Use /renamegroupchat instead.`); if (isGroupchat) { if (!user.can('lock')) this.checkCan('declare', null, room); const existingRoom = Rooms.search(toID(target)); if (existingRoom && !existingRoom.settings.modjoin) { return this.errorReply(`Your groupchat name is too similar to existing chat room '${existingRoom.title}'.`); } if (this.filter(target) !== target) { 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 (target.includes(',') || target.includes('|') || target.includes('[') || target.includes('-')) { return this.errorReply("Room titles can't contain any of: ,|[-"); } } else { this.checkCan('makeroom'); } const creatorID = room.roomid.split('-')[1]; const id = isGroupchat ? `groupchat-${creatorID}-${toID(target)}` as RoomID : undefined; const title = isGroupchat ? `[G] ${target}` : target; const oldID = room.roomid; room.rename(title, id); Chat.handleRoomRename(oldID, id || toID(target) as RoomID, room); this.modlog(`RENAME${isGroupchat ? 'GROUPCHAT' : 'ROOM'}`, null, `from ${oldTitle}`); const privacy = room.settings.isPrivate === true ? "Private" : !room.settings.isPrivate ? "Public" : `${room.settings.isPrivate.charAt(0).toUpperCase()}${room.settings.isPrivate.slice(1)}`; if (!isGroupchat) { Rooms.global.notifyRooms( room.settings.isPrivate === true ? ['upperstaff'] : ['upperstaff', 'staff'], Utils.html`|raw|
${privacy} chat room ${oldTitle} renamed to ${target}
` ); } room.add(Utils.html`|raw|
The room has been renamed to ${target}
`).update(); }, renameroomhelp: [`/renameroom [new title] - Renames the current room to [new title]. Case-sensitive. Requires ~`], hideroom: 'privateroom', hiddenroom: 'privateroom', secretroom: 'privateroom', publicroom: 'privateroom', unlistroom: 'privateroom', privateroom(target, room, user, connection, cmd) { room = this.requireRoom(); const battle = room.battle || room.bestOf; if (battle) { this.checkCan('editprivacy', null, room); if (battle.forcedSettings.privacy) { return this.errorReply(`This battle is required to be public because a player has a name prefixed by '${battle.forcedSettings.privacy}'.`); } if (room.tour?.forcePublic) { return this.errorReply(`This battle can't be hidden, because the tournament is set to be forced public.`); } } else if (room.settings.isPersonal) { this.checkCan('editroom', null, room); } else { // registered chatrooms show up on the room list and so require // higher permissions to modify privacy settings this.checkCan('makeroom'); } let setting: boolean | 'hidden' | 'unlisted'; switch (cmd) { case 'privateroom': return this.parse('/help privateroom'); case 'publicroom': setting = false; break; case 'secretroom': this.checkCan('rangeban'); setting = true; break; case 'unlistroom': this.checkCan('rangeban'); setting = 'unlisted'; break; default: if (room.settings.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 (this.meansNo(target)) { return this.errorReply(`Please specify what privacy setting you want for this room: /hiddenroom, /secretroom, or /publicroom`); } if (!setting) { if (!room.settings.isPrivate) { return this.errorReply(`This room is already public.`); } if (room.parent?.settings.isPrivate) { return this.errorReply(`This room's parent ${room.parent.title} must be public for this room to be public.`); } if (room.settings.isPersonal && !battle) { 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.id)) { const privacySetters = [...room.privacySetter].join(', '); return this.errorReply(`You can't make the room public since you didn't make it private - only ${privacySetters} can.`); } room.privacySetter.delete(user.id); if (room.privacySetter.size) { const privacySetters = [...room.privacySetter].join(', '); return this.sendReply(`You are no longer forcing the room to stay private, but ${privacySetters} also need${Chat.plural(room.privacySetter, "", "s")} to use /publicroom to make the room public.`); } } room.privacySetter = null; this.addModAction(`${user.name} made this room public.`); this.modlog('PUBLICROOM'); room.setPrivate(false); } else { const settingName = (setting === true ? 'secret' : setting); if (room.subRooms && !room.bestOf) { if (settingName === 'secret') return this.errorReply("Secret rooms cannot have subrooms."); for (const subRoom of room.subRooms.values()) { if (!subRoom.settings.isPrivate) { return this.errorReply(`Subroom ${subRoom.title} must be private to make this room private.`); } } } if (room.settings.isPrivate === setting) { if (room.privacySetter && !room.privacySetter.has(user.id)) { room.privacySetter.add(user.id); 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}.`); } this.addModAction(`${user.name} made this room ${settingName}.`); this.modlog(`${settingName.toUpperCase()}ROOM`); if (!room.settings.isPersonal && !battle) room.setSection(); room.setPrivate(setting); room.privacySetter = new Set([user.id]); } }, 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 ~`, ], hidenext(target, room, user) { const groupConfig = Config.groups[Users.PLAYER_SYMBOL]; if (!groupConfig?.editprivacy) return this.errorReply(`/hidenext - Access denied.`); if (this.meansNo(target)) { user.battleSettings.hidden = false; user.update(); this.sendReply("Your next battle will be publicly visible."); } else { user.battleSettings.hidden = true; user.update(); this.sendReply(`Your next battle will be hidden${Rooms.RoomBattle.battleForcedSetting(user, 'privacy') ? `, unless it is rated` : ``}.`); } }, hidenexthelp: [ `/hidenext - Sets your next battle to be hidden.`, `/hidenext off - Sets your next battle to be publicly visible.`, ], officialchatroom: 'officialroom', officialroom() { this.parse(`/setroomsection official`); }, roomspotlight(target, room, user) { this.checkCan('makeroom'); room = this.requireRoom(); if (!target) return this.parse(`/help roomspotlight`); if (!room.persist) { return this.errorReply(`/roomspotlight - You can't spotlight this room.`); } if (this.meansNo(target)) { if (!room.settings.spotlight) return this.errorReply(`This chatroom is not being spotlighted.`); this.addModAction(`${user.name} removed this chatroom from the spotlight.`); this.globalModlog('UNSPOTLIGHT'); delete room.settings.spotlight; room.saveSettings(); } else { if (room.settings.spotlight === target) return this.errorReply("This chat room is already spotlighted."); this.addModAction(`${user.name} spotlighted this room with the message "${target}".`); this.globalModlog('SPOTLIGHT'); room.settings.spotlight = target; room.saveSettings(); } }, roomspotlighthelp: [ `/roomspotlight [spotlight] - Makes the room this command is used in a spotlight room for the [spotlight] category on the roomlist. Requires: ~`, `/roomspotlight off - Removes the room this command is used in from the list of spotlight rooms. Requires: ~`, ], setsubroom: 'subroom', subroom(target, room, user) { room = this.requireRoom(); if (!user.can('makeroom')) return this.errorReply(`/subroom - Access denied. Did you mean /subrooms?`); if (!target) return this.parse('/help subroom'); if (!room.persist) return this.errorReply(`Temporary rooms cannot be subrooms.`); if (room.parent) { return this.errorReply(`This room is already a subroom. To change which room this subroom belongs to, remove the subroom first.`); } if (room.subRooms) { return this.errorReply(`This room is already a parent room, and a parent room cannot be made as a subroom.`); } const parent = Rooms.search(target); if (!parent) return this.errorReply(`The room '${target}' does not exist.`); if (parent.type !== 'chat') return this.errorReply(`Parent room '${target}' must be a chat room.`); if (parent.parent) return this.errorReply(`Subrooms cannot have subrooms.`); if (parent.settings.isPrivate === true) return this.errorReply(`Only public and hidden rooms can have subrooms.`); if (parent.settings.isPrivate && !room.settings.isPrivate) { return this.errorReply(`Private rooms cannot have public subrooms.`); } if (!parent.persist) return this.errorReply(`Temporary rooms cannot be parent rooms.`); if (room === parent) return this.errorReply(`You cannot set a room to be a subroom of itself.`); const settingsList = Rooms.global.settingsList; const parentIndex = settingsList.findIndex(r => r.title === parent.title); const index = settingsList.findIndex(r => r.title === room.title); // Ensure that the parent room gets loaded before the subroom. if (parentIndex > index) { [settingsList[parentIndex], settingsList[index]] = [settingsList[index], settingsList[parentIndex]]; } room.setParent(parent); this.modlog('SUBROOM', null, `of ${parent.title}`); return this.addModAction(`This room was set as a subroom of ${parent.title} by ${user.name}.`); }, removesubroom: 'unsubroom', desubroom: 'unsubroom', unsubroom(target, room, user) { room = this.requireRoom(); this.checkCan('makeroom'); if (!room.parent || !room.persist) { return this.errorReply(`This room is not currently a subroom of a public room.`); } room.setParent(null); this.modlog('UNSUBROOM'); return this.addModAction(`This room was unset as a subroom by ${user.name}.`); }, unsubroomhelp: [`/unsubroom - Unmarks the current room as a subroom. Requires: ~`], parentroom: 'subrooms', subrooms(target, room, user, connection, cmd) { room = this.requireRoom(); if (cmd === 'parentroom') { if (!room.parent) return this.errorReply(`This room is not a subroom.`); return this.sendReply(`This is a subroom of ${room.parent.title}.`); } if (!room.persist) return this.errorReply(`Temporary rooms cannot have subrooms.`); if (!this.runBroadcast()) return; const showSecret = !this.broadcasting && user.can('mute', null, room); const subRooms = room.getSubRooms(showSecret); if (!subRooms.length) return this.sendReply(`This room doesn't have any subrooms.`); const subRoomText = subRooms.map( subRoom => Utils.html`${subRoom.title}
${subRoom.settings.desc}` ); return this.sendReplyBox(`

${Utils.escapeHTML(room.title)}'s subroom${Chat.plural(subRooms)}:

`); }, subroomhelp: [ `/subroom [room] - Marks the current room as a subroom of [room]. Requires: ~`, `/unsubroom - Unmarks the current room as a subroom. Requires: ~`, `/subrooms - Displays the current room's subrooms.`, `/parentroom - Displays the current room's parent room.`, ], roomdesc(target, room, user) { room = this.requireRoom(); if (!target) { if (!this.runBroadcast()) return; if (!room.settings.desc) return this.sendReply(`This room does not have a description set.`); this.sendReplyBox(Utils.html`The room description is: ${room.settings.desc}`); return; } this.checkCan('makeroom'); if (target.length > 80) { return this.errorReply(`Error: Room description is too long (must be at most 80 characters).`); } const normalizedTarget = ' ' + target.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + ' '; if (normalizedTarget.includes(' welcome ')) { return this.errorReply(`Error: Room description must not contain the word "welcome".`); } if (normalizedTarget.startsWith(' discuss ')) { return this.errorReply(`Error: Room description must not start with the word "discuss".`); } if (normalizedTarget.startsWith(' talk about ') || normalizedTarget.startsWith(' talk here about ')) { return this.errorReply(`Error: Room description must not start with the phrase "talk about".`); } room.settings.desc = target; this.sendReply(`(The room description is now: ${target})`); this.privateModAction(`${user.name} changed the roomdesc to: "${target}".`); this.modlog('ROOMDESC', null, `to "${target}"`); room.saveSettings(); }, roomdeschelp: [`/roomdesc [description] - Sets the [description] of the current room. Requires: ~`], topic: 'roomintro', roomintro(target, room, user, connection, cmd) { room = this.requireRoom(); if (!target) { if (!this.runBroadcast()) return; if (!room.settings.introMessage) return this.sendReply("This room does not have an introduction set."); this.sendReply('|raw|
' + room.settings.introMessage.replace(/\n/g, '') + '
'); if (!this.broadcasting && user.can('declare', null, room, 'roomintro') && cmd !== 'topic') { const code = Utils.escapeHTML(room.settings.introMessage).replace(/\n/g, '
'); this.sendReplyBox(`
Source:/roomintro ${code}
`); } return; } this.checkCan('editroom', null, room); if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deleteroomintro"?'); this.checkHTML(target); if (!target.includes("<")) { // not HTML, do some simple URL linking const re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g; target = target.replace(re, '$1'); } if (target.substr(0, 11) === '/roomintro ') target = target.substr(11); room.settings.introMessage = target.replace(/\r/g, ''); this.sendReply("(The room introduction has been changed to:)"); this.sendReply(`|raw|
${room.settings.introMessage.replace(/\n/g, '')}
`); this.privateModAction(`${user.name} changed the roomintro.`); this.modlog('ROOMINTRO'); this.roomlog(room.settings.introMessage.replace(/\n/g, '')); room.saveSettings(); }, roomintrohelp: [ `/roomintro - Display the room introduction of the current room.`, `/roomintro [content] - Set an introduction for the room. Requires: # ~`, ], deletetopic: 'deleteroomintro', deleteroomintro(target, room, user) { room = this.requireRoom(); this.checkCan('declare', null, room); if (!room.settings.introMessage) return this.errorReply("This room does not have a introduction set."); this.privateModAction(`${user.name} deleted the roomintro.`); this.modlog('DELETEROOMINTRO'); this.roomlog(target); delete room.settings.introMessage; room.saveSettings(); }, deleteroomintrohelp: [`/deleteroomintro - Deletes the current room's introduction. Requires: # ~`], stafftopic: 'staffintro', staffintro(target, room, user, connection, cmd) { room = this.requireRoom(); if (!target) { this.checkCan('mute', null, room); if (!room.settings.staffMessage) return this.sendReply("This room does not have a staff introduction set."); this.sendReply(`|raw|
${room.settings.staffMessage.replace(/\n/g, ``)}
`); if (user.can('ban', null, room, 'staffintro') && cmd !== 'stafftopic') { const code = Utils.escapeHTML(room.settings.staffMessage).replace(/\n/g, '
'); this.sendReplyBox(`
Source:/staffintro ${code}
`); } return; } this.checkCan('ban', null, room); this.checkChat(); if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deletestaffintro"?'); this.checkHTML(target); if (!target.includes("<")) { // not HTML, do some simple URL linking const re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g; target = target.replace(re, '$1'); } if (target.substr(0, 12) === '/staffintro ') target = target.substr(12); room.settings.staffMessage = target.replace(/\r/g, ''); this.sendReply("(The staff introduction has been changed to:)"); this.sendReply(`|raw|
${target.replace(/\n/g, ``)}
`); this.privateModAction(`${user.name} changed the staffintro.`); this.modlog('STAFFINTRO'); this.roomlog(room.settings.staffMessage.replace(/\n/g, ``)); room.saveSettings(); }, staffintrohelp: [`/staffintro [content] - Set an introduction for staff members. Requires: @ # ~`], deletestafftopic: 'deletestaffintro', deletestaffintro(target, room, user) { room = this.requireRoom(); this.checkCan('ban', null, room); if (!room.settings.staffMessage) return this.errorReply("This room does not have a staff introduction set."); this.privateModAction(`${user.name} deleted the staffintro.`); this.modlog('DELETESTAFFINTRO'); this.roomlog(target); delete room.settings.staffMessage; room.saveSettings(); }, deletestaffintrohelp: [`/deletestaffintro - Deletes the current room's staff introduction. Requires: @ # ~`], roomalias(target, room, user) { room = this.requireRoom(); if (!target) { if (!this.runBroadcast()) return; if (!room.settings.aliases) return this.sendReplyBox("This room does not have any aliases."); return this.sendReplyBox(`This room has the following aliases: ${room.settings.aliases.join(", ")}`); } this.checkCan('makeroom'); if (target.includes(',')) { this.errorReply(`Invalid room alias: ${target.trim()}`); return this.parse('/help roomalias'); } const alias = toID(target); if (!alias.length) return this.errorReply("Only alphanumeric characters are valid in an alias."); if (Rooms.get(alias) || Rooms.aliases.has(alias)) { return this.errorReply("You cannot set an alias to an existing room or alias."); } if (room.settings.isPersonal) return this.errorReply("Personal rooms can't have aliases."); Rooms.aliases.set(alias, room.roomid); this.privateModAction(`${user.name} added the room alias '${alias}'.`); this.modlog('ROOMALIAS', null, alias); if (!room.settings.aliases) room.settings.aliases = []; room.settings.aliases.push(alias); room.saveSettings(); }, roomaliashelp: [ `/roomalias - displays a list of all room aliases of the room the command was entered in.`, `/roomalias [alias] - adds the given room alias to the room the command was entered in. Requires: ~`, `/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: ~`, ], deleteroomalias: 'removeroomalias', deroomalias: 'removeroomalias', unroomalias: 'removeroomalias', removeroomalias(target, room, user) { room = this.requireRoom(); if (!room.settings.aliases) return this.errorReply("This room does not have any aliases."); this.checkCan('makeroom'); if (target.includes(',')) { this.errorReply(`Invalid room alias: ${target.trim()}`); return this.parse('/help removeroomalias'); } const alias = toID(target); if (!alias || !Rooms.aliases.has(alias)) return this.errorReply("Please specify an existing alias."); if (Rooms.aliases.get(alias) !== room.roomid) { return this.errorReply("You may only remove an alias from the current room."); } this.privateModAction(`${user.name} removed the room alias '${alias}'.`); this.modlog('REMOVEALIAS', null, alias); const aliasIndex = room.settings.aliases.indexOf(alias); if (aliasIndex >= 0) { room.settings.aliases.splice(aliasIndex, 1); if (!room.settings.aliases.length) room.settings.aliases = undefined; Rooms.aliases.delete(alias); room.saveSettings(); } }, removeroomaliashelp: [ `/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: ~`, ], resettierdisplay: 'roomtierdisplay', roomtierdisplay(target, room, user, connection, cmd) { room = this.requireRoom(); const resetTier = cmd === 'resettierdisplay'; if (!target) { if (!this.runBroadcast()) return; return this.sendReplyBox( `This room is currently displaying ${room.settings.dataCommandTierDisplay} as the tier when using /data.` ); } this.checkCan('declare', null, room); const displayIDToName: { [k: string]: Room['settings']['dataCommandTierDisplay'] } = { tiers: 'tiers', doublestiers: 'doubles tiers', nationaldextiers: 'National Dex tiers', numbers: 'numbers', }; if (!resetTier) { if (!(toID(target) in displayIDToName)) { this.errorReply(`Invalid tier display: ${target.trim()}`); return this.parse(`/help roomtierdisplay`); } room.settings.dataCommandTierDisplay = displayIDToName[toID(target)]; this.sendReply(`(The room's tier display is now: ${displayIDToName[toID(target)]})`); this.privateModAction(`${user.name} changed the room's tier display to: ${displayIDToName[toID(target)]}.`); this.modlog('ROOMTIERDISPLAY', null, `to ${displayIDToName[toID(target)]}`); } else { room.settings.dataCommandTierDisplay = 'tiers'; this.sendReply(`(The room's tier display is now: tiers)`); this.privateModAction(`${user.name} reset the room's tier display.`); this.modlog('RESETTIERDISPLAY', null, `to tiers`); } room.saveSettings(); }, roomtierdisplayhelp: [ `/roomtierdisplay - displays the current room's display.`, `/roomtierdisplay [option] - changes the current room's tier display. Valid options are: tiers, doubles tiers, numbers. Requires: # ~`, `/resettierdisplay - resets the current room's tier display. Requires: # ~`, ], setroomsection: 'roomsection', roomsection(target, room, user) { room = this.requireRoom(); const sectionNames = RoomSections.sectionNames; if (!target) { if (!this.runBroadcast()) return; this.sendReplyBox(Utils.html`This room is ${room.settings.section ? `in the ${sectionNames[room.settings.section]} section` : `not in a section`}.`); return; } this.checkCan('gdeclare'); const section = room.setSection(target); this.sendReply(`The room section is now: ${section ? sectionNames[section] : 'none'}`); this.privateGlobalModAction(`${user.name} changed the room section of ${room.title} to ${section ? sectionNames[section] : 'none'}.`); this.globalModlog('ROOMSECTION', null, section || 'none'); }, roomsectionhelp: [ `/roomsection [section] - Sets the room this is used in to the specified [section]. Requires: ~`, `Valid sections: ${sections.join(', ')}`, ], roomdefaultformat(target, room, user) { room = this.requireRoom(); this.checkCan('editroom', null, room); if (!target) { this.checkBroadcast(); if (room.settings.defaultFormat) { this.sendReply(`This room's default format is ${room.settings.defaultFormat}.`); } else { this.sendReply(`This room has no default format.`); } return; } if (this.meansNo(target)) { delete room.settings.defaultFormat; room.saveSettings(); this.modlog(`DEFAULTFORMAT`, null, 'off'); this.privateModAction(`${user.name} removed this room's default format.`); return; } target = toID(target); const format = Dex.formats.get(target); if (format.effectType === 'Format') { target = format.name; } const { isMatch } = this.extractFormat(target); if (!isMatch) throw new Chat.ErrorMessage(`Unrecognized format or mod "${target}"`); room.settings.defaultFormat = target; room.saveSettings(); this.modlog(`DEFAULTFORMAT`, null, target); this.privateModAction(`${user.name} set this room's default format to ${target}.`); }, roomdefaultformathelp: [ `/roomdefaultformat [format] or [mod] or gen[number] - Sets this room's default format/mod. Requires: # ~`, `/roomdefaultformat off - Clears this room's default format/mod. Requires: # ~`, `Affected commands: /details, /coverage, /effectiveness, /weakness, /learn`, ], }; export const roomSettings: Chat.SettingsHandler[] = [ // modchat (room, user) => ({ label: "Modchat", permission: 'modchat', options: [ 'off', 'autoconfirmed', 'trusted', ...RANKS.slice(1).filter(symbol => Users.Auth.hasPermission(user, 'modchat', symbol, room)), ].map(rank => [rank, rank === (room.settings.modchat || 'off') || `modchat ${rank || 'off'}`]), }), (room, user) => ({ label: "Modjoin", permission: room.settings.isPersonal ? user.can('editroom', null, room) : user.can('makeroom'), options: [ 'off', 'autoconfirmed', // groupchat ROs can set modjoin, but only to + // first rank is for modjoin off ...RANKS.slice(1, room.settings.isPersonal && !user.can('makeroom') ? 2 : undefined), ].map(rank => [rank, rank === (room.settings.modjoin || 'off') || `modjoin ${rank || 'off'}`]), }), room => ({ label: "Language", permission: 'editroom', options: [...Chat.languages].map( ([id, name]) => [name, id === (room.settings.language || 'english') || `roomlanguage ${id || 'off'}`] ), }), room => ({ label: "Stretch filter", permission: 'editroom', options: [ [`off`, !room.settings.filterStretching || 'stretchfilter off'], [`on`, room.settings.filterStretching || 'stretchfilter on'], ], }), room => ({ label: "Caps filter", permission: 'editroom', options: [ [`off`, !room.settings.filterCaps || 'capsfilter off'], [`on`, room.settings.filterCaps || 'capsfilter on'], ], }), room => ({ label: "Emoji filter", permission: 'editroom', options: [ [`off`, !room.settings.filterEmojis || 'emojifilter off'], [`on`, room.settings.filterEmojis || 'emojifilter on'], ], }), room => ({ label: "Link filter", permission: 'editroom', options: [ [`off`, !room.settings.filterLinks || 'linkfilter off'], [`on`, room.settings.filterLinks || 'linkfilter on'], ], }), room => ({ label: "Slowchat", permission: room.userCount < SLOWCHAT_USER_REQUIREMENT ? 'bypassall' as any : 'editroom', options: ['off', 5, 10, 20, 30, 60].map( time => [`${time}`, time === (room.settings.slowchat || 'off') || `slowchat ${time || 'off'}`] ), }), room => ({ label: "/data Tier display", permission: 'editroom', options: [ [`tiers`, (room.settings.dataCommandTierDisplay ?? 'tiers') === 'tiers' || `roomtierdisplay tiers`], [`doubles tiers`, room.settings.dataCommandTierDisplay === 'doubles tiers' || `roomtierdisplay doubles tiers`], [ `National Dex tiers`, room.settings.dataCommandTierDisplay === 'National Dex tiers' || `roomtierdisplay National Dex tiers`, ], [`numbers`, room.settings.dataCommandTierDisplay === 'numbers' || `roomtierdisplay numbers`], ], }), room => ({ label: "/requestshow", permission: 'declare', options: [ [`off`, !room.settings.requestShowEnabled || `showapprovals off`], [`on`, room.settings.requestShowEnabled || `showapprovals on`], ], }), ]; export const pages: Chat.PageTable = { permissions(args, user, connection) { this.title = `[Permissions]`; const room = this.requireRoom(); this.checkCan('mute', null, room); const roomGroups = ['default', 'all users', ...Config.groupsranking.slice(1)]; const permissions = room.settings.permissions || {}; let buf = `

Command permissions for ${room.title}

`; buf += `
`; buf += ``; let atLeastOne = false; for (const permission in permissions) { const requiredRank = permissions[permission]; atLeastOne = true; buf += ``; } if (!atLeastOne) { buf += ``; } buf += `
PermissionRequired rank
${permission}`; if (room.auth.atLeast(user, '#')) { buf += roomGroups.map(group => ( requiredRank === group ? Utils.html`` : Utils.html`` )).join(' '); } else { buf += Utils.html``; } buf += `
You don't have any permissions configured.
`; buf += `

Use /permissions to add new permissions

`; return buf; }, };