"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 punishments_exports = {}; __export(punishments_exports, { Punishments: () => Punishments }); module.exports = __toCommonJS(punishments_exports); var import_lib = require("../lib"); /** * Punishments * Pokemon Showdown - http://pokemonshowdown.com/ * * Handles the punishing of users on PS. * * There are four types of global punishments on PS. Locks, bans, namelocks and rangelocks. * This file contains the lists of users that have been punished (both IPs and usernames), * as well as the functions that handle the execution of said punishments. * * @license MIT license */ const PUNISHMENT_FILE = "config/punishments.tsv"; const ROOM_PUNISHMENT_FILE = "config/room-punishments.tsv"; const SHAREDIPS_FILE = "config/sharedips.tsv"; const SHAREDIPS_BLACKLIST_FILE = "config/sharedips-blacklist.tsv"; const WHITELISTED_NAMES_FILE = "config/name-whitelist.tsv"; const RANGELOCK_DURATION = 60 * 60 * 1e3; const LOCK_DURATION = 48 * 60 * 60 * 1e3; const GLOBALBAN_DURATION = 7 * 24 * 60 * 60 * 1e3; const BATTLEBAN_DURATION = 48 * 60 * 60 * 1e3; const GROUPCHATBAN_DURATION = 7 * 24 * 60 * 60 * 1e3; const MOBILE_PUNISHMENT_DURATIION = 6 * 60 * 60 * 1e3; const ROOMBAN_DURATION = 48 * 60 * 60 * 1e3; const BLACKLIST_DURATION = 365 * 24 * 60 * 60 * 1e3; const USERID_REGEX = /^[a-z0-9]+$/; const PUNISH_TRUSTED = false; const PUNISHMENT_POINT_VALUES = { MUTE: 2, BLACKLIST: 3, ROOMBAN: 4 }; const AUTOLOCK_POINT_THRESHOLD = 8; const AUTOWEEKLOCK_THRESHOLD = 5; const AUTOWEEKLOCK_DAYS_TO_SEARCH = 60; const MAX_PUNISHMENT_TIMER_LENGTH = 24 * 60 * 60 * 1e3; const GROUPCHAT_PARTICIPANT_OVERLAP_THRESHOLD = 5; const GROUPCHAT_MONITOR_INTERVAL = 10 * 60 * 1e3; class PunishmentMap extends Map { constructor(roomid) { super(); this.roomid = roomid; } removeExpiring(punishments) { for (const [i, punishment] of punishments.entries()) { if (Date.now() > punishment.expireTime) { punishments.splice(i, 1); } } } get(k) { const punishments = super.get(k); if (punishments) { this.removeExpiring(punishments); if (punishments.length) return punishments; this.delete(k); } return void 0; } has(k) { return !!this.get(k); } getByType(k, type) { return this.get(k)?.filter((p) => p.type === type)?.[0]; } each(callback) { for (const [k, punishments] of super.entries()) { this.removeExpiring(punishments); if (punishments.length) { for (const punishment of punishments) { callback(punishment, k, this); } } else { this.delete(k); } } } deleteOne(k, punishment) { const list = this.get(k); if (!list) return; for (let i = list.length - 1; i >= 0; i--) { const cur = list[i]; if (punishment.type === cur.type && cur.id === punishment.id) { list.splice(i, 1); } } if (!list.length) { this.delete(k); } return true; } add(k, punishment) { let list = this.get(k); if (!list) { list = []; this.set(k, list); } for (let i = list.length - 1; i >= 0; i--) { const curPunishment = list[i]; if (punishment.type === curPunishment.type) { if (curPunishment.expireTime >= punishment.expireTime) { curPunishment.reason = punishment.reason; return this; } list.splice(i, 1); } } list.push(punishment); return this; } } class NestedPunishmentMap extends Map { nestedSet(k1, k2, value) { if (!this.get(k1)) { this.set(k1, new PunishmentMap(k1)); } this.get(k1).add(k2, value); } nestedGet(k1, k2) { const subMap = this.get(k1); if (!subMap) return subMap; const punishments = subMap.get(k2); if (punishments?.length) { return punishments; } return void 0; } nestedGetByType(k1, k2, type) { return this.nestedGet(k1, k2)?.find((p) => p.type === type); } nestedHas(k1, k2) { return !!this.nestedGet(k1, k2); } nestedDelete(k1, k2) { const subMap = this.get(k1); if (!subMap) return; subMap.delete(k2); if (!subMap.size) this.delete(k1); } nestedEach(callback) { for (const [k1, subMap] of this.entries()) { for (const [k2, punishments] of subMap.entries()) { subMap.removeExpiring(punishments); if (punishments.length) { for (const punishment of punishments) { callback(punishment, k1, k2); } } else { this.nestedDelete(k1, k2); } } } } } const Punishments = new class { constructor() { /** * ips is an ip:punishment Map */ this.ips = new PunishmentMap(); /** * userids is a userid:punishment Map */ this.userids = new PunishmentMap(); /** * roomUserids is a roomid:userid:punishment nested Map */ this.roomUserids = new NestedPunishmentMap(); /** * roomIps is a roomid:ip:punishment Map */ this.roomIps = new NestedPunishmentMap(); /** * sharedIps is an ip:note Map */ this.sharedIps = /* @__PURE__ */ new Map(); /** * AddressRange:note map. In a separate map so we iterate a massive map a lot less. * (AddressRange is a bit of a premature optimization, but it saves us a conversion call on some trafficked spots) */ this.sharedRanges = /* @__PURE__ */ new Map(); /** * sharedIpBlacklist is an ip:note Map */ this.sharedIpBlacklist = /* @__PURE__ */ new Map(); /** * namefilterwhitelist is a whitelistedname:whitelister Map */ this.namefilterwhitelist = /* @__PURE__ */ new Map(); /** * Connection flood table. Separate table from IP bans. */ this.cfloods = /* @__PURE__ */ new Set(); /** * Participants in groupchats whose creators were banned from using groupchats. * Object keys are roomids of groupchats; values are Sets of user IDs. */ this.bannedGroupchatParticipants = {}; /** roomid:timestamp map */ this.lastGroupchatMonitorTime = {}; /** * Map */ this.offlineWarns = /* @__PURE__ */ new Map(); /** * punishType is an allcaps string, for global punishments they can be * anything in the punishmentTypes map. * * This map can be extended with custom punishments by chat plugins. * * Keys in the map correspond to PunishInfo */ this.punishmentTypes = new Map([ ...global.Punishments?.punishmentTypes || [], ["LOCK", { desc: "locked" }], ["BAN", { desc: "globally banned" }], ["NAMELOCK", { desc: "namelocked" }], ["GROUPCHATBAN", { desc: "banned from using groupchats" }], ["BATTLEBAN", { desc: "banned from battling" }] ]); /** * For room punishments, they can be anything in the roomPunishmentTypes map. * * This map can be extended with custom punishments by chat plugins. * * Keys in the map correspond to punishTypes, values signify the way they * should be displayed in /alt. * By default, this includes: * - 'ROOMBAN' * - 'BLACKLIST' * - 'MUTE' (used by getRoomPunishments) * */ this.roomPunishmentTypes = new Map([ // references to global.Punishments? are here because if you hotpatch punishments without hotpatching chat, // old punishment types won't be loaded into here, which might cause issues. This guards against that. ...global.Punishments?.roomPunishmentTypes || [], ["ROOMBAN", { desc: "banned", activatePunishMonitor: true }], ["BLACKLIST", { desc: "blacklisted", activatePunishMonitor: true }], ["MUTE", { desc: "muted", activatePunishMonitor: true }] ]); this.sortedTypes = ["TICKETBAN", "LOCK", "NAMELOCK", "BAN"]; this.sortedRoomTypes = [...global.Punishments?.sortedRoomTypes || [], "ROOMBAN", "BLACKLIST"]; this.interactions = { NAMELOCK: { overrides: ["LOCK"] } }; this.roomInteractions = { BLACKLIST: { overrides: ["ROOMBAN"] } }; this.PunishmentMap = PunishmentMap; this.NestedPunishmentMap = NestedPunishmentMap; setImmediate(() => { void Punishments.loadPunishments(); void Punishments.loadRoomPunishments(); void Punishments.loadBanlist(); void Punishments.loadSharedIps(); void Punishments.loadSharedIpBlacklist(); void Punishments.loadWhitelistedNames(); }); } // punishments.tsv is in the format: // punishType, userid, ips/usernames, expiration time, reason // room-punishments.tsv is in the format: // punishType, roomid:userid, ips/usernames, expiration time, reason async loadPunishments() { const data = await (0, import_lib.FS)(PUNISHMENT_FILE).readIfExists(); if (!data) return; for (const row of data.split("\n")) { if (!row || row === "\r") continue; const [type, id, altKeys, expireTimeStr, ...reason] = row.trim().split(" "); const expireTime = Number(expireTimeStr); if (type === "Punishment") continue; const keys = altKeys.split(",").concat(id); const punishment = { type, id, expireTime, reason: reason.join(" ") }; if (Date.now() >= expireTime) { continue; } for (const key of keys) { if (!key.trim()) continue; if (!USERID_REGEX.test(key)) { Punishments.ips.add(key, punishment); } else { Punishments.userids.add(key, punishment); } } } } async loadRoomPunishments() { const data = await (0, import_lib.FS)(ROOM_PUNISHMENT_FILE).readIfExists(); if (!data) return; for (const row of data.split("\n")) { if (!row || row === "\r") continue; const [type, id, altKeys, expireTimeStr, ...reason] = row.trim().split(" "); const expireTime = Number(expireTimeStr); if (type === "Punishment") continue; const [roomid, userid] = id.split(":"); if (!userid) continue; const keys = altKeys.split(",").concat(userid); const punishment = { type, id: userid, expireTime, reason: reason.join(" ") }; if (Date.now() >= expireTime) { continue; } for (const key of keys) { if (!USERID_REGEX.test(key)) { Punishments.roomIps.nestedSet(roomid, key, punishment); } else { Punishments.roomUserids.nestedSet(roomid, key, punishment); } } } } savePunishments() { (0, import_lib.FS)(PUNISHMENT_FILE).writeUpdate(() => { const saveTable = Punishments.getPunishments(); let buf = "Punishment User ID IPs and alts Expires Reason\r\n"; for (const [id, entry] of saveTable) { buf += Punishments.renderEntry(entry, id); } return buf; }, { throttle: 5e3 }); } saveRoomPunishments() { (0, import_lib.FS)(ROOM_PUNISHMENT_FILE).writeUpdate(() => { const saveTable = []; for (const roomid of Punishments.roomIps.keys()) { for (const [userid, punishment] of Punishments.getPunishments(roomid, true)) { saveTable.push([`${roomid}:${userid}`, punishment]); } } let buf = "Punishment Room ID:User ID IPs and alts Expires Reason\r\n"; for (const [id, entry] of saveTable) { buf += Punishments.renderEntry(entry, id); } return buf; }, { throttle: 5e3 }); } getEntry(entryId) { let entry = null; Punishments.ips.each((punishment, ip) => { const { type, id, expireTime, reason, rest } = punishment; if (id !== entryId) return; if (entry) { entry.ips.push(ip); return; } entry = { userids: [], ips: [ip], punishType: type, expireTime, reason, rest: rest || [] }; }); Punishments.userids.each((punishment, userid) => { const { type, id, expireTime, reason, rest } = punishment; if (id !== entryId) return; if (!entry) { entry = { userids: [], ips: [], punishType: type, expireTime, reason, rest: rest || [] }; } if (userid !== id) entry.userids.push(toID(userid)); }); return entry; } appendPunishment(entry, id, filename, allowNonUserIDs) { if (!allowNonUserIDs && id.startsWith("#")) return; const buf = Punishments.renderEntry(entry, id); return (0, import_lib.FS)(filename).append(buf); } renderEntry(entry, id) { const keys = entry.ips.concat(entry.userids).join(","); const row = [entry.punishType, id, keys, entry.expireTime, entry.reason, ...entry.rest]; return row.join(" ") + "\r\n"; } async loadBanlist() { const data = await (0, import_lib.FS)("config/ipbans.txt").readIfExists(); if (!data) return; const rangebans = []; for (const row of data.split("\n")) { const ip = row.split("#")[0].trim(); if (!ip) continue; if (ip.includes("/")) { rangebans.push(ip); } else if (!Punishments.ips.has(ip)) { Punishments.ips.add(ip, { type: "LOCK", id: "#ipban", expireTime: Infinity, reason: "" }); } } Punishments.checkRangeBanned = IPTools.checker(rangebans); } /** * sharedips.tsv is in the format: * IP, type (in this case always SHARED), note */ async loadSharedIps() { const data = await (0, import_lib.FS)(SHAREDIPS_FILE).readIfExists(); if (!data) return; let needsSave = false; for (const row of data.replace("\r", "").split("\n")) { if (!row) continue; const [ip, type, note] = row.trim().split(" "); if (ip === "IP") { continue; } if (IPTools.ipRegex.test(note)) { Punishments.sharedIps.set(note, ip); needsSave = true; continue; } if (!IPTools.ipRegex.test(ip)) { const pattern = IPTools.stringToRange(ip); if (pattern) { Punishments.sharedRanges.set(pattern, note); } else { Monitor.adminlog(`Invalid range data in '${SHAREDIPS_FILE}': "${row}".`); } continue; } if (type !== "SHARED") continue; Punishments.sharedIps.set(ip, note); } if (needsSave) { void Punishments.saveSharedIps(); } } appendSharedIp(ip, note) { const pattern = IPTools.stringToRange(ip); let ipString = ip; if (pattern && pattern.minIP !== pattern.maxIP) { ipString = IPTools.rangeToString(pattern); } const buf = `${ipString} SHARED ${note}\r `; return (0, import_lib.FS)(SHAREDIPS_FILE).append(buf); } saveSharedIps() { let buf = "IP Type Note\r\n"; for (const [ip, note] of Punishments.sharedIps) { buf += `${ip} SHARED ${note}\r `; } for (const [range, note] of Punishments.sharedRanges) { buf += `${IPTools.rangeToString(range)} SHARED ${note}\r `; } return (0, import_lib.FS)(SHAREDIPS_FILE).write(buf); } /** * sharedips.tsv is in the format: * IP, type (in this case always SHARED), note */ async loadSharedIpBlacklist() { const data = await (0, import_lib.FS)(SHAREDIPS_BLACKLIST_FILE).readIfExists(); if (!data) return; for (const row of data.replace("\r", "").split("\n")) { if (!row) continue; const [ip, reason] = row.trim().split(" "); if (!IPTools.ipRangeRegex.test(ip)) continue; if (!reason) continue; Punishments.sharedIpBlacklist.set(ip, reason); } } appendSharedIpBlacklist(ip, reason) { const buf = `${ip} ${reason}\r `; return (0, import_lib.FS)(SHAREDIPS_BLACKLIST_FILE).append(buf); } saveSharedIpBlacklist() { let buf = `IP Reason\r `; Punishments.sharedIpBlacklist.forEach((reason, ip) => { buf += `${ip} ${reason}\r `; }); return (0, import_lib.FS)(SHAREDIPS_BLACKLIST_FILE).write(buf); } async loadWhitelistedNames() { const data = await (0, import_lib.FS)(WHITELISTED_NAMES_FILE).readIfExists(); if (!data) return; const lines = data.split("\n"); lines.shift(); for (const line of lines) { const [userid, whitelister] = line.split(" "); this.namefilterwhitelist.set(toID(userid), toID(whitelister)); } } appendWhitelistedName(name, whitelister) { return (0, import_lib.FS)(WHITELISTED_NAMES_FILE).append(`${toID(name)} ${toID(whitelister)}\r `); } saveNameWhitelist() { let buf = `Userid Whitelister \r `; Punishments.namefilterwhitelist.forEach((userid, whitelister) => { buf += `${userid} ${whitelister}\r `; }); return (0, import_lib.FS)(WHITELISTED_NAMES_FILE).write(buf); } /********************************************************* * Adding and removing *********************************************************/ async punish(user, punishment, ignoreAlts, bypassPunishmentfilter = false) { user = Users.get(user) || user; if (typeof user === "string") { return Punishments.punishName(user, punishment); } Punishments.checkInteractions(user.getLastId(), punishment); if (!punishment.id) punishment.id = user.getLastId(); const userids = /* @__PURE__ */ new Set(); const ips = /* @__PURE__ */ new Set(); const mobileIps = /* @__PURE__ */ new Set(); const affected = ignoreAlts ? [user] : user.getAltUsers(PUNISH_TRUSTED, true); for (const alt of affected) { await this.punishInner(alt, punishment, userids, ips, mobileIps); } const { type, id, expireTime, reason, rest } = punishment; userids.delete(id); void Punishments.appendPunishment({ userids: [...userids], ips: [...ips], punishType: type, expireTime, reason, rest: rest || [] }, id, PUNISHMENT_FILE); if (mobileIps.size) { const mobileExpireTime = Date.now() + MOBILE_PUNISHMENT_DURATIION; const mobilePunishment = { type, id, expireTime: mobileExpireTime, reason, rest }; for (const mobileIp of mobileIps) { Punishments.ips.add(mobileIp, mobilePunishment); } } if (!bypassPunishmentfilter) Chat.punishmentfilter(user, punishment); return affected; } async punishInner(user, punishment, userids, ips, mobileIps) { const existingPunishment = Punishments.userids.getByType(user.locked || toID(user.name), punishment.type); if (existingPunishment) { if (existingPunishment.expireTime > punishment.expireTime) { punishment.expireTime = existingPunishment.expireTime; } const types = ["LOCK", "NAMELOCK", "BAN"]; if (types.indexOf(existingPunishment.type) > types.indexOf(punishment.type)) { punishment.type = existingPunishment.type; } } for (const ip of user.ips) { const { hostType } = await IPTools.lookup(ip); if (hostType !== "mobile") { Punishments.ips.add(ip, punishment); ips.add(ip); } else { mobileIps.add(ip); } } const lastUserId = user.getLastId(); if (!lastUserId.startsWith("guest")) { Punishments.userids.add(lastUserId, punishment); } if (user.locked && !user.locked.startsWith("#")) { Punishments.userids.add(user.locked, punishment); userids.add(user.locked); } if (user.autoconfirmed) { Punishments.userids.add(user.autoconfirmed, punishment); userids.add(user.autoconfirmed); } if (user.trusted) { Punishments.userids.add(user.trusted, punishment); userids.add(user.trusted); } } punishName(userid, punishment) { if (!punishment.id) punishment.id = userid; const foundKeys = Punishments.search(userid).map(([key]) => key); const userids = /* @__PURE__ */ new Set([userid]); const ips = /* @__PURE__ */ new Set(); Punishments.checkInteractions(userid, punishment); for (const key of foundKeys) { if (key.includes(".")) { ips.add(key); } else { userids.add(key); } } for (const id2 of userids) { Punishments.userids.add(id2, punishment); } for (const ip of ips) { Punishments.ips.add(ip, punishment); } const { type, id, expireTime, reason, rest } = punishment; const affected = Users.findUsers([...userids], [...ips], { includeTrusted: PUNISH_TRUSTED, forPunishment: true }); userids.delete(id); void Punishments.appendPunishment({ userids: [...userids], ips: [...ips], punishType: type, expireTime, reason, rest: rest || [] }, id, PUNISHMENT_FILE); Chat.punishmentfilter(userid, punishment); return affected; } unpunish(id, punishType) { id = toID(id); const punishment = Punishments.userids.getByType(id, punishType); if (punishment) { id = punishment.id; } let success = false; Punishments.ips.each((cur, key) => { const { type: curPunishmentType, id: curId } = cur; if (curId === id && curPunishmentType === punishType) { Punishments.ips.deleteOne(key, cur); success = id; } }); Punishments.userids.each((cur, key) => { const { type: curPunishmentType, id: curId } = cur; if (curId === id && curPunishmentType === punishType) { Punishments.userids.deleteOne(key, cur); success = id; } }); if (success) { Punishments.savePunishments(); } return success; } roomPunish(room, user, punishment) { if (typeof user === "string") { return Punishments.roomPunishName(room, user, punishment); } if (!punishment.id) punishment.id = user.getLastId(); Punishments.checkInteractions(punishment.id, punishment, toID(room)); const roomid = typeof room !== "string" ? room.roomid : room; const userids = /* @__PURE__ */ new Set(); const ips = /* @__PURE__ */ new Set(); const affected = user.getAltUsers(PUNISH_TRUSTED, true); for (const curUser of affected) { this.roomPunishInner(roomid, curUser, punishment, userids, ips); } const { type, id, expireTime, reason, rest } = punishment; userids.delete(id); void Punishments.appendPunishment({ userids: [...userids], ips: [...ips], punishType: type, expireTime, reason, rest: rest || [] }, roomid + ":" + id, ROOM_PUNISHMENT_FILE); if (typeof room !== "string") { room = room; if (!(room.settings.isPrivate === true || room.settings.isPersonal)) { void Punishments.monitorRoomPunishments(user); } } return affected; } roomPunishInner(roomid, user, punishment, userids, ips) { for (const ip of user.ips) { Punishments.roomIps.nestedSet(roomid, ip, punishment); ips.add(ip); } if (!user.id.startsWith("guest")) { Punishments.roomUserids.nestedSet(roomid, user.id, punishment); } if (user.autoconfirmed) { Punishments.roomUserids.nestedSet(roomid, user.autoconfirmed, punishment); userids.add(user.autoconfirmed); } if (user.trusted) { Punishments.roomUserids.nestedSet(roomid, user.trusted, punishment); userids.add(user.trusted); } } checkInteractions(userid, punishment, room) { const punishments = Punishments.search(userid); const results = []; const info = Punishments[room ? "roomInteractions" : "interactions"][punishment.type]; if (!info) return; for (const [k, curRoom, curPunishment] of punishments) { if (k !== userid || room && curRoom !== room) continue; if (info.overrides.includes(curPunishment.type)) { results.push(curPunishment); if (room) { Punishments.roomUnpunish(room, userid, curPunishment.type); } else { Punishments.unpunish(userid, curPunishment.type); } } } return results; } roomPunishName(room, userid, punishment) { if (!punishment.id) punishment.id = userid; const roomid = typeof room !== "string" ? room.roomid : room; const foundKeys = Punishments.search(userid).map(([key]) => key); Punishments.checkInteractions(userid, punishment, roomid); const userids = /* @__PURE__ */ new Set([userid]); const ips = /* @__PURE__ */ new Set(); for (const key of foundKeys) { if (key.includes(".")) { ips.add(key); } else { userids.add(key); } } for (const id2 of userids) { Punishments.roomUserids.nestedSet(roomid, id2, punishment); } for (const ip of ips) { Punishments.roomIps.nestedSet(roomid, ip, punishment); } const { type, id, expireTime, reason, rest } = punishment; const affected = Users.findUsers([...userids], [...ips], { includeTrusted: PUNISH_TRUSTED, forPunishment: true }); userids.delete(id); void Punishments.appendPunishment({ userids: [...userids], ips: [...ips], punishType: type, expireTime, reason, rest: rest || [] }, roomid + ":" + id, ROOM_PUNISHMENT_FILE); if (typeof room !== "string") { room = room; if (!(room.settings.isPrivate === true || room.settings.isPersonal)) { void Punishments.monitorRoomPunishments(userid); } } return affected; } /** * @param ignoreWrite skip persistent storage */ roomUnpunish(room, id, punishType, ignoreWrite = false) { const roomid = typeof room !== "string" ? room.roomid : room; id = toID(id); const punishment = Punishments.roomUserids.nestedGetByType(roomid, id, punishType); if (punishment) { id = punishment.id; } let success; const ipSubMap = Punishments.roomIps.get(roomid); if (ipSubMap) { for (const [key, punishmentList] of ipSubMap) { for (const [i, cur] of punishmentList.entries()) { if (cur.id === id && cur.type === punishType) { punishmentList.splice(i, 1); success = id; } } if (!punishmentList.length) { ipSubMap.delete(key); } } } const useridSubMap = Punishments.roomUserids.get(roomid); if (useridSubMap) { for (const [key, punishmentList] of useridSubMap) { for (const [i, cur] of punishmentList.entries()) { if (cur.id === id && cur.type === punishType) { punishmentList.splice(i, 1); success = id; } if (!punishmentList.length) { useridSubMap.delete(key); } } } } if (success && !ignoreWrite) { Punishments.saveRoomPunishments(); } return success; } addRoomPunishmentType(opts, desc, callback) { if (typeof opts === "string") { if (!desc) throw new Error("Desc argument must be provided if type is string"); opts = { onActivate: callback, desc, type: opts }; } this.roomPunishmentTypes.set(opts.type, opts); if (!this.sortedRoomTypes.includes(opts.type)) this.sortedRoomTypes.unshift(opts.type); } addPunishmentType(opts, desc, callback) { if (typeof opts === "string") { if (!desc) throw new Error("Desc argument must be provided if type is string"); opts = { onActivate: callback, desc, type: opts }; } this.punishmentTypes.set(opts.type, opts); if (!this.sortedTypes.includes(opts.type)) this.sortedTypes.unshift(opts.type); } /********************************************************* * Specific punishments *********************************************************/ async ban(user, expireTime, id, ignoreAlts, ...reason) { if (!expireTime) expireTime = Date.now() + GLOBALBAN_DURATION; const punishment = { type: "BAN", id, expireTime, reason: reason.join(" ") }; const affected = await Punishments.punish(user, punishment, ignoreAlts); for (const curUser of affected) { curUser.locked = punishment.id; curUser.disconnectAll(); } return affected; } unban(name) { return Punishments.unpunish(name, "BAN"); } async lock(user, expireTime, id, ignoreAlts, reason, bypassPunishmentfilter = false, rest) { if (!expireTime) expireTime = Date.now() + LOCK_DURATION; const punishment = { type: "LOCK", id, expireTime, reason, rest }; const userObject = Users.get(user); if (userObject) userObject.locked = punishment.id; const affected = await Punishments.punish(user, punishment, ignoreAlts, bypassPunishmentfilter); for (const curUser of affected) { Punishments.checkPunishmentTime(curUser, punishment); curUser.locked = punishment.id; curUser.updateIdentity(); } return affected; } async autolock(user, room, source, reason, message, week = false, namelock) { if (!message) message = reason; let punishment = `LOCK`; let expires = null; if (week) { expires = Date.now() + 7 * 24 * 60 * 60 * 1e3; punishment = `WEEKLOCK`; } const userid = toID(user); if (Users.get(user)?.locked) return false; const name = typeof user === "string" ? user : user.name; if (namelock) { punishment = `NAME${punishment}`; await Punishments.namelock(user, expires, toID(namelock), false, `Autonamelock: ${name}: ${reason}`); } else { await Punishments.lock(user, expires, userid, false, `Autolock: ${name}: ${reason}`); } Monitor.log(`[${source}] ${punishment}ED: ${message}`); const logEntry = { action: `AUTO${punishment}`, visualRoomID: typeof room !== "string" ? room.roomid : room, ip: typeof user !== "string" ? user.latestIp : null, userid, note: reason, isGlobal: true }; if (typeof user !== "string") logEntry.ip = user.latestIp; const roomObject = Rooms.get(room); const userObject = Users.get(user); if (roomObject) { roomObject.modlog(logEntry); } else { Rooms.global.modlog(logEntry); } if (roomObject?.battle && userObject?.connections[0]) { Chat.parse("/savereplay forpunishment", roomObject, userObject, userObject.connections[0]); } const roomauth = Rooms.global.destroyPersonalRooms(userid); if (roomauth.length) { Monitor.log(`[CrisisMonitor] Autolocked user ${name} has public roomauth (${roomauth.join(", ")}), and should probably be demoted.`); } } unlock(name) { const user = Users.get(name); let id = toID(name); const success = []; if (user?.locked && !user.namelocked) { id = user.locked; user.locked = null; user.namelocked = null; user.destroyPunishmentTimer(); user.updateIdentity(); success.push(user.getLastName()); } if (!id.startsWith("#")) { for (const curUser of Users.users.values()) { if (curUser.locked === id) { curUser.locked = null; curUser.namelocked = null; curUser.destroyPunishmentTimer(); curUser.updateIdentity(); success.push(curUser.getLastName()); } } } if (["LOCK", "YEARLOCK"].some((type) => Punishments.unpunish(name, type))) { if (!success.length) success.push(name); } if (!success.length) return void 0; if (!success.some((v) => toID(v) === id)) { success.push(id); } return success; } /** * Sets the punishment timer for a user, * to either MAX_PUNISHMENT_TIMER_LENGTH or the amount of time left on the punishment. * It also expires a punishment if the time is up. */ checkPunishmentTime(user, punishment) { if (user.punishmentTimer) { clearTimeout(user.punishmentTimer); user.punishmentTimer = null; } if (user.locked && user.locked.startsWith("#")) return; const { id, expireTime } = punishment; const timeLeft = expireTime - Date.now(); if (timeLeft <= 1) { if (user.locked === id) Punishments.unlock(user.id); return; } const waitTime = Math.min(timeLeft, MAX_PUNISHMENT_TIMER_LENGTH); user.punishmentTimer = setTimeout(() => { global.Punishments.checkPunishmentTime(user, punishment); }, waitTime); } async namelock(user, expireTime, id, ignoreAlts, ...reason) { if (!expireTime) expireTime = Date.now() + LOCK_DURATION; const punishment = { type: "NAMELOCK", id, expireTime, reason: reason.join(" ") }; const affected = await Punishments.punish(user, punishment, ignoreAlts); for (const curUser of affected) { Punishments.checkPunishmentTime(curUser, punishment); curUser.locked = punishment.id; curUser.namelocked = punishment.id; curUser.resetName(true); curUser.updateIdentity(); } return affected; } unnamelock(name) { const user = Users.get(name); let id = toID(name); const success = []; if (user?.namelocked) name = user.namelocked; const unpunished = Punishments.unpunish(name, "NAMELOCK"); if (user?.locked) { id = user.locked; user.locked = null; user.namelocked = null; user.destroyPunishmentTimer(); user.resetName(); success.push(user.getLastName()); } if (!id.startsWith("#")) { for (const curUser of Users.users.values()) { if (curUser.locked === id) { curUser.locked = null; curUser.namelocked = null; curUser.destroyPunishmentTimer(); curUser.resetName(); success.push(curUser.getLastName()); } } } if (unpunished && !success.length) success.push(name); if (!success.length) return false; if (!success.some((v) => toID(v) === id)) { success.push(id); } return success; } battleban(user, expireTime, id, ...reason) { if (!expireTime) expireTime = Date.now() + BATTLEBAN_DURATION; const punishment = { type: "BATTLEBAN", id, expireTime, reason: reason.join(" ") }; for (const games of user.games.keys()) { const game = Rooms.get(games).getGame(Tournaments.Tournament); if (!game) continue; if (game.isTournamentStarted) { game.disqualifyUser(user.id, null, null); } else if (!game.isTournamentStarted) { game.removeUser(user.id); } } return Punishments.punish(user, punishment, false); } unbattleban(userid) { const user = Users.get(userid); if (user) { const punishment = Punishments.isBattleBanned(user); if (punishment) userid = punishment.id; } return Punishments.unpunish(userid, "BATTLEBAN"); } isBattleBanned(user) { if (!user) throw new Error(`Trying to check if a non-existent user is battlebanned.`); let punishment = Punishments.userids.getByType(user.id, "BATTLEBAN"); if (punishment) return punishment; if (user.autoconfirmed) { punishment = Punishments.userids.getByType(user.autoconfirmed, "BATTLEBAN"); if (punishment) return punishment; } for (const ip of user.ips) { punishment = Punishments.ips.getByType(ip, "BATTLEBAN"); if (punishment) { if (Punishments.isSharedIp(ip) && user.autoconfirmed) return; return punishment; } } } /** * Bans a user from using groupchats. Returns an array of roomids of the groupchat they created, if any. * We don't necessarily want to delete these, since we still need to warn the participants, * and make a modnote of the participant names, which doesn't seem appropriate for a Punishments method. */ async groupchatBan(user, expireTime, id, reason) { if (!expireTime) expireTime = Date.now() + GROUPCHATBAN_DURATION; const punishment = { type: "GROUPCHATBAN", id, expireTime, reason }; const groupchatsCreated = []; const targetUser = Users.get(user); if (targetUser) { for (const roomid of targetUser.inRooms || []) { const targetRoom = Rooms.get(roomid); if (!targetRoom?.roomid.startsWith("groupchat-")) continue; targetRoom.game?.removeBannedUser?.(targetUser); targetUser.leaveRoom(targetRoom.roomid); if (targetRoom.auth.get(targetUser) === Users.HOST_SYMBOL) { groupchatsCreated.push(targetRoom.roomid); Punishments.bannedGroupchatParticipants[targetRoom.roomid] = new Set( // Room#users is a UserTable where the keys are IDs, // but typed as strings so that they can be used as object keys. Object.keys(targetRoom.users).filter((u) => !targetRoom.users[u].can("lock")) ); } } } await Punishments.punish(user, punishment, false); return groupchatsCreated; } groupchatUnban(user) { let userid = typeof user === "object" ? user.id : user; const punishment = Punishments.isGroupchatBanned(user); if (punishment) userid = punishment.id; return Punishments.unpunish(userid, "GROUPCHATBAN"); } isGroupchatBanned(user) { const userid = toID(user); const targetUser = Users.get(user); let punishment = Punishments.userids.getByType(userid, "GROUPCHATBAN"); if (punishment) return punishment; if (targetUser?.autoconfirmed) { punishment = Punishments.userids.getByType(targetUser.autoconfirmed, "GROUPCHATBAN"); if (punishment) return punishment; } if (targetUser && !targetUser.trusted) { for (const ip of targetUser.ips) { punishment = Punishments.ips.getByType(ip, "GROUPCHATBAN"); if (punishment) { if (Punishments.isSharedIp(ip) && targetUser.autoconfirmed) return; return punishment; } } } } isTicketBanned(user) { const ips = []; if (typeof user === "object") { ips.push(...user.ips); ips.unshift(user.latestIp); user = user.id; } const punishment = Punishments.userids.getByType(user, "TICKETBAN"); if (punishment) return punishment; if (ips.some((ip) => Punishments.isSharedIp(ip))) return false; for (const ip of ips) { const curPunishment = Punishments.ips.getByType(ip, "TICKETBAN"); if (curPunishment) return curPunishment; } return false; } /** * Monitors a groupchat, watching in case too many users who had participated in * a groupchat that was deleted because its owner was groupchatbanned join. */ monitorGroupchatJoin(room, newUser) { if (Punishments.lastGroupchatMonitorTime[room.roomid] > Date.now() - GROUPCHAT_MONITOR_INTERVAL) return; const newUserID = toID(newUser); for (const [roomid, participants] of Object.entries(Punishments.bannedGroupchatParticipants)) { if (!participants.has(newUserID)) continue; let overlap = 0; for (const participant of participants) { if (participant in room.users || room.auth.has(participant)) overlap++; } if (overlap > GROUPCHAT_PARTICIPANT_OVERLAP_THRESHOLD) { let html = `|html|[GroupchatMonitor] The groupchat \xAB${room.roomid}\xBB `; if (Config.modloglink) html += `(logs) `; html += `includes ${overlap} participants from forcibly deleted groupchat \xAB${roomid}\xBB`; if (Config.modloglink) html += ` (logs)`; html += `.`; Rooms.global.notifyRooms(["staff"], html); Punishments.lastGroupchatMonitorTime[room.roomid] = Date.now(); } } } punishRange(range, reason, expireTime, punishType) { if (!expireTime) expireTime = Date.now() + RANGELOCK_DURATION; if (!punishType) punishType = "LOCK"; const punishment = { type: punishType, id: "#rangelock", expireTime, reason }; Punishments.ips.add(range, punishment); const ips = []; const parsedRange = IPTools.stringToRange(range); if (!parsedRange) throw new Error(`Invalid IP range: ${range}`); const { minIP, maxIP } = parsedRange; for (let ipNumber = minIP; ipNumber <= maxIP; ipNumber++) { ips.push(IPTools.numberToIP(ipNumber)); } void Punishments.appendPunishment({ userids: [], ips, punishType, expireTime, reason, rest: [] }, "#rangelock", PUNISHMENT_FILE, true); } banRange(range, reason, expireTime) { if (!expireTime) expireTime = Date.now() + RANGELOCK_DURATION; const punishment = { type: "BAN", id: "#rangelock", expireTime, reason }; Punishments.ips.add(range, punishment); } roomBan(room, user, expireTime, id, ...reason) { if (!expireTime) expireTime = Date.now() + ROOMBAN_DURATION; const punishment = { type: "ROOMBAN", id, expireTime, reason: reason.join(" ") }; const affected = Punishments.roomPunish(room, user, punishment); for (const curUser of affected) { room.game?.removeBannedUser?.(curUser); curUser.leaveRoom(room.roomid); } if (room.subRooms) { for (const subRoom of room.subRooms.values()) { for (const curUser of affected) { subRoom.game?.removeBannedUser?.(curUser); curUser.leaveRoom(subRoom.roomid); } } } return affected; } roomBlacklist(room, user, expireTime, id, ...reason) { if (!expireTime) expireTime = Date.now() + BLACKLIST_DURATION; const punishment = { type: "BLACKLIST", id, expireTime, reason: reason.join(" ") }; const affected = Punishments.roomPunish(room, user, punishment); for (const curUser of affected) { Punishments.roomUnban(room, curUser.id || curUser); room.game?.removeBannedUser?.(curUser); curUser.leaveRoom(room.roomid); } if (room.subRooms) { for (const subRoom of room.subRooms.values()) { for (const curUser of affected) { subRoom.game?.removeBannedUser?.(curUser); curUser.leaveRoom(subRoom.roomid); } } } return affected; } roomUnban(room, userid) { const user = Users.get(userid); if (user) { const punishment = Punishments.isRoomBanned(user, room.roomid); if (punishment) userid = punishment.id; } return Punishments.roomUnpunish(room, userid, "ROOMBAN"); } /** * @param ignoreWrite Flag to skip persistent storage. */ roomUnblacklist(room, userid, ignoreWrite) { const user = Users.get(userid); if (user) { const punishment = Punishments.isRoomBanned(user, room.roomid); if (punishment) userid = punishment.id; } return Punishments.roomUnpunish(room, userid, "BLACKLIST", ignoreWrite); } roomUnblacklistAll(room) { const roombans = Punishments.roomUserids.get(room.roomid); if (!roombans) return false; const unblacklisted = []; roombans.each(({ type }, userid) => { if (type === "BLACKLIST") { Punishments.roomUnblacklist(room, userid, true); unblacklisted.push(userid); } }); if (unblacklisted.length === 0) return false; Punishments.saveRoomPunishments(); return unblacklisted; } addSharedIp(ip, note) { const pattern = IPTools.stringToRange(ip); const isRange = pattern && pattern.minIP !== pattern.maxIP; if (isRange) { Punishments.sharedRanges.set(pattern, note); } else { Punishments.sharedIps.set(ip, note); } void Punishments.appendSharedIp(ip, note); for (const user of Users.users.values()) { const sharedIp = user.ips.some( (curIP) => isRange ? IPTools.checkPattern([pattern], IPTools.ipToNumber(curIP)) : curIP === ip ); if (user.locked && user.locked !== user.id && sharedIp) { if (!user.autoconfirmed) { user.semilocked = `#sharedip ${user.locked}`; } user.locked = null; user.namelocked = null; user.destroyPunishmentTimer(); user.updateIdentity(); } } } isSharedIp(ip) { if (this.sharedIps.has(ip)) return true; const num = IPTools.ipToNumber(ip); for (const range of this.sharedRanges.keys()) { if (IPTools.checkPattern([range], num)) { return true; } } return false; } removeSharedIp(ip) { const pattern = IPTools.stringToRange(ip); if (pattern && pattern.minIP !== pattern.maxIP) { const isMatch = (range) => range.minIP === pattern.minIP && range.maxIP === pattern.maxIP; Punishments.sharedRanges = new Map([...Punishments.sharedRanges].filter(([range]) => !isMatch(range))); } else { Punishments.sharedIps.delete(ip); } void Punishments.saveSharedIps(); } addBlacklistedSharedIp(ip, reason) { void Punishments.appendSharedIpBlacklist(ip, reason); Punishments.sharedIpBlacklist.set(ip, reason); } removeBlacklistedSharedIp(ip) { Punishments.sharedIpBlacklist.delete(ip); void Punishments.saveSharedIpBlacklist(); } whitelistName(name, whitelister) { if (this.namefilterwhitelist.has(name)) return false; name = toID(name); whitelister = toID(whitelister); this.namefilterwhitelist.set(name, whitelister); void this.appendWhitelistedName(name, whitelister); return true; } unwhitelistName(name) { name = toID(name); if (!this.namefilterwhitelist.has(name)) return false; this.namefilterwhitelist.delete(name); void this.saveNameWhitelist(); return true; } /********************************************************* * Checking *********************************************************/ /** * Returns an array of [key, roomid, punishment] pairs. * * @param searchId userid or IP */ search(searchId) { const results = []; Punishments.ips.each((punishment, ip) => { const { id } = punishment; if (searchId === id || searchId === ip) { results.push([ip, "", punishment]); } }); Punishments.userids.each((punishment, userid) => { const { id } = punishment; if (searchId === id || searchId === userid) { results.push([userid, "", punishment]); } }); Punishments.roomIps.nestedEach((punishment, roomid, ip) => { const { id: punishUserid } = punishment; if (searchId === punishUserid || searchId === ip) { results.push([ip, roomid, punishment]); } }); Punishments.roomUserids.nestedEach((punishment, roomid, userid) => { const { id: punishUserid } = punishment; if (searchId === punishUserid || searchId === userid) { results.push([userid, roomid, punishment]); } }); return results; } getPunishType(name) { let punishment = Punishments.userids.get(toID(name)); if (punishment) return punishment[0].type; const user = Users.get(name); if (!user) return; punishment = Punishments.ipSearch(user.latestIp); if (punishment) return punishment[0].type; return ""; } hasPunishType(name, types, ip) { if (typeof types === "string") types = [types]; const byName = Punishments.userids.get(name)?.some((p) => types.includes(p.type)); if (!ip) return byName; return byName || Punishments.ipSearch(ip)?.some((p) => types.includes(p.type)); } hasRoomPunishType(room, name, types) { if (typeof types === "string") types = [types]; if (typeof room.roomid === "string") room = room.roomid; return Punishments.roomUserids.nestedGet(room, name)?.some((p) => types.includes(p.type)); } byWeight(punishments, room = false) { if (!punishments) return []; return import_lib.Utils.sortBy( punishments, (p) => -(room ? this.sortedRoomTypes : this.sortedTypes).indexOf(p.type) ); } ipSearch(ip, type) { const allPunishments = []; let punishment = Punishments.ips.get(ip); if (punishment) { if (type) return punishment.find((p) => p.type === type); allPunishments.push(...punishment); } let dotIndex = ip.lastIndexOf("."); for (let i = 0; i < 4 && dotIndex > 0; i++) { ip = ip.substr(0, dotIndex); punishment = Punishments.ips.get(ip + ".*"); if (punishment) { if (type) return punishment.find((p) => p.type === type); allPunishments.push(...punishment); } dotIndex = ip.lastIndexOf("."); } return allPunishments.length ? allPunishments : void 0; } /** Defined in Punishments.loadBanlist */ checkRangeBanned(ip) { return false; } checkName(user, userid, registered) { if (userid.startsWith("guest")) return; for (const roomid of user.inRooms) { Punishments.checkNewNameInRoom(user, userid, roomid); } let punishments = []; const idPunishments = Punishments.userids.get(userid); if (idPunishments) { punishments = idPunishments; } const battleban = Punishments.isBattleBanned(user); if (battleban) punishments.push(battleban); if (user.namelocked) { let punishment = Punishments.userids.get(user.namelocked)?.[0]; if (!punishment) punishment = { type: "NAMELOCK", id: user.namelocked, expireTime: 0, reason: "" }; punishments.push(punishment); } if (user.locked) { let punishment = Punishments.userids.get(user.locked)?.[0]; if (!punishment) punishment = { type: "LOCK", id: user.locked, expireTime: 0, reason: "" }; punishments.push(punishment); } const ticket = Chat.pages?.help ? `` : ""; if (!punishments.length) return; Punishments.byWeight(punishments); for (const punishment of punishments) { const id = punishment.type; const punishmentInfo = this.punishmentTypes.get(id); const punishUserid = punishment.id; const reason = punishment.reason ? import_lib.Utils.html`\n\nReason: ${punishment.reason}` : ""; let appeal = ``; if (user.permalocked && Config.appealurl) { appeal += ` Permanent punishments can be appealed: ${Config.appealurl}`; } else if (ticket) { appeal += ` If you feel you were unfairly punished or wish to otherwise appeal, you can ${ticket}.`; } else if (Config.appealurl) { appeal += ` If you wish to appeal your punishment, please use: ${Config.appealurl}`; } const bannedUnder = punishUserid !== userid ? ` because you have the same IP as banned user: ${punishUserid}` : ""; if (id === "BATTLEBAN") { if (punishUserid !== user.id && Punishments.isSharedIp(user.latestIp) && user.autoconfirmed) { Punishments.unpunish(userid, "BATTLEBAN"); } else { void Punishments.punish(user, punishment, false); user.cancelReady(); if (!Punishments.userids.getByType(userid, "BATTLEBAN")) { const appealLink = ticket || (Config.appealurl ? `appeal at: ${Config.appealurl}` : ``); user.send( `|popup||html|You are banned from battling${punishment.id !== userid ? ` because you have the same IP as banned user: ${punishUserid}` : ""}. Your battle ban will expire in a few days.${punishment.reason ? import_lib.Utils.html`\n\nReason: ${punishment.reason}` : ``}${appealLink ? ` Or you can ${appealLink}.` : ``}` ); user.notified.punishment = true; continue; } } } if ((id === "LOCK" || id === "NAMELOCK") && punishUserid !== userid && Punishments.isSharedIp(user.latestIp)) { if (!user.autoconfirmed) { user.semilocked = `#sharedip ${user.locked}`; } user.locked = null; user.namelocked = null; user.destroyPunishmentTimer(); user.updateIdentity(); return; } if (id === "BAN") { const appealUrl = Config.banappealurl || Config.appealurl; user.popup( `Your username (${user.name}) is banned${bannedUnder}. Your ban will expire in a few days.${reason}${appealUrl ? `||||Or you can appeal at: ${appealUrl}` : ``}` ); user.notified.punishment = true; if (registered) void Punishments.punish(user, punishment, false); user.disconnectAll(); return; } if (id === "NAMELOCK" || user.namelocked) { user.send(`|popup||html|You are namelocked and can't have a username${bannedUnder}. Your namelock will expire in a few days.${reason}${appeal}`); user.locked = punishUserid; user.namelocked = punishUserid; user.resetName(); user.updateIdentity(); } else if (id === "LOCK") { if (punishUserid === "#hostfilter" || punishUserid === "#ipban") { user.send(`|popup||html|Your IP (${user.latestIp}) is currently locked due to being a proxy. We automatically lock these connections since they are used to spam, hack, or otherwise attack our server. Disable any proxies you are using to connect to PS. `); } else if (user.latestHostType === "proxy" && user.locked !== user.id) { user.send(`|popup||html|You are locked${bannedUnder} on the IP (${user.latestIp}), which is a proxy. We automatically lock these connections since they are used to spam, hack, or otherwise attack our server. Disable any proxies you are using to connect to PS. `); } else if (!user.notified.lock) { user.send(`|popup||html|You are locked${bannedUnder}. ${user.permalocked ? `This lock is permanent.` : `Your lock will expire in a few days.`}${reason}${appeal}`); } user.notified.lock = true; user.locked = punishUserid; user.updateIdentity(); } else if (punishmentInfo?.onActivate) { punishmentInfo.onActivate.call(this, user, punishment, null, punishment.id === user.id); } Punishments.checkPunishmentTime(user, punishment); } } checkIp(user, connection) { const ip = connection.ip; let punishments = Punishments.ipSearch(ip); if (!punishments && Punishments.checkRangeBanned(ip)) { punishments = [{ type: "LOCK", id: "#ipban", expireTime: Infinity, reason: "" }]; } if (punishments) { const isSharedIP = Punishments.isSharedIp(ip); let sharedAndHasPunishment = false; for (const punishment of punishments) { if (isSharedIP) { if (!user.locked && !user.autoconfirmed) { user.semilocked = `#sharedip ${punishment.id}`; } sharedAndHasPunishment = true; } else { if (["BAN", "LOCK", "NAMELOCK"].includes(punishment.type)) { user.locked = punishment.id; if (punishment.type === "NAMELOCK") { user.namelocked = punishment.id; user.resetName(true); } } else { const info = Punishments.punishmentTypes.get(punishment.type); info?.onActivate?.call(this, user, punishment, null, punishment.id === user.id); } } } if (!sharedAndHasPunishment) Punishments.checkPunishmentTime(user, Punishments.byWeight(punishments)[0]); } return IPTools.lookup(ip).then(({ dnsbl, host, hostType }) => { user = connection.user || user; if (hostType === "proxy" && !user.trusted && !user.locked) { user.locked = "#hostfilter"; } else if (dnsbl && !user.autoconfirmed) { user.semilocked = "#dnsbl"; } if (host) { user.latestHost = host; user.latestHostType = hostType; } Chat.hostfilter(host || "", user, connection, hostType); }); } /** * IP bans need to be checked separately since we don't even want to * make a User object if an IP is banned. */ checkIpBanned(connection) { const ip = connection.ip; if (Punishments.cfloods.has(ip) || Monitor.countConnection(ip) && Punishments.cfloods.add(ip)) { connection.send(`|popup||modal|PS is under heavy load and cannot accommodate your connection right now.`); return "#cflood"; } if (Punishments.isSharedIp(ip)) return false; let banned = false; const punishment = Punishments.ipSearch(ip, "BAN"); if (punishment) { banned = punishment.id; } if (!banned) return false; const appealUrl = Config.banappealurl || Config.appealurl; connection.send( `|popup||modal|You are banned because you have the same IP (${ip}) as banned user '${banned}'. Your ban will expire in a few days.${appealUrl ? `||||Or you can appeal at: ${appealUrl}` : ``}` ); Monitor.notice(`CONNECT BLOCKED - IP BANNED: ${ip} (${banned})`); return banned; } checkNameInRoom(user, roomid) { let punishment = Punishments.roomUserids.nestedGet(roomid, user.id); if (!punishment && user.autoconfirmed) { punishment = Punishments.roomUserids.nestedGet(roomid, user.autoconfirmed); } if (punishment?.some((p) => p.type === "ROOMBAN" || p.type === "BLACKLIST")) { return true; } const room = Rooms.get(roomid); if (room.parent) { return Punishments.checkNameInRoom(user, room.parent.roomid); } return false; } /** * @param userid The name into which the user is renamed. */ checkNewNameInRoom(user, userid, roomid) { let punishments = Punishments.roomUserids.nestedGet(roomid, userid) || null; if (!punishments) { const room = Rooms.get(roomid); if (room.parent) { punishments = Punishments.roomUserids.nestedGet(room.parent.roomid, userid) || null; } } if (punishments) { for (const punishment of punishments) { const info = this.roomPunishmentTypes.get(punishment.type); if (info?.onActivate) { info.onActivate.call(this, user, punishment, Rooms.get(roomid), punishment.id === user.id); continue; } if (punishment.type !== "ROOMBAN" && punishment.type !== "BLACKLIST") return null; const room = Rooms.get(roomid); room.game?.removeBannedUser?.(user); user.leaveRoom(room.roomid); } return punishments; } return null; } /** * @return Descriptive text for the remaining time until the punishment expires, if any. */ checkLockExpiration(userid) { if (!userid) return ``; const punishment = Punishments.userids.getByType(userid, "LOCK"); const user = Users.get(userid); if (user?.permalocked) return ` (never expires; you are permalocked)`; return Punishments.checkPunishmentExpiration(punishment); } checkPunishmentExpiration(punishment) { if (!punishment) return ``; const expiresIn = new Date(punishment.expireTime).getTime() - Date.now(); const expiresDays = Math.round(expiresIn / 1e3 / 60 / 60 / 24); let expiresText = ""; if (expiresDays >= 1) { expiresText = `in around ${Chat.count(expiresDays, "days")}`; } else { expiresText = `soon`; } if (expiresIn > 1) return ` (expires ${expiresText})`; } isRoomBanned(user, roomid) { if (!user) throw new Error(`Trying to check if a non-existent user is room banned.`); let punishments = Punishments.roomUserids.nestedGet(roomid, user.id); for (const p of punishments || []) { if (p.type === "ROOMBAN" || p.type === "BLACKLIST") return p; } if (user.autoconfirmed) { punishments = Punishments.roomUserids.nestedGet(roomid, user.autoconfirmed); for (const p of punishments || []) { if (p.type === "ROOMBAN" || p.type === "BLACKLIST") return p; } } if (!user.trusted) { for (const ip of user.ips) { punishments = Punishments.roomIps.nestedGet(roomid, ip); if (punishments) { for (const punishment of punishments) { if (punishment.type === "ROOMBAN") { return punishment; } else if (punishment.type === "BLACKLIST") { if (Punishments.isSharedIp(ip) && user.autoconfirmed) continue; return punishment; } } } } } for (const id of user.previousIDs) { punishments = Punishments.roomUserids.nestedGet(roomid, id); for (const p of punishments || []) { if (["ROOMBAN", "BLACKLIST"].includes(p.type)) return p; } } const room = Rooms.get(roomid); if (!room) throw new Error(`Trying to ban a user from a nonexistent room: ${roomid}`); if (room.parent) return Punishments.isRoomBanned(user, room.parent.roomid); } isGlobalBanned(user) { if (!user) throw new Error(`Trying to check if a non-existent user is global banned.`); const punishment = Punishments.userids.getByType(user.id, "BAN") || Punishments.userids.getByType(user.id, "FORCEBAN"); if (punishment) return punishment; } isBlacklistedSharedIp(ip) { const pattern = IPTools.stringToRange(ip); if (!pattern) { throw new Error(`Invalid IP address: '${ip}'`); } for (const [blacklisted, reason] of this.sharedIpBlacklist) { const range = IPTools.stringToRange(blacklisted); if (!range) throw new Error("Falsy range in sharedIpBlacklist"); if (IPTools.rangeIntersects(range, pattern)) return reason; } return false; } /** * Returns an array of all room punishments associated with a user. * * options.publicOnly will make this only return public room punishments. * options.checkIps will also check the IP of the user for IP-based punishments. */ getRoomPunishments(user, options = {}) { if (!user) return []; const userid = toID(user); const punishments = []; for (const curRoom of Rooms.global.chatRooms) { if (!curRoom || curRoom.settings.isPrivate === true || options.publicOnly && curRoom.settings.isPersonal) continue; let punishment = Punishments.roomUserids.nestedGet(curRoom.roomid, userid); if (punishment) { for (const p of punishment) { punishments.push([curRoom, p]); } continue; } else if (options?.checkIps) { if (typeof user !== "string") { let longestIPPunishment; for (const ip of user.ips) { punishment = Punishments.roomIps.nestedGet(curRoom.roomid, ip); if (punishment && (!longestIPPunishment || punishment[2] > longestIPPunishment[2])) { longestIPPunishment = punishment; } } if (longestIPPunishment) { for (const p of longestIPPunishment) { punishments.push([curRoom, p]); } continue; } } } if (typeof user !== "string" && curRoom.muteQueue) { for (const entry of curRoom.muteQueue) { if (userid === entry.userid || user.guestNum === entry.guestNum || user.autoconfirmed && user.autoconfirmed === entry.autoconfirmed) { punishments.push([curRoom, { type: "MUTE", id: entry.userid, expireTime: entry.time, reason: "" }]); } } } } return punishments; } getPunishments(roomid, ignoreMutes) { const punishmentTable = []; if (roomid && (!Punishments.roomIps.has(roomid) || !Punishments.roomUserids.has(roomid))) return punishmentTable; (roomid ? Punishments.roomIps.get(roomid) : Punishments.ips).each((punishment, ip) => { const { type, id, expireTime, reason, rest } = punishment; if (id !== "#rangelock" && id.startsWith("#")) return; let entry = punishmentTable.find((e) => e[0] === id && e[1].punishType === type)?.[1]; if (entry) { entry.ips.push(ip); return; } entry = { userids: [], ips: [ip], punishType: type, expireTime, reason, rest: rest || [] }; punishmentTable.push([id, entry]); }); (roomid ? Punishments.roomUserids.get(roomid) : Punishments.userids).each((punishment, userid) => { const { type, id, expireTime, reason, rest } = punishment; if (id.startsWith("#")) return; let entry = punishmentTable.find(([curId, cur]) => id === curId && cur.punishType === type)?.[1]; if (!entry) { entry = { userids: [], ips: [], punishType: type, expireTime, reason, rest: rest || [] }; punishmentTable.push([id, entry]); } if (userid !== id) entry.userids.push(userid); }); if (roomid && ignoreMutes !== false) { const room = Rooms.get(roomid); if (room?.muteQueue) { for (const mute of room.muteQueue) { punishmentTable.push([mute.userid, { userids: [], ips: [], punishType: "MUTE", expireTime: mute.time, reason: "", rest: [] }]); } } } return punishmentTable; } visualizePunishments(punishments, user) { let buf = ""; buf += `

List of active punishments:

`; buf += ``; buf += ``; buf += `Username`; buf += `Punishment type`; buf += `Expire time`; buf += `Reason`; buf += `Alts`; if (user.can("ip")) buf += `IPs`; buf += ``; for (const [userid, punishment] of punishments) { const expiresIn = new Date(punishment.expireTime).getTime() - Date.now(); if (expiresIn < 1e3) continue; const expireString = Chat.toDurationString(expiresIn, { precision: 1 }); buf += ``; buf += `${userid}`; buf += `${punishment.punishType}`; buf += `${expireString}`; buf += `${punishment.reason || " - "}`; buf += `${punishment.userids.join(", ") || " - "}`; if (user.can("ip")) buf += `${punishment.ips.join(", ") || " - "}`; buf += ``; } buf += ``; buf += `
`; return buf; } /** * Notifies staff if a user has three or more room punishments. */ async monitorRoomPunishments(user) { if (user.locked) return; const userid = toID(user); const minPunishments = typeof Config.monitorminpunishments === "number" ? Config.monitorminpunishments : 3; if (!minPunishments) return; let punishments = Punishments.getRoomPunishments(user, { checkIps: true, publicOnly: true }); punishments = punishments.filter(([room, punishment]) => Punishments.roomPunishmentTypes.get(punishment.type)?.activatePunishMonitor); if (punishments.length >= minPunishments) { let points = 0; const punishmentText = punishments.map(([room, punishment]) => { const { type: punishType, id: punishUserid, reason } = punishment; if (punishType in PUNISHMENT_POINT_VALUES) points += PUNISHMENT_POINT_VALUES[punishType]; let punishDesc = Punishments.roomPunishmentTypes.get(punishType)?.desc; if (!punishDesc) punishDesc = `punished`; if (punishUserid !== userid) punishDesc += ` as ${punishUserid}`; const trimmedReason = reason?.trim(); if (trimmedReason && !trimmedReason.startsWith("(PROOF:")) punishDesc += `: ${trimmedReason}`; return `<<${room}>> (${punishDesc})`; }).join(", "); if (Config.punishmentautolock && points >= AUTOLOCK_POINT_THRESHOLD) { const rooms = punishments.map(([room]) => room).join(", "); const reason = `Autolocked for having punishments in ${punishments.length} rooms: ${rooms}`; const message = `${user.name || userid} was locked for having punishments in ${punishments.length} rooms: ${punishmentText}`; const globalPunishments = await Rooms.Modlog.getGlobalPunishments(userid, AUTOWEEKLOCK_DAYS_TO_SEARCH); const isWeek = globalPunishments !== null && globalPunishments >= AUTOWEEKLOCK_THRESHOLD; void Punishments.autolock(user, "staff", "PunishmentMonitor", reason, message, isWeek); if (typeof user !== "string") { user.popup( `|modal|You've been locked for breaking the rules in multiple chatrooms. If you feel that your lock was unjustified, you can still PM staff members (%, @, ~) to discuss it${Config.appealurl ? ` or you can appeal: ${Config.appealurl}` : "."} Your lock will expire in a few days.` ); } } else { Monitor.log(`[PunishmentMonitor] ${user.name || userid} currently has punishments in ${punishments.length} rooms: ${punishmentText}`); } } } renameRoom(oldID, newID) { for (const table of [Punishments.roomUserids, Punishments.roomIps]) { const entry = table.get(oldID); if (entry) { table.set(newID, entry); table.delete(oldID); } } Punishments.saveRoomPunishments(); } }(); //# sourceMappingURL=punishments.js.map