{
"version": 3,
"sources": ["../../../../server/chat-commands/moderation.ts"],
"sourcesContent": ["/**\r\n * Moderation commands\r\n * Pokemon Showdown - http://pokemonshowdown.com/\r\n *\r\n * These are commands for staff.\r\n *\r\n * For the API, see chat-plugins/COMMANDS.md\r\n *\r\n * @license MIT\r\n */\r\nimport {Utils} from '../../lib';\r\nimport {RoomSection, RoomSections} from './room-settings';\r\n\r\n/* eslint no-else-return: \"error\" */\r\n\r\nconst MAX_REASON_LENGTH = 600;\r\nconst MUTE_LENGTH = 7 * 60 * 1000;\r\nconst HOURMUTE_LENGTH = 60 * 60 * 1000;\r\nconst DAY = 24 * 60 * 60 * 1000;\r\n\r\n/** Require reasons for punishment commands */\r\nconst REQUIRE_REASONS = true;\r\n\r\n/**\r\n * Promotes a user within a room. Returns a User object if a popup should be shown to the user,\r\n * and null otherwise. Throws a Chat.ErrorMesage on an error.\r\n *\r\n * @param promoter the User object of the user who is promoting\r\n * @param room the Room in which the promotion is happening\r\n * @param userid the ID of the user to promote\r\n * @param symbol the GroupSymbol to promote to\r\n * @param username the username of the user to promote\r\n * @param force whether or not to forcibly promote\r\n */\r\nexport function runPromote(\r\n\tpromoter: User,\r\n\troom: Room,\r\n\tuserid: ID,\r\n\tsymbol: GroupSymbol,\r\n\tusername?: string,\r\n\tforce?: boolean\r\n) {\r\n\tconst targetUser = Users.getExact(userid);\r\n\tusername = username || userid;\r\n\tif (!username) return;\r\n\r\n\tif (userid.length > 18) {\r\n\t\tthrow new Chat.ErrorMessage(`User '${username}' does not exist (the username is too long).`);\r\n\t}\r\n\tif (!targetUser && !Users.isUsernameKnown(userid) && !force) {\r\n\t\tthrow new Chat.ErrorMessage(`User '${username}' is offline and unrecognized, and so can't be promoted.`);\r\n\t}\r\n\tif (targetUser && !targetUser.registered) {\r\n\t\tthrow new Chat.ErrorMessage(`User '${username}' is unregistered, and so can't be promoted.`);\r\n\t}\r\n\r\n\tlet currentSymbol: GroupSymbol | 'whitelist' = room.auth.getDirect(userid);\r\n\tif (room.auth.has(userid) && currentSymbol === Users.Auth.defaultSymbol()) {\r\n\t\tcurrentSymbol = 'whitelist';\r\n\t}\r\n\tconst currentGroup = Users.Auth.getGroup(currentSymbol);\r\n\tconst currentGroupName = currentGroup.name || \"regular user\";\r\n\r\n\tconst nextGroup = Config.groups[symbol];\r\n\r\n\tif (currentSymbol === symbol) {\r\n\t\tthrow new Chat.ErrorMessage(`User '${username}' is already a ${nextGroup?.name || symbol || 'regular user'} in this room.`);\r\n\t}\r\n\tif (!promoter.can('makeroom')) {\r\n\t\tif (currentGroup.id && !promoter.can(`room${currentGroup.id || 'voice'}` as 'roomvoice', null, room)) {\r\n\t\t\tthrow new Chat.ErrorMessage(`Access denied for promoting/demoting ${username} from ${currentGroupName}.`);\r\n\t\t}\r\n\t\tif (symbol !== ' ' && !promoter.can(`room${nextGroup.id || 'voice'}` as 'roomvoice', null, room)) {\r\n\t\t\tthrow new Chat.ErrorMessage(`Access denied for promoting/demoting ${username} to ${nextGroup.name}.`);\r\n\t\t}\r\n\t}\r\n\tif (targetUser?.locked && room.persist && room.settings.isPrivate !== true && nextGroup.rank > 2) {\r\n\t\tthrow new Chat.ErrorMessage(`${username} is locked and can't be promoted.`);\r\n\t}\r\n\r\n\tif (symbol === Users.Auth.defaultSymbol()) {\r\n\t\troom.auth.delete(userid);\r\n\t} else {\r\n\t\troom.auth.set(userid, symbol);\r\n\t}\r\n\r\n\tif (targetUser) {\r\n\t\ttargetUser.updateIdentity(room.roomid);\r\n\t\tif (room.subRooms) {\r\n\t\t\tfor (const subRoom of room.subRooms.values()) {\r\n\t\t\t\ttargetUser.updateIdentity(subRoom.roomid);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Only show popup if: user is online and in the room, the room is public, and not a groupchat or a battle.\r\n\tif (targetUser && room.users[targetUser.id] && room.persist && room.settings.isPrivate !== true) {\r\n\t\treturn targetUser;\r\n\t}\r\n\treturn null;\r\n}\r\n\r\nexport function runCrisisDemote(userid: ID) {\r\n\tconst from = [];\r\n\tconst section = Users.globalAuth.sectionLeaders.get(userid);\r\n\tif (section) {\r\n\t\tfrom.push(`Section Leader (${RoomSections.sectionNames[section] || section})`);\r\n\t\tUsers.globalAuth.deleteSection(userid);\r\n\t}\r\n\tconst globalGroup = Users.globalAuth.get(userid);\r\n\tif (globalGroup && globalGroup !== ' ') {\r\n\t\tfrom.push(globalGroup);\r\n\t\tUsers.globalAuth.delete(userid);\r\n\t}\r\n\tfor (const room of Rooms.global.chatRooms) {\r\n\t\tif (!room.settings.isPrivate && room.auth.isStaff(userid)) {\r\n\t\t\tlet oldGroup: string = room.auth.getDirect(userid);\r\n\t\t\tif (oldGroup === ' ') {\r\n\t\t\t\toldGroup = 'whitelist in ';\r\n\t\t\t} else {\r\n\t\t\t\troom.auth.set(userid, '+');\r\n\t\t\t}\r\n\t\t\tfrom.push(`${oldGroup}${room.roomid}`);\r\n\t\t}\r\n\t}\r\n\treturn from;\r\n}\r\n\r\nPunishments.addPunishmentType({\r\n\ttype: 'YEARLOCK',\r\n\tdesc: \"Locked for a year\",\r\n\tonActivate: (user, punishment) => {\r\n\t\tuser.locked = user.id;\r\n\t\tChat.punishmentfilter(user, punishment);\r\n\t},\r\n});\r\n\r\nexport const commands: Chat.ChatCommands = {\r\n\troomowner(target, room, user) {\r\n\t\troom = this.requireRoom();\r\n\t\tif (!room.persist) {\r\n\t\t\treturn this.sendReply(\"/roomowner - This room isn't designed for per-room moderation to be added\");\r\n\t\t}\r\n\t\tif (!target) return this.parse('/help roomowner');\r\n\t\tconst {targetUser, targetUsername, rest} = this.splitUser(target, {exactName: true});\r\n\t\tif (rest) return this.errorReply(`This command does not support specifying a reason.`);\r\n\t\tconst userid = toID(targetUsername);\r\n\r\n\t\tif (!Users.isUsernameKnown(userid)) {\r\n\t\t\treturn this.errorReply(`User '${targetUsername}' is offline and unrecognized, and so can't be promoted.`);\r\n\t\t}\r\n\r\n\t\tthis.checkCan('makeroom');\r\n\t\tif (room.auth.getDirect(userid) === '#') return this.errorReply(`${targetUsername} is already a room owner.`);\r\n\r\n\t\troom.auth.set(userid, '#');\r\n\t\tconst message = `${targetUsername} was appointed Room Owner by ${user.name}.`;\r\n\t\tif (room.settings.isPrivate === true) {\r\n\t\t\tthis.addModAction(message);\r\n\t\t\tRooms.get(`upperstaff`)?.addByUser(user, `<<${room.roomid}>> ${message}`).update();\r\n\t\t} else {\r\n\t\t\tthis.addGlobalModAction(message);\r\n\t\t}\r\n\t\tthis.modlog('ROOMOWNER', userid);\r\n\t\tif (targetUser) {\r\n\t\t\ttargetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.roomid}.`);\r\n\t\t\troom.onUpdateIdentity(targetUser);\r\n\t\t\tif (room.subRooms) {\r\n\t\t\t\tfor (const subRoom of room.subRooms.values()) {\r\n\t\t\t\t\tsubRoom.onUpdateIdentity(targetUser);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\troom.saveSettings();\r\n\t},\r\n\troomownerhelp: [`/roomowner [username] - Appoints [username] as a room owner. Requires: &`],\r\n\r\n\troomdemote: 'roompromote',\r\n\tforceroompromote: 'roompromote',\r\n\tforceroomdemote: 'roompromote',\r\n\troompromote(target, room, user, connection, cmd) {\r\n\t\tif (!room) {\r\n\t\t\t// this command isn't marked as room-only because it's usable in PMs through /invite\r\n\t\t\treturn this.errorReply(\"This command is only available in rooms\");\r\n\t\t}\r\n\t\tthis.checkChat();\r\n\t\tif (!target) return this.parse('/help roompromote');\r\n\r\n\t\tconst force = cmd.startsWith('force');\r\n\t\tconst users = target.split(',').map(part => part.trim());\r\n\t\tlet nextSymbol = users.pop() as GroupSymbol | 'deauth';\r\n\t\tif (nextSymbol === 'deauth') nextSymbol = Users.Auth.defaultSymbol();\r\n\t\tconst nextGroup = Users.Auth.getGroup(nextSymbol);\r\n\r\n\t\tif (!nextSymbol) {\r\n\t\t\treturn this.errorReply(\"Please specify a group such as /roomvoice or /roomdeauth\");\r\n\t\t}\r\n\t\tif (!Config.groups[nextSymbol]) {\r\n\t\t\tif (!force || !user.can('bypassall')) {\r\n\t\t\t\tthis.errorReply(`Group '${nextSymbol}' does not exist.`);\r\n\t\t\t\tif (user.can('bypassall')) {\r\n\t\t\t\t\tthis.errorReply(`If you want to promote to a nonexistent group, use /forceroompromote`);\r\n\t\t\t\t}\r\n\t\t\t\treturn;\r\n\t\t\t} else if (!Users.Auth.isValidSymbol(nextSymbol)) {\r\n\t\t\t\t// yes I know this excludes astral-plane characters and includes combining characters\r\n\t\t\t\treturn this.errorReply(`Admins can forcepromote to nonexistent groups only if they are one character long`);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (!force && (nextGroup.globalonly || (nextGroup.battleonly && !room.battle))) {\r\n\t\t\treturn this.errorReply(`Group 'room${nextGroup.id || nextSymbol}' does not exist as a room rank.`);\r\n\t\t}\r\n\t\tconst nextGroupName = nextGroup.name || \"regular user\";\r\n\r\n\t\tfor (const toPromote of users) {\r\n\t\t\tconst userid = toID(toPromote);\r\n\t\t\tif (!userid) return this.parse('/help roompromote');\r\n\r\n\t\t\t// weird ts bug (?) - 7022\r\n\t\t\t// it implicitly is 'any' because it has no annotation and is \"is referenced directly or indirectly in its own initializer.\"\r\n\t\t\t// dunno why this happens, but for now we can just cast over it.\r\n\t\t\tconst oldSymbol: GroupSymbol = room.auth.getDirect(userid);\r\n\t\t\tlet shouldPopup;\r\n\t\t\ttry {\r\n\t\t\t\tshouldPopup = runPromote(user, room, userid, nextSymbol, toPromote, force);\r\n\t\t\t} catch (err: any) {\r\n\t\t\t\tif (err.name?.endsWith('ErrorMessage')) {\r\n\t\t\t\t\tthis.errorReply(err.message);\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\t\t\t\tthrow err;\r\n\t\t\t}\r\n\t\t\tconst targetUser = Users.getExact(userid);\r\n\t\t\tconst name = targetUser?.name || toPromote;\r\n\r\n\t\t\tif (this.pmTarget && targetUser) {\r\n\t\t\t\tconst text = `${targetUser.name} was invited (and promoted to Room ${nextGroupName}) by ${user.name}.`;\r\n\t\t\t\troom.add(`|c|${user.getIdentity(room)}|/log ${text}`).update();\r\n\t\t\t\tthis.modlog('INVITE', targetUser, null, {noip: 1, noalts: 1});\r\n\t\t\t} else if (\r\n\t\t\t\tnextSymbol in Config.groups && oldSymbol in Config.groups &&\r\n\t\t\t\tnextGroup.rank < Config.groups[oldSymbol].rank\r\n\t\t\t) {\r\n\t\t\t\tif (targetUser && room.users[targetUser.id] && !nextGroup.modlog) {\r\n\t\t\t\t\t// if the user can't see the demotion message (i.e. rank < %), it is shown in the chat\r\n\t\t\t\t\ttargetUser.send(`>${room.roomid}\\n(You were demoted to Room ${nextGroupName} by ${user.name}.)`);\r\n\t\t\t\t}\r\n\t\t\t\tthis.privateModAction(`${name} was demoted to Room ${nextGroupName} by ${user.name}.`);\r\n\t\t\t\tthis.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid, '(demote)');\r\n\t\t\t\tshouldPopup?.popup(`You were demoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);\r\n\t\t\t} else if (nextSymbol === '#') {\r\n\t\t\t\tthis.addModAction(`${name} was promoted to ${nextGroupName} by ${user.name}.`);\r\n\t\t\t\tconst logRoom = Rooms.get(room.settings.isPrivate === true ? 'upperstaff' : 'staff');\r\n\t\t\t\tlogRoom?.addByUser(user, `<<${room.roomid}>> ${name} was appointed Room Owner by ${user.name}`);\r\n\t\t\t\tthis.modlog('ROOM OWNER', userid);\r\n\t\t\t\tshouldPopup?.popup(`You were promoted to ${nextGroupName} by ${user.name} in ${room.roomid}.`);\r\n\t\t\t} else {\r\n\t\t\t\tthis.addModAction(`${name} was promoted to Room ${nextGroupName} by ${user.name}.`);\r\n\t\t\t\tthis.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid);\r\n\t\t\t\tshouldPopup?.popup(`You were promoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);\r\n\t\t\t}\r\n\r\n\t\t\tif (targetUser) {\r\n\t\t\t\ttargetUser.updateIdentity(room.roomid);\r\n\t\t\t\tif (room.subRooms) {\r\n\t\t\t\t\tfor (const subRoom of room.subRooms.values()) {\r\n\t\t\t\t\t\ttargetUser.updateIdentity(subRoom.roomid);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tif (targetUser.trusted && !Users.isTrusted(targetUser.id)) {\r\n\t\t\t\t\ttargetUser.trusted = '';\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\troom.saveSettings();\r\n\t},\r\n\troompromotehelp: [\r\n\t\t`/roompromote OR /roomdemote [comma-separated usernames], [group symbol] - Promotes/demotes the user(s) to the specified room rank. Requires: @ # &`,\r\n\t\t`/room[group] [comma-separated usernames] - Promotes/demotes the user(s) to the specified room rank. Requires: @ # &`,\r\n\t\t`/roomdeauth [comma-separated usernames] - Removes all room rank from the user(s). Requires: @ # &`,\r\n\t],\r\n\r\n\tauth: 'authority',\r\n\tstafflist: 'authority',\r\n\tglobalauth: 'authority',\r\n\tauthlist: 'authority',\r\n\tauthority(target, room, user, connection) {\r\n\t\tif (target && target !== '+') {\r\n\t\t\tconst targetRoom = Rooms.search(target);\r\n\t\t\tconst availableRoom = targetRoom?.checkModjoin(user);\r\n\t\t\tif (targetRoom && availableRoom) return this.parse(`/roomauth1 ${target}`);\r\n\t\t\treturn this.parse(`/userauth ${target}`);\r\n\t\t}\r\n\t\tconst showAll = !!target;\r\n\t\tconst rankLists: {[k: string]: string[]} = {};\r\n\t\tfor (const [id, symbol] of Users.globalAuth) {\r\n\t\t\tif (symbol === ' ' || (symbol === '+' && !showAll)) continue;\r\n\t\t\tif (!rankLists[symbol]) rankLists[symbol] = [];\r\n\t\t\trankLists[symbol].push(Users.globalAuth.usernames.get(id) || id);\r\n\t\t}\r\n\r\n\t\tconst buffer = Utils.sortBy(\r\n\t\t\tObject.entries(rankLists) as [GroupSymbol, string[]][],\r\n\t\t\t([symbol]) => -Users.Auth.getGroup(symbol).rank\r\n\t\t).filter(\r\n\t\t\t([symbol]) => symbol !== Users.SECTIONLEADER_SYMBOL\r\n\t\t).map(\r\n\t\t\t([symbol, names]) => (\r\n\t\t\t\t`${(Config.groups[symbol] ? `**${Config.groups[symbol].name}s** (${symbol})` : symbol)}:\\n` +\r\n\t\t\t\tUtils.sortBy(names, name => toID(name)).join(\", \")\r\n\t\t\t)\r\n\t\t);\r\n\t\tif (!showAll) buffer.push(`(Use \\`\\`/auth +\\`\\` to show global voice users.)`);\r\n\r\n\t\tif (!buffer.length) return connection.popup(\"This server has no global authority.\");\r\n\t\tconnection.popup(buffer.join(\"\\n\\n\"));\r\n\t},\r\n\tauthhelp: [\r\n\t\t`/auth - Show global staff for the server.`,\r\n\t\t`/auth + - Show global staff for the server, including voices.`,\r\n\t\t`/auth [room] - Show what roomauth a room has.`,\r\n\t\t`/auth [user] - Show what global and roomauth a user has.`,\r\n\t],\r\n\r\n\troomstaff: 'roomauth',\r\n\troomauth1: 'roomauth',\r\n\troomauth(target, room, user, connection, cmd) {\r\n\t\tlet userLookup = '';\r\n\t\tif (cmd === 'roomauth1') userLookup = `\\n\\nTo look up auth for a user, use /userauth ${target}`;\r\n\t\tlet targetRoom = room;\r\n\t\tif (target) targetRoom = Rooms.search(target)!;\r\n\t\tif (!targetRoom?.checkModjoin(user)) {\r\n\t\t\treturn this.errorReply(`The room \"${target}\" does not exist.`);\r\n\t\t}\r\n\t\tconst showAll = user.can('mute', null, targetRoom);\r\n\r\n\t\tconst rankLists: {[groupSymbol: string]: ID[]} = {};\r\n\t\tfor (const [id, rank] of targetRoom.auth) {\r\n\t\t\tif (rank === ' ' && !showAll) continue;\r\n\t\t\tif (!rankLists[rank]) rankLists[rank] = [];\r\n\t\t\trankLists[rank].push(id);\r\n\t\t}\r\n\r\n\t\tconst buffer = Utils.sortBy(\r\n\t\t\tObject.entries(rankLists) as [GroupSymbol, ID[]][],\r\n\t\t\t([symbol]) => -Users.Auth.getGroup(symbol).rank\r\n\t\t).map(([symbol, names]) => {\r\n\t\t\tlet group = Config.groups[symbol] ? `${Config.groups[symbol].name}s (${symbol})` : symbol;\r\n\t\t\tif (symbol === ' ') group = 'Whitelisted (this list is only visible to staff)';\r\n\t\t\treturn `${group}:\\n` +\r\n\t\t\t\tUtils.sortBy(names).map(userid => {\r\n\t\t\t\t\tconst isOnline = Users.get(userid)?.statusType === 'online';\r\n\t\t\t\t\t// targetRoom guaranteed to exist above\r\n\t\t\t\t\treturn userid in targetRoom!.users && isOnline ? `**${userid}**` : userid;\r\n\t\t\t\t}).join(', ');\r\n\t\t});\r\n\r\n\t\tlet curRoom = targetRoom;\r\n\t\twhile (curRoom.parent) {\r\n\t\t\tconst modjoinSetting = curRoom.settings.modjoin === true ? curRoom.settings.modchat : curRoom.settings.modjoin;\r\n\t\t\tconst roomType = (modjoinSetting ? `modjoin ${modjoinSetting} ` : '');\r\n\t\t\tconst inheritedUserType = (modjoinSetting ? ` of rank ${modjoinSetting} and above` : '');\r\n\t\t\tif (curRoom.parent) {\r\n\t\t\t\tconst also = buffer.length === 0 ? `` : ` also`;\r\n\t\t\t\tbuffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType}${also} have authority in this room.`);\r\n\t\t\t}\r\n\t\t\tcurRoom = curRoom.parent;\r\n\t\t}\r\n\t\tif (!buffer.length) {\r\n\t\t\tconnection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tif (!curRoom.settings.isPrivate) {\r\n\t\t\tbuffer.push(`${curRoom.title} is a public room, so global auth with no relevant roomauth will have authority in this room.`);\r\n\t\t} else if (curRoom.settings.isPrivate === 'hidden' || curRoom.settings.isPrivate === 'voice') {\r\n\t\t\tbuffer.push(`${curRoom.title} is a hidden room, so global auth with no relevant roomauth will have authority in this room.`);\r\n\t\t}\r\n\t\tbuffer.push(`Names in **bold** are online.`);\r\n\t\tif (targetRoom !== room) buffer.unshift(`${targetRoom.title} room auth:`);\r\n\t\tconnection.popup(`${buffer.join(\"\\n\\n\")}${userLookup}`);\r\n\t},\r\n\troomauthhelp: [\r\n\t\t`/roomauth [room] - Shows a list of the staff and authority in the given [room].`,\r\n\t\t`If no room is given, it defaults to the current room.`,\r\n\t],\r\n\r\n\tuserauth(target, room, user, connection) {\r\n\t\tconst targetId = toID(target) || user.id;\r\n\t\tconst targetUser = Users.getExact(targetId);\r\n\t\tconst targetUsername = targetUser?.name || target;\r\n\r\n\t\tconst buffer = [];\r\n\t\tlet innerBuffer = [];\r\n\t\tconst group = Users.globalAuth.get(targetId);\r\n\t\tif (group !== ' ' || Users.isTrusted(targetId)) {\r\n\t\t\tbuffer.push(`Global auth: ${group === ' ' ? 'trusted' : group}`);\r\n\t\t}\r\n\t\tconst sectionLeader = Users.globalAuth.sectionLeaders.get(targetId);\r\n\t\tif (sectionLeader) {\r\n\t\t\tbuffer.push(`Section leader: ${RoomSections.sectionNames[sectionLeader]}`);\r\n\t\t}\r\n\t\tfor (const curRoom of Rooms.rooms.values()) {\r\n\t\t\tif (curRoom.settings.isPrivate) continue;\r\n\t\t\tif (!curRoom.auth.has(targetId)) continue;\r\n\t\t\tinnerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);\r\n\t\t}\r\n\t\tif (innerBuffer.length) {\r\n\t\t\tbuffer.push(`Room auth: ${innerBuffer.join(', ')}`);\r\n\t\t}\r\n\t\tif (targetId === user.id || user.can('lock')) {\r\n\t\t\tinnerBuffer = [];\r\n\t\t\tfor (const curRoom of Rooms.rooms.values()) {\r\n\t\t\t\tif (!curRoom.settings.isPrivate) continue;\r\n\t\t\t\tif (curRoom.settings.isPrivate === true) continue;\r\n\t\t\t\tif (!curRoom.auth.has(targetId)) continue;\r\n\t\t\t\tinnerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);\r\n\t\t\t}\r\n\t\t\tif (innerBuffer.length) {\r\n\t\t\t\tbuffer.push(`Hidden room auth: ${innerBuffer.join(', ')}`);\r\n\t\t\t}\r\n\t\t}\r\n\t\tif (targetId === user.id || user.can('makeroom')) {\r\n\t\t\tinnerBuffer = [];\r\n\t\t\tfor (const chatRoom of Rooms.global.chatRooms) {\r\n\t\t\t\tif (!chatRoom.settings.isPrivate) continue;\r\n\t\t\t\tif (chatRoom.settings.isPrivate !== true) continue;\r\n\t\t\t\tif (!chatRoom.auth.has(targetId)) continue;\r\n\t\t\t\tinnerBuffer.push(chatRoom.auth.getDirect(targetId).trim() + chatRoom.roomid);\r\n\t\t\t}\r\n\t\t\tif (innerBuffer.length) {\r\n\t\t\t\tbuffer.push(`Private room auth: ${innerBuffer.join(', ')}`);\r\n\t\t\t}\r\n\t\t}\r\n\t\tif (!buffer.length) {\r\n\t\t\tbuffer.push(\"No global or room auth.\");\r\n\t\t}\r\n\r\n\t\tbuffer.unshift(`${targetUsername} user auth:`);\r\n\t\tconnection.popup(buffer.join(\"\\n\\n\"));\r\n\t},\r\n\tuserauthhelp: [\r\n\t\t`/userauth [username] - Shows all authority visible to the user for the given [username].`,\r\n\t\t`If no username is given, it defaults to the current user.`,\r\n\t],\r\n\r\n\tsectionleaders(target, room, user, connection) {\r\n\t\tconst usernames = Users.globalAuth.usernames;\r\n\t\tconst buffer = [];\r\n\t\tconst sections: {[k in RoomSection]: Set ${Utils.escapeHTML(user.name)} has banned you from the room ${room.roomid} ` +\r\n\t\t\t\t`${(room.subRooms ? ` and its subrooms` : ``)}${week ? ' for a week' : ''}.` +\r\n\t\t\t\t` Reason: ${Utils.escapeHTML(publicReason)} To appeal the ban, PM the staff member that banned you${room.persist ? ` or a room owner. ` +\r\n\t\t\t\t`
${Utils.escapeHTML(user.name)} has blacklisted you from the room ${room.roomid}${(room.subRooms ? ` and its subrooms` : '')}. Reason: ${Utils.escapeHTML(reason)}
` +\r\n\t\t\t\t`To appeal the ban, PM the staff member that blacklisted you${room.persist ? ` or a room owner.
` : `.`}`\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\tthis.privateModAction(`${name} was blacklisted from ${room.title} by ${user.name}.${reason ? ` (${reason})` : ''}`);\r\n\r\n\t\tconst affected = Punishments.roomBlacklist(room, targetUser, null, null, reason);\r\n\r\n\t\tif (!room.settings.isPrivate && room.persist) {\r\n\t\t\tconst acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);\r\n\t\t\tlet displayMessage = '';\r\n\t\t\tif (affected.length > 1) {\r\n\t\t\t\tdisplayMessage = `${name}'s ${(acAccount ? ` ac account: ${acAccount},` : '')} blacklisted alts: ${affected.slice(1).map(curUser => curUser.getLastName()).join(\", \")}`;\r\n\t\t\t\tthis.privateModAction(displayMessage);\r\n\t\t\t} else if (acAccount) {\r\n\t\t\t\tdisplayMessage = `${name}'s ac account: ${acAccount}`;\r\n\t\t\t\tthis.privateModAction(displayMessage);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (!room.settings.isPrivate && room.persist) {\r\n\t\t\tthis.globalModlog(\"BLACKLIST\", targetUser, reason);\r\n\t\t} else {\r\n\t\t\t// Room modlog only\r\n\t\t\tthis.modlog(\"BLACKLIST\", targetUser, reason);\r\n\t\t}\r\n\t\treturn true;\r\n\t},\r\n\tblacklisthelp: [\r\n\t\t`/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # &`,\r\n\t\t`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # &`,\r\n\t\t`/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # &`,\r\n\t\t`/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # &`,\r\n\t],\r\n\r\n\tforcebattleban: 'battleban',\r\n\tasync battleban(target, room, user, connection, cmd) {\r\n\t\troom = this.requireRoom();\r\n\t\tif (!target) return this.parse(`/help battleban`);\r\n\r\n\t\tconst {targetUser, targetUsername, rest: reason} = this.splitUser(target);\r\n\t\tif (!targetUser) return this.errorReply(`User ${targetUsername} not found.`);\r\n\t\tif (target.length > MAX_REASON_LENGTH) {\r\n\t\t\treturn this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);\r\n\t\t}\r\n\t\tif (!reason) {\r\n\t\t\treturn this.errorReply(`Battle bans require a reason.`);\r\n\t\t}\r\n\t\tconst includesUrl = reason.includes(`.${Config.routes.root}/`); // lgtm [js/incomplete-url-substring-sanitization]\r\n\t\tif (!room.battle && !includesUrl && cmd !== 'forcebattleban') {\r\n\t\t\t return this.errorReply(`Battle bans require a battle replay if used outside of a battle; if the battle has expired, use /forcebattleban.`);\r\n\t\t}\r\n\t\tif (!user.can('rangeban', targetUser)) {\r\n\t\t\tthis.errorReply(`Battlebans have been deprecated. Alternatives:`);\r\n\t\t\tthis.errorReply(`- timerstalling and bragging about it: lock`);\r\n\t\t\tthis.errorReply(`- other timerstalling: they're not timerstalling, leave them alone`);\r\n\t\t\tthis.errorReply(`- bad nicknames: lock, locks prevent nicknames from appearing; you should always have been locking for this`);\r\n\t\t\tthis.errorReply(`- ladder cheating: gban, get a moderator if necessary`);\r\n\t\t\tthis.errorReply(`- serious ladder cheating: permaban, get an administrator`);\r\n\t\t\tthis.errorReply(`- other: get an administrator`);\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tif (Punishments.isBattleBanned(targetUser)) {\r\n\t\t\treturn this.errorReply(`User '${targetUser.name}' is already banned from battling.`);\r\n\t\t}\r\n\t\tthis.privateGlobalModAction(`${targetUser.name} was banned from starting new battles by ${user.name} (${reason})`);\r\n\r\n\t\tif (targetUser.trusted) {\r\n\t\t\tMonitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} was banned from battling by ${user.name}, and should probably be demoted.`);\r\n\t\t}\r\n\r\n\t\tthis.globalModlog(\"BATTLEBAN\", targetUser, reason);\r\n\t\tLadders.cancelSearches(targetUser);\r\n\t\tawait Punishments.battleban(targetUser, null, null, reason);\r\n\t\ttargetUser.popup(`|modal|${user.name} has prevented you from starting new battles for 2 days (${reason})`);\r\n\r\n\t\t// Automatically upload replays as evidence/reference to the punishment\r\n\t\tif (room.battle) this.parse('/savereplay forpunishment');\r\n\t\treturn true;\r\n\t},\r\n\tbattlebanhelp: [\r\n\t\t`/battleban [username], [reason] - [DEPRECATED]`,\r\n\t\t`Prevents the user from starting new battles for 2 days and shows them the [reason]. Requires: &`,\r\n\t],\r\n\r\n\tunbattleban(target, room, user) {\r\n\t\tif (!target) return this.parse('/help unbattleban');\r\n\t\tthis.checkCan('lock');\r\n\r\n\t\tconst targetUser = Users.get(target);\r\n\t\tconst unbanned = Punishments.unbattleban(target);\r\n\r\n\t\tif (unbanned) {\r\n\t\t\tthis.addModAction(`${unbanned} was allowed to battle again by ${user.name}.`);\r\n\t\t\tthis.globalModlog(\"UNBATTLEBAN\", toID(target));\r\n\t\t\tif (targetUser) targetUser.popup(`${user.name} has allowed you to battle again.`);\r\n\t\t} else {\r\n\t\t\tthis.errorReply(`User ${target} is not banned from battling.`);\r\n\t\t}\r\n\t},\r\n\tunbattlebanhelp: [`/unbattleban [username] - [DEPRECATED] Allows a user to battle again. Requires: % @ &`],\r\n\r\n\tmonthgroupchatban: 'groupchatban',\r\n\tmonthgcban: 'groupchatban',\r\n\tgcban: 'groupchatban',\r\n\tasync groupchatban(target, room, user, connection, cmd) {\r\n\t\troom = this.requireRoom();\r\n\t\tif (!target) return this.parse(`/help groupchatban`);\r\n\t\tif (!user.can('rangeban')) {\r\n\t\t\treturn this.errorReply(\r\n\t\t\t\t`/groupchatban has been deprecated.\\n` +\r\n\t\t\t\t`For future groupchat misuse, lock the creator, it will take away their trusted status and their ability to make groupchats.`\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\tconst {targetUser, targetUsername, rest: reason} = this.splitUser(target);\r\n\t\tif (!targetUser) return this.errorReply(`User ${targetUsername} not found.`);\r\n\t\tif (target.length > MAX_REASON_LENGTH) {\r\n\t\t\treturn this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);\r\n\t\t}\r\n\r\n\t\tconst isMonth = cmd.startsWith('month');\r\n\r\n\t\tif (!isMonth && Punishments.isGroupchatBanned(targetUser)) {\r\n\t\t\treturn this.errorReply(`User '${targetUser.name}' is already banned from using groupchats.`);\r\n\t\t}\r\n\r\n\t\tconst reasonText = reason ? `: ${reason}` : ``;\r\n\t\tthis.privateGlobalModAction(`${targetUser.name} was banned from using groupchats for a ${isMonth ? 'month' : 'week'} by ${user.name}${reasonText}.`);\r\n\r\n\t\tif (targetUser.trusted) {\r\n\t\t\tMonitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} was banned from using groupchats by ${user.name}, and should probably be demoted.`);\r\n\t\t}\r\n\r\n\t\tconst createdGroupchats = await Punishments.groupchatBan(\r\n\t\t\ttargetUser, (isMonth ? Date.now() + 30 * DAY : null), null, reason\r\n\t\t);\r\n\t\ttargetUser.popup(`|modal|${user.name} has banned you from using groupchats for a ${isMonth ? 'month' : 'week'}${reasonText}`);\r\n\t\tthis.globalModlog(\"GROUPCHATBAN\", targetUser, ` by ${user.id}${reasonText}`);\r\n\r\n\t\tfor (const roomid of createdGroupchats) {\r\n\t\t\tconst targetRoom = Rooms.get(roomid);\r\n\t\t\tif (!targetRoom) continue;\r\n\t\t\tconst participants = targetRoom.warnParticipants?.(\r\n\t\t\t\t`This groupchat (${targetRoom.title}) has been deleted due to inappropriate conduct by its creator, ${targetUser.name}.` +\r\n\t\t\t\t` Do not attempt to recreate it, or you may be punished.${reason ? ` (reason: ${reason})` : ``}`\r\n\t\t\t);\r\n\r\n\t\t\tif (participants) {\r\n\t\t\t\tconst modlogEntry = {\r\n\t\t\t\t\taction: 'NOTE',\r\n\t\t\t\t\tloggedBy: user.id,\r\n\t\t\t\t\tisGlobal: true,\r\n\t\t\t\t\tnote: `participants in ${roomid} (creator: ${targetUser.id}): ${participants.join(', ')}`,\r\n\t\t\t\t};\r\n\t\t\t\ttargetRoom.modlog(modlogEntry);\r\n\t\t\t}\r\n\r\n\t\t\ttargetRoom.destroy();\r\n\t\t}\r\n\t},\r\n\tgroupchatbanhelp: [\r\n\t\t`/groupchatban [user], [optional reason]`,\r\n\t\t`/monthgroupchatban [user], [optional reason]`,\r\n\t\t`Bans the user from joining or creating groupchats for a week (or month). Requires: % @ &`,\r\n\t],\r\n\r\n\tungcban: 'ungroupchatban',\r\n\tgcunban: 'ungroupchatban',\r\n\tgroucphatunban: 'ungroupchatban',\r\n\tungroupchatban(target, room, user) {\r\n\t\tif (!target) return this.parse('/help ungroupchatban');\r\n\t\tthis.checkCan('lock');\r\n\r\n\t\tconst targetUser = Users.get(target);\r\n\t\tconst unbanned = Punishments.groupchatUnban(targetUser || toID(target));\r\n\r\n\t\tif (unbanned) {\r\n\t\t\tthis.addGlobalModAction(`${unbanned} was ungroupchatbanned by ${user.name}.`);\r\n\t\t\tthis.globalModlog(\"UNGROUPCHATBAN\", toID(target), ` by ${user.id}`);\r\n\t\t\tif (targetUser) targetUser.popup(`${user.name} has allowed you to use groupchats again.`);\r\n\t\t} else {\r\n\t\t\tthis.errorReply(`User ${target} is not banned from using groupchats.`);\r\n\t\t}\r\n\t},\r\n\tungroupchatbanhelp: [`/ungroupchatban [user] - Allows a groupchatbanned user to use groupchats again. Requires: % @ &`],\r\n\r\n\tnameblacklist: 'blacklistname',\r\n\tblacklistname(target, room, user) {\r\n\t\troom = this.requireRoom();\r\n\t\tif (!target) return this.parse('/help blacklistname');\r\n\t\tthis.checkChat();\r\n\t\tthis.checkCan('editroom', null, room);\r\n\t\tif (!room.persist) {\r\n\t\t\treturn this.errorReply(\"This room is not going to last long enough for a blacklist to matter - just ban the user\");\r\n\t\t}\r\n\r\n\t\tconst [targetStr, reason] = target.split('|').map(val => val.trim());\r\n\t\tif (!targetStr || (!reason && REQUIRE_REASONS)) {\r\n\t\t\treturn this.errorReply(\"Usage: /blacklistname name1, name2, ... | reason\");\r\n\t\t}\r\n\r\n\t\tconst targets = targetStr.split(',').map(s => toID(s));\r\n\r\n\t\tconst duplicates = targets.filter(userid => (\r\n\t\t\t// can be asserted, room should always exist\r\n\t\t\tPunishments.roomUserids.nestedGetByType(room!.roomid, userid, 'BLACKLIST')\r\n\t\t));\r\n\t\tif (duplicates.length) {\r\n\t\t\treturn this.errorReply(`[${duplicates.join(', ')}] ${Chat.plural(duplicates, \"are\", \"is\")} already blacklisted.`);\r\n\t\t}\r\n\r\n\t\tfor (const userid of targets) {\r\n\t\t\tif (!userid) return this.errorReply(`User '${userid}' is not a valid userid.`);\r\n\t\t\tif (!Users.Auth.hasPermission(user, 'ban', room.auth.get(userid), room)) {\r\n\t\t\t\treturn this.errorReply(`/blacklistname - Access denied: ${userid} is of equal or higher authority than you.`);\r\n\t\t\t}\r\n\r\n\t\t\tPunishments.roomBlacklist(room, userid, null, null, reason);\r\n\r\n\t\t\tconst trusted = Users.isTrusted(userid);\r\n\t\t\tif (trusted && room.settings.isPrivate !== true) {\r\n\t\t\t\tMonitor.log(`[CrisisMonitor] Trusted user ${userid}${(trusted !== userid ? ` (${trusted})` : ``)} was nameblacklisted from ${room.roomid} by ${user.name}, and should probably be demoted.`);\r\n\t\t\t}\r\n\t\t\tif (!room.settings.isPrivate && room.persist) {\r\n\t\t\t\tthis.globalModlog(\"NAMEBLACKLIST\", userid, reason);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tthis.privateModAction(\r\n\t\t\t`${targets.join(', ')}${Chat.plural(targets, \" were\", \" was\")} nameblacklisted from ${room.title} by ${user.name}.`\r\n\t\t);\r\n\t\treturn true;\r\n\t},\r\n\tblacklistnamehelp: [\r\n\t\t`/blacklistname OR /nameblacklist [name1, name2, etc.] | reason - Blacklists all name(s) from the room you are in for a year. Requires: # &`,\r\n\t],\r\n\r\n\tunab: 'unblacklist',\r\n\tunblacklist(target, room, user) {\r\n\t\troom = this.requireRoom();\r\n\t\tif (!target) return this.parse('/help unblacklist');\r\n\t\tthis.checkCan('editroom', null, room);\r\n\r\n\t\tconst name = Punishments.roomUnblacklist(room, target);\r\n\r\n\t\tif (name) {\r\n\t\t\tthis.privateModAction(`${name} was unblacklisted by ${user.name}.`);\r\n\t\t\tif (!room.settings.isPrivate && room.persist) {\r\n\t\t\t\tthis.globalModlog(\"UNBLACKLIST\", name);\r\n\t\t\t}\r\n\t\t} else {\r\n\t\t\tthis.errorReply(`User '${target}' is not blacklisted.`);\r\n\t\t}\r\n\t},\r\n\tunblacklisthelp: [`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # &`],\r\n\r\n\tunblacklistall(target, room, user) {\r\n\t\troom = this.requireRoom();\r\n\t\tthis.checkCan('editroom', null, room);\r\n\r\n\t\tif (!target) {\r\n\t\t\tuser.lastCommand = '/unblacklistall';\r\n\t\t\tthis.errorReply(\"THIS WILL UNBLACKLIST ALL BLACKLISTED USERS IN THIS ROOM.\");\r\n\t\t\tthis.errorReply(\"To confirm, use: /unblacklistall confirm\");\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tif (user.lastCommand !== '/unblacklistall' || target !== 'confirm') {\r\n\t\t\treturn this.parse('/help unblacklistall');\r\n\t\t}\r\n\t\tuser.lastCommand = '';\r\n\t\tconst unblacklisted = Punishments.roomUnblacklistAll(room);\r\n\t\tif (!unblacklisted) return this.errorReply(\"No users are currently blacklisted in this room to unblacklist.\");\r\n\t\tthis.addModAction(`All blacklists in this room have been lifted by ${user.name}.`);\r\n\t\tthis.modlog('UNBLACKLISTALL');\r\n\t\tthis.roomlog(`Unblacklisted users: ${unblacklisted.join(', ')}`);\r\n\t},\r\n\tunblacklistallhelp: [`/unblacklistall - Unblacklists all blacklisted users in the current room. Requires: # &`],\r\n\r\n\texpiringbls: 'showblacklist',\r\n\texpiringblacklists: 'showblacklist',\r\n\tblacklists: 'showblacklist',\r\n\tshowbl: 'showblacklist',\r\n\tshowblacklist(target, room, user, connection, cmd) {\r\n\t\tif (target) room = Rooms.search(target)!;\r\n\t\tif (!room) return this.errorReply(`The room \"${target}\" was not found.`);\r\n\t\tthis.checkCan('mute', null, room);\r\n\t\tconst SOON_EXPIRING_TIME = 3 * 30 * 24 * 60 * 60 * 1000; // 3 months\r\n\r\n\t\tif (!room.persist) return this.errorReply(\"This room does not support blacklists.\");\r\n\r\n\t\tconst roomUserids = Punishments.roomUserids.get(room.roomid);\r\n\t\tif (!roomUserids || roomUserids.size === 0) {\r\n\t\t\treturn this.sendReply(\"This room has no blacklisted users.\");\r\n\t\t}\r\n\t\tconst blMap = new Map