"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var chat_exports = {}; __export(chat_exports, { Chat: () => Chat, CommandContext: () => CommandContext, ErrorMessage: () => ErrorMessage, Interruption: () => Interruption, MessageContext: () => MessageContext, PageContext: () => PageContext }); module.exports = __toCommonJS(chat_exports); var import_friends = require("./friends"); var import_lib = require("../lib"); var Artemis = __toESM(require("./artemis")); var import_sim = require("../sim"); var pathModule = __toESM(require("path")); var JSX = __toESM(require("./chat-jsx")); var import_chat_formatter = require("./chat-formatter"); /** * Chat * Pokemon Showdown - http://pokemonshowdown.com/ * * This handles chat and chat commands sent from users to chatrooms * and PMs. The main function you're looking for is Chat.parse * (scroll down to its definition for details) * * Individual commands are put in: * chat-commands/ - "core" commands that shouldn't be modified * chat-plugins/ - other commands that can be safely modified * * The command API is (mostly) documented in chat-plugins/COMMANDS.md * * @license MIT */ const LINK_WHITELIST = [ "*.pokemonshowdown.com", "psim.us", "smogtours.psim.us", "*.smogon.com", "*.pastebin.com", "*.hastebin.com" ]; const MAX_MESSAGE_LENGTH = 1e3; const BROADCAST_COOLDOWN = 20 * 1e3; const MESSAGE_COOLDOWN = 5 * 60 * 1e3; const MAX_PARSE_RECURSION = 10; const VALID_COMMAND_TOKENS = "/!"; const BROADCAST_TOKEN = "!"; const PLUGIN_DATABASE_PATH = "./databases/chat-plugins.db"; const MAX_PLUGIN_LOADING_DEPTH = 3; const ProbeModule = require("probe-image-size"); const probe = ProbeModule; const EMOJI_REGEX = /[\p{Emoji_Modifier_Base}\p{Emoji_Presentation}\uFE0F]/u; const TRANSLATION_DIRECTORY = pathModule.resolve(__dirname, "..", "translations"); class PatternTester { constructor() { this.elements = []; this.fastElements = /* @__PURE__ */ new Set(); this.regexp = null; } fastNormalize(elem) { return elem.slice(0, -1); } update() { const slowElements = this.elements.filter((elem) => !this.fastElements.has(this.fastNormalize(elem))); if (slowElements.length) { this.regexp = new RegExp("^(" + slowElements.map((elem) => "(?:" + elem + ")").join("|") + ")", "i"); } } register(...elems) { for (const elem of elems) { this.elements.push(elem); if (/^[^ ^$?|()[\]]+ $/.test(elem)) { this.fastElements.add(this.fastNormalize(elem)); } } this.update(); } testCommand(text) { const spaceIndex = text.indexOf(" "); if (this.fastElements.has(spaceIndex >= 0 ? text.slice(0, spaceIndex) : text)) { return true; } if (!this.regexp) return false; return this.regexp.test(text); } test(text) { if (!text.includes("\n")) return null; if (this.testCommand(text)) return text; const pmMatches = /^(\/(?:pm|w|whisper|msg) [^,]*, ?)(.*)/i.exec(text); if (pmMatches && this.testCommand(pmMatches[2])) { if (text.split("\n").every((line) => line.startsWith(pmMatches[1]))) { return text.replace(/\n\/(?:pm|w|whisper|msg) [^,]*, ?/g, "\n"); } return text; } return null; } } class ErrorMessage extends Error { constructor(message) { super(message); this.name = "ErrorMessage"; Error.captureStackTrace(this, ErrorMessage); } } class Interruption extends Error { constructor() { super(""); this.name = "Interruption"; Error.captureStackTrace(this, ErrorMessage); } } class MessageContext { constructor(user, language = null) { this.user = user; this.language = language; this.recursionDepth = 0; } splitOne(target) { const commaIndex = target.indexOf(","); if (commaIndex < 0) { return [target.trim(), ""]; } return [target.slice(0, commaIndex).trim(), target.slice(commaIndex + 1).trim()]; } meansYes(text) { switch (text.toLowerCase().trim()) { case "on": case "enable": case "yes": case "true": case "allow": case "1": return true; } return false; } meansNo(text) { switch (text.toLowerCase().trim()) { case "off": case "disable": case "no": case "false": case "disallow": case "0": return true; } return false; } /** * Given an array of strings (or a comma-delimited string), check the * first and last string for a format/mod/gen. If it exists, remove * it from the array. * * @returns `format` (null if no format was found), `dex` (the dex * for the format/mod, or the default dex if none was found), and * `targets` (the rest of the array). */ splitFormat(target, atLeastOneTarget) { const targets = typeof target === "string" ? target.split(",") : target; if (!targets[0].trim()) targets.pop(); if (targets.length > (atLeastOneTarget ? 1 : 0)) { const { dex: dex2, format: format2, isMatch } = this.extractFormat(targets[0].trim()); if (isMatch) { targets.shift(); return { dex: dex2, format: format2, targets }; } } if (targets.length > 1) { const { dex: dex2, format: format2, isMatch } = this.extractFormat(targets[targets.length - 1].trim()); if (isMatch) { targets.pop(); return { dex: dex2, format: format2, targets }; } } const room = this.room; const { dex, format } = this.extractFormat(room?.settings.defaultFormat || room?.battle?.format); return { dex, format, targets }; } extractFormat(formatOrMod) { if (!formatOrMod) { return { dex: import_sim.Dex.includeData(), format: null, isMatch: false }; } const format = import_sim.Dex.formats.get(formatOrMod); if (format.exists) { return { dex: import_sim.Dex.forFormat(format), format, isMatch: true }; } if (toID(formatOrMod) in import_sim.Dex.dexes) { return { dex: import_sim.Dex.mod(toID(formatOrMod)).includeData(), format: null, isMatch: true }; } return this.extractFormat(); } splitUser(target, { exactName } = {}) { const [inputUsername, rest] = this.splitOne(target).map((str) => str.trim()); const targetUser = Users.get(inputUsername, exactName); return { targetUser, inputUsername, targetUsername: targetUser ? targetUser.name : inputUsername, rest }; } requireUser(target, options = {}) { const { targetUser, targetUsername, rest } = this.splitUser(target, options); if (!targetUser) { throw new Chat.ErrorMessage(`The user "${targetUsername}" is offline or misspelled.`); } if (!options.allowOffline && !targetUser.connected) { throw new Chat.ErrorMessage(`The user "${targetUsername}" is offline.`); } return { targetUser, rest }; } getUserOrSelf(target, { exactName } = {}) { if (!target.trim()) return this.user; return Users.get(target, exactName); } tr(strings, ...keys) { return Chat.tr(this.language, strings, ...keys); } } class PageContext extends MessageContext { constructor(options) { super(options.user, options.language); this.connection = options.connection; this.room = null; this.pageid = options.pageid; this.args = this.pageid.split("-"); this.initialized = false; this.title = "Page"; } checkCan(permission, target = null, room = null) { if (!this.user.can(permission, target, room)) { throw new Chat.ErrorMessage(`

Permission denied.

`); } return true; } privatelyCheckCan(permission, target = null, room = null) { if (!this.user.can(permission, target, room)) { this.pageDoesNotExist(); } return true; } pageDoesNotExist() { throw new Chat.ErrorMessage(`Page "${this.pageid}" not found`); } requireRoom(pageid) { const room = this.extractRoom(pageid); if (!room) { throw new Chat.ErrorMessage(`Invalid link: This page requires a room ID.`); } this.room = room; return room; } extractRoom(pageid) { if (!pageid) pageid = this.pageid; const parts = pageid.split("-"); const room = Rooms.get(parts[2]) || null; this.room = room; return room; } setHTML(html) { const roomid = this.room ? `[${this.room.roomid}] ` : ""; let content = `|title|${roomid}${this.title} |pagehtml|${html}`; if (!this.initialized) { content = `|init|html ${content}`; this.initialized = true; } this.send(content); } errorReply(message) { this.setHTML(`

${message}

`); } send(content) { this.connection.send(`>${this.pageid} ${content}`); } close() { this.send("|deinit"); } async resolve(pageid) { if (pageid) this.pageid = pageid; const parts = this.pageid.split("-"); parts.shift(); if (!this.connection.openPages) this.connection.openPages = /* @__PURE__ */ new Set(); this.connection.openPages.add(parts.join("-")); let handler = Chat.pages; while (handler) { if (typeof handler === "function") { break; } handler = handler[parts.shift() || "default"] || handler[""]; } this.args = parts; let res; try { if (typeof handler !== "function") this.pageDoesNotExist(); res = await handler.call(this, parts, this.user, this.connection); } catch (err) { if (err.name?.endsWith("ErrorMessage")) { if (err.message) this.errorReply(err.message); return; } if (err.name.endsWith("Interruption")) { return; } Monitor.crashlog(err, "A chat page", { user: this.user.name, room: this.room && this.room.roomid, pageid: this.pageid }); this.setHTML( `
Pokemon Showdown crashed!
Don't worry, we're working on fixing it.
` ); } if (typeof res === "object" && res) res = JSX.render(res); if (typeof res === "string") { this.setHTML(res); res = void 0; } return res; } } class CommandContext extends MessageContext { constructor(options) { super( options.user, options.room && options.room.settings.language ? options.room.settings.language : options.user.language ); this.message = options.message || ``; this.recursionDepth = options.recursionDepth || 0; this.pmTarget = options.pmTarget || null; this.room = options.room || null; this.connection = options.connection; this.cmd = options.cmd || ""; this.cmdToken = options.cmdToken || ""; this.target = options.target || ``; this.fullCmd = options.fullCmd || ""; this.handler = null; this.isQuiet = options.isQuiet || false; this.bypassRoomCheck = options.bypassRoomCheck || false; this.broadcasting = false; this.broadcastToRoom = true; this.broadcastPrefix = options.broadcastPrefix || ""; this.broadcastMessage = ""; } // TODO: return should be void | boolean | Promise parse(msg, options = {}) { if (typeof msg === "string") { const subcontext = new CommandContext({ message: msg, user: this.user, connection: this.connection, room: this.room, pmTarget: this.pmTarget, recursionDepth: this.recursionDepth + 1, bypassRoomCheck: this.bypassRoomCheck, ...options }); if (subcontext.recursionDepth > MAX_PARSE_RECURSION) { throw new Error("Too much command recursion"); } return subcontext.parse(); } let message = this.message; const parsedCommand = Chat.parseCommand(message); if (parsedCommand) { this.cmd = parsedCommand.cmd; this.fullCmd = parsedCommand.fullCmd; this.cmdToken = parsedCommand.cmdToken; this.target = parsedCommand.target; this.handler = parsedCommand.handler; } if (!this.bypassRoomCheck && this.room && !(this.user.id in this.room.users)) { return this.popupReply(`You tried to send "${message}" to the room "${this.room.roomid}" but it failed because you were not in that room.`); } if (this.user.statusType === "idle" && !["unaway", "unafk", "back"].includes(this.cmd)) { this.user.setStatusType("online"); } try { if (this.handler) { if (this.handler.disabled) { throw new Chat.ErrorMessage( `The command /${this.cmd} is temporarily unavailable due to technical difficulties. Please try again in a few hours.` ); } message = this.run(this.handler); } else { if (this.cmdToken) { if (!(this.shouldBroadcast() && !/[a-z0-9]/.test(this.cmd.charAt(0)))) { this.commandDoesNotExist(); } } else if (!VALID_COMMAND_TOKENS.includes(message.charAt(0)) && VALID_COMMAND_TOKENS.includes(message.trim().charAt(0))) { message = message.trim(); if (!message.startsWith(BROADCAST_TOKEN)) { message = message.charAt(0) + message; } } message = this.checkChat(message); } } catch (err) { if (err.name?.endsWith("ErrorMessage")) { this.errorReply(err.message); this.update(); return false; } if (err.name.endsWith("Interruption")) { this.update(); return; } Monitor.crashlog(err, "A chat command", { user: this.user.name, room: this.room?.roomid, pmTarget: this.pmTarget?.name, message: this.message }); this.sendReply(`|html|
Pokemon Showdown crashed!
Don't worry, we're working on fixing it.
`); return; } if (message && typeof message.then === "function") { this.update(); return message.then((resolvedMessage) => { if (resolvedMessage && resolvedMessage !== true) { this.sendChatMessage(resolvedMessage); } this.update(); if (resolvedMessage === false) return false; }).catch((err) => { if (err.name?.endsWith("ErrorMessage")) { this.errorReply(err.message); this.update(); return false; } if (err.name.endsWith("Interruption")) { this.update(); return; } Monitor.crashlog(err, "An async chat command", { user: this.user.name, room: this.room?.roomid, pmTarget: this.pmTarget?.name, message: this.message }); this.sendReply(`|html|
Pokemon Showdown crashed!
Don't worry, we're working on fixing it.
`); return false; }); } else if (message && message !== true) { this.sendChatMessage(message); message = true; } this.update(); return message; } sendChatMessage(message) { if (this.pmTarget) { const blockInvites = this.pmTarget.settings.blockInvites; if (blockInvites && /^<<.*>>$/.test(message.trim())) { if (!this.user.can("lock") && blockInvites === true || !Users.globalAuth.atLeast(this.user, blockInvites)) { Chat.maybeNotifyBlocked(`invite`, this.pmTarget, this.user); return this.errorReply(`${this.pmTarget.name} is blocking room invites.`); } } Chat.sendPM(message, this.user, this.pmTarget); } else if (this.room) { this.room.add(`|c|${this.user.getIdentity(this.room)}|${message}`); if (this.room.game && this.room.game.onLogMessage) { this.room.game.onLogMessage(message, this.user); } } else { this.connection.popup(`Your message could not be sent: ${message} It needs to be sent to a user or room.`); } } run(handler) { if (typeof handler === "string") handler = Chat.commands[handler]; if (!handler.broadcastable && this.cmdToken === "!") { this.errorReply(`The command "${this.fullCmd}" can't be broadcast.`); this.errorReply(`Use /${this.fullCmd} instead.`); return false; } let result = handler.call(this, this.target, this.room, this.user, this.connection, this.cmd, this.message); if (result === void 0) result = false; return result; } checkFormat(room, user, message) { if (!room) return true; if (!room.settings.filterStretching && !room.settings.filterCaps && !room.settings.filterEmojis && !room.settings.filterLinks) { return true; } if (user.can("mute", null, room)) return true; if (room.settings.filterStretching && /(.+?)\1{5,}/i.test(user.name)) { throw new Chat.ErrorMessage(`Your username contains too much stretching, which this room doesn't allow.`); } if (room.settings.filterLinks) { const bannedLinks = this.checkBannedLinks(message); if (bannedLinks.length) { throw new Chat.ErrorMessage( `You have linked to ${bannedLinks.length > 1 ? "unrecognized external websites" : "an unrecognized external website"} (${bannedLinks.join(", ")}), which this room doesn't allow.` ); } } if (room.settings.filterCaps && /[A-Z\s]{6,}/.test(user.name)) { throw new Chat.ErrorMessage(`Your username contains too many capital letters, which this room doesn't allow.`); } if (room.settings.filterEmojis && EMOJI_REGEX.test(user.name)) { throw new Chat.ErrorMessage(`Your username contains emojis, which this room doesn't allow.`); } message = message.trim().replace(/[ \u0000\u200B-\u200F]+/g, " "); if (room.settings.filterStretching && /(.+?)\1{7,}/i.test(message)) { throw new Chat.ErrorMessage(`Your message contains too much stretching, which this room doesn't allow.`); } if (room.settings.filterCaps && /[A-Z\s]{18,}/.test(message)) { throw new Chat.ErrorMessage(`Your message contains too many capital letters, which this room doesn't allow.`); } if (room.settings.filterEmojis && EMOJI_REGEX.test(message)) { throw new Chat.ErrorMessage(`Your message contains emojis, which this room doesn't allow.`); } return true; } checkSlowchat(room, user) { if (!room?.settings.slowchat) return true; if (user.can("show", null, room)) return true; const lastActiveSeconds = (Date.now() - user.lastMessageTime) / 1e3; if (lastActiveSeconds < room.settings.slowchat) { throw new Chat.ErrorMessage(this.tr`This room has slow-chat enabled. You can only talk once every ${room.settings.slowchat} seconds.`); } return true; } checkBanwords(room, message) { if (!room) return true; if (!room.banwordRegex) { if (room.settings.banwords && room.settings.banwords.length) { room.banwordRegex = new RegExp("(?:\\b|(?!\\w))(?:" + room.settings.banwords.join("|") + ")(?:\\b|\\B(?!\\w))", "i"); } else { room.banwordRegex = true; } } if (!message) return true; if (room.banwordRegex !== true && room.banwordRegex.test(message)) { throw new Chat.ErrorMessage(`Your username, status, or message contained a word banned by this room.`); } return this.checkBanwords(room.parent, message); } checkGameFilter() { if (!this.room?.game || !this.room.game.onChatMessage) return; return this.room.game.onChatMessage(this.message, this.user); } pmTransform(originalMessage, sender, receiver) { if (!sender) { if (this.room) throw new Error(`Not a PM`); sender = this.user; receiver = this.pmTarget; } const targetIdentity = typeof receiver === "string" ? ` ${receiver}` : receiver ? receiver.getIdentity() : "~"; const prefix = `|pm|${sender.getIdentity()}|${targetIdentity}|`; return originalMessage.split("\n").map((message) => { if (message.startsWith("||")) { return prefix + `/text ` + message.slice(2); } else if (message.startsWith(`|html|`)) { return prefix + `/raw ` + message.slice(6); } else if (message.startsWith(`|uhtml|`)) { const [uhtmlid, html] = import_lib.Utils.splitFirst(message.slice(7), "|"); return prefix + `/uhtml ${uhtmlid},${html}`; } else if (message.startsWith(`|uhtmlchange|`)) { const [uhtmlid, html] = import_lib.Utils.splitFirst(message.slice(13), "|"); return prefix + `/uhtmlchange ${uhtmlid},${html}`; } else if (message.startsWith(`|modaction|`)) { return prefix + `/log ` + message.slice(11); } else if (message.startsWith(`|raw|`)) { return prefix + `/raw ` + message.slice(5); } else if (message.startsWith(`|error|`)) { return prefix + `/error ` + message.slice(7); } else if (message.startsWith(`|c~|`)) { return prefix + message.slice(4); } else if (message.startsWith(`|c|~|/`)) { return prefix + message.slice(5); } else if (message.startsWith(`|c|~|`)) { return prefix + `/text ` + message.slice(5); } return prefix + `/text ` + message; }).join(` `); } sendReply(data) { if (this.isQuiet) return; if (this.broadcasting && this.broadcastToRoom) { this.add(data); } else { if (!this.room) { data = this.pmTransform(data); this.connection.send(data); } else { this.connection.sendTo(this.room, data); } } } errorReply(message) { if (this.bypassRoomCheck) { return this.popupReply( `|html|${message.replace(/\n/ig, "
")}
` ); } this.sendReply(`|error|` + message.replace(/\n/g, ` |error|`)); } addBox(htmlContent) { if (typeof htmlContent !== "string") htmlContent = JSX.render(htmlContent); this.add(`|html|
${htmlContent}
`); } sendReplyBox(htmlContent) { if (typeof htmlContent !== "string") htmlContent = JSX.render(htmlContent); this.sendReply(`|c|${this.room && this.broadcasting ? this.user.getIdentity() : "~"}|/raw
${htmlContent}
`); } popupReply(message) { this.connection.popup(message); } add(data) { if (this.room) { this.room.add(data); } else { this.send(data); } } send(data) { if (this.room) { this.room.send(data); } else { data = this.pmTransform(data); this.user.send(data); if (this.pmTarget && this.pmTarget !== this.user) { this.pmTarget.send(data); } } } /** like privateModAction, but also notify Staff room */ privateGlobalModAction(msg) { this.privateModAction(msg); if (this.room?.roomid !== "staff") { Rooms.get("staff")?.addByUser(this.user, `${this.room ? `<<${this.room.roomid}>>` : ``} ${msg}`).update(); } } addGlobalModAction(msg) { this.addModAction(msg); if (this.room?.roomid !== "staff") { Rooms.get("staff")?.addByUser(this.user, `${this.room ? `<<${this.room.roomid}>>` : ``} ${msg}`).update(); } } privateModAction(msg) { if (this.room) { if (this.room.roomid === "staff") { this.room.addByUser(this.user, `(${msg})`); } else { this.room.sendModsByUser(this.user, `(${msg})`); this.roomlog(`(${msg})`); } } else { const data = this.pmTransform(`|modaction|${msg}`); this.user.send(data); if (this.pmTarget && this.pmTarget !== this.user && this.pmTarget.isStaff) { this.pmTarget.send(data); } } } globalModlog(action, user = null, note = null, ip) { const entry = { action, isGlobal: true, loggedBy: this.user.id, note: note?.replace(/\n/gm, " ") || "" }; if (user) { if (typeof user === "string") { entry.userid = toID(user); } else { entry.ip = user.latestIp; const userid = user.getLastId(); entry.userid = userid; if (user.autoconfirmed && user.autoconfirmed !== userid) entry.autoconfirmedID = user.autoconfirmed; const alts = user.getAltUsers(false, true).slice(1).map((alt) => alt.getLastId()); if (alts.length) entry.alts = alts; } } if (ip) entry.ip = ip; if (this.room) { this.room.modlog(entry); } else { Rooms.global.modlog(entry); } } modlog(action, user = null, note = null, options = {}) { const entry = { action, loggedBy: this.user.id, note: note?.replace(/\n/gm, " ") || "" }; if (user) { if (typeof user === "string") { entry.userid = toID(user); } else { const userid = user.getLastId(); entry.userid = userid; if (!options.noalts) { if (user.autoconfirmed && user.autoconfirmed !== userid) entry.autoconfirmedID = user.autoconfirmed; const alts = user.getAltUsers(false, true).slice(1).map((alt) => alt.getLastId()); if (alts.length) entry.alts = alts; } if (!options.noip) entry.ip = user.latestIp; } } (this.room || Rooms.global).modlog(entry); } parseSpoiler(reason) { if (!reason) return { publicReason: "", privateReason: "" }; let publicReason = reason; let privateReason = reason; const targetLowercase = reason.toLowerCase(); if (targetLowercase.includes("spoiler:") || targetLowercase.includes("spoilers:")) { const proofIndex = targetLowercase.indexOf(targetLowercase.includes("spoilers:") ? "spoilers:" : "spoiler:"); const proofOffset = targetLowercase.includes("spoilers:") ? 9 : 8; const proof = reason.slice(proofIndex + proofOffset).trim(); publicReason = reason.slice(0, proofIndex).trim(); privateReason = `${publicReason}${proof ? ` (PROOF: ${proof})` : ""}`; } return { publicReason, privateReason }; } roomlog(data) { if (this.room) this.room.roomlog(data); } stafflog(data) { (Rooms.get("staff") || Rooms.lobby || this.room)?.roomlog(data); } addModAction(msg) { if (this.room) { this.room.addByUser(this.user, msg); } else { this.send(`|modaction|${msg}`); } } update() { if (this.room) this.room.update(); } filter(message) { return Chat.filter(message, this); } statusfilter(status) { return Chat.statusfilter(status, this.user); } checkCan(permission, target = null, room = null) { if (!Users.Auth.hasPermission(this.user, permission, target, room, this.fullCmd)) { throw new Chat.ErrorMessage(`${this.cmdToken}${this.fullCmd} - Access denied.`); } } privatelyCheckCan(permission, target = null, room = null) { this.handler.isPrivate = true; if (Users.Auth.hasPermission(this.user, permission, target, room, this.fullCmd)) { return true; } this.commandDoesNotExist(); } canUseConsole() { if (!this.user.hasConsoleAccess(this.connection)) { throw new Chat.ErrorMessage( this.cmdToken + this.fullCmd + " - Requires console access, please set up `Config.consoleips`." ); } return true; } shouldBroadcast() { return this.cmdToken === BROADCAST_TOKEN; } checkBroadcast(overrideCooldown, suppressMessage) { if (this.broadcasting || !this.shouldBroadcast()) { return true; } if (this.user.locked && !(this.room?.roomid.startsWith("help-") || this.pmTarget?.can("lock"))) { this.errorReply(`You cannot broadcast this command's information while locked.`); throw new Chat.ErrorMessage(`To see it for yourself, use: /${this.message.slice(1)}`); } if (this.room && !this.user.can("show", null, this.room)) { this.errorReply(`You need to be voiced to broadcast this command's information.`); throw new Chat.ErrorMessage(`To see it for yourself, use: /${this.message.slice(1)}`); } if (!this.room && !this.pmTarget) { this.errorReply(`Broadcasting a command with "!" in a PM or chatroom will show it that user or room.`); throw new Chat.ErrorMessage(`To see it for yourself, use: /${this.message.slice(1)}`); } const broadcastMessage = (suppressMessage || this.message).toLowerCase().replace(/[^a-z0-9\s!,]/g, ""); const cooldownMessage = overrideCooldown === true ? null : overrideCooldown || broadcastMessage; if (cooldownMessage && this.room && this.room.lastBroadcast === cooldownMessage && this.room.lastBroadcastTime >= Date.now() - BROADCAST_COOLDOWN) { throw new Chat.ErrorMessage(`You can't broadcast this because it was just broadcasted. If this was intentional, use !rebroadcast ${this.message}`); } const message = this.checkChat(suppressMessage || this.message); if (!message) { throw new Chat.ErrorMessage(`To see it for yourself, use: /${this.message.slice(1)}`); } this.message = message; this.broadcastMessage = broadcastMessage; return true; } runBroadcast(overrideCooldown, suppressMessage = null) { if (this.broadcasting || !this.shouldBroadcast()) { return true; } if (!this.broadcastMessage) { this.checkBroadcast(overrideCooldown, suppressMessage); } this.broadcasting = true; const message = `${this.broadcastPrefix}${suppressMessage || this.message}`; if (this.pmTarget) { this.sendReply(`|c~|${message}`); } else { this.sendReply(`|c|${this.user.getIdentity(this.room)}|${message}`); } if (this.room) { this.language = this.room.settings.language || null; if (overrideCooldown !== true) { this.room.lastBroadcast = overrideCooldown || this.broadcastMessage; this.room.lastBroadcastTime = Date.now(); } } return true; } checkChat(message = null, room = null, targetUser = null) { if (!targetUser && this.pmTarget) { targetUser = this.pmTarget; } if (targetUser) { room = null; } else if (!room) { room = this.room; } const user = this.user; const connection = this.connection; if (!user.named) { throw new Chat.ErrorMessage(this.tr`You must choose a name before you can talk.`); } if (!user.can("bypassall")) { const lockType = user.namelocked ? this.tr`namelocked` : user.locked ? this.tr`locked` : ``; const lockExpiration = Punishments.checkLockExpiration(user.namelocked || user.locked); if (room) { if (lockType && !room.settings.isHelp) { this.sendReply(`|html|${this.tr`Get help with this`}`); if (user.locked === "#hostfilter") { throw new Chat.ErrorMessage(this.tr`You are locked due to your proxy / VPN and can't talk in chat.`); } else { throw new Chat.ErrorMessage(this.tr`You are ${lockType} and can't talk in chat. ${lockExpiration}`); } } if (!room.persist && !room.roomid.startsWith("help-") && !(user.registered || user.autoconfirmed)) { this.sendReply( this.tr`|html|
You must be registered to chat in temporary rooms (like battles).
` + this.tr`You may register in the menu.` ); throw new Chat.Interruption(); } if (room.isMuted(user)) { throw new Chat.ErrorMessage(this.tr`You are muted and cannot talk in this room.`); } if (room.settings.modchat && !room.auth.atLeast(user, room.settings.modchat)) { if (room.settings.modchat === "autoconfirmed") { throw new Chat.ErrorMessage( this.tr`Because moderated chat is set, your account must be at least one week old and you must have won at least one ladder game to speak in this room.` ); } if (room.settings.modchat === "trusted") { throw new Chat.ErrorMessage( this.tr`Because moderated chat is set, your account must be staff in a public room or have a global rank to speak in this room.` ); } const groupName = Config.groups[room.settings.modchat] && Config.groups[room.settings.modchat].name || room.settings.modchat; throw new Chat.ErrorMessage( this.tr`Because moderated chat is set, you must be of rank ${groupName} or higher to speak in this room.` ); } if (!this.bypassRoomCheck && !(user.id in room.users)) { connection.popup(`You can't send a message to this room without being in it.`); return null; } } if (targetUser) { if (!(user.registered || user.autoconfirmed)) { this.sendReply( this.tr`|html|
You must be registered to send private messages.
` + this.tr`You may register in the menu.` ); throw new Chat.Interruption(); } if (targetUser.id !== user.id && !(targetUser.registered || targetUser.autoconfirmed)) { throw new Chat.ErrorMessage(this.tr`That user is unregistered and cannot be PMed.`); } if (lockType && !targetUser.can("lock")) { this.sendReply(`|html|${this.tr`Get help with this`}`); if (user.locked === "#hostfilter") { throw new Chat.ErrorMessage(this.tr`You are locked due to your proxy / VPN and can only private message members of the global moderation team.`); } else { throw new Chat.ErrorMessage(this.tr`You are ${lockType} and can only private message members of the global moderation team. ${lockExpiration}`); } } if (targetUser.locked && !user.can("lock")) { throw new Chat.ErrorMessage(this.tr`The user "${targetUser.name}" is locked and cannot be PMed.`); } if (Config.pmmodchat && !Users.globalAuth.atLeast(user, Config.pmmodchat) && !Users.Auth.hasPermission(targetUser, "promote", Config.pmmodchat)) { const groupName = Config.groups[Config.pmmodchat] && Config.groups[Config.pmmodchat].name || Config.pmmodchat; throw new Chat.ErrorMessage(this.tr`On this server, you must be of rank ${groupName} or higher to PM users.`); } if (!this.checkCanPM(targetUser)) { Chat.maybeNotifyBlocked("pm", targetUser, user); if (!targetUser.can("lock")) { throw new Chat.ErrorMessage(this.tr`This user is blocking private messages right now.`); } else { this.sendReply(`|html|${this.tr`If you need help, try opening a help ticket`}`); throw new Chat.ErrorMessage(this.tr`This ${Config.groups[targetUser.tempGroup].name} is too busy to answer private messages right now. Please contact a different staff member.`); } } if (!this.checkCanPM(user, targetUser)) { throw new Chat.ErrorMessage(this.tr`You are blocking private messages right now.`); } } } if (typeof message !== "string") return true; if (!message) { throw new Chat.ErrorMessage(this.tr`Your message can't be blank.`); } let length = message.length; length += 10 * message.replace(/[^\ufdfd]*/g, "").length; if (length > MAX_MESSAGE_LENGTH && !user.can("ignorelimits")) { throw new Chat.ErrorMessage(this.tr`Your message is too long: ` + message); } message = message.replace( /[\u0300-\u036f\u0483-\u0489\u0610-\u0615\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06ED\u0E31\u0E34-\u0E3A\u0E47-\u0E4E]{3,}/g, "" ); if (/[\u3164\u115f\u1160\u239b-\u23b9]/.test(message)) { throw new Chat.ErrorMessage(this.tr`Your message contains banned characters.`); } if (Config.restrictLinks && !user.autoconfirmed) { if (this.checkBannedLinks(message).length && !(targetUser?.can("lock") || room?.settings.isHelp)) { throw new Chat.ErrorMessage("Your account must be autoconfirmed to send links to other users, except for global staff."); } } this.checkFormat(room, user, message); this.checkSlowchat(room, user); if (!user.can("bypassall")) this.checkBanwords(room, user.name); if (user.userMessage && !user.can("bypassall")) this.checkBanwords(room, user.userMessage); if (room && !user.can("mute", null, room)) this.checkBanwords(room, message); const gameFilter = this.checkGameFilter(); if (typeof gameFilter === "string") { if (gameFilter === "") throw new Chat.Interruption(); throw new Chat.ErrorMessage(gameFilter); } if (room?.settings.highTraffic && toID(message).replace(/[^a-z]+/, "").length < 2 && !user.can("show", null, room)) { throw new Chat.ErrorMessage( this.tr`Due to this room being a high traffic room, your message must contain at least two letters.` ); } if (room) { const normalized = message.trim(); if (!user.can("bypassall") && ["help", "lobby"].includes(room.roomid) && normalized === user.lastMessage && Date.now() - user.lastMessageTime < MESSAGE_COOLDOWN && !Config.nothrottle) { throw new Chat.ErrorMessage(this.tr`You can't send the same message again so soon.`); } user.lastMessage = message; user.lastMessageTime = Date.now(); } if (Chat.filters.length) { return this.filter(message); } return message; } checkCanPM(targetUser, user) { if (!user) user = this.user; if (user.id === targetUser.id) return true; const setting = targetUser.settings.blockPMs; if (user.can("lock") || !setting) return true; if (setting === true && !user.can("lock")) return false; const friends = targetUser.friends || /* @__PURE__ */ new Set(); if (setting === "friends") return friends.has(user.id); return Users.globalAuth.atLeast(user, setting); } checkPMHTML(targetUser) { if (!(this.room && targetUser.id in this.room.users) && !this.user.can("addhtml")) { throw new Chat.ErrorMessage("You do not have permission to use PM HTML to users who are not in this room."); } const friends = targetUser.friends || /* @__PURE__ */ new Set(); if (targetUser.settings.blockPMs && (targetUser.settings.blockPMs === true || targetUser.settings.blockPMs === "friends" && !friends.has(this.user.id) || !Users.globalAuth.atLeast(this.user, targetUser.settings.blockPMs)) && !this.user.can("lock")) { Chat.maybeNotifyBlocked("pm", targetUser, this.user); throw new Chat.ErrorMessage("This user is currently blocking PMs."); } if (targetUser.locked && !this.user.can("lock")) { throw new Chat.ErrorMessage("This user is currently locked, so you cannot send them HTML."); } return true; } checkBannedLinks(message) { return (message.match(Chat.linkRegex) || []).filter((link) => { link = link.toLowerCase(); const domainMatches = /^(?:http:\/\/|https:\/\/)?(?:[^/]*\.)?([^/.]*\.[^/.]*)\.?($|\/|:)/.exec(link); const domain = domainMatches?.[1]; const hostMatches = /^(?:http:\/\/|https:\/\/)?([^/]*[^/.])\.?($|\/|:)/.exec(link); let host = hostMatches?.[1]; if (host?.startsWith("www.")) host = host.slice(4); if (!domain || !host) return null; return !(LINK_WHITELIST.includes(host) || LINK_WHITELIST.includes(`*.${domain}`)); }); } /* eslint-enable @typescript-eslint/prefer-optional-chain */ checkEmbedURI(uri) { if (uri.startsWith("https://")) return uri; if (uri.startsWith("//")) return uri; if (uri.startsWith("data:")) { return uri; } else { throw new Chat.ErrorMessage("Image URLs must begin with 'https://' or 'data:'; 'http://' cannot be used."); } } /** * This is a quick and dirty first-pass "is this good HTML" check. The full * sanitization is done on the client by Caja in `src/battle-log.ts` * `BattleLog.sanitizeHTML`. */ checkHTML(htmlContent) { htmlContent = ("" + (htmlContent || "")).trim(); if (!htmlContent) return ""; if (/>here.?]]'); } const tags = htmlContent.match(/|<\/?[^<>]*/g); if (tags) { const ILLEGAL_TAGS = [ "script", "head", "body", "html", "canvas", "base", "meta", "link", "iframe" ]; const LEGAL_AUTOCLOSE_TAGS = [ // void elements (no-close tags) "br", "area", "embed", "hr", "img", "source", "track", "input", "wbr", "col", // autoclose tags "p", "li", "dt", "dd", "option", "tr", "th", "td", "thead", "tbody", "tfoot", "colgroup", // PS custom element "psicon", "youtube" ]; const stack = []; for (const tag of tags) { const isClosingTag = tag.charAt(1) === "/"; const contentEndLoc = tag.endsWith("/") ? -1 : void 0; const tagContent = tag.slice(isClosingTag ? 2 : 1, contentEndLoc).replace(/\s+/, " ").trim(); const tagNameEndIndex = tagContent.indexOf(" "); const tagName = tagContent.slice(0, tagNameEndIndex >= 0 ? tagNameEndIndex : void 0).toLowerCase(); if (tagName === "!--") continue; if (isClosingTag) { if (LEGAL_AUTOCLOSE_TAGS.includes(tagName)) continue; if (!stack.length) { throw new Chat.ErrorMessage(`Extraneous without an opening tag.`); } const expectedTagName = stack.pop(); if (tagName !== expectedTagName) { throw new Chat.ErrorMessage(`Extraneous where was expected.`); } continue; } if (ILLEGAL_TAGS.includes(tagName) || !/^[a-z]+[0-9]?$/.test(tagName)) { throw new Chat.ErrorMessage(`Illegal tag <${tagName}> can't be used here.`); } if (!LEGAL_AUTOCLOSE_TAGS.includes(tagName)) { stack.push(tagName); } if (tagName === "img") { if (!this.room || this.room.settings.isPersonal && !this.user.can("lock")) { throw new Chat.ErrorMessage( `This tag is not allowed: <${tagContent}>. Images are not allowed outside of chatrooms.` ); } if (!/width ?= ?(?:[0-9]+|"[0-9]+")/i.test(tagContent) || !/height ?= ?(?:[0-9]+|"[0-9]+")/i.test(tagContent)) { this.errorReply(`This image is missing a width/height attribute: <${tagContent}>`); throw new Chat.ErrorMessage(`Images without predefined width/height cause problems with scrolling because loading them changes their height.`); } const srcMatch = / src ?= ?(?:"|')?([^ "']+)(?: ?(?:"|'))?/i.exec(tagContent); if (srcMatch) { this.checkEmbedURI(srcMatch[1]); } else { this.errorReply(`This image has a broken src attribute: <${tagContent}>`); throw new Chat.ErrorMessage(`The src attribute must exist and have no spaces in the URL`); } } if (tagName === "button") { if ((!this.room || this.room.settings.isPersonal || this.room.settings.isPrivate === true) && !this.user.can("lock")) { const buttonName = / name ?= ?"([^"]*)"/i.exec(tagContent)?.[1]; const buttonValue = / value ?= ?"([^"]*)"/i.exec(tagContent)?.[1]; const msgCommandRegex = /^\/(?:msg|pm|w|whisper|botmsg) /; const botmsgCommandRegex = /^\/msgroom (?:[a-z0-9-]+), ?\/botmsg /; if (buttonName === "send" && buttonValue && msgCommandRegex.test(buttonValue)) { const [pmTarget] = buttonValue.replace(msgCommandRegex, "").split(","); const auth = this.room ? this.room.auth : Users.globalAuth; if (auth.get(toID(pmTarget)) !== "*" && toID(pmTarget) !== this.user.id) { this.errorReply(`This button is not allowed: <${tagContent}>`); throw new Chat.ErrorMessage(`Your scripted button can't send PMs to ${pmTarget}, because that user is not a Room Bot.`); } } else if (buttonName === "send" && buttonValue && botmsgCommandRegex.test(buttonValue)) { } else if (buttonName) { this.errorReply(`This button is not allowed: <${tagContent}>`); this.errorReply(`You do not have permission to use most buttons. Here are the two types you're allowed to use:`); this.errorReply(`1. Linking to a room: `); throw new Chat.ErrorMessage(`2. Sending a message to a Bot: `); } } } } if (stack.length) { throw new Chat.ErrorMessage(`Missing .`); } } return htmlContent; } /** * This is to be used for commands that replicate other commands * (for example, `/pm username, command` or `/msgroom roomid, command`) * to ensure they do not crash with too many levels of recursion. */ checkRecursion() { if (this.recursionDepth > 5) { throw new Chat.ErrorMessage(`/${this.cmd} - Too much command recursion has occurred.`); } } requireRoom(id) { if (!this.room) { throw new Chat.ErrorMessage(`/${this.cmd} - must be used in a chat room, not a ${this.pmTarget ? "PM" : "console"}`); } if (id && this.room.roomid !== id) { const targetRoom = Rooms.get(id); if (!targetRoom) { throw new Chat.ErrorMessage(`This command can only be used in the room '${id}', but that room does not exist.`); } throw new Chat.ErrorMessage(`This command can only be used in the ${targetRoom.title} room.`); } return this.room; } // eslint-disable-next-line @typescript-eslint/type-annotation-spacing requireGame(constructor, subGame = false) { const room = this.requireRoom(); if (subGame) { if (!room.subGame) { throw new Chat.ErrorMessage(`This command requires a sub-game of ${constructor.name} (this room has no sub-game).`); } const game2 = room.getGame(constructor, subGame); if (!game2) { throw new Chat.ErrorMessage(`This command requires a sub-game of ${constructor.name} (this sub-game is ${room.subGame.title}).`); } return game2; } if (!room.game) { throw new Chat.ErrorMessage(`This command requires a game of ${constructor.name} (this room has no game).`); } const game = room.getGame(constructor); if (!game) { throw new Chat.ErrorMessage(`This command requires a game of ${constructor.name} (this game is ${room.game.title}).`); } return game; } requireMinorActivity(constructor) { const room = this.requireRoom(); if (!room.minorActivity) { throw new Chat.ErrorMessage(`This command requires a ${constructor.name} (this room has no minor activity).`); } const game = room.getMinorActivity(constructor); if (!game) { throw new Chat.ErrorMessage(`This command requires a ${constructor.name} (this minor activity is a(n) ${room.minorActivity.name}).`); } return game; } commandDoesNotExist() { if (this.cmdToken === "!") { throw new Chat.ErrorMessage(`The command "${this.cmdToken}${this.fullCmd}" does not exist.`); } throw new Chat.ErrorMessage( `The command "${this.cmdToken}${this.fullCmd}" does not exist. To send a message starting with "${this.cmdToken}${this.fullCmd}", type "${this.cmdToken}${this.cmdToken}${this.fullCmd}".` ); } refreshPage(pageid) { if (this.connection.openPages?.has(pageid)) { this.parse(`/join view-${pageid}`); } } closePage(pageid) { for (const connection of this.user.connections) { if (connection.openPages?.has(pageid)) { connection.send(`>view-${pageid} |deinit`); connection.openPages.delete(pageid); if (!connection.openPages.size) { connection.openPages = null; } } } } } const Chat = new class { constructor() { this.translationsLoaded = false; /** * As per the node.js documentation at https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_args, * timers with durations that are too long for a 32-bit signed integer will be invoked after 1 millisecond, * which tends to cause unexpected behavior. */ this.MAX_TIMEOUT_DURATION = 2147483647; this.Friends = new import_friends.FriendsDatabase(); this.PM = import_friends.PM; this.multiLinePattern = new PatternTester(); this.destroyHandlers = [Artemis.destroy]; this.crqHandlers = {}; this.handlers = /* @__PURE__ */ Object.create(null); /** The key is the name of the plugin. */ this.plugins = {}; /** Will be empty except during hotpatch */ this.oldPlugins = {}; this.roomSettings = []; /********************************************************* * Load chat filters *********************************************************/ this.filters = []; this.namefilters = []; this.hostfilters = []; this.loginfilters = []; this.punishmentfilters = []; this.nicknamefilters = []; this.statusfilters = []; /********************************************************* * Translations *********************************************************/ /** language id -> language name */ this.languages = /* @__PURE__ */ new Map(); /** language id -> (english string -> translated string) */ this.translations = /* @__PURE__ */ new Map(); /** * SQL handler * * All chat plugins share one database. * Chat.databaseReadyPromise will be truthy if the database is not yet ready. */ this.database = (0, import_lib.SQL)(module, { file: "Config" in global && Config.nofswriting ? ":memory:" : PLUGIN_DATABASE_PATH, processes: global.Config?.chatdbprocesses || 1 }); this.databaseReadyPromise = null; this.MessageContext = MessageContext; this.CommandContext = CommandContext; this.PageContext = PageContext; this.ErrorMessage = ErrorMessage; this.Interruption = Interruption; // JSX handling this.JSX = JSX; this.html = JSX.html; this.h = JSX.h; this.Fragment = JSX.Fragment; this.packageData = {}; this.formatText = import_chat_formatter.formatText; this.linkRegex = import_chat_formatter.linkRegex; this.stripFormatting = import_chat_formatter.stripFormatting; this.filterWords = {}; this.monitors = {}; void this.loadTranslations().then(() => { Chat.translationsLoaded = true; }); } filter(message, context) { const originalMessage = message; for (const curFilter of Chat.filters) { const output = curFilter.call( context, message, context.user, context.room, context.connection, context.pmTarget, originalMessage ); if (output === false) return null; if (!output && output !== void 0) return output; if (output !== void 0) message = output; } return message; } namefilter(name, user) { if (!Config.disablebasicnamefilter) { name = name.replace( // eslint-disable-next-line no-misleading-character-class /[^a-zA-Z0-9 /\\.~()<>^*%&=+$#_'?!"\u00A1-\u00BF\u00D7\u00F7\u02B9-\u0362\u2012-\u2027\u2030-\u205E\u2050-\u205F\u2190-\u23FA\u2500-\u2BD1\u2E80-\u32FF\u3400-\u9FFF\uF900-\uFAFF\uFE00-\uFE6F-]+/g, "" ); name = name.replace(/[\u00a1\u2580-\u2590\u25A0\u25Ac\u25AE\u25B0\u2a0d\u534d\u5350]/g, ""); if (name.includes("@") && name.includes(".")) return ""; if (/[a-z0-9]\.(com|net|org|us|uk|co|gg|tk|ml|gq|ga|xxx|download|stream)\b/i.test(name)) name = name.replace(/\./g, ""); const nameSymbols = name.replace( /[^\u00A1-\u00BF\u00D7\u00F7\u02B9-\u0362\u2012-\u2027\u2030-\u205E\u2050-\u205F\u2090-\u23FA\u2500-\u2BD1]+/g, "" ); if (nameSymbols.length > 4 || /[^a-z0-9][a-z0-9][^a-z0-9]/.test(name.toLowerCase() + " ") || /[\u00ae\u00a9].*[a-zA-Z0-9]/.test(name)) { name = name.replace( // eslint-disable-next-line no-misleading-character-class /[\u00A1-\u00BF\u00D7\u00F7\u02B9-\u0362\u2012-\u2027\u2030-\u205E\u2050-\u205F\u2190-\u23FA\u2500-\u2BD1\u2E80-\u32FF\u3400-\u9FFF\uF900-\uFAFF\uFE00-\uFE6F]+/g, "" ).replace(/[^A-Za-z0-9]{2,}/g, " ").trim(); } } name = name.replace(/^[^A-Za-z0-9]+/, ""); name = name.replace(/@/g, ""); if (/[A-Za-z0-9]/.test(name.slice(18))) { name = name.replace(/[^A-Za-z0-9]+/g, ""); } else { name = name.slice(0, 18); } name = import_sim.Dex.getName(name); for (const curFilter of Chat.namefilters) { name = curFilter(name, user); if (!name) return ""; } return name; } hostfilter(host, user, connection, hostType) { for (const curFilter of Chat.hostfilters) { curFilter(host, user, connection, hostType); } } loginfilter(user, oldUser, usertype) { for (const curFilter of Chat.loginfilters) { curFilter(user, oldUser, usertype); } } punishmentfilter(user, punishment) { for (const curFilter of Chat.punishmentfilters) { curFilter(user, punishment); } } nicknamefilter(nickname, user) { for (const curFilter of Chat.nicknamefilters) { const filtered = curFilter(nickname, user); if (filtered === false) return false; if (!filtered) return ""; } return nickname; } statusfilter(status, user) { status = status.replace(/\|/g, ""); for (const curFilter of Chat.statusfilters) { status = curFilter(status, user); if (!status) return ""; } return status; } async loadTranslations() { const directories = await (0, import_lib.FS)(TRANSLATION_DIRECTORY).readdir(); Chat.languages.set("english", "English"); for (const dirname of directories) { if (/[^a-z0-9]/.test(dirname)) continue; const dir = (0, import_lib.FS)(`${TRANSLATION_DIRECTORY}/${dirname}`); const languageID = import_sim.Dex.toID(dirname); const files = await dir.readdir(); for (const filename of files) { if (!filename.endsWith(".js")) continue; const content = require(`${TRANSLATION_DIRECTORY}/${dirname}/${filename}`).translations; if (!Chat.translations.has(languageID)) { Chat.translations.set(languageID, /* @__PURE__ */ new Map()); } const translationsSoFar = Chat.translations.get(languageID); if (content.name && !Chat.languages.has(languageID)) { Chat.languages.set(languageID, content.name); } if (content.strings) { for (const key in content.strings) { const keyLabels = []; const valLabels = []; const newKey = key.replace(/\${.+?}/g, (str) => { keyLabels.push(str); return "${}"; }).replace(/\[TN: ?.+?\]/g, ""); const val = content.strings[key].replace(/\${.+?}/g, (str) => { valLabels.push(str); return "${}"; }).replace(/\[TN: ?.+?\]/g, ""); translationsSoFar.set(newKey, [val, keyLabels, valLabels]); } } } if (!Chat.languages.has(languageID)) { Chat.languages.set(languageID, "Unknown Language"); } } } tr(language, strings = "", ...keys) { if (!language) language = "english"; const trString = typeof strings === "string" ? strings : strings.join("${}"); if (Chat.translationsLoaded && !Chat.translations.has(language)) { throw new Error(`Trying to translate to a nonexistent language: ${language}`); } if (!strings.length) { return (fStrings, ...fKeys) => Chat.tr(language, fStrings, ...fKeys); } const entry = Chat.translations.get(language)?.get(trString); let [translated, keyLabels, valLabels] = entry || ["", [], []]; if (!translated) translated = trString; if (keys.length) { let reconstructed = ""; const left = keyLabels.slice(); for (const [i, str] of translated.split("${}").entries()) { reconstructed += str; if (keys[i]) { let index = left.indexOf(valLabels[i]); if (index < 0) { index = left.findIndex((val) => !!val); } if (index < 0) index = i; reconstructed += keys[index]; left[index] = null; } } translated = reconstructed; } return translated; } async prepareDatabase() { if (!import_friends.PM.isParentProcess) return; if (!Config.usesqlite) return; const { hasDBInfo } = await this.database.get( `SELECT count(*) AS hasDBInfo FROM sqlite_master WHERE type = 'table' AND name = 'db_info'` ); if (!hasDBInfo) await this.database.runFile("./databases/schemas/chat-plugins.sql"); const result = await this.database.get( `SELECT value as curVersion FROM db_info WHERE key = 'version'` ); const curVersion = parseInt(result.curVersion); if (!curVersion) throw new Error(`db_info table is present, but schema version could not be parsed`); const migrationsFolder = "./databases/migrations/chat-plugins"; const migrationsToRun = []; for (const migrationFile of await (0, import_lib.FS)(migrationsFolder).readdir()) { const migrationVersion = parseInt(/v(\d+)\.sql$/.exec(migrationFile)?.[1] || ""); if (!migrationVersion) continue; if (migrationVersion > curVersion) { migrationsToRun.push({ version: migrationVersion, file: migrationFile }); Monitor.adminlog(`Pushing to migrationsToRun: ${migrationVersion} at ${migrationFile} - mainModule ${process.mainModule === module} !process.send ${!process.send}`); } } import_lib.Utils.sortBy(migrationsToRun, ({ version }) => version); for (const { file } of migrationsToRun) { await this.database.runFile(pathModule.resolve(migrationsFolder, file)); } Chat.destroyHandlers.push(() => void Chat.database?.destroy()); } /** * Command parser * * Usage: * Chat.parse(message, room, user, connection) * * Parses the message. If it's a command, the command is executed, if * not, it's displayed directly in the room. * * Examples: * Chat.parse("/join lobby", room, user, connection) * will make the user join the lobby. * * Chat.parse("Hi, guys!", room, user, connection) * will return "Hi, guys!" if the user isn't muted, or * if he's muted, will warn him that he's muted. * * The return value is the return value of the command handler, if any, * or the message, if there wasn't a command. This value could be a success * or failure (few commands report these) or a Promise for when the command * is done executing, if it's not currently done. * * @param message - the message the user is trying to say * @param room - the room the user is trying to say it in * @param user - the user that sent the message * @param connection - the connection the user sent the message from */ parse(message, room, user, connection) { Chat.loadPlugins(); const initialRoomlogLength = room?.log.getLineCount(); const context = new CommandContext({ message, room, user, connection }); const start = Date.now(); const result = context.parse(); if (typeof result?.then === "function") { void result.then(() => { this.logSlowMessage(start, context); }); } else { this.logSlowMessage(start, context); } if (room && room.log.getLineCount() !== initialRoomlogLength) { room.messagesSent++; for (const [handler, numMessages] of room.nthMessageHandlers) { if (room.messagesSent % numMessages === 0) handler(room, message); } } return result; } logSlowMessage(start, context) { const timeUsed = Date.now() - start; if (timeUsed < 1e3) return; if (context.cmd === "search" || context.cmd === "savereplay") return; const logMessage = `[slow command] ${timeUsed}ms - ${context.user.name} (${context.connection.ip}): <${context.room ? context.room.roomid : context.pmTarget ? `PM:${context.pmTarget?.name}` : "CMD"}> ${context.message.replace(/\n/ig, " ")}`; Monitor.slow(logMessage); } sendPM(message, user, pmTarget, onlyRecipient = null) { const buf = `|pm|${user.getIdentity()}|${pmTarget.getIdentity()}|${message}`; if (onlyRecipient) return onlyRecipient.send(buf); user.send(buf); if (pmTarget !== user) pmTarget.send(buf); pmTarget.lastPM = user.id; user.lastPM = pmTarget.id; } getPluginName(file) { const nameWithExt = pathModule.relative(__dirname, file).replace(/^chat-(?:commands|plugins)./, ""); let name = nameWithExt.slice(0, nameWithExt.lastIndexOf(".")); if (name.endsWith("/index")) name = name.slice(0, -6); return name; } loadPluginFile(file) { if (!file.endsWith(".js")) return; this.loadPlugin(require(file), this.getPluginName(file)); } loadPluginDirectory(dir, depth = 0) { for (const file of (0, import_lib.FS)(dir).readdirSync()) { const path = pathModule.resolve(dir, file); if ((0, import_lib.FS)(path).isDirectorySync()) { depth++; if (depth > MAX_PLUGIN_LOADING_DEPTH) continue; this.loadPluginDirectory(path, depth); } else { try { this.loadPluginFile(path); } catch (e) { Monitor.crashlog(e, "A loading chat plugin"); continue; } } } } annotateCommands(commandTable, namespace = "") { for (const cmd2 in commandTable) { const entry = commandTable[cmd2]; if (typeof entry === "object") { this.annotateCommands(entry, `${namespace}${cmd2} `); } if (typeof entry === "string") { const base = commandTable[entry]; if (!base) continue; if (!base.aliases) base.aliases = []; if (!base.aliases.includes(cmd2)) base.aliases.push(cmd2); continue; } if (typeof entry !== "function") continue; const handlerCode = entry.toString(); entry.requiresRoom = /requireRoom\((?:'|"|`)(.*?)(?:'|"|`)/.exec(handlerCode)?.[1] || /this\.requireRoom\(/.test(handlerCode); entry.hasRoomPermissions = /\bthis\.(checkCan|can)\([^,)\n]*, [^,)\n]*,/.test(handlerCode); entry.broadcastable = cmd2.endsWith("help") || /\bthis\.(?:(check|can|run|should)Broadcast)\(/.test(handlerCode); entry.isPrivate = /\bthis\.(?:privately(Check)?Can|commandDoesNotExist)\(/.test(handlerCode); entry.requiredPermission = /this\.(?:checkCan|privately(?:Check)?Can)\(['`"]([a-zA-Z0-9]+)['"`](\)|, )/.exec(handlerCode)?.[1]; if (!entry.aliases) entry.aliases = []; const runsCommand = /this.run\((?:'|"|`)(.*?)(?:'|"|`)\)/.exec(handlerCode); if (runsCommand) { const [, baseCommand] = runsCommand; const baseEntry = commandTable[baseCommand]; if (baseEntry) { if (baseEntry.requiresRoom) entry.requiresRoom = baseEntry.requiresRoom; if (baseEntry.hasRoomPermissions) entry.hasRoomPermissions = baseEntry.hasRoomPermissions; if (baseEntry.broadcastable) entry.broadcastable = baseEntry.broadcastable; if (baseEntry.isPrivate) entry.isPrivate = baseEntry.isPrivate; } } entry.cmd = cmd2; entry.fullCmd = `${namespace}${cmd2}`; } return commandTable; } loadPlugin(plugin, name) { plugin = { ...plugin }; if (plugin.commands) { Object.assign(Chat.commands, this.annotateCommands(plugin.commands)); } if (plugin.pages) { Object.assign(Chat.pages, plugin.pages); } if (plugin.destroy) { Chat.destroyHandlers.push(plugin.destroy); } if (plugin.crqHandlers) { Object.assign(Chat.crqHandlers, plugin.crqHandlers); } if (plugin.roomSettings) { if (!Array.isArray(plugin.roomSettings)) plugin.roomSettings = [plugin.roomSettings]; Chat.roomSettings = Chat.roomSettings.concat(plugin.roomSettings); } if (plugin.chatfilter) Chat.filters.push(plugin.chatfilter); if (plugin.namefilter) Chat.namefilters.push(plugin.namefilter); if (plugin.hostfilter) Chat.hostfilters.push(plugin.hostfilter); if (plugin.loginfilter) Chat.loginfilters.push(plugin.loginfilter); if (plugin.punishmentfilter) Chat.punishmentfilters.push(plugin.punishmentfilter); if (plugin.nicknamefilter) Chat.nicknamefilters.push(plugin.nicknamefilter); if (plugin.statusfilter) Chat.statusfilters.push(plugin.statusfilter); if (plugin.onRenameRoom) { if (!Chat.handlers["onRenameRoom"]) Chat.handlers["onRenameRoom"] = []; Chat.handlers["onRenameRoom"].push(plugin.onRenameRoom); } if (plugin.onRoomClose) { if (!Chat.handlers["onRoomClose"]) Chat.handlers["onRoomClose"] = []; Chat.handlers["onRoomClose"].push(plugin.onRoomClose); } if (plugin.handlers) { for (const handlerName in plugin.handlers) { if (!Chat.handlers[handlerName]) Chat.handlers[handlerName] = []; Chat.handlers[handlerName].push(plugin.handlers[handlerName]); } } Chat.plugins[name] = plugin; } loadPlugins(oldPlugins) { if (Chat.commands) return; if (oldPlugins) Chat.oldPlugins = oldPlugins; void (0, import_lib.FS)("package.json").readIfExists().then((data) => { if (data) Chat.packageData = JSON.parse(data); }); Chat.commands = /* @__PURE__ */ Object.create(null); Chat.pages = /* @__PURE__ */ Object.create(null); this.loadPluginDirectory("dist/server/chat-commands"); Chat.baseCommands = Chat.commands; Chat.basePages = Chat.pages; Chat.commands = Object.assign(/* @__PURE__ */ Object.create(null), Chat.baseCommands); Chat.pages = Object.assign(/* @__PURE__ */ Object.create(null), Chat.basePages); this.loadPlugin(Config, "config"); this.loadPlugin(Tournaments, "tournaments"); this.loadPluginDirectory("dist/server/chat-plugins"); Chat.oldPlugins = {}; import_lib.Utils.sortBy(Chat.filters, (filter) => -(filter.priority || 0)); } destroy() { for (const handler of Chat.destroyHandlers) { handler(); } } runHandlers(name, ...args) { const handlers = this.handlers[name]; if (!handlers) return; for (const h of handlers) { void h.call(this, ...args); } } handleRoomRename(oldID, newID, room) { Chat.runHandlers("onRenameRoom", oldID, newID, room); } handleRoomClose(roomid, user, connection) { Chat.runHandlers("onRoomClose", roomid, user, connection, roomid.startsWith("view-")); } /** * Takes a chat message and returns data about any command it's * trying to use. * * Returning `null` means the chat message isn't trying to use * a command, and returning `{handler: null}` means it's trying * to use a command that doesn't exist. */ parseCommand(message, recursing = false) { if (!message.trim()) return null; if (message.startsWith(`>> `)) { message = `/eval ${message.slice(3)}`; } else if (message.startsWith(`>>> `)) { message = `/evalbattle ${message.slice(4)}`; } else if (message.startsWith(">>sql ")) { message = `/evalsql ${message.slice(6)}`; } else if (message.startsWith(`/me`) && /[^A-Za-z0-9 ]/.test(message.charAt(3))) { message = `/mee ${message.slice(3)}`; } else if (message.startsWith(`/ME`) && /[^A-Za-z0-9 ]/.test(message.charAt(3))) { message = `/MEE ${message.slice(3)}`; } const cmdToken = message.charAt(0); if (!VALID_COMMAND_TOKENS.includes(cmdToken)) return null; if (cmdToken === message.charAt(1)) return null; if (cmdToken === BROADCAST_TOKEN && /[^A-Za-z0-9]/.test(message.charAt(1))) return null; let [cmd2, target] = import_lib.Utils.splitFirst(message.slice(1), " "); cmd2 = cmd2.toLowerCase(); if (cmd2.endsWith(",")) cmd2 = cmd2.slice(0, -1); let curCommands = Chat.commands; let commandHandler; let fullCmd = cmd2; let prevCmdName = ""; do { if (cmd2 in curCommands) { commandHandler = curCommands[cmd2]; } else { commandHandler = void 0; } if (typeof commandHandler === "string") { commandHandler = curCommands[commandHandler]; } else if (Array.isArray(commandHandler) && !recursing) { return this.parseCommand(cmdToken + "help " + fullCmd.slice(0, -4), true); } if (commandHandler && typeof commandHandler === "object") { [cmd2, target] = import_lib.Utils.splitFirst(target, " "); cmd2 = cmd2.toLowerCase(); prevCmdName = fullCmd; fullCmd += " " + cmd2; curCommands = commandHandler; } } while (commandHandler && typeof commandHandler === "object"); if (!commandHandler && (curCommands.default || curCommands[""])) { commandHandler = curCommands.default || curCommands[""]; fullCmd = prevCmdName; target = `${cmd2}${target ? ` ${target}` : ""}`; cmd2 = fullCmd.split(" ").shift(); if (typeof commandHandler === "string") { commandHandler = curCommands[commandHandler]; } } if (!commandHandler && !recursing) { for (const g in Config.groups) { const groupid = Config.groups[g].id; if (fullCmd === groupid) { return this.parseCommand(`/promote ${target}, ${g}`, true); } else if (fullCmd === "global" + groupid) { return this.parseCommand(`/globalpromote ${target}, ${g}`, true); } else if (fullCmd === "de" + groupid || fullCmd === "un" + groupid || fullCmd === "globalde" + groupid || fullCmd === "deglobal" + groupid) { return this.parseCommand(`/demote ${target}`, true); } else if (fullCmd === "room" + groupid) { return this.parseCommand(`/roompromote ${target}, ${g}`, true); } else if (fullCmd === "forceroom" + groupid) { return this.parseCommand(`/forceroompromote ${target}, ${g}`, true); } else if (fullCmd === "roomde" + groupid || fullCmd === "deroom" + groupid || fullCmd === "roomun" + groupid) { return this.parseCommand(`/roomdemote ${target}`, true); } } } return { cmd: cmd2, cmdToken, target, fullCmd, handler: commandHandler }; } allCommands(table = Chat.commands) { const results = []; for (const cmd2 in table) { const handler = table[cmd2]; if (Array.isArray(handler) || !handler || ["string", "boolean"].includes(typeof handler)) { continue; } if (typeof handler === "object") { results.push(...this.allCommands(handler)); continue; } results.push(handler); } if (table !== Chat.commands) return results; return results.filter((handler, i) => results.indexOf(handler) === i); } /** * Strips HTML from a string. */ stripHTML(htmlContent) { if (!htmlContent) return ""; return htmlContent.replace(/<[^>]*>/g, ""); } /** * Validates input regex and ensures it won't crash. */ validateRegex(word) { word = word.trim(); if (word.endsWith("|") && !word.endsWith("\\|") || word.startsWith("|")) { throw new Chat.ErrorMessage(`Your regex was rejected because it included an unterminated |.`); } try { new RegExp(word); } catch (e) { throw new Chat.ErrorMessage( e.message.startsWith("Invalid regular expression: ") ? e.message : `Invalid regular expression: /${word}/: ${e.message}` ); } } /** * Returns singular (defaulting to '') if num is 1, or plural * (defaulting to 's') otherwise. Helper function for pluralizing * words. */ plural(num, pluralSuffix = "s", singular = "") { if (num && typeof num.length === "number") { num = num.length; } else if (num && typeof num.size === "number") { num = num.size; } else { num = Number(num); } return num !== 1 ? pluralSuffix : singular; } /** * Counts the thing passed. * * Chat.count(2, "days") === "2 days" * Chat.count(1, "days") === "1 day" * Chat.count(["foo"], "things are") === "1 thing is" * */ count(num, pluralSuffix, singular = "") { if (num && typeof num.length === "number") { num = num.length; } else if (num && typeof num.size === "number") { num = num.size; } else { num = Number(num); } if (!singular) { if (pluralSuffix.endsWith("s")) { singular = pluralSuffix.slice(0, -1); } else if (pluralSuffix.endsWith("s have")) { singular = pluralSuffix.slice(0, -6) + " has"; } else if (pluralSuffix.endsWith("s were")) { singular = pluralSuffix.slice(0, -6) + " was"; } } const space = singular.startsWith("<") ? "" : " "; return `${num}${space}${num > 1 ? pluralSuffix : singular}`; } /** * Returns a timestamp in the form {yyyy}-{MM}-{dd} {hh}:{mm}:{ss}. * * options.human = true will reports hours human-readable */ toTimestamp(date, options = {}) { const human = options.human; let parts = [ date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds() ]; if (human) { parts.push(parts[3] >= 12 ? "pm" : "am"); parts[3] = parts[3] % 12 || 12; } parts = parts.map((val) => val < 10 ? "0" + val : "" + val); return parts.slice(0, 3).join("-") + " " + parts.slice(3, human ? 5 : 6).join(":") + (human ? "" + parts[6] : ""); } /** * Takes a number of milliseconds, and reports the duration in English: hours, minutes, etc. * * options.hhmmss = true will instead report the duration in 00:00:00 format * */ toDurationString(val, options = {}) { const date = new Date(+val); if (isNaN(date.getTime())) return "forever"; const parts = [ date.getUTCFullYear() - 1970, date.getUTCMonth(), date.getUTCDate() - 1, date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds() ]; const roundingBoundaries = [6, 15, 12, 30, 30]; const unitNames = ["second", "minute", "hour", "day", "month", "year"]; const positiveIndex = parts.findIndex((elem) => elem > 0); let precision = options?.precision ? options.precision : 3; if (options?.hhmmss) { const str = parts.slice(positiveIndex).map((value) => value < 10 ? "0" + value : "" + value).join(":"); return str.length === 2 ? "00:" + str : str; } if (positiveIndex + precision < parts.length && precision > 0 && positiveIndex >= 0) { if (parts[positiveIndex + precision] >= roundingBoundaries[positiveIndex + precision - 1]) { parts[positiveIndex + precision - 1]++; } } let precisionIndex = 5; while (precisionIndex > positiveIndex && !parts[precisionIndex]) { precisionIndex--; } precision = Math.min(precision, precisionIndex - positiveIndex + 1); return parts.slice(positiveIndex).reverse().map((value, index) => `${value} ${unitNames[index]}${value !== 1 ? "s" : ""}`).reverse().slice(0, precision).join(" ").trim(); } /** * Takes an array and turns it into a sentence string by adding commas and the word "and" */ toListString(arr, conjunction = "and") { if (!arr.length) return ""; if (arr.length === 1) return arr[0]; if (arr.length === 2) return `${arr[0]} ${conjunction.trim()} ${arr[1]}`; return `${arr.slice(0, -1).join(", ")}, ${conjunction.trim()} ${arr.slice(-1)[0]}`; } /** * Takes an array and turns it into a sentence string by adding commas and the word "or" */ toOrList(arr) { if (!arr.length) return ""; if (arr.length === 1) return arr[0]; if (arr.length === 2) return `${arr[0]} or ${arr[1]}`; return `${arr.slice(0, -1).join(", ")}, or ${arr.slice(-1)[0]}`; } /** * Convert multiline HTML into a single line without losing whitespace (so *
 blocks still render correctly). Linebreaks inside <> are replaced
   * with ` `, and linebreaks outside <> are replaced with `
`.
   *
   * PS's protocol often requires sending a block of HTML in a single line,
   * so this ensures any block of HTML ends up as a single line.
   */
  collapseLineBreaksHTML(htmlContent) {
    htmlContent = htmlContent.replace(/<[^>]*>/g, (tag) => tag.replace(/\n/g, " "));
    htmlContent = htmlContent.replace(/\n/g, "
");
    return htmlContent;
  }
  /**
   * Takes a string of text and transforms it into a block of html using the details tag.
   * If it has a newline, will make the 3 lines the preview, and fill the rest in.
   * @param str string to block
   */
  getReadmoreBlock(str, isCode, cutoff = 3) {
    const params = str.slice(+str.startsWith("\n")).split("\n");
    const output = [];
    for (const [i, param] of params.entries()) {
      if (output.length < cutoff && param.length > 80 && cutoff > 2)
        cutoff--;
      if (param.length > cutoff * 160 && i < cutoff)
        cutoff = i;
      output.push(import_lib.Utils[isCode ? "escapeHTMLForceWrap" : "escapeHTML"](param));
    }
    if (output.length > cutoff) {
      return `
${output.slice(0, cutoff).join("
")}
${output.slice(cutoff).join("
")}
`; } else { const tag = isCode ? `code` : `div`; return `<${tag} style="white-space: pre-wrap; display: table; tab-size: 3">${output.join("
")}`; } } getReadmoreCodeBlock(str, cutoff) { return Chat.getReadmoreBlock(str, true, cutoff); } getDataPokemonHTML(species, gen = 8, tier = "") { let buf = '
  • '; buf += '' + (tier || species.tier) + " "; buf += ` `; buf += `${species.name} `; buf += ''; if (species.types) { for (const type of species.types) { buf += `${type}`; } } buf += " "; if (gen >= 3) { buf += ''; if (species.abilities["1"] && (gen >= 4 || import_sim.Dex.abilities.get(species.abilities["1"]).gen === 3)) { buf += '' + species.abilities["0"] + "
    " + species.abilities["1"] + "
    "; } else { buf += '' + species.abilities["0"] + ""; } if (species.abilities["H"] && species.abilities["S"]) { buf += '' + species.abilities["H"] + "
    (" + species.abilities["S"] + ")
    "; } else if (species.abilities["H"]) { buf += '' + species.abilities["H"] + ""; } else if (species.abilities["S"]) { buf += '(' + species.abilities["S"] + ")"; } else { buf += ''; } buf += "
    "; } buf += ''; buf += 'HP
    ' + species.baseStats.hp + "
    "; buf += 'Atk
    ' + species.baseStats.atk + "
    "; buf += 'Def
    ' + species.baseStats.def + "
    "; if (gen <= 1) { buf += 'Spc
    ' + species.baseStats.spa + "
    "; } else { buf += 'SpA
    ' + species.baseStats.spa + "
    "; buf += 'SpD
    ' + species.baseStats.spd + "
    "; } buf += 'Spe
    ' + species.baseStats.spe + "
    "; buf += 'BST
    ' + species.bst + "
    "; buf += "
    "; buf += "
  • "; return `
      ${buf}
    `; } getDataMoveHTML(move) { let buf = `
    • `; buf += `${move.name} `; const encodedMoveType = encodeURIComponent(move.type); buf += `${move.type}`; buf += `${move.category} `; if (move.basePower) { buf += `Power
      ${typeof move.basePower === "number" ? move.basePower : "\u2014"}
      `; } buf += `Accuracy
      ${typeof move.accuracy === "number" ? move.accuracy + "%" : "\u2014"}
      `; const basePP = move.pp || 1; const pp = Math.floor(move.noPPBoosts ? basePP : basePP * 8 / 5); buf += `PP
      ${pp}
      `; buf += `${move.shortDesc || move.desc} `; buf += `
    `; return buf; } getDataAbilityHTML(ability) { let buf = `
    • `; buf += `${ability.name} `; buf += `${ability.shortDesc || ability.desc} `; buf += `
    `; return buf; } getDataItemHTML(item) { let buf = `
    • `; buf += ` ${item.name} `; buf += `${item.shortDesc || item.desc} `; buf += `
    `; return buf; } /** * Gets the dimension of the image at url. Returns 0x0 if the image isn't found, as well as the relevant error. */ getImageDimensions(url) { return probe(url); } parseArguments(str, delim = ",", opts = { useIDs: true }) { const result = {}; for (const part of str.split(delim)) { let [key, val] = import_lib.Utils.splitFirst(part, opts.paramDelim || (opts.paramDelim = "=")).map((f) => f.trim()); if (opts.useIDs) key = toID(key); if (!toID(key) || !opts.allowEmpty && !toID(val)) { throw new Chat.ErrorMessage(`Invalid option ${part}. Must be in [key]${opts.paramDelim}[value] format.`); } if (!result[key]) result[key] = []; result[key].push(val); } return result; } /** * Normalize a message for the purposes of applying chat filters. * * Not used by PS itself, but feel free to use it in your own chat filters. */ normalize(message) { message = message.replace(/'/g, "").replace(/[^A-Za-z0-9]+/g, " ").trim(); if (!/[A-Za-z][A-Za-z]/.test(message)) { message = message.replace(/ */g, ""); } else if (!message.includes(" ")) { message = message.replace(/([A-Z])/g, " $1").trim(); } return " " + message.toLowerCase() + " "; } /** * Generates dimensions to fit an image at url into a maximum size of maxWidth x maxHeight, * preserving aspect ratio. * * @return [width, height, resized] */ async fitImage(url, maxHeight = 300, maxWidth = 300) { const { height, width } = await Chat.getImageDimensions(url); if (width <= maxWidth && height <= maxHeight) return [width, height, false]; const ratio = Math.min(maxHeight / height, maxWidth / width); return [Math.round(width * ratio), Math.round(height * ratio), true]; } refreshPageFor(pageid, roomid, checkPrefix = false, ignoreUsers = null) { const room = Rooms.get(roomid); if (!room) return false; for (const id in room.users) { if (ignoreUsers?.includes(id)) continue; const u = room.users[id]; for (const conn of u.connections) { if (conn.openPages) { for (const page of conn.openPages) { if (checkPrefix ? page.startsWith(pageid) : page === pageid) { void this.parse(`/j view-${page}`, room, u, conn); } } } } } } /** * Notifies a targetUser that a user was blocked from reaching them due to a setting they have enabled. */ maybeNotifyBlocked(blocked, targetUser, user) { const prefix = `|pm|&|${targetUser.getIdentity()}|/nonotify `; const options = 'or change it in the menu in the upper right.'; if (blocked === "pm") { if (!targetUser.notified.blockPMs) { targetUser.send(`${prefix}The user '${import_lib.Utils.escapeHTML(user.name)}' attempted to PM you but was blocked. To enable PMs, use /unblockpms ${options}`); targetUser.notified.blockPMs = true; } } else if (blocked === "challenge") { if (!targetUser.notified.blockChallenges) { targetUser.send(`${prefix}The user '${import_lib.Utils.escapeHTML(user.name)}' attempted to challenge you to a battle but was blocked. To enable challenges, use /unblockchallenges ${options}`); targetUser.notified.blockChallenges = true; } } else if (blocked === "invite") { if (!targetUser.notified.blockInvites) { targetUser.send(`${prefix}The user '${import_lib.Utils.escapeHTML(user.name)}' attempted to invite you to a room but was blocked. To enable invites, use /unblockinvites.`); targetUser.notified.blockInvites = true; } } } /** Helper function to ensure no state issues occur when regex testing for links. */ isLink(possibleUrl) { this.linkRegex.lastIndex = -1; return this.linkRegex.test(possibleUrl); } registerMonitor(id, entry) { if (!Chat.filterWords[id]) Chat.filterWords[id] = []; Chat.monitors[id] = entry; } resolvePage(pageid, user, connection) { return new PageContext({ pageid, user, connection, language: user.language }).resolve(); } }(); Chat.escapeHTML = import_lib.Utils.escapeHTML; Chat.splitFirst = import_lib.Utils.splitFirst; CommandContext.prototype.can = CommandContext.prototype.checkCan; CommandContext.prototype.canTalk = CommandContext.prototype.checkChat; CommandContext.prototype.canBroadcast = CommandContext.prototype.checkBroadcast; CommandContext.prototype.canHTML = CommandContext.prototype.checkHTML; CommandContext.prototype.canEmbedURI = CommandContext.prototype.checkEmbedURI; CommandContext.prototype.privatelyCan = CommandContext.prototype.privatelyCheckCan; CommandContext.prototype.requiresRoom = CommandContext.prototype.requireRoom; CommandContext.prototype.targetUserOrSelf = function(target, exactName) { const user = this.getUserOrSelf(target, exactName); this.targetUser = user; this.inputUsername = target; this.targetUsername = user?.name || target; return user; }; CommandContext.prototype.splitTarget = function(target, exactName) { const { targetUser, inputUsername, targetUsername, rest } = this.splitUser(target, exactName); this.targetUser = targetUser; this.inputUsername = inputUsername; this.targetUsername = targetUsername; return rest; }; if (!process.send) { Chat.database.spawn(Config.chatdbprocesses || 1); Chat.databaseReadyPromise = Chat.prepareDatabase(); } else if (process.mainModule === module) { global.Monitor = { crashlog(error, source = "A chat child process", details = null) { const repr = JSON.stringify([error.name, error.message, source, details]); process.send(`THROW @!!@${repr} ${error.stack}`); } }; process.on("uncaughtException", (err) => { Monitor.crashlog(err, "A chat database process"); }); process.on("unhandledRejection", (err) => { Monitor.crashlog(err, "A chat database process"); }); global.Config = require("./config-loader").Config; import_lib.Repl.start("chat-db", (cmd) => eval(cmd)); } //# sourceMappingURL=chat.js.map