"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var moderation_exports = {};
__export(moderation_exports, {
  commands: () => commands,
  runCrisisDemote: () => runCrisisDemote,
  runPromote: () => runPromote
});
module.exports = __toCommonJS(moderation_exports);
var import_lib = require("../../lib");
var import_room_settings = require("./room-settings");
/**
 * Moderation commands
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * These are commands for staff.
 *
 * For the API, see chat-plugins/COMMANDS.md
 *
 * @license MIT
 */
const MAX_REASON_LENGTH = 600;
const MUTE_LENGTH = 7 * 60 * 1e3;
const HOURMUTE_LENGTH = 60 * 60 * 1e3;
const DAY = 24 * 60 * 60 * 1e3;
const REQUIRE_REASONS = true;
function runPromote(promoter, room, userid, symbol, username, force) {
  const targetUser = Users.getExact(userid);
  username = username || userid;
  if (!username)
    return;
  if (userid.length > 18) {
    throw new Chat.ErrorMessage(`User '${username}' does not exist (the username is too long).`);
  }
  if (!targetUser && !Users.isUsernameKnown(userid) && !force) {
    throw new Chat.ErrorMessage(`User '${username}' is offline and unrecognized, and so can't be promoted.`);
  }
  if (targetUser && !targetUser.registered) {
    throw new Chat.ErrorMessage(`User '${username}' is unregistered, and so can't be promoted.`);
  }
  let currentSymbol = room.auth.getDirect(userid);
  if (room.auth.has(userid) && currentSymbol === Users.Auth.defaultSymbol()) {
    currentSymbol = "whitelist";
  }
  const currentGroup = Users.Auth.getGroup(currentSymbol);
  const currentGroupName = currentGroup.name || "regular user";
  const nextGroup = Config.groups[symbol];
  if (currentSymbol === symbol) {
    throw new Chat.ErrorMessage(`User '${username}' is already a ${nextGroup?.name || symbol || "regular user"} in this room.`);
  }
  if (!promoter.can("makeroom")) {
    if (currentGroup.id && !promoter.can(`room${currentGroup.id || "voice"}`, null, room)) {
      throw new Chat.ErrorMessage(`Access denied for promoting/demoting ${username} from ${currentGroupName}.`);
    }
    if (symbol !== " " && !promoter.can(`room${nextGroup.id || "voice"}`, null, room)) {
      throw new Chat.ErrorMessage(`Access denied for promoting/demoting ${username} to ${nextGroup.name}.`);
    }
  }
  if (targetUser?.locked && room.persist && room.settings.isPrivate !== true && nextGroup.rank > 2) {
    throw new Chat.ErrorMessage(`${username} is locked and can't be promoted.`);
  }
  if (symbol === Users.Auth.defaultSymbol()) {
    room.auth.delete(userid);
  } else {
    room.auth.set(userid, symbol);
  }
  if (targetUser) {
    targetUser.updateIdentity(room.roomid);
    if (room.subRooms) {
      for (const subRoom of room.subRooms.values()) {
        targetUser.updateIdentity(subRoom.roomid);
      }
    }
  }
  if (targetUser && room.users[targetUser.id] && room.persist && room.settings.isPrivate !== true) {
    return targetUser;
  }
  return null;
}
function runCrisisDemote(userid) {
  const from = [];
  const section = Users.globalAuth.sectionLeaders.get(userid);
  if (section) {
    from.push(`Section Leader (${import_room_settings.RoomSections.sectionNames[section] || section})`);
    Users.globalAuth.deleteSection(userid);
  }
  const globalGroup = Users.globalAuth.get(userid);
  if (globalGroup && globalGroup !== " ") {
    from.push(globalGroup);
    Users.globalAuth.delete(userid);
  }
  for (const room of Rooms.global.chatRooms) {
    if (!room.settings.isPrivate && room.auth.isStaff(userid)) {
      let oldGroup = room.auth.getDirect(userid);
      if (oldGroup === " ") {
        oldGroup = "whitelist in ";
      } else {
        room.auth.set(userid, "+");
      }
      from.push(`${oldGroup}${room.roomid}`);
    }
  }
  return from;
}
Punishments.addPunishmentType({
  type: "YEARLOCK",
  desc: "Locked for a year",
  onActivate: (user, punishment) => {
    user.locked = user.id;
    Chat.punishmentfilter(user, punishment);
  }
});
const commands = {
  roomowner(target, room, user) {
    room = this.requireRoom();
    if (!room.persist) {
      return this.sendReply("/roomowner - This room isn't designed for per-room moderation to be added");
    }
    if (!target)
      return this.parse("/help roomowner");
    const { targetUser, targetUsername, rest } = this.splitUser(target, { exactName: true });
    if (rest)
      return this.errorReply(`This command does not support specifying a reason.`);
    const userid = toID(targetUsername);
    if (!Users.isUsernameKnown(userid)) {
      return this.errorReply(`User '${targetUsername}' is offline and unrecognized, and so can't be promoted.`);
    }
    this.checkCan("makeroom");
    if (room.auth.getDirect(userid) === "#")
      return this.errorReply(`${targetUsername} is already a room owner.`);
    room.auth.set(userid, "#");
    const message = `${targetUsername} was appointed Room Owner by ${user.name}.`;
    if (room.settings.isPrivate === true) {
      this.addModAction(message);
      Rooms.get(`upperstaff`)?.addByUser(user, `<<${room.roomid}>> ${message}`).update();
    } else {
      this.addGlobalModAction(message);
    }
    this.modlog("ROOMOWNER", userid);
    if (targetUser) {
      targetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.roomid}.`);
      room.onUpdateIdentity(targetUser);
      if (room.subRooms) {
        for (const subRoom of room.subRooms.values()) {
          subRoom.onUpdateIdentity(targetUser);
        }
      }
    }
    room.saveSettings();
  },
  roomownerhelp: [`/roomowner [username] - Appoints [username] as a room owner. Requires: &`],
  roomdemote: "roompromote",
  forceroompromote: "roompromote",
  forceroomdemote: "roompromote",
  roompromote(target, room, user, connection, cmd) {
    if (!room) {
      return this.errorReply("This command is only available in rooms");
    }
    this.checkChat();
    if (!target)
      return this.parse("/help roompromote");
    const force = cmd.startsWith("force");
    const users = target.split(",").map((part) => part.trim());
    let nextSymbol = users.pop();
    if (nextSymbol === "deauth")
      nextSymbol = Users.Auth.defaultSymbol();
    const nextGroup = Users.Auth.getGroup(nextSymbol);
    if (!nextSymbol) {
      return this.errorReply("Please specify a group such as /roomvoice or /roomdeauth");
    }
    if (!Config.groups[nextSymbol]) {
      if (!force || !user.can("bypassall")) {
        this.errorReply(`Group '${nextSymbol}' does not exist.`);
        if (user.can("bypassall")) {
          this.errorReply(`If you want to promote to a nonexistent group, use /forceroompromote`);
        }
        return;
      } else if (!Users.Auth.isValidSymbol(nextSymbol)) {
        return this.errorReply(`Admins can forcepromote to nonexistent groups only if they are one character long`);
      }
    }
    if (!force && (nextGroup.globalonly || nextGroup.battleonly && !room.battle)) {
      return this.errorReply(`Group 'room${nextGroup.id || nextSymbol}' does not exist as a room rank.`);
    }
    const nextGroupName = nextGroup.name || "regular user";
    for (const toPromote of users) {
      const userid = toID(toPromote);
      if (!userid)
        return this.parse("/help roompromote");
      const oldSymbol = room.auth.getDirect(userid);
      let shouldPopup;
      try {
        shouldPopup = runPromote(user, room, userid, nextSymbol, toPromote, force);
      } catch (err) {
        if (err.name?.endsWith("ErrorMessage")) {
          this.errorReply(err.message);
          continue;
        }
        throw err;
      }
      const targetUser = Users.getExact(userid);
      const name = targetUser?.name || toPromote;
      if (this.pmTarget && targetUser) {
        const text = `${targetUser.name} was invited (and promoted to Room ${nextGroupName}) by ${user.name}.`;
        room.add(`|c|${user.getIdentity(room)}|/log ${text}`).update();
        this.modlog("INVITE", targetUser, null, { noip: 1, noalts: 1 });
      } else if (nextSymbol in Config.groups && oldSymbol in Config.groups && nextGroup.rank < Config.groups[oldSymbol].rank) {
        if (targetUser && room.users[targetUser.id] && !nextGroup.modlog) {
          targetUser.send(`>${room.roomid}
(You were demoted to Room ${nextGroupName} by ${user.name}.)`);
        }
        this.privateModAction(`${name} was demoted to Room ${nextGroupName} by ${user.name}.`);
        this.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid, "(demote)");
        shouldPopup?.popup(`You were demoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);
      } else if (nextSymbol === "#") {
        this.addModAction(`${name} was promoted to ${nextGroupName} by ${user.name}.`);
        const logRoom = Rooms.get(room.settings.isPrivate === true ? "upperstaff" : "staff");
        logRoom?.addByUser(user, `<<${room.roomid}>> ${name} was appointed Room Owner by ${user.name}`);
        this.modlog("ROOM OWNER", userid);
        shouldPopup?.popup(`You were promoted to ${nextGroupName} by ${user.name} in ${room.roomid}.`);
      } else {
        this.addModAction(`${name} was promoted to Room ${nextGroupName} by ${user.name}.`);
        this.modlog(`ROOM${nextGroupName.toUpperCase()}`, userid);
        shouldPopup?.popup(`You were promoted to Room ${nextGroupName} by ${user.name} in ${room.roomid}.`);
      }
      if (targetUser) {
        targetUser.updateIdentity(room.roomid);
        if (room.subRooms) {
          for (const subRoom of room.subRooms.values()) {
            targetUser.updateIdentity(subRoom.roomid);
          }
        }
        if (targetUser.trusted && !Users.isTrusted(targetUser.id)) {
          targetUser.trusted = "";
        }
      }
    }
    room.saveSettings();
  },
  roompromotehelp: [
    `/roompromote OR /roomdemote [comma-separated usernames], [group symbol] - Promotes/demotes the user(s) to the specified room rank. Requires: @ # &`,
    `/room[group] [comma-separated usernames] - Promotes/demotes the user(s) to the specified room rank. Requires: @ # &`,
    `/roomdeauth [comma-separated usernames] - Removes all room rank from the user(s). Requires: @ # &`
  ],
  auth: "authority",
  stafflist: "authority",
  globalauth: "authority",
  authlist: "authority",
  authority(target, room, user, connection) {
    if (target && target !== "+") {
      const targetRoom = Rooms.search(target);
      const availableRoom = targetRoom?.checkModjoin(user);
      if (targetRoom && availableRoom)
        return this.parse(`/roomauth1 ${target}`);
      return this.parse(`/userauth ${target}`);
    }
    const showAll = !!target;
    const rankLists = {};
    for (const [id, symbol] of Users.globalAuth) {
      if (symbol === " " || symbol === "+" && !showAll)
        continue;
      if (!rankLists[symbol])
        rankLists[symbol] = [];
      rankLists[symbol].push(Users.globalAuth.usernames.get(id) || id);
    }
    const buffer = import_lib.Utils.sortBy(
      Object.entries(rankLists),
      ([symbol]) => -Users.Auth.getGroup(symbol).rank
    ).filter(
      ([symbol]) => symbol !== Users.SECTIONLEADER_SYMBOL
    ).map(
      ([symbol, names]) => `${Config.groups[symbol] ? `**${Config.groups[symbol].name}s** (${symbol})` : symbol}:
` + import_lib.Utils.sortBy(names, (name) => toID(name)).join(", ")
    );
    if (!showAll)
      buffer.push(`(Use \`\`/auth +\`\` to show global voice users.)`);
    if (!buffer.length)
      return connection.popup("This server has no global authority.");
    connection.popup(buffer.join("\n\n"));
  },
  authhelp: [
    `/auth - Show global staff for the server.`,
    `/auth + - Show global staff for the server, including voices.`,
    `/auth [room] - Show what roomauth a room has.`,
    `/auth [user] - Show what global and roomauth a user has.`
  ],
  roomstaff: "roomauth",
  roomauth1: "roomauth",
  roomauth(target, room, user, connection, cmd) {
    let userLookup = "";
    if (cmd === "roomauth1")
      userLookup = `

To look up auth for a user, use /userauth ${target}`;
    let targetRoom = room;
    if (target)
      targetRoom = Rooms.search(target);
    if (!targetRoom?.checkModjoin(user)) {
      return this.errorReply(`The room "${target}" does not exist.`);
    }
    const showAll = user.can("mute", null, targetRoom);
    const rankLists = {};
    for (const [id, rank] of targetRoom.auth) {
      if (rank === " " && !showAll)
        continue;
      if (!rankLists[rank])
        rankLists[rank] = [];
      rankLists[rank].push(id);
    }
    const buffer = import_lib.Utils.sortBy(
      Object.entries(rankLists),
      ([symbol]) => -Users.Auth.getGroup(symbol).rank
    ).map(([symbol, names]) => {
      let group = Config.groups[symbol] ? `${Config.groups[symbol].name}s (${symbol})` : symbol;
      if (symbol === " ")
        group = "Whitelisted (this list is only visible to staff)";
      return `${group}:
` + import_lib.Utils.sortBy(names).map((userid) => {
        const isOnline = Users.get(userid)?.statusType === "online";
        return userid in targetRoom.users && isOnline ? `**${userid}**` : userid;
      }).join(", ");
    });
    let curRoom = targetRoom;
    while (curRoom.parent) {
      const modjoinSetting = curRoom.settings.modjoin === true ? curRoom.settings.modchat : curRoom.settings.modjoin;
      const roomType = modjoinSetting ? `modjoin ${modjoinSetting} ` : "";
      const inheritedUserType = modjoinSetting ? ` of rank ${modjoinSetting} and above` : "";
      if (curRoom.parent) {
        const also = buffer.length === 0 ? `` : ` also`;
        buffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType}${also} have authority in this room.`);
      }
      curRoom = curRoom.parent;
    }
    if (!buffer.length) {
      connection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);
      return;
    }
    if (!curRoom.settings.isPrivate) {
      buffer.push(`${curRoom.title} is a public room, so global auth with no relevant roomauth will have authority in this room.`);
    } else if (curRoom.settings.isPrivate === "hidden" || curRoom.settings.isPrivate === "voice") {
      buffer.push(`${curRoom.title} is a hidden room, so global auth with no relevant roomauth will have authority in this room.`);
    }
    buffer.push(`Names in **bold** are online.`);
    if (targetRoom !== room)
      buffer.unshift(`${targetRoom.title} room auth:`);
    connection.popup(`${buffer.join("\n\n")}${userLookup}`);
  },
  roomauthhelp: [
    `/roomauth [room] - Shows a list of the staff and authority in the given [room].`,
    `If no room is given, it defaults to the current room.`
  ],
  userauth(target, room, user, connection) {
    const targetId = toID(target) || user.id;
    const targetUser = Users.getExact(targetId);
    const targetUsername = targetUser?.name || target;
    const buffer = [];
    let innerBuffer = [];
    const group = Users.globalAuth.get(targetId);
    if (group !== " " || Users.isTrusted(targetId)) {
      buffer.push(`Global auth: ${group === " " ? "trusted" : group}`);
    }
    const sectionLeader = Users.globalAuth.sectionLeaders.get(targetId);
    if (sectionLeader) {
      buffer.push(`Section leader: ${import_room_settings.RoomSections.sectionNames[sectionLeader]}`);
    }
    for (const curRoom of Rooms.rooms.values()) {
      if (curRoom.settings.isPrivate)
        continue;
      if (!curRoom.auth.has(targetId))
        continue;
      innerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);
    }
    if (innerBuffer.length) {
      buffer.push(`Room auth: ${innerBuffer.join(", ")}`);
    }
    if (targetId === user.id || user.can("lock")) {
      innerBuffer = [];
      for (const curRoom of Rooms.rooms.values()) {
        if (!curRoom.settings.isPrivate)
          continue;
        if (curRoom.settings.isPrivate === true)
          continue;
        if (!curRoom.auth.has(targetId))
          continue;
        innerBuffer.push(curRoom.auth.getDirect(targetId).trim() + curRoom.roomid);
      }
      if (innerBuffer.length) {
        buffer.push(`Hidden room auth: ${innerBuffer.join(", ")}`);
      }
    }
    if (targetId === user.id || user.can("makeroom")) {
      innerBuffer = [];
      for (const chatRoom of Rooms.global.chatRooms) {
        if (!chatRoom.settings.isPrivate)
          continue;
        if (chatRoom.settings.isPrivate !== true)
          continue;
        if (!chatRoom.auth.has(targetId))
          continue;
        innerBuffer.push(chatRoom.auth.getDirect(targetId).trim() + chatRoom.roomid);
      }
      if (innerBuffer.length) {
        buffer.push(`Private room auth: ${innerBuffer.join(", ")}`);
      }
    }
    if (!buffer.length) {
      buffer.push("No global or room auth.");
    }
    buffer.unshift(`${targetUsername} user auth:`);
    connection.popup(buffer.join("\n\n"));
  },
  userauthhelp: [
    `/userauth [username] - Shows all authority visible to the user for the given [username].`,
    `If no username is given, it defaults to the current user.`
  ],
  sectionleaders(target, room, user, connection) {
    const usernames = Users.globalAuth.usernames;
    const buffer = [];
    const sections = /* @__PURE__ */ Object.create(null);
    for (const [id, username] of usernames) {
      const sectionid2 = Users.globalAuth.sectionLeaders.get(id);
      if (!sectionid2)
        continue;
      if (!sections[sectionid2])
        sections[sectionid2] = /* @__PURE__ */ new Set();
      sections[sectionid2].add(username);
    }
    let sectionid;
    for (sectionid in sections) {
      if (!sections[sectionid].size)
        continue;
      buffer.push(`**${import_room_settings.RoomSections.sectionNames[sectionid]}**:
` + import_lib.Utils.sortBy([...sections[sectionid]]).join(", "));
    }
    if (!buffer.length)
      throw new Chat.ErrorMessage(`There are no Section Leaders currently.`);
    connection.popup(buffer.join(`

`));
  },
  sectionleadershelp: [
    `/sectionleaders - Shows the current room sections and their section leaders.`
  ],
  async autojoin(target, room, user, connection) {
    const targets = target.split(",");
    if (targets.length > 16 || connection.inRooms.size > 1) {
      return connection.popup("To prevent DoS attacks, you can only use /autojoin for 16 or fewer rooms, when you haven't joined any rooms yet. Please use /join for each room separately.");
    }
    Rooms.global.autojoinRooms(user, connection);
    const autojoins = [];
    const promises = targets.map(
      (roomid) => user.tryJoinRoom(roomid, connection).then((ret) => {
        if (ret === Rooms.RETRY_AFTER_LOGIN) {
          autojoins.push(roomid);
        }
      })
    );
    await Promise.all(promises);
    connection.autojoins = autojoins.join(",");
  },
  autojoinhelp: [`/autojoin [rooms] - Automatically joins all the given rooms.`],
  joim: "join",
  j: "join",
  async join(target, room, user, connection) {
    target = target.trim();
    if (!target)
      return this.parse("/help join");
    if (target.startsWith("http://"))
      target = target.slice(7);
    if (target.startsWith("https://"))
      target = target.slice(8);
    if (target.startsWith(`${Config.routes.client}/`))
      target = target.slice(Config.routes.client.length + 1);
    if (target.startsWith(`${Config.routes.replays}/`))
      target = `battle-${target.slice(Config.routes.replays.length + 1)}`;
    if (target.startsWith("psim.us/"))
      target = target.slice(8);
    const numRooms = [...Rooms.rooms.values()].filter((r) => user.id in r.users).length;
    if (!user.can("altsself") && !target.startsWith("view-") && numRooms >= 50) {
      return connection.sendTo(target, `|noinit||You can only join 50 rooms at a time.`);
    }
    const ret = await user.tryJoinRoom(target, connection);
    if (ret === Rooms.RETRY_AFTER_LOGIN) {
      connection.sendTo(
        target,
        `|noinit|namerequired|The room '${target}' does not exist or requires a login to join.`
      );
    }
  },
  joinhelp: [`/join [roomname] - Attempt to join the room [roomname].`],
  leave: "part",
  part(target, room, user, connection) {
    const targetRoom = target ? Rooms.search(target) : room;
    if (!targetRoom) {
      if (target.startsWith("view-")) {
        connection.openPages?.delete(target.slice(5));
        if (!connection.openPages?.size)
          connection.openPages = null;
        Chat.handleRoomClose(target, user, connection);
        return;
      }
      return this.errorReply(`The room '${target}' does not exist.`);
    }
    Chat.handleRoomClose(targetRoom.roomid, user, connection);
    user.leaveRoom(targetRoom, connection);
  },
  leavehelp: [`/leave - Leave the current room, or a given room.`],
  /*********************************************************
   * Moderating: Punishments
   *********************************************************/
  kick: "warn",
  k: "warn",
  warn(target, room, user) {
    if (!target)
      return this.parse("/help warn");
    this.checkChat();
    if (room?.settings.isPersonal && !user.can("warn")) {
      return this.errorReply("Warning is unavailable in group chats.");
    }
    const globalWarn = !room || ["staff", "adminlog"].includes(room.roomid) || room.roomid.startsWith("help-") || room.battle && !room.parent;
    const { targetUser, inputUsername, targetUsername, rest: reason } = this.splitUser(target);
    const targetID = toID(targetUsername);
    const { privateReason, publicReason } = this.parseSpoiler(reason);
    const saveReplay = globalWarn && room?.battle;
    if (!targetUser?.connected) {
      if (!globalWarn)
        return this.errorReply(`User '${targetUsername}' not found.`);
      if (room) {
        this.checkCan("warn", null, room);
      } else {
        this.checkCan("lock");
      }
      this.addGlobalModAction(
        `${targetID} was warned by ${user.name} while offline.${publicReason ? ` (${publicReason})` : ``}`
      );
      this.globalModlog("WARN OFFLINE", targetUser || targetID, privateReason);
      Punishments.offlineWarns.set(targetID, reason);
      if (saveReplay)
        this.parse("/savereplay forpunishment");
      return;
    }
    if (!globalWarn && !(targetUser.id in room.users)) {
      return this.errorReply(`User ${targetUsername} is not in the room ${room.roomid}.`);
    }
    if (publicReason.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    if (room) {
      this.checkCan("warn", targetUser, room);
    } else {
      this.checkCan("lock", targetUser);
    }
    if (targetUser.can("makeroom"))
      return this.errorReply("You are not allowed to warn upper staff members.");
    const now = Date.now();
    const timeout = now - targetUser.lastWarnedAt;
    if (timeout < 15 * 1e3) {
      const remainder = (15 - timeout / 1e3).toFixed(2);
      return this.errorReply(`You must wait ${remainder} more seconds before you can warn ${targetUser.name} again.`);
    }
    const logMessage = `${targetUser.name} was warned by ${user.name}.${publicReason ? ` (${publicReason})` : ``}`;
    if (globalWarn) {
      this.addGlobalModAction(logMessage);
      this.globalModlog("WARN", targetUser, privateReason);
    } else {
      this.addModAction(logMessage);
      this.modlog("WARN", targetUser, privateReason, { noalts: 1 });
    }
    targetUser.send(`|c|~|/warn ${publicReason}`);
    const userid = targetUser.getLastId();
    if (room) {
      this.add(`|hidelines|unlink|${userid}`);
      if (userid !== toID(inputUsername))
        this.add(`|hidelines|unlink|${toID(inputUsername)}`);
    }
    targetUser.lastWarnedAt = now;
    if (saveReplay)
      this.parse("/savereplay forpunishment");
    return true;
  },
  warnhelp: [
    `/warn OR /k [username], [reason] - Warns a user showing them the site rules and [reason] in an overlay.`,
    `/warn OR /k [username], [reason] spoiler: [private reason] - Warns a user, marking [private reason] only in the modlog.`,
    `Requires: % @ # &`
  ],
  redirect: "redir",
  redir(target, room, user, connection) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help redirect");
    if (room.settings.isPrivate || room.settings.isPersonal) {
      return this.errorReply("Users cannot be redirected from private or personal rooms.");
    }
    const { targetUser, targetUsername, rest: targetRoomid } = this.splitUser(target);
    const targetRoom = Rooms.search(targetRoomid);
    if (!targetRoom || targetRoom.settings.modjoin || targetRoom.settings.staffRoom) {
      return this.errorReply(`The room "${targetRoomid}" does not exist.`);
    }
    this.checkCan("warn", targetUser, room);
    this.checkCan("warn", targetUser, targetRoom);
    if (!user.can("rangeban", targetUser)) {
      this.errorReply(`Redirects have been deprecated. Instead of /redirect, use <<room links>> or /invite to guide users to the correct room, and punish if users don't cooperate.`);
      return;
    }
    if (!targetUser?.connected) {
      return this.errorReply(`User ${targetUsername} not found.`);
    }
    if (targetRoom.roomid === "global")
      return this.errorReply(`Users cannot be redirected to the global room.`);
    if (targetRoom.settings.isPrivate || targetRoom.settings.isPersonal) {
      return this.errorReply(`The room "${targetRoom.title}" is not public.`);
    }
    if (targetUser.inRooms.has(targetRoom.roomid)) {
      return this.errorReply(`User ${targetUser.name} is already in the room ${targetRoom.title}!`);
    }
    if (!targetUser.inRooms.has(room.roomid)) {
      return this.errorReply(`User ${targetUsername} is not in the room ${room.roomid}.`);
    }
    targetUser.leaveRoom(room.roomid);
    targetUser.popup(`You are in the wrong room; please go to <<${targetRoom.roomid}>> instead`);
    this.addModAction(`${targetUser.name} was redirected to room ${targetRoom.title} by ${user.name}.`);
    this.modlog("REDIRECT", targetUser, `to ${targetRoom.title}`, { noip: 1, noalts: 1 });
    targetUser.leaveRoom(room);
  },
  redirhelp: [
    `/redirect OR /redir [username], [roomname] - [DEPRECATED]`,
    `Attempts to redirect the [username] to the [roomname]. Requires: &`
  ],
  m: "mute",
  mute(target, room, user, connection, cmd) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help mute");
    this.checkChat();
    const { targetUser, inputUsername, targetUsername, rest: reason } = this.splitUser(target);
    if (!targetUser)
      return this.errorReply(`User '${targetUsername}' not found.`);
    if (reason.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    const { publicReason, privateReason } = this.parseSpoiler(reason);
    const muteDuration = cmd === "hm" || cmd === "hourmute" ? HOURMUTE_LENGTH : MUTE_LENGTH;
    this.checkCan("mute", targetUser, room);
    if (targetUser.can("makeroom"))
      return this.errorReply("You are not allowed to mute upper staff members.");
    const canBeMutedFurther = (room.getMuteTime(targetUser) || 0) <= muteDuration * 5 / 6;
    if (targetUser.locked || room.isMuted(targetUser) && !canBeMutedFurther || Punishments.isRoomBanned(targetUser, room.roomid)) {
      const alreadyPunishment = targetUser.locked ? "locked" : room.isMuted(targetUser) ? "muted" : "room banned";
      const problem = ` but was already ${alreadyPunishment}`;
      if (!reason) {
        return this.privateModAction(`${targetUser.name} would be muted by ${user.name} ${problem}.`);
      }
      return this.addModAction(`${targetUser.name} would be muted by ${user.name} ${problem}. (${publicReason})`);
    }
    if (targetUser.id in room.users) {
      targetUser.popup(`|modal|${user.name} has muted you in ${room.roomid} for ${Chat.toDurationString(muteDuration)}. ${publicReason}`);
    }
    this.addModAction(`${targetUser.name} was muted by ${user.name} for ${Chat.toDurationString(muteDuration)}.${publicReason ? ` (${publicReason})` : ``}`);
    this.modlog(`${cmd.includes("h") ? "HOUR" : ""}MUTE`, targetUser, privateReason);
    if (targetUser.autoconfirmed && targetUser.autoconfirmed !== targetUser.id) {
      const displayMessage = `${targetUser.name}'s ac account: ${targetUser.autoconfirmed}`;
      this.privateModAction(displayMessage);
    }
    const userid = targetUser.getLastId();
    this.add(`|hidelines|unlink|${userid}`);
    if (userid !== toID(inputUsername))
      this.add(`|hidelines|unlink|${toID(inputUsername)}`);
    room.mute(targetUser, muteDuration);
  },
  mutehelp: [`/mute OR /m [username], [reason] - Mutes a user with reason for 7 minutes. Requires: % @ # &`],
  hm: "hourmute",
  hourmute(target) {
    if (!target)
      return this.parse("/help hourmute");
    this.run("mute");
  },
  hourmutehelp: [`/hourmute OR /hm [username], [reason] - Mutes a user with reason for an hour. Requires: % @ # &`],
  um: "unmute",
  unmute(target, room, user) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help unmute");
    const { targetUser, targetUsername, rest } = this.splitUser(target);
    if (rest)
      return this.errorReply(`This command does not support specifying a reason.`);
    this.checkChat();
    this.checkCan("mute", null, room);
    const successfullyUnmuted = room.unmute(
      targetUser?.id || toID(targetUsername),
      `Your mute in '${room.title}' has been lifted.`
    );
    if (successfullyUnmuted) {
      this.addModAction(`${targetUser ? targetUser.name : successfullyUnmuted} was unmuted by ${user.name}.`);
      this.modlog("UNMUTE", targetUser || successfullyUnmuted, null, { noip: 1, noalts: 1 });
    } else {
      this.errorReply(`${targetUser ? targetUser.name : targetUsername} is not muted.`);
    }
  },
  unmutehelp: [`/unmute [username] - Removes mute from user. Requires: % @ # &`],
  rb: "ban",
  weekban: "ban",
  wb: "ban",
  wrb: "ban",
  forceroomban: "ban",
  forceweekban: "ban",
  weekroomban: "ban",
  forcerb: "ban",
  roomban: "ban",
  b: "ban",
  ban(target, room, user, connection, cmd) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help ban");
    this.checkChat();
    const week = ["wrb", "wb"].includes(cmd) || cmd.includes("week");
    const { targetUser, inputUsername, targetUsername, rest: reason } = this.splitUser(target);
    const { publicReason, privateReason } = this.parseSpoiler(reason);
    if (!targetUser)
      return this.errorReply(`User '${targetUsername}' not found.`);
    if (reason.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    this.checkCan("ban", targetUser, room);
    if (targetUser.can("makeroom"))
      return this.errorReply("You are not allowed to ban upper staff members.");
    if (Punishments.hasRoomPunishType(room, toID(targetUsername), "BLACKLIST")) {
      return this.errorReply(`This user is already blacklisted from ${room.roomid}.`);
    }
    const name = targetUser.getLastName();
    const userid = targetUser.getLastId();
    const force = cmd.startsWith("force");
    if (targetUser.trusted) {
      if (!force) {
        return this.sendReply(
          `${name} is a trusted user. If you are sure you would like to ban them, use /force${week ? "week" : "room"}ban.`
        );
      }
    } else if (force) {
      return this.errorReply(`Use /${week ? "week" : "room"}ban; ${name} is not a trusted user.`);
    }
    if (!reason && !week && Punishments.isRoomBanned(targetUser, room.roomid)) {
      const problem = " but was already banned";
      return this.privateModAction(`${name} would be banned by ${user.name} ${problem}.`);
    }
    if (targetUser.trusted && room.settings.isPrivate !== true && !room.settings.isPersonal) {
      Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} ${targetUser.trusted !== targetUser.id ? ` (${targetUser.trusted})` : ``} was roombanned from ${room.roomid} by ${user.name}, and should probably be demoted.`);
    }
    if (targetUser.id in room.users || user.can("lock")) {
      targetUser.popup(
        `|modal||html|<p>${import_lib.Utils.escapeHTML(user.name)} has banned you from the room ${room.roomid} ${room.subRooms ? ` and its subrooms` : ``}${week ? " for a week" : ""}.</p>${publicReason ? `<p>Reason: ${import_lib.Utils.escapeHTML(publicReason)}</p>` : ``}<p>To appeal the ban, PM the staff member that banned you${room.persist ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.roomid}">List Room Staff</button></p>` : `.</p>`}`
      );
    }
    this.addModAction(`${name} was banned ${week ? " for a week" : ""} from ${room.title} by ${user.name}.${publicReason ? ` (${publicReason})` : ``}`);
    const time = week ? Date.now() + 7 * 24 * 60 * 60 * 1e3 : null;
    const affected = Punishments.roomBan(room, targetUser, time, null, privateReason);
    if (!room.settings.isPrivate && room.persist) {
      const acAccount = targetUser.autoconfirmed !== userid && targetUser.autoconfirmed;
      let displayMessage = "";
      if (affected.length > 1) {
        displayMessage = `${name}'s ${acAccount ? ` ac account: ${acAccount}, ` : ``} banned alts: ${affected.slice(1).map((curUser) => curUser.getLastName()).join(", ")}`;
        this.privateModAction(displayMessage);
      } else if (acAccount) {
        displayMessage = `${name}'s ac account: ${acAccount}`;
        this.privateModAction(displayMessage);
      }
    }
    room.hideText([
      ...affected.map((u) => u.id),
      toID(inputUsername)
    ]);
    if (room.settings.isPrivate !== true && room.persist) {
      this.globalModlog(`${week ? "WEEK" : ""}ROOMBAN`, targetUser, privateReason);
    } else {
      this.modlog(`${week ? "WEEK" : ""}ROOMBAN`, targetUser, privateReason);
    }
    return true;
  },
  banhelp: [
    `/ban [username], [reason] - Bans the user from the room you are in. Requires: @ # &`,
    `/weekban [username], [reason] - Bans the user from the room you are in for a week. Requires: @ # &`
  ],
  unroomban: "unban",
  roomunban: "unban",
  unban(target, room, user, connection) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help unban");
    this.checkCan("ban", null, room);
    const name = Punishments.roomUnban(room, target);
    if (name) {
      this.addModAction(`${name} was unbanned from ${room.title} by ${user.name}.`);
      if (room.settings.isPrivate !== true && room.persist) {
        this.globalModlog("UNROOMBAN", name);
      }
    } else {
      this.errorReply(`User '${target}' is not banned from this room.`);
    }
  },
  unbanhelp: [`/unban [username] - Unbans the user from the room you are in. Requires: @ # &`],
  forcelock: "lock",
  forceweeklock: "lock",
  forcemonthlock: "lock",
  l: "lock",
  ipmute: "lock",
  wl: "lock",
  weeklock: "lock",
  monthlock: "lock",
  async lock(target, room, user, connection, cmd) {
    const week = cmd === "wl" || cmd.includes("week");
    const month = cmd.includes("month");
    const force = cmd.includes("force");
    if (!target) {
      if (week)
        return this.parse("/help weeklock");
      return this.parse("/help lock");
    }
    const { targetUser, inputUsername, targetUsername, rest: reason } = this.splitUser(target);
    let userid = toID(targetUsername);
    if (!targetUser && !Punishments.search(userid).length && !force) {
      return this.errorReply(
        `User '${targetUsername}' not found. Use \`\`/force${month ? "month" : week ? "week" : ""}lock\`\` if you need to to lock them anyway.`
      );
    }
    if (reason.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    this.checkCan("lock", userid);
    if (month)
      this.checkCan("rangeban");
    let name;
    if (targetUser) {
      name = targetUser.getLastName();
      userid = targetUser.getLastId();
      if (targetUser.locked && !targetUser.locked.startsWith("#") && !week && !month) {
        return this.privateModAction(`${name} would be locked by ${user.name} but was already locked.`);
      }
    } else {
      name = targetUsername;
      userid = toID(targetUsername);
    }
    if (Users.isTrusted(userid)) {
      if (force) {
        const from = runCrisisDemote(userid);
        Monitor.log(`[CrisisMonitor] ${name} was locked by ${user.name} and demoted from ${from.join(", ")}.`);
        this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from.join(", ")}`);
      } else {
        return this.sendReply(`${name} is a trusted user. If you are sure you would like to lock them use /force${month ? "month" : week ? "week" : ""}lock.`);
      }
    } else if (force && targetUser) {
      return this.errorReply(`Use /lock; ${name} is not a trusted user and is online.`);
    }
    const { privateReason, publicReason } = this.parseSpoiler(reason);
    const duration = week ? Date.now() + 7 * 24 * 60 * 60 * 1e3 : month ? Date.now() + 30 * 24 * 60 * 60 * 1e3 : null;
    let affected = [];
    if (targetUser) {
      const ignoreAlts = Punishments.isSharedIp(targetUser.latestIp);
      affected = await Punishments.lock(targetUser, duration, null, ignoreAlts, publicReason);
    } else {
      affected = await Punishments.lock(userid, duration, null, false, publicReason);
    }
    this.globalModlog(
      (force ? `FORCE` : ``) + (week ? "WEEKLOCK" : month ? "MONTHLOCK" : "LOCK"),
      targetUser || userid,
      privateReason
    );
    const durationMsg = week ? " for a week" : month ? " for a month" : "";
    this.addGlobalModAction(`${name} was locked from talking${durationMsg} by ${user.name}.` + (publicReason ? ` (${publicReason})` : ""));
    if (room && !room.settings.isHelp) {
      room.hideText([
        ...affected.map((u) => u.id),
        toID(inputUsername)
      ]);
    }
    const acAccount = targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed;
    let displayMessage = "";
    if (affected.length > 1) {
      displayMessage = `${name}'s ${acAccount ? ` ac account: ${acAccount}, ` : ""} locked alts: ${affected.slice(1).map((curUser) => curUser.getLastName()).join(", ")}`;
      this.privateModAction(displayMessage);
    } else if (acAccount) {
      displayMessage = `${name}'s ac account: ${acAccount}`;
      this.privateModAction(displayMessage);
    }
    if (targetUser) {
      let message = `|popup||html|${user.name} has locked you from talking in chats, battles, and PMing regular users${durationMsg}`;
      if (publicReason)
        message += `

Reason: ${publicReason}`;
      let appeal = "";
      if (Chat.pages.help) {
        appeal += `<a href="view-help-request--appeal"><button class="button"><strong>Appeal your punishment</strong></button></a>`;
      } else if (Config.appealurl) {
        appeal += `appeal: <a href="${Config.appealurl}">${Config.appealurl}</a>`;
      }
      if (appeal)
        message += `

If you feel that your lock was unjustified, you can ${appeal}.`;
      message += `

Your lock will expire in a few days.`;
      targetUser.send(message);
      const roomauth = Rooms.global.destroyPersonalRooms(userid);
      if (roomauth.length) {
        Monitor.log(`[CrisisMonitor] Locked user ${name} has public roomauth (${roomauth.join(", ")}), and should probably be demoted.`);
      }
    }
    if (room?.battle)
      this.parse("/savereplay forpunishment");
    return true;
  },
  lockhelp: [
    `/lock OR /l [username], [reason] - Locks the user from talking in all chats. Requires: % @ &`,
    `/weeklock OR /wl [username], [reason] - Same as /lock, but locks users for a week.`,
    `/lock OR /l [username], [reason] spoiler: [private reason] - Marks [private reason] in modlog only.`
  ],
  unlock(target, room, user) {
    if (!target)
      return this.parse("/help unlock");
    this.checkCan("lock");
    const targetUser = Users.get(target);
    if (targetUser?.namelocked) {
      return this.errorReply(`User ${targetUser.name} is namelocked, not locked. Use /unnamelock to unnamelock them.`);
    }
    let reason = "";
    if (targetUser?.locked && targetUser.locked.startsWith("#")) {
      reason = ` (${targetUser.locked})`;
    }
    const unlocked = Punishments.unlock(target);
    if (unlocked) {
      this.addGlobalModAction(`${unlocked.join(", ")} ${unlocked.length > 1 ? "were" : "was"} unlocked by ${user.name}.${reason}`);
      if (!reason)
        this.globalModlog("UNLOCK", toID(target));
      if (targetUser)
        targetUser.popup(`${user.name} has unlocked you.`);
    } else {
      this.errorReply(`User '${target}' is not locked.`);
    }
  },
  unlockname(target, room, user) {
    if (!target)
      return this.parse("/help unlock");
    this.checkCan("lock");
    const userid = toID(target);
    if (userid.startsWith("guest")) {
      return this.errorReply(`You cannot unlock the guest userid - provide their original username instead.`);
    }
    const punishment = Punishments.userids.getByType(userid, "LOCK") || Punishments.userids.getByType(userid, "NAMELOCK");
    if (!punishment)
      return this.errorReply("This name isn't locked.");
    if (punishment.id === userid || Users.get(userid)?.previousIDs.includes(punishment.id)) {
      return this.errorReply(`"${userid}" was specifically locked by a staff member (check the global modlog). Use /unlock if you really want to unlock this name.`);
    }
    Punishments.userids.delete(userid);
    Punishments.savePunishments();
    for (const curUser of Users.findUsers([userid], [])) {
      const locked = Punishments.hasPunishType(curUser.id, ["LOCK", "NAMELOCK"], curUser.latestIp);
      if (curUser.locked && !curUser.locked.startsWith("#") && !locked) {
        curUser.locked = null;
        curUser.namelocked = null;
        curUser.destroyPunishmentTimer();
        curUser.updateIdentity();
      }
    }
    this.addGlobalModAction(`The name '${target}' was unlocked by ${user.name}.`);
    this.globalModlog("UNLOCKNAME", userid);
  },
  unrangelock: "unlockip",
  rangeunlock: "unlockip",
  unlockip(target, room, user) {
    target = target.trim();
    if (!target)
      return this.parse("/help unlock");
    this.checkCan("globalban");
    const range = target.endsWith("*");
    if (range)
      this.checkCan("rangeban");
    if (!(range ? IPTools.ipRangeRegex : IPTools.ipRegex).test(target)) {
      return this.errorReply("Please enter a valid IP address.");
    }
    const punishment = Punishments.ips.get(target);
    if (!punishment)
      return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
    Punishments.ips.delete(target);
    Punishments.savePunishments();
    for (const curUser of Users.findUsers([], [target])) {
      if ((range ? curUser.locked === "#rangelock" : !curUser.locked?.startsWith("#")) && !Punishments.getPunishType(curUser.id)) {
        curUser.locked = null;
        if (curUser.namelocked) {
          curUser.namelocked = null;
          curUser.resetName();
        }
        curUser.destroyPunishmentTimer();
        curUser.updateIdentity();
      }
    }
    this.privateGlobalModAction(`${user.name} unlocked the ${range ? "IP range" : "IP"}: ${target}`);
    this.globalModlog(`UNLOCK${range ? "RANGE" : "IP"}`, null, null, target);
  },
  unlockiphelp: [`/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ &`],
  unlocknamehelp: [`/unlockname [name] - Unlocks a punished alt, leaving the original lock intact. Requires: % @ &`],
  unlockhelp: [
    `/unlock [username] - Unlocks the user. Requires: % @ &`,
    `/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ &`,
    `/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ &`
  ],
  forceglobalban: "globalban",
  gban: "globalban",
  async globalban(target, room, user, connection, cmd) {
    if (!target)
      return this.parse("/help globalban");
    const force = cmd.includes("force");
    const { targetUser, inputUsername, targetUsername, rest: reason } = this.splitUser(target);
    let userid = toID(targetUsername);
    if (!targetUser && !force) {
      return this.errorReply(`User '${targetUsername}' not found. Use /forceglobalban to ban them anyway.`);
    }
    if (reason.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    if (!reason && REQUIRE_REASONS) {
      return this.errorReply("Global bans require a reason.");
    }
    this.checkCan("globalban", targetUser);
    let name;
    if (targetUser) {
      name = targetUser.getLastName();
      userid = targetUser.getLastId();
    } else {
      name = targetUsername;
    }
    if (Users.isTrusted(userid)) {
      if (force) {
        const from = runCrisisDemote(userid);
        Monitor.log(`[CrisisMonitor] ${name} was globally banned by ${user.name} and demoted from ${from?.join(", ")}.`);
        this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from?.join(", ")}`);
      } else {
        return this.sendReply(`${name} is a trusted user. If you are sure you would like to ban them use /forceglobalban.`);
      }
    }
    const roomauth = Rooms.global.destroyPersonalRooms(userid);
    if (roomauth.length) {
      Monitor.log(`[CrisisMonitor] Globally banned user ${name} has public roomauth (${roomauth.join(", ")}), and should probably be demoted.`);
    }
    const { privateReason, publicReason } = this.parseSpoiler(reason);
    targetUser?.popup(
      `|modal|${user.name} has globally banned you.${publicReason ? `

Reason: ${publicReason}` : ``} ${Config.appealurl ? `

If you feel that your ban was unjustified, you can appeal:
${Config.appealurl}` : ``}

Your ban will expire in a few days.`
    );
    this.addGlobalModAction(`${name} was globally banned by ${user.name}.${publicReason ? ` (${publicReason})` : ``}`);
    const affected = await Punishments.ban(userid, null, null, false, publicReason);
    const acAccount = targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed;
    let displayMessage = "";
    if (affected.length > 1) {
      let guests = affected.length - 1;
      const affectedAlts = affected.slice(1).map((curUser) => curUser.getLastName()).filter((alt) => !alt.startsWith("[Guest "));
      guests -= affectedAlts.length;
      displayMessage = `${name}'s ${acAccount ? `ac account: ${acAccount}, ` : ``} banned alts: ${affectedAlts.join(", ")} ${guests ? ` [${guests} guests]` : ``}`;
      this.privateModAction(displayMessage);
      for (const id of affectedAlts) {
        this.add(`|hidelines|unlink|${toID(id)}`);
      }
    } else if (acAccount) {
      displayMessage = `${name}'s ac account: ${acAccount}`;
      this.privateModAction(displayMessage);
    }
    room?.hideText([
      ...affected.map((u) => u.id),
      toID(inputUsername)
    ]);
    this.globalModlog(`${force ? `FORCE` : ""}BAN`, targetUser, privateReason);
    return true;
  },
  globalbanhelp: [
    `/globalban OR /gban [username], [reason] - Kick user from all rooms and ban user's IP address with reason. Requires: @ &`,
    `/globalban OR /gban [username], [reason] spoiler: [private reason] - Marks [private reason] in modlog only.`
  ],
  globalunban: "unglobalban",
  unglobalban(target, room, user) {
    if (!target)
      return this.parse(`/help unglobalban`);
    this.checkCan("globalban");
    const name = Punishments.unban(target);
    if (!name) {
      return this.errorReply(`User '${target}' is not globally banned.`);
    }
    this.addGlobalModAction(`${name} was globally unbanned by ${user.name}.`);
    this.globalModlog("UNBAN", target);
  },
  unglobalbanhelp: [`/unglobalban [username] - Unban a user. Requires: @ &`],
  deroomvoiceall(target, room, user) {
    room = this.requireRoom();
    this.checkCan("editroom", null, room);
    if (!room.auth.size)
      return this.errorReply("Room does not have roomauth.");
    if (!target) {
      user.lastCommand = "/deroomvoiceall";
      this.errorReply("THIS WILL DEROOMVOICE ALL ROOMVOICED USERS.");
      this.errorReply("To confirm, use: /deroomvoiceall confirm");
      return;
    }
    if (user.lastCommand !== "/deroomvoiceall" || target !== "confirm") {
      return this.parse("/help deroomvoiceall");
    }
    user.lastCommand = "";
    let count = 0;
    for (const [userid, symbol] of room.auth) {
      if (symbol === "+") {
        room.auth.delete(userid);
        if (userid in room.users)
          room.users[userid].updateIdentity(room.roomid);
        count++;
      }
    }
    if (!count) {
      return this.sendReply("(This room has zero roomvoices)");
    }
    room.saveSettings();
    this.addModAction(`All ${count} roomvoices have been cleared by ${user.name}.`);
    this.modlog("DEROOMVOICEALL");
  },
  deroomvoiceallhelp: [`/deroomvoiceall - Devoice all roomvoiced users. Requires: # &`],
  // this is a separate command for two reasons
  // a - yearticketban is preferred over /ht yearban
  // b - it would be messy to switch
  //   from both Punishments.punishRange and #punish in /ht ban
  //   since this takes ips / userids
  async yearticketban(target, room, user) {
    this.checkCan("rangeban");
    target = target.trim();
    let reason = "";
    [target, reason] = this.splitOne(target);
    let isIP = false;
    let descriptor = "";
    if (IPTools.ipRangeRegex.test(target)) {
      isIP = true;
      if (IPTools.ipRegex.test(target)) {
        descriptor = "the IP ";
      } else {
        descriptor = "the IP range ";
      }
    } else {
      target = toID(target);
    }
    if (!target)
      return this.parse(`/help yearticketban`);
    const expireTime = Date.now() + 365 * 24 * 60 * 60 * 1e3;
    if (isIP) {
      Punishments.punishRange(target, reason, expireTime, "TICKETBAN");
    } else {
      await Punishments.punish(target, {
        type: "TICKETBAN",
        id: target,
        expireTime,
        reason,
        rest: []
      }, true);
    }
    this.addGlobalModAction(
      `${user.name} banned ${descriptor}${target} from opening tickets for a year${reason ? ` (${reason})` : ""}`
    );
    this.globalModlog(
      "YEARTICKETBAN",
      isIP ? null : target,
      reason,
      isIP ? target : void 0
    );
  },
  yearticketbanhelp: [
    `/yearticketban [IP/userid] - Ban an IP or a userid from opening tickets for a year. `,
    `Accepts wildcards to ban ranges. Requires: &`
  ],
  rangeban: "banip",
  yearbanip: "banip",
  banip(target, room, user, connection, cmd) {
    const [ip, reason] = this.splitOne(target);
    if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip))
      return this.parse("/help banip");
    if (!reason)
      return this.errorReply("/banip requires a ban reason");
    this.checkCan("rangeban");
    const ipDesc = `IP ${ip.endsWith("*") ? `range ` : ``}${ip}`;
    const year = cmd.startsWith("year");
    const time = year ? Date.now() + 365 * 24 * 60 * 60 * 1e3 : null;
    const curPunishment = Punishments.ipSearch(ip, "BAN");
    if (curPunishment?.type === "BAN" && !time) {
      return this.errorReply(`The ${ipDesc} is already temporarily banned.`);
    }
    Punishments.punishRange(ip, reason, time, "BAN");
    const duration = year ? "year" : "hour";
    if (!this.room || this.room.roomid !== "staff") {
      this.sendReply(`You ${duration}-banned the ${ipDesc}.`);
    }
    this.room = Rooms.get("staff") || null;
    this.addGlobalModAction(
      `${user.name} ${duration}-banned the ${ipDesc}: ${reason}`
    );
    this.globalModlog(
      `${year ? "YEAR" : ""}RANGEBAN`,
      null,
      `${ip.endsWith("*") ? ip : `[${ip}]`}: ${reason}`
    );
  },
  baniphelp: [
    `/banip [ip] OR /yearbanip [ip] - Globally bans this IP or IP range for an hour. Accepts wildcards to ban ranges.`,
    `Existing users on the IP will not be banned. Requires: &`
  ],
  unrangeban: "unbanip",
  unbanip(target, room, user) {
    target = target.trim();
    if (!target) {
      return this.parse("/help unbanip");
    }
    this.checkCan("rangeban");
    if (!Punishments.ips.has(target)) {
      return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
    }
    Punishments.ips.delete(target);
    this.addGlobalModAction(`${user.name} unbanned the ${target.endsWith("*") ? "IP range" : "IP"}: ${target}`);
    this.modlog("UNRANGEBAN", null, target);
  },
  unbaniphelp: [`/unbanip [ip] - Unbans. Accepts wildcards to ban ranges. Requires: &`],
  forceyearlockname: "yearlockname",
  yearlockid: "yearlockname",
  forceyearlockid: "yearlockname",
  yearlockuserid: "yearlockname",
  forceyearlockuserid: "yearlockname",
  yearlockname(target, room, user) {
    this.checkCan("rangeban");
    const [targetUsername, rest] = import_lib.Utils.splitFirst(target, ",").map((k) => k.trim());
    const targetUser = Users.get(targetUsername);
    const targetUserid = toID(targetUsername);
    if (!targetUserid || targetUserid.length > 18) {
      return this.errorReply(`Invalid userid.`);
    }
    const force = this.cmd.includes("force");
    if (targetUser?.registered && !force) {
      return this.errorReply(`That user is registered. Either permalock them normally or use /forceyearlockname.`);
    }
    const punishment = {
      type: "YEARLOCK",
      id: targetUserid,
      expireTime: Date.now() + 365 * 24 * 60 * 60 * 1e3,
      reason: rest || ""
    };
    Punishments.userids.add(targetUserid, punishment);
    Punishments.savePunishments();
    this.addGlobalModAction(`${user.name} locked the userid '${targetUserid}' for a year${rest ? ` (${rest})` : ""}.`);
    this.globalModlog(`${force ? `FORCE` : ""}YEARLOCKNAME`, targetUserid, rest);
    if (targetUser) {
      Chat.punishmentfilter(targetUser, punishment);
      targetUser.locked = targetUserid;
    }
  },
  rangelock: "lockip",
  yearlockip: "lockip",
  yearnamelockip: "lockip",
  lockip(target, room, user, connection, cmd) {
    const [ip, reason] = this.splitOne(target);
    if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip))
      return this.parse("/help lockip");
    if (!reason)
      return this.errorReply("/lockip requires a lock reason");
    this.checkCan("rangeban");
    const ipDesc = ip.endsWith("*") ? `IP range ${ip}` : `IP ${ip}`;
    const year = cmd.startsWith("year");
    const curPunishment = Punishments.byWeight(Punishments.ipSearch(ip) || [])[0];
    if (!year && curPunishment && (curPunishment.type === "BAN" || curPunishment.type === "LOCK")) {
      const punishDesc = curPunishment.type === "BAN" ? `temporarily banned` : `temporarily locked`;
      return this.errorReply(`The ${ipDesc} is already ${punishDesc}.`);
    }
    const time = year ? Date.now() + 365 * 24 * 60 * 60 * 1e3 : null;
    const type = cmd.includes("name") ? "NAMELOCK" : "LOCK";
    Punishments.punishRange(ip, reason, time, type);
    if (year) {
      this.addGlobalModAction(`${user.name} year-${type.toLowerCase()}ed the ${ipDesc}: ${reason}`);
    } else {
      this.addGlobalModAction(`${user.name} hour-${type.toLowerCase()}ed the ${ipDesc}: ${reason}`);
    }
    this.globalModlog(
      `${year ? "YEAR" : "RANGE"}${type}`,
      null,
      `${ip.endsWith("*") ? ip : `[${ip}]`}: ${reason}`,
      ip.endsWith("*") ? `${ip.slice(0, -2)}` : ip
    );
  },
  lockiphelp: [
    `/lockip [ip] - Globally locks this IP or IP range for an hour. Accepts wildcards to ban ranges.`,
    `/yearlockip [ip] - Globally locks this IP or IP range for one year. Accepts wildcards to ban ranges.`,
    `/yearnamelockip [ip] - Namelocks this IP or IP range for one year. Accepts wildcards to ban ranges.`,
    `Existing users on the IP will not be banned. Requires: &`
  ],
  /*********************************************************
   * Moderating: Other
   *********************************************************/
  mn: "modnote",
  modnote(target, room, user, connection) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help modnote");
    this.checkChat();
    if (target.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The note is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    this.checkCan("receiveauthmessages", null, room);
    target = target.replace(/\n/g, "; ");
    let targeted = /\[([^\]]+)\]/.exec(target)?.[1] || null;
    if (!targeted) {
      targeted = target.split(/[,-]/)[0]?.trim() || "";
      if (!targeted || !(Users.get(targeted) || Punishments.search(target).length || IPTools.ipRegex.test(targeted)) || toID(targeted) === toID(target)) {
        targeted = null;
      }
    }
    let targetUserid, targetIP;
    if (targeted) {
      if (IPTools.ipRegex.test(targeted)) {
        targetIP = targeted;
      } else {
        targetUserid = toID(targeted);
      }
    }
    if (["staff", "upperstaff"].includes(room.roomid) || Rooms.Modlog.getSharedID(room.roomid) && user.can("modlog")) {
      this.globalModlog("NOTE", targetUserid || null, target, targetIP);
    } else {
      this.modlog("NOTE", targetUserid || null, target);
    }
    this.privateModAction(`${user.name} notes: ${target}`);
  },
  modnotehelp: [`/modnote [note] - Adds a moderator note that can be read through modlog. Requires: % @ # &`],
  globalpromote: "promote",
  promote(target, room, user, connection, cmd) {
    if (!target)
      return this.parse("/help promote");
    const { targetUser, targetUsername, rest: nextGroupName } = this.splitUser(target, { exactName: true });
    const userid = toID(targetUsername);
    const name = targetUser?.name || targetUsername;
    if (!userid)
      return this.parse("/help promote");
    const currentGroup = targetUser?.tempGroup || Users.globalAuth.get(userid);
    let nextGroup = nextGroupName;
    if (nextGroupName === "deauth")
      nextGroup = Users.Auth.defaultSymbol();
    if (!nextGroup) {
      return this.errorReply("Please specify a group such as /globalvoice or /globaldeauth");
    }
    if (!Config.groups[nextGroup]) {
      return this.errorReply(`Group '${nextGroup}' does not exist.`);
    }
    if (!cmd.startsWith("global")) {
      let groupid = Config.groups[nextGroup].id;
      if (!groupid && nextGroup === Users.Auth.defaultSymbol())
        groupid = "deauth";
      if (Config.groups[nextGroup].globalonly)
        return this.errorReply(`Did you mean "/global${groupid}"?`);
      if (Config.groups[nextGroup].roomonly)
        return this.errorReply(`Did you mean "/room${groupid}"?`);
      return this.errorReply(`Did you mean "/room${groupid}" or "/global${groupid}"?`);
    }
    if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) {
      return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
    }
    const groupName = Config.groups[nextGroup].name || "regular user";
    if (currentGroup === nextGroup) {
      return this.errorReply(`User '${name}' is already a ${groupName}`);
    }
    if (!Users.Auth.hasPermission(user, "promote", currentGroup)) {
      this.errorReply(`/${cmd} - Access denied for promoting from ${currentGroup}`);
      this.errorReply(`You can only promote to/from: ${Users.Auth.listJurisdiction(user, "promote")}`);
      return;
    }
    if (!Users.Auth.hasPermission(user, "promote", nextGroup)) {
      this.errorReply(`/${cmd} - Access denied for promoting to ${groupName}`);
      this.errorReply(`You can only promote to/from: ${Users.Auth.listJurisdiction(user, "promote")}`);
      return;
    }
    if (!Users.isUsernameKnown(userid)) {
      return this.errorReply(`/globalpromote - WARNING: '${name}' is offline and unrecognized. The username might be misspelled (either by you or the person who told you) or unregistered. Use /forcepromote if you're sure you want to risk it.`);
    }
    if (targetUser && !targetUser.registered) {
      return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
    }
    if (nextGroup === Users.Auth.defaultSymbol()) {
      Users.globalAuth.delete(targetUser ? targetUser.id : userid);
    } else {
      Users.globalAuth.set(targetUser ? targetUser.id : userid, nextGroup);
    }
    if (Users.Auth.getGroup(nextGroup).rank < Users.Auth.getGroup(currentGroup).rank) {
      this.privateGlobalModAction(`${name} was demoted to Global ${groupName} by ${user.name}.`);
      this.globalModlog(`GLOBAL ${groupName.toUpperCase()}`, userid, `(demote)`);
      if (targetUser)
        targetUser.popup(`You were demoted to Global ${groupName} by ${user.name}.`);
    } else {
      this.addGlobalModAction(`${name} was promoted to Global ${groupName} by ${user.name}.`);
      this.globalModlog(`GLOBAL ${groupName.toUpperCase()}`, userid);
      if (targetUser)
        targetUser.popup(`You were promoted to Global ${groupName} by ${user.name}.`);
    }
    if (targetUser) {
      targetUser.updateIdentity();
      Rooms.global.checkAutojoin(targetUser);
      if (targetUser.trusted && !Users.isTrusted(targetUser.id)) {
        targetUser.trusted = "";
      }
    }
  },
  promotehelp: [`/promote [username], [group] - Promotes the user to the specified group. Requires: &`],
  untrustuser: "trustuser",
  unconfirmuser: "trustuser",
  confirmuser: "trustuser",
  forceconfirmuser: "trustuser",
  forcetrustuser: "trustuser",
  trustuser(target, room, user, connection, cmd) {
    if (!target)
      return this.parse("/help trustuser");
    this.checkCan("promote");
    const force = cmd.includes("force");
    const untrust = cmd.includes("un");
    const { targetUser, targetUsername, rest } = this.splitUser(target, { exactName: true });
    if (rest)
      return this.errorReply(`This command does not support specifying a reason.`);
    const userid = toID(targetUsername);
    const name = targetUser?.name || targetUsername;
    const currentGroup = Users.globalAuth.get(userid);
    if (untrust) {
      if (currentGroup !== Users.Auth.defaultSymbol()) {
        return this.errorReply(`User '${name}' is trusted indirectly through global rank ${currentGroup}. Demote them from that rank to remove trusted status.`);
      }
      const trustedSourceRooms = Rooms.global.chatRooms.filter((authRoom) => authRoom.persist && authRoom.settings.isPrivate !== true && authRoom.auth.isStaff(userid)).map((authRoom) => authRoom.auth.get(userid) + authRoom.roomid).join(" ");
      if (trustedSourceRooms.length && !Users.globalAuth.has(userid)) {
        return this.errorReply(`User '${name}' is trusted indirectly through room ranks ${trustedSourceRooms}. Demote them from those ranks to remove trusted status.`);
      }
      if (!Users.globalAuth.has(userid))
        return this.errorReply(`User '${name}' is not trusted.`);
      if (targetUser) {
        targetUser.setGroup(Users.Auth.defaultSymbol());
      } else {
        Users.globalAuth.delete(userid);
      }
      this.privateGlobalModAction(`${name} was set to no longer be a trusted user by ${user.name}.`);
      this.globalModlog("UNTRUSTUSER", userid);
    } else {
      if (!targetUser && !force)
        return this.errorReply(`User '${name}' is offline. Use /force${cmd} if you're sure.`);
      if (currentGroup) {
        if (Users.globalAuth.has(userid)) {
          if (currentGroup === Users.Auth.defaultSymbol())
            return this.errorReply(`User '${name}' is already trusted.`);
          return this.errorReply(`User '${name}' has a global rank higher than trusted.`);
        }
      }
      if (targetUser) {
        targetUser.setGroup(Users.Auth.defaultSymbol(), true);
      } else {
        Users.globalAuth.set(userid, Users.Auth.defaultSymbol());
      }
      this.privateGlobalModAction(`${name} was set as a trusted user by ${user.name}.`);
      this.globalModlog("TRUSTUSER", userid);
    }
  },
  trustuserhelp: [
    `/trustuser [username] - Trusts the user (makes them immune to locks). Requires: &`,
    `/untrustuser [username] - Removes the trusted user status from the user. Requires: &`
  ],
  desectionleader: "sectionleader",
  sectionleader(target, room, user, connection, cmd) {
    this.checkCan("gdeclare");
    room = this.requireRoom();
    const demoting = cmd === "desectionleader";
    if (!target || target.split(",").length < 2 && !demoting)
      return this.parse(`/help sectionleader`);
    const { targetUser, targetUsername, rest: sectionid } = this.splitUser(target, { exactName: true });
    const userid = toID(targetUsername);
    const section = demoting ? Users.globalAuth.sectionLeaders.get(userid) : room.validateSection(sectionid);
    const name = targetUser ? targetUser.name : targetUsername;
    if (Users.globalAuth.sectionLeaders.has(targetUser?.id || userid) && !demoting) {
      throw new Chat.ErrorMessage(`${name} is already a Section Leader of ${import_room_settings.RoomSections.sectionNames[section]}.`);
    } else if (!Users.globalAuth.sectionLeaders.has(targetUser?.id || userid) && demoting) {
      throw new Chat.ErrorMessage(`${name} is not a Section Leader.`);
    }
    const staffRoom = Rooms.get("staff");
    if (!demoting) {
      Users.globalAuth.setSection(userid, section);
      this.addGlobalModAction(`${name} was appointed Section Leader of ${import_room_settings.RoomSections.sectionNames[section]} by ${user.name}.`);
      this.globalModlog(`SECTION LEADER`, userid, section);
      if (targetUser) {
        if (!Users.globalAuth.atLeast(targetUser, Users.SECTIONLEADER_SYMBOL)) {
          this.parse(`/globalsectionleader ${userid}`);
        }
      } else {
        this.sendReply(`User ${userid} is offline and unrecognized, and so can't be globally promoted.`);
      }
      targetUser?.popup(`You were appointed Section Leader of ${import_room_settings.RoomSections.sectionNames[section]} by ${user.name}.`);
    } else {
      const group = Users.globalAuth.get(userid);
      Users.globalAuth.deleteSection(userid);
      this.privateGlobalModAction(`${name} was demoted from Section Leader of ${import_room_settings.RoomSections.sectionNames[section]} by ${user.name}.`);
      if (group === " ")
        this.sendReply(`They are also no longer manually trusted. If they should be, use '/trustuser'.`);
      this.globalModlog(`DESECTION LEADER`, userid, section);
      if (staffRoom?.auth.getDirect(userid) === "\u25B8")
        this.parse(`/msgroom staff,/roomdeauth ${userid}`);
      targetUser?.popup(`You were demoted from Section Leader of ${import_room_settings.RoomSections.sectionNames[section]} by ${user.name}.`);
    }
    if (targetUser) {
      targetUser.updateIdentity();
      Rooms.global.checkAutojoin(targetUser);
      if (targetUser.trusted && !Users.isTrusted(targetUser.id)) {
        targetUser.trusted = "";
      }
    }
  },
  sectionleaderhelp: [
    `/sectionleader [target user], [sectionid] - Appoints [target user] Section Leader.`,
    `/desectionleader [target user] - Demotes [target user] from Section Leader.`,
    `Valid sections: ${import_room_settings.RoomSections.sections.join(", ")}`,
    `If you want to change which section someone leads, demote them and then re-promote them in the desired section.`,
    `Requires: &`
  ],
  globaldemote: "demote",
  demote(target) {
    if (!target)
      return this.parse("/help demote");
    this.run("promote");
  },
  demotehelp: [`/demote [username], [group] - Demotes the user to the specified group. Requires: &`],
  forcepromote(target, room, user, connection) {
    this.checkCan("forcepromote");
    const { targetUsername, rest: nextGroupName } = this.splitUser(target, { exactName: true });
    let name = this.filter(targetUsername);
    if (!name)
      return;
    name = name.slice(0, 18);
    const nextGroup = nextGroupName;
    if (!Config.groups[nextGroup])
      return this.errorReply(`Group '${nextGroup}' does not exist.`);
    if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) {
      return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
    }
    if (Users.isUsernameKnown(name)) {
      return this.errorReply("/forcepromote - Don't forcepromote unless you have to.");
    }
    Users.globalAuth.set(name, nextGroup);
    this.addGlobalModAction(`${name} was promoted to Global ${Config.groups[nextGroup].name || "regular user"} by ${user.name}.`);
    this.globalModlog(`GLOBAL${(Config.groups[nextGroup].name || "regular").toUpperCase()}`, toID(name));
  },
  devoice: "deauth",
  deauth(target, room, user) {
    return this.parse(`/demote ${target}, deauth`);
  },
  deglobalvoice: "globaldeauth",
  deglobalauth: "globaldeauth",
  globaldevoice: "globaldeauth",
  globaldeauth(target, room, user) {
    return this.parse(`/globaldemote ${target}, deauth`);
  },
  deroomvoice: "roomdeauth",
  roomdevoice: "roomdeauth",
  deroomauth: "roomdeauth",
  roomdeauth(target, room, user) {
    return this.parse(`/roomdemote ${target}, deauth`);
  },
  declare(target, room, user) {
    target = target.trim();
    if (!target)
      return this.parse("/help declare");
    room = this.requireRoom();
    this.checkCan("declare", null, room);
    this.checkChat();
    if (target.length > 2e3)
      return this.errorReply("Declares should not exceed 2000 characters.");
    for (const id in room.users) {
      room.users[id].sendTo(room, `|notify|${room.title} announcement!|${target}`);
    }
    this.add(import_lib.Utils.html`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
    this.modlog("DECLARE", null, target);
  },
  declarehelp: [`/declare [message] - Anonymously announces a message. Requires: # * &`],
  htmldeclare(target, room, user) {
    if (!target)
      return this.parse("/help htmldeclare");
    room = this.requireRoom();
    this.checkCan("declare", null, room);
    this.checkChat();
    this.checkHTML(target);
    for (const u in room.users) {
      Users.get(u)?.sendTo(
        room,
        `|notify|${room.title} announcement!|${import_lib.Utils.stripHTML(target)}`
      );
    }
    this.add(`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
    this.modlog(`HTMLDECLARE`, null, target);
  },
  htmldeclarehelp: [`/htmldeclare [message] - Anonymously announces a message using safe HTML. Requires: # * &`],
  gdeclare: "globaldeclare",
  globaldeclare(target, room, user) {
    if (!target)
      return this.parse("/help globaldeclare");
    this.checkCan("gdeclare");
    this.checkHTML(target);
    for (const u of Users.users.values()) {
      if (u.connected)
        u.send(`|pm|&|${u.tempGroup}${u.name}|/raw <div class="broadcast-blue"><b>${target}</b></div>`);
    }
    this.globalModlog(`GLOBALDECLARE`, null, target);
  },
  globaldeclarehelp: [`/globaldeclare [message] - Anonymously sends a private message to all the users on the site. Requires: &`],
  cdeclare: "chatdeclare",
  chatdeclare(target, room, user) {
    if (!target)
      return this.parse("/help chatdeclare");
    this.checkCan("gdeclare");
    this.checkHTML(target);
    for (const curRoom of Rooms.rooms.values()) {
      if (curRoom.type !== "battle") {
        curRoom.addRaw(`<div class="broadcast-blue"><b>${target}</b></div>`).update();
      }
    }
    this.globalModlog(`CHATDECLARE`, null, target);
  },
  chatdeclarehelp: [`/cdeclare [message] - Anonymously announces a message to all chatrooms on the server. Requires: &`],
  wall: "announce",
  announce(target, room, user) {
    if (!target)
      return this.parse("/help announce");
    if (room)
      this.checkCan("announce", null, room);
    this.checkChat(target);
    return `/announce ${target}`;
  },
  announcehelp: [`/announce OR /wall [message] - Makes an announcement. Requires: % @ # &`],
  notifyoffrank: "notifyrank",
  notifyrank(target, room, user, connection, cmd) {
    room = this.requireRoom();
    if (!target)
      return this.parse(`/help notifyrank`);
    this.checkCan("addhtml", null, room);
    this.checkChat();
    let [rank, titleNotification] = this.splitOne(target);
    if (rank === "all")
      rank = ` `;
    if (!(rank in Config.groups))
      return this.errorReply(`Group '${rank}' does not exist.`);
    const id = `${room.roomid}-rank-${Config.groups[rank].id || `all`}`;
    if (cmd === "notifyoffrank") {
      if (rank === " ") {
        room.send(`|tempnotifyoff|${id}`);
      } else {
        room.sendRankedUsers(`|tempnotifyoff|${id}`, rank);
      }
    } else {
      let [title, notificationHighlight] = this.splitOne(titleNotification);
      if (!title)
        title = `${room.title} ${Config.groups[rank].name ? `${Config.groups[rank].name}+ ` : ``}message!`;
      if (!user.can("addhtml")) {
        title += ` (notification from ${user.name})`;
      }
      const [notification, highlight] = this.splitOne(notificationHighlight);
      if (notification.length > 300)
        return this.errorReply(`Notifications should not exceed 300 characters.`);
      const message = `|tempnotify|${id}|${title}|${notification}${highlight ? `|${highlight}` : ``}`;
      if (rank === " ") {
        room.send(message);
      } else {
        room.sendRankedUsers(message, rank);
      }
      this.modlog(`NOTIFYRANK`, null, target);
    }
  },
  notifyrankhelp: [
    `/notifyrank [rank], [title], [message], [highlight] - Sends a notification to users who are [rank] or higher (and highlight on [highlight], if specified). Requires: # * &`,
    `/notifyoffrank [rank] - Closes the notification previously sent with /notifyrank [rank]. Requires: # * &`
  ],
  notifyoffuser: "notifyuser",
  notifyuser(target, room, user, connection, cmd) {
    room = this.requireRoom();
    if (!target)
      return this.parse(`/help notifyuser`);
    this.checkCan("addhtml", null, room);
    this.checkChat();
    const { targetUser, targetUsername, rest: titleNotification } = this.splitUser(target);
    if (!targetUser?.connected)
      return this.errorReply(`User '${targetUsername}' not found.`);
    const id = `${room.roomid}-user-${toID(targetUsername)}`;
    if (cmd === "notifyoffuser") {
      room.sendUser(targetUser, `|tempnotifyoff|${id}`);
      this.sendReply(`Closed the notification previously sent to ${targetUser.name}.`);
    } else {
      let [title, notification] = this.splitOne(titleNotification);
      if (!title)
        title = `${room.title} notification!`;
      if (!user.can("addhtml")) {
        title += ` (notification from ${user.name})`;
      }
      if (notification.length > 300)
        return this.errorReply(`Notifications should not exceed 300 characters.`);
      const message = `|tempnotify|${id}|${title}|${notification}`;
      room.sendUser(targetUser, message);
      this.sendReply(`Sent a notification to ${targetUser.name}.`);
    }
  },
  notifyuserhelp: [
    `/notifyuser [username], [title], [message] - Sends a notification to [user]. Requires: # * &`,
    `/notifyoffuser [user] - Closes the notification previously sent with /notifyuser [user]. Requires: # * &`
  ],
  fr: "forcerename",
  ofr: "forcerename",
  offlineforcerename: "forcerename",
  forcerename(target, room, user) {
    if (!target)
      return this.parse("/help forcerename");
    const { targetUser, targetUsername, rest: reason } = this.splitUser(target, { exactName: true });
    const offline = this.cmd.startsWith("o");
    const targetID = targetUser?.id || toID(targetUsername);
    if (!targetUser && !offline) {
      const { targetUser: targetUserInexact, inputUsername } = this.splitUser(target);
      if (targetUserInexact) {
        return this.errorReply(`User has already changed their name to '${targetUserInexact.name}'.`);
      }
      return this.errorReply(`User '${inputUsername}' not found. (use /offlineforcerename to rename anyway.)`);
    }
    if (Punishments.namefilterwhitelist.has(targetID)) {
      this.errorReply(`That name is blocked from being forcerenamed.`);
      if (user.can("bypassall")) {
        this.errorReply(`Use /noforcerename remove to remove it from the list if you wish to rename it.`);
      }
      return false;
    }
    this.checkCan("forcerename", targetID);
    const { publicReason, privateReason } = this.parseSpoiler(reason);
    Monitor.forceRenames.set(targetID, false);
    let forceRenameMessage;
    if (targetUser?.connected) {
      forceRenameMessage = `was forced to choose a new name by ${user.name}${publicReason ? `: ${publicReason}` : ``}`;
      this.globalModlog("FORCERENAME", targetUser, reason);
      Ladders.cancelSearches(targetUser);
      targetUser.send(`|nametaken||${user.name} considers your name inappropriate${publicReason ? `: ${publicReason}` : ``}`);
    } else {
      forceRenameMessage = `was forced to choose a new name by ${user.name} while offline${publicReason ? `: ${publicReason}` : ``}`;
      this.globalModlog("FORCERENAME OFFLINE", targetUser, privateReason);
    }
    Monitor.forceRenames.set(targetID, false);
    if (room?.roomid !== "staff") {
      if (room?.roomid.startsWith("help-")) {
        this.addModAction(`${targetUser?.name || targetID} ${forceRenameMessage}`);
      } else {
        this.privateModAction(`${targetUser?.name || targetID} ${forceRenameMessage}`);
      }
    }
    const roomMessage = this.pmTarget ? `<PM:${this.pmTarget.id}>` : room && room.roomid !== "staff" ? `\xAB<a href="/${room.roomid}" target="_blank">${room.roomid}</a>\xBB ` : "";
    const rankMessage = targetUser?.getAccountStatusString() || "";
    Rooms.global.notifyRooms(
      ["staff"],
      `|html|${roomMessage}` + import_lib.Utils.html`<span class="username">${targetUser?.name || targetID}</span> ${rankMessage} ${forceRenameMessage}`
    );
    targetUser?.resetName(true);
    return true;
  },
  forcerenamehelp: [
    `/forcerename OR /fr [username], [reason] - Forcibly change a user's name and shows them the [reason]. Requires: % @ &`,
    `/allowname [username] - Unmarks a forcerenamed username, stopping staff from being notified when it is used. Requires % @ &`
  ],
  nfr: "noforcerename",
  noforcerename: {
    add(target, room, user) {
      const [targetUsername, rest] = import_lib.Utils.splitFirst(target, ",").map((f) => f.trim());
      const targetId = toID(targetUsername);
      if (!targetId)
        return this.parse("/help noforcerename");
      this.checkCan("bypassall");
      if (!Punishments.whitelistName(targetId, user.name)) {
        return this.errorReply(`${targetUsername} is already on the noforcerename list.`);
      }
      this.addGlobalModAction(`${user.name} added the name ${targetId} to the no forcerename list.${rest ? ` (${rest})` : ""}`);
      this.globalModlog("NOFORCERENAME", targetId, rest);
    },
    remove(target, room, user) {
      const { targetUsername, rest } = this.splitUser(target);
      const targetId = toID(targetUsername);
      if (!targetId)
        return this.parse("/help noforcerename");
      this.checkCan("bypassall");
      if (!Punishments.namefilterwhitelist.has(targetId)) {
        return this.errorReply(`${targetUsername} is not on the noforcerename list.`);
      }
      Punishments.unwhitelistName(targetId);
      this.addGlobalModAction(`${user.name} removed ${targetId} from the no forcerename list.${rest ? ` (${rest})` : ""}`);
      this.globalModlog("UNNOFORCERENAME", targetId, rest);
    }
  },
  noforcerenamehelp: [
    `/noforcerename add OR /nfr add [username] - Adds [username] to the list of users who can't be forcerenamed by staff.`,
    `/noforcerename remove OR /nfr remove [username] - Removes [username] from the list of users who can't be forcerenamed by staff.`
  ],
  forceclearstatus(target, room, user) {
    const { targetUser, rest: reason } = this.requireUser(target, { allowOffline: true });
    this.checkCan("forcerename", targetUser);
    if (!targetUser.userMessage)
      return this.errorReply(this.tr`${targetUser.name} does not have a status set.`);
    const displayReason = reason ? `: ${reason}` : ``;
    this.privateGlobalModAction(this.tr`${targetUser.name}'s status "${targetUser.userMessage}" was cleared by ${user.name}${displayReason}.`);
    this.globalModlog("CLEARSTATUS", targetUser, ` from "${targetUser.userMessage}"${displayReason}`);
    targetUser.clearStatus();
    targetUser.popup(`${user.name} has cleared your status message for being inappropriate${displayReason || "."}`);
  },
  nl: "namelock",
  forcenamelock: "namelock",
  weeknamelock: "namelock",
  wnl: "namelock",
  fwnl: "namelock",
  forceweeknamelock: "namelock",
  async namelock(target, room, user, connection, cmd) {
    if (!target)
      return this.parse("/help namelock");
    const week = cmd.includes("w");
    const force = cmd.includes("f");
    const { targetUser, inputUsername, targetUsername, rest: reason } = this.splitUser(target);
    const userid = toID(targetUsername);
    if (!targetUser && !force) {
      return this.errorReply(
        `User '${targetUsername}' not found. Use \`\`/force${week ? "week" : ""}namelock\`\` if you need to namelock them anyway.`
      );
    }
    if (targetUser && targetUser.id !== toID(inputUsername) && !force) {
      return this.errorReply(`${inputUsername} has already changed their name to ${targetUser.name}. To namelock anyway, use /force${week ? "week" : ""}namelock.`);
    }
    this.checkCan("forcerename", userid);
    if (targetUser?.namelocked && !week) {
      return this.errorReply(`User '${targetUser.name}' is already namelocked.`);
    }
    if (!force && !week) {
      const existingPunishments = Punishments.search(userid);
      for (const [, , punishment] of existingPunishments) {
        if (punishment.type === "LOCK" && punishment.expireTime - Date.now() > 2 * DAY) {
          this.errorReply(`User '${userid}' is already normally locked for more than 2 days.`);
          this.errorReply(`Use /weeknamelock to namelock them instead, so you don't decrease the existing punishment.`);
          return this.errorReply(`If you really need to override this, use /forcenamelock.`);
        }
      }
    }
    const { privateReason, publicReason } = this.parseSpoiler(reason);
    const reasonText = publicReason ? ` (${publicReason})` : `.`;
    this.privateGlobalModAction(`${targetUser?.name || userid} was ${week ? "week" : ""}namelocked by ${user.name}${reasonText}`);
    this.globalModlog(`${force ? `FORCE` : ``}${week ? "WEEK" : ""}NAMELOCK`, targetUser || userid, privateReason);
    const roomauth = Rooms.global.destroyPersonalRooms(userid);
    if (roomauth.length) {
      Monitor.log(`[CrisisMonitor] Namelocked user ${userid} has public roomauth (${roomauth.join(", ")}), and should probably be demoted.`);
    }
    if (targetUser) {
      Ladders.cancelSearches(targetUser);
      targetUser.popup(`|modal|${user.name} has locked your name and you can't change names anymore${reasonText}`);
    }
    const duration = week ? 7 * 24 * 60 * 60 * 1e3 : 48 * 60 * 60 * 1e3;
    await Punishments.namelock(userid, Date.now() + duration, null, false, publicReason);
    if (room?.battle)
      this.parse("/savereplay forpunishment");
    Monitor.forceRenames.set(userid, false);
    if (connection.openPages) {
      for (const page of connection.openPages) {
        if (page.includes("usersearch-")) {
          this.refreshPage(page);
        }
      }
    }
    return true;
  },
  namelockhelp: [`/namelock OR /nl [user], [reason] - Name locks a [user] and shows the [reason]. Requires: % @ &`],
  unl: "unnamelock",
  unnamelock(target, room, user) {
    if (!target)
      return this.parse("/help unnamelock");
    this.checkCan("forcerename");
    const targetUser = Users.get(target);
    let reason = "";
    if (targetUser?.namelocked) {
      reason = ` (${targetUser.namelocked})`;
    }
    const unlocked = Punishments.unnamelock(target);
    if (!unlocked) {
      return this.errorReply(`User '${target}' is not namelocked.`);
    }
    this.addGlobalModAction(`${unlocked} was unnamelocked by ${user.name}.${reason}`);
    if (!reason)
      this.globalModlog("UNNAMELOCK", toID(target));
    if (targetUser)
      targetUser.popup(`${user.name} has unnamelocked you.`);
  },
  unnamelockhelp: [`/unnamelock [username] - Unnamelocks the user. Requires: % @ &`],
  hidetextalts: "hidetext",
  hidealttext: "hidetext",
  hidealtstext: "hidetext",
  htext: "hidetext",
  forcehidetext: "hidetext",
  hidelines: "hidetext",
  hlines: "hidetext",
  cleartext: "hidetext",
  ctext: "hidetext",
  clearaltstext: "hidetext",
  clearlines: "hidetext",
  forcecleartext: "hidetext",
  hidetext(target, room, user, connection, cmd) {
    if (!target)
      return this.parse(`/help hidetext`);
    room = this.requireRoom();
    const hasLineCount = cmd.includes("lines");
    const hideRevealButton = cmd.includes("clear") || cmd === "ctext";
    let { targetUser, inputUsername, targetUsername: name, rest: reason } = this.splitUser(target);
    let lineCount = 0;
    if (/^[0-9]+\s*(,|$)/.test(reason)) {
      if (hasLineCount) {
        let lineCountString;
        [lineCountString, reason] = import_lib.Utils.splitFirst(reason, ",").map((p) => p.trim());
        lineCount = parseInt(lineCountString);
      } else if (!cmd.includes("force")) {
        return this.errorReply(`Your reason was a number; use /hidelines if you wanted to clear a specific number of lines, or /forcehidetext if you really wanted your reason to be a number.`);
      }
    }
    const showAlts = cmd.includes("alt");
    if (!lineCount && hasLineCount) {
      return this.errorReply(`You must specify a number of messages to clear. To clear all messages, use /hidetext.`);
    }
    if (reason.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    if (!targetUser && !room.log.hasUsername(name)) {
      return this.errorReply(`User ${name} not found or has no roomlogs.`);
    }
    if (lineCount && showAlts) {
      return this.errorReply(`You can't specify a line count when using /hidealtstext.`);
    }
    const userid = toID(inputUsername);
    this.checkCan("mute", null, room);
    const sender = user === targetUser ? null : user;
    let message = "";
    if (targetUser && showAlts) {
      message = `${name}'s alts messages were cleared from ${room.title} by ${user.name}.${reason ? ` (${reason})` : ``}`;
      room.sendByUser(sender, message);
      this.modlog("HIDEALTSTEXT", targetUser, reason, { noip: 1 });
      room.hideText([
        userid,
        ...targetUser.previousIDs,
        ...targetUser.getAltUsers(true).map((curUser) => curUser.getLastId())
      ]);
    } else {
      if (lineCount > 0) {
        message = `${lineCount} of ${name}'s messages were cleared from ${room.title} by ${user.name}.${reason ? ` (${reason})` : ``}`;
        room.sendByUser(sender, message);
      } else {
        message = `${name}'s messages were cleared from ${room.title} by ${user.name}.${reason ? ` (${reason})` : ``}`;
        room.sendByUser(sender, message);
      }
      this.modlog("HIDETEXT", targetUser || userid, reason, { noip: 1, noalts: 1 });
      room.hideText([userid], lineCount, hideRevealButton);
      this.roomlog(`|c|${user.getIdentity()}|/log ${message}`);
    }
  },
  hidetexthelp: [
    `/hidetext [username], [optional reason] - Removes a user's messages from chat, with an optional reason. Requires: % @ # &`,
    `/hidealtstext [username], [optional reason] - Removes a user's messages and their alternate accounts' messages from the chat, with an optional reason.  Requires: % @ # &`,
    `/hidelines [username], [number], [optional reason] - Removes the [number] most recent messages from a user, with an optional reason. Requires: % @ # &`,
    `Use /cleartext, /clearaltstext, and /clearlines to remove messages without displaying a button to reveal them.`
  ],
  ab: "blacklist",
  bl: "blacklist",
  forceblacklist: "blacklist",
  forcebl: "blacklist",
  blacklist(target, room, user, connection, cmd) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help blacklist");
    this.checkChat();
    if (toID(target) === "show")
      return this.errorReply(`You're looking for /showbl`);
    const { targetUser, targetUsername, rest: reason } = this.splitUser(target);
    if (!targetUser) {
      this.errorReply(`User ${targetUsername} not found.`);
      return this.errorReply(`If you want to blacklist an offline account by name (not IP), consider /blacklistname`);
    }
    this.checkCan("editroom", targetUser, room);
    if (!room.persist) {
      return this.errorReply(`This room is not going to last long enough for a blacklist to matter - just ban the user`);
    }
    const punishment = Punishments.isRoomBanned(targetUser, room.roomid);
    if (punishment && punishment.type === "BLACKLIST") {
      return this.errorReply(`This user is already blacklisted from this room.`);
    }
    const force = cmd === "forceblacklist" || cmd === "forcebl";
    if (targetUser.trusted) {
      if (!force) {
        return this.sendReply(
          `${targetUser.name} is a trusted user. If you are sure you would like to blacklist them use /forceblacklist.`
        );
      }
    } else if (force) {
      return this.errorReply(`Use /blacklist; ${targetUser.name} is not a trusted user.`);
    }
    if (!reason && REQUIRE_REASONS) {
      return this.errorReply(`Blacklists require a reason.`);
    }
    if (reason.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    const name = targetUser.getLastName();
    const userid = targetUser.getLastId();
    if (targetUser.trusted && room.settings.isPrivate !== true) {
      Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name}${targetUser.trusted !== targetUser.id ? ` (${targetUser.trusted})` : ""} was blacklisted from ${room.roomid} by ${user.name}, and should probably be demoted.`);
    }
    if (targetUser.id in room.users || user.can("lock")) {
      targetUser.popup(
        `|modal||html|<p>${import_lib.Utils.escapeHTML(user.name)} has blacklisted you from the room ${room.roomid}${room.subRooms ? ` and its subrooms` : ""}. Reason: ${import_lib.Utils.escapeHTML(reason)}</p><p>To appeal the ban, PM the staff member that blacklisted you${room.persist ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.roomid}">List Room Staff</button></p>` : `.</p>`}`
      );
    }
    this.privateModAction(`${name} was blacklisted from ${room.title} by ${user.name}.${reason ? ` (${reason})` : ""}`);
    const affected = Punishments.roomBlacklist(room, targetUser, null, null, reason);
    if (!room.settings.isPrivate && room.persist) {
      const acAccount = targetUser.autoconfirmed !== userid && targetUser.autoconfirmed;
      let displayMessage = "";
      if (affected.length > 1) {
        displayMessage = `${name}'s ${acAccount ? ` ac account: ${acAccount},` : ""} blacklisted alts: ${affected.slice(1).map((curUser) => curUser.getLastName()).join(", ")}`;
        this.privateModAction(displayMessage);
      } else if (acAccount) {
        displayMessage = `${name}'s ac account: ${acAccount}`;
        this.privateModAction(displayMessage);
      }
    }
    if (!room.settings.isPrivate && room.persist) {
      this.globalModlog("BLACKLIST", targetUser, reason);
    } else {
      this.modlog("BLACKLIST", targetUser, reason);
    }
    return true;
  },
  blacklisthelp: [
    `/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # &`,
    `/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # &`,
    `/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # &`,
    `/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # &`
  ],
  forcebattleban: "battleban",
  async battleban(target, room, user, connection, cmd) {
    room = this.requireRoom();
    if (!target)
      return this.parse(`/help battleban`);
    const { targetUser, targetUsername, rest: reason } = this.splitUser(target);
    if (!targetUser)
      return this.errorReply(`User ${targetUsername} not found.`);
    if (target.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    if (!reason) {
      return this.errorReply(`Battle bans require a reason.`);
    }
    const includesUrl = reason.includes(`.${Config.routes.root}/`);
    if (!room.battle && !includesUrl && cmd !== "forcebattleban") {
      return this.errorReply(`Battle bans require a battle replay if used outside of a battle; if the battle has expired, use /forcebattleban.`);
    }
    if (!user.can("rangeban", targetUser)) {
      this.errorReply(`Battlebans have been deprecated. Alternatives:`);
      this.errorReply(`- timerstalling and bragging about it: lock`);
      this.errorReply(`- other timerstalling: they're not timerstalling, leave them alone`);
      this.errorReply(`- bad nicknames: lock, locks prevent nicknames from appearing; you should always have been locking for this`);
      this.errorReply(`- ladder cheating: gban, get a moderator if necessary`);
      this.errorReply(`- serious ladder cheating: permaban, get an administrator`);
      this.errorReply(`- other: get an administrator`);
      return;
    }
    if (Punishments.isBattleBanned(targetUser)) {
      return this.errorReply(`User '${targetUser.name}' is already banned from battling.`);
    }
    this.privateGlobalModAction(`${targetUser.name} was banned from starting new battles by ${user.name} (${reason})`);
    if (targetUser.trusted) {
      Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} was banned from battling by ${user.name}, and should probably be demoted.`);
    }
    this.globalModlog("BATTLEBAN", targetUser, reason);
    Ladders.cancelSearches(targetUser);
    await Punishments.battleban(targetUser, null, null, reason);
    targetUser.popup(`|modal|${user.name} has prevented you from starting new battles for 2 days (${reason})`);
    if (room.battle)
      this.parse("/savereplay forpunishment");
    return true;
  },
  battlebanhelp: [
    `/battleban [username], [reason] - [DEPRECATED]`,
    `Prevents the user from starting new battles for 2 days and shows them the [reason]. Requires: &`
  ],
  unbattleban(target, room, user) {
    if (!target)
      return this.parse("/help unbattleban");
    this.checkCan("lock");
    const targetUser = Users.get(target);
    const unbanned = Punishments.unbattleban(target);
    if (unbanned) {
      this.addModAction(`${unbanned} was allowed to battle again by ${user.name}.`);
      this.globalModlog("UNBATTLEBAN", toID(target));
      if (targetUser)
        targetUser.popup(`${user.name} has allowed you to battle again.`);
    } else {
      this.errorReply(`User ${target} is not banned from battling.`);
    }
  },
  unbattlebanhelp: [`/unbattleban [username] - [DEPRECATED] Allows a user to battle again. Requires: % @ &`],
  monthgroupchatban: "groupchatban",
  monthgcban: "groupchatban",
  gcban: "groupchatban",
  async groupchatban(target, room, user, connection, cmd) {
    room = this.requireRoom();
    if (!target)
      return this.parse(`/help groupchatban`);
    if (!user.can("rangeban")) {
      return this.errorReply(
        `/groupchatban has been deprecated.
For future groupchat misuse, lock the creator, it will take away their trusted status and their ability to make groupchats.`
      );
    }
    const { targetUser, targetUsername, rest: reason } = this.splitUser(target);
    if (!targetUser)
      return this.errorReply(`User ${targetUsername} not found.`);
    if (target.length > MAX_REASON_LENGTH) {
      return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
    }
    const isMonth = cmd.startsWith("month");
    if (!isMonth && Punishments.isGroupchatBanned(targetUser)) {
      return this.errorReply(`User '${targetUser.name}' is already banned from using groupchats.`);
    }
    const reasonText = reason ? `: ${reason}` : ``;
    this.privateGlobalModAction(`${targetUser.name} was banned from using groupchats for a ${isMonth ? "month" : "week"} by ${user.name}${reasonText}.`);
    if (targetUser.trusted) {
      Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} was banned from using groupchats by ${user.name}, and should probably be demoted.`);
    }
    const createdGroupchats = await Punishments.groupchatBan(
      targetUser,
      isMonth ? Date.now() + 30 * DAY : null,
      null,
      reason
    );
    targetUser.popup(`|modal|${user.name} has banned you from using groupchats for a ${isMonth ? "month" : "week"}${reasonText}`);
    this.globalModlog("GROUPCHATBAN", targetUser, ` by ${user.id}${reasonText}`);
    for (const roomid of createdGroupchats) {
      const targetRoom = Rooms.get(roomid);
      if (!targetRoom)
        continue;
      const participants = targetRoom.warnParticipants?.(
        `This groupchat (${targetRoom.title}) has been deleted due to inappropriate conduct by its creator, ${targetUser.name}. Do not attempt to recreate it, or you may be punished.${reason ? ` (reason: ${reason})` : ``}`
      );
      if (participants) {
        const modlogEntry = {
          action: "NOTE",
          loggedBy: user.id,
          isGlobal: true,
          note: `participants in ${roomid} (creator: ${targetUser.id}): ${participants.join(", ")}`
        };
        targetRoom.modlog(modlogEntry);
      }
      targetRoom.destroy();
    }
  },
  groupchatbanhelp: [
    `/groupchatban [user], [optional reason]`,
    `/monthgroupchatban [user], [optional reason]`,
    `Bans the user from joining or creating groupchats for a week (or month). Requires: % @ &`
  ],
  ungcban: "ungroupchatban",
  gcunban: "ungroupchatban",
  groucphatunban: "ungroupchatban",
  ungroupchatban(target, room, user) {
    if (!target)
      return this.parse("/help ungroupchatban");
    this.checkCan("lock");
    const targetUser = Users.get(target);
    const unbanned = Punishments.groupchatUnban(targetUser || toID(target));
    if (unbanned) {
      this.addGlobalModAction(`${unbanned} was ungroupchatbanned by ${user.name}.`);
      this.globalModlog("UNGROUPCHATBAN", toID(target), ` by ${user.id}`);
      if (targetUser)
        targetUser.popup(`${user.name} has allowed you to use groupchats again.`);
    } else {
      this.errorReply(`User ${target} is not banned from using groupchats.`);
    }
  },
  ungroupchatbanhelp: [`/ungroupchatban [user] - Allows a groupchatbanned user to use groupchats again. Requires: % @ &`],
  nameblacklist: "blacklistname",
  blacklistname(target, room, user) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help blacklistname");
    this.checkChat();
    this.checkCan("editroom", null, room);
    if (!room.persist) {
      return this.errorReply("This room is not going to last long enough for a blacklist to matter - just ban the user");
    }
    const [targetStr, reason] = target.split("|").map((val) => val.trim());
    if (!targetStr || !reason && REQUIRE_REASONS) {
      return this.errorReply("Usage: /blacklistname name1, name2, ... | reason");
    }
    const targets = targetStr.split(",").map((s) => toID(s));
    const duplicates = targets.filter((userid) => (
      // can be asserted, room should always exist
      Punishments.roomUserids.nestedGetByType(room.roomid, userid, "BLACKLIST")
    ));
    if (duplicates.length) {
      return this.errorReply(`[${duplicates.join(", ")}] ${Chat.plural(duplicates, "are", "is")} already blacklisted.`);
    }
    for (const userid of targets) {
      if (!userid)
        return this.errorReply(`User '${userid}' is not a valid userid.`);
      if (!Users.Auth.hasPermission(user, "ban", room.auth.get(userid), room)) {
        return this.errorReply(`/blacklistname - Access denied: ${userid} is of equal or higher authority than you.`);
      }
      Punishments.roomBlacklist(room, userid, null, null, reason);
      const trusted = Users.isTrusted(userid);
      if (trusted && room.settings.isPrivate !== true) {
        Monitor.log(`[CrisisMonitor] Trusted user ${userid}${trusted !== userid ? ` (${trusted})` : ``} was nameblacklisted from ${room.roomid} by ${user.name}, and should probably be demoted.`);
      }
      if (!room.settings.isPrivate && room.persist) {
        this.globalModlog("NAMEBLACKLIST", userid, reason);
      }
    }
    this.privateModAction(
      `${targets.join(", ")}${Chat.plural(targets, " were", " was")} nameblacklisted from ${room.title} by ${user.name}.`
    );
    return true;
  },
  blacklistnamehelp: [
    `/blacklistname OR /nameblacklist [name1, name2, etc.] | reason - Blacklists all name(s) from the room you are in for a year. Requires: # &`
  ],
  unab: "unblacklist",
  unblacklist(target, room, user) {
    room = this.requireRoom();
    if (!target)
      return this.parse("/help unblacklist");
    this.checkCan("editroom", null, room);
    const name = Punishments.roomUnblacklist(room, target);
    if (name) {
      this.privateModAction(`${name} was unblacklisted by ${user.name}.`);
      if (!room.settings.isPrivate && room.persist) {
        this.globalModlog("UNBLACKLIST", name);
      }
    } else {
      this.errorReply(`User '${target}' is not blacklisted.`);
    }
  },
  unblacklisthelp: [`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # &`],
  unblacklistall(target, room, user) {
    room = this.requireRoom();
    this.checkCan("editroom", null, room);
    if (!target) {
      user.lastCommand = "/unblacklistall";
      this.errorReply("THIS WILL UNBLACKLIST ALL BLACKLISTED USERS IN THIS ROOM.");
      this.errorReply("To confirm, use: /unblacklistall confirm");
      return;
    }
    if (user.lastCommand !== "/unblacklistall" || target !== "confirm") {
      return this.parse("/help unblacklistall");
    }
    user.lastCommand = "";
    const unblacklisted = Punishments.roomUnblacklistAll(room);
    if (!unblacklisted)
      return this.errorReply("No users are currently blacklisted in this room to unblacklist.");
    this.addModAction(`All blacklists in this room have been lifted by ${user.name}.`);
    this.modlog("UNBLACKLISTALL");
    this.roomlog(`Unblacklisted users: ${unblacklisted.join(", ")}`);
  },
  unblacklistallhelp: [`/unblacklistall - Unblacklists all blacklisted users in the current room. Requires: # &`],
  expiringbls: "showblacklist",
  expiringblacklists: "showblacklist",
  blacklists: "showblacklist",
  showbl: "showblacklist",
  showblacklist(target, room, user, connection, cmd) {
    if (target)
      room = Rooms.search(target);
    if (!room)
      return this.errorReply(`The room "${target}" was not found.`);
    this.checkCan("mute", null, room);
    const SOON_EXPIRING_TIME = 3 * 30 * 24 * 60 * 60 * 1e3;
    if (!room.persist)
      return this.errorReply("This room does not support blacklists.");
    const roomUserids = Punishments.roomUserids.get(room.roomid);
    if (!roomUserids || roomUserids.size === 0) {
      return this.sendReply("This room has no blacklisted users.");
    }
    const blMap = /* @__PURE__ */ new Map();
    let ips = "";
    for (const [userid, punishmentList] of roomUserids) {
      for (const punishment of punishmentList) {
        const { type, id, expireTime } = punishment;
        if (type === "BLACKLIST") {
          if (!blMap.has(id))
            blMap.set(id, [expireTime]);
          if (id !== userid)
            blMap.get(id).push(userid);
        }
      }
    }
    if (user.can("ip")) {
      const roomIps = Punishments.roomIps.get(room.roomid);
      if (roomIps) {
        ips = "/ips";
        for (const [ip, punishments] of roomIps) {
          for (const punishment of punishments) {
            const { type, id } = punishment;
            if (type === "BLACKLIST") {
              if (!blMap.has(id))
                blMap.set(id, []);
              blMap.get(id).push(ip);
            }
          }
        }
      }
    }
    const soonExpiring = cmd === "expiringblacklists" || cmd === "expiringbls";
    let buf = import_lib.Utils.html`Blacklist for ${room.title}${soonExpiring ? ` (expiring within 3 months)` : ""}:<br />`;
    for (const [userid, data] of blMap) {
      const [expireTime, ...alts] = data;
      if (soonExpiring && expireTime > Date.now() + SOON_EXPIRING_TIME)
        continue;
      const expiresIn = new Date(expireTime).getTime() - Date.now();
      const expiresDays = Math.round(expiresIn / 1e3 / 60 / 60 / 24);
      buf += `- <strong>${userid}</strong>, for ${Chat.count(expiresDays, "days")}`;
      if (alts.length)
        buf += `, alts${ips}: ${alts.join(", ")}`;
      buf += `<br />`;
    }
    this.sendReplyBox(buf);
  },
  showblacklisthelp: [
    `/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # &`,
    `/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # &`
  ]
};
//# sourceMappingURL=moderation.js.map
