mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-26 10:48:53 -05:00
1350 lines
37 KiB
JavaScript
1350 lines
37 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
|
|
let Punishments = module.exports;
|
|
|
|
const FS = require('./fs');
|
|
|
|
const PUNISHMENT_FILE = 'config/punishments.tsv';
|
|
const ROOM_PUNISHMENT_FILE = 'config/room-punishments.tsv';
|
|
const SHAREDIPS_FILE = 'config/sharedips.tsv';
|
|
|
|
const RANGELOCK_DURATION = 60 * 60 * 1000; // 1 hour
|
|
const LOCK_DURATION = 48 * 60 * 60 * 1000; // 48 hours
|
|
const BAN_DURATION = 7 * 24 * 60 * 60 * 1000; // 1 week
|
|
|
|
const ROOMBAN_DURATION = 48 * 60 * 60 * 1000; // 48 hours
|
|
const BLACKLIST_DURATION = 365 * 24 * 60 * 60 * 1000; // 1 year
|
|
|
|
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;
|
|
|
|
/**
|
|
* a punishment is an array: [punishType, userid, expireTime, reason]
|
|
* @typedef {[string, string, number, string]} Punishment
|
|
*/
|
|
|
|
class PunishmentMap extends Map/*:: <string, Punishment> */ {
|
|
get(k) {
|
|
const punishment = super.get(k);
|
|
if (punishment) {
|
|
if (Date.now() < punishment[2]) return punishment;
|
|
this.delete(k);
|
|
}
|
|
return undefined;
|
|
}
|
|
has(k) {
|
|
return !!this.get(k);
|
|
}
|
|
forEach(callback) {
|
|
super.forEach((punishment, k) => {
|
|
if (Date.now() < punishment[2]) return callback(punishment, k);
|
|
this.delete(k);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ips is an ip:punishment Map
|
|
*/
|
|
Punishments.ips = new PunishmentMap();
|
|
|
|
/**
|
|
* userids is a userid:punishment Map
|
|
*/
|
|
Punishments.userids = new PunishmentMap();
|
|
|
|
/**
|
|
* @augments {Map<string, Punishment>}
|
|
*/
|
|
class NestedPunishmentMap extends Map {
|
|
nestedSet(k1, k2, value) {
|
|
if (!this.get(k1)) {
|
|
this.set(k1, new Map());
|
|
}
|
|
this.get(k1).set(k2, value);
|
|
}
|
|
nestedGet(k1, k2) {
|
|
const subMap = this.get(k1);
|
|
if (!subMap) return subMap;
|
|
const punishment = subMap.get(k2);
|
|
if (punishment) {
|
|
if (Date.now() < punishment[2]) return punishment;
|
|
this.nestedDelete(k1, k2);
|
|
}
|
|
return undefined;
|
|
}
|
|
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);
|
|
}
|
|
nestedForEach(callback) {
|
|
this.forEach((subMap, k1) => {
|
|
subMap.forEach((punishment, k2) => {
|
|
if (Date.now() < punishment[2]) return callback(punishment, k1, k2);
|
|
this.nestedDelete(k1, k2);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* roomUserids is a roomid:userid:punishment nested Map
|
|
*/
|
|
Punishments.roomUserids = new NestedPunishmentMap();
|
|
|
|
/**
|
|
* roomIps is a roomid:ip:punishment Map
|
|
*/
|
|
Punishments.roomIps = new NestedPunishmentMap();
|
|
|
|
/**
|
|
* sharedIps is an ip:note Map
|
|
*/
|
|
Punishments.sharedIps = new Map();
|
|
|
|
|
|
/*********************************************************
|
|
* Persistence
|
|
*********************************************************/
|
|
|
|
// punishType is an allcaps string, for global punishments they can be one of the following:
|
|
// 'LOCK'
|
|
// 'BAN'
|
|
// 'NAMELOCK'
|
|
|
|
// 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)
|
|
|
|
Punishments.roomPunishmentTypes = new Map([
|
|
['ROOMBAN', 'banned'],
|
|
['BLACKLIST', 'blacklisted'],
|
|
['MUTE', 'muted'],
|
|
]);
|
|
|
|
// punishments.tsv is in the format:
|
|
// punishType, userid, ips/usernames, expiration time
|
|
// room-punishments.tsv is in the format:
|
|
// punishType, roomid:userid, ips/usernames, expiration time
|
|
|
|
|
|
Punishments.loadPunishments = async function () {
|
|
const data = await FS(PUNISHMENT_FILE).readTextIfExists();
|
|
if (!data) return;
|
|
for (const row of data.split("\n")) {
|
|
if (!row || row === '\r') continue;
|
|
const [punishType, id, altKeys, expireTimeStr, ...rest] = row.trim().split("\t");
|
|
const expireTime = Number(expireTimeStr);
|
|
if (punishType === "Punishment") continue;
|
|
const keys = altKeys.split(',').concat(id);
|
|
|
|
const punishment = [punishType, id, expireTime].concat(rest);
|
|
if (Date.now() >= expireTime) {
|
|
continue;
|
|
}
|
|
for (let j = 0; j < keys.length; j++) {
|
|
const key = keys[j];
|
|
if (!USERID_REGEX.test(key)) {
|
|
Punishments.ips.set(key, punishment);
|
|
} else {
|
|
Punishments.userids.set(key, punishment);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Punishments.loadRoomPunishments = async function () {
|
|
const data = await FS(ROOM_PUNISHMENT_FILE).readTextIfExists();
|
|
if (!data) return;
|
|
for (const row of data.split("\n")) {
|
|
if (!row || row === '\r') continue;
|
|
const [punishType, id, altKeys, expireTimeStr, ...rest] = row.trim().split("\t");
|
|
const expireTime = Number(expireTimeStr);
|
|
if (punishType === "Punishment") continue;
|
|
const [roomid, userid] = id.split(':');
|
|
if (!userid) continue; // invalid format
|
|
const keys = altKeys.split(',').concat(userid);
|
|
|
|
const punishment = [punishType, userid, expireTime].concat(rest);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Punishments.savePunishments = function () {
|
|
const saveTable = new Map();
|
|
Punishments.ips.forEach((punishment, ip) => {
|
|
const [punishType, id, ...rest] = punishment;
|
|
if (id.charAt(0) === '#') return;
|
|
let entry = saveTable.get(id);
|
|
|
|
if (entry) {
|
|
entry.keys.push(ip);
|
|
return;
|
|
}
|
|
|
|
entry = {
|
|
keys: [ip],
|
|
punishType: punishType,
|
|
rest: rest,
|
|
};
|
|
saveTable.set(id, entry);
|
|
});
|
|
Punishments.userids.forEach((punishment, userid) => {
|
|
const [punishType, id, ...rest] = punishment;
|
|
if (id.charAt(0) === '#') return;
|
|
let entry = saveTable.get(id);
|
|
|
|
if (!entry) {
|
|
entry = {
|
|
keys: [],
|
|
punishType: punishType,
|
|
rest: rest,
|
|
};
|
|
saveTable.set(id, entry);
|
|
}
|
|
|
|
if (userid !== id) entry.keys.push(userid);
|
|
});
|
|
|
|
let buf = 'Punishment\tUser ID\tIPs and alts\tExpires\r\n';
|
|
saveTable.forEach((entry, id) => {
|
|
buf += Punishments.renderEntry(entry, id);
|
|
});
|
|
|
|
FS(PUNISHMENT_FILE).write(buf);
|
|
};
|
|
|
|
Punishments.saveRoomPunishments = function () {
|
|
const saveTable = new Map();
|
|
Punishments.roomIps.nestedForEach((punishment, roomid, ip) => {
|
|
const [punishType, punishUserid, ...rest] = punishment;
|
|
const id = roomid + ':' + punishUserid;
|
|
if (id.charAt(0) === '#') return;
|
|
let entry = saveTable.get(id);
|
|
|
|
if (entry) {
|
|
entry.keys.push(ip);
|
|
return;
|
|
}
|
|
|
|
entry = {
|
|
keys: [ip],
|
|
punishType: punishType,
|
|
rest: rest,
|
|
};
|
|
saveTable.set(id, entry);
|
|
});
|
|
Punishments.roomUserids.nestedForEach((punishment, roomid, userid) => {
|
|
const [punishType, punishUserid, ...rest] = punishment;
|
|
const id = roomid + ':' + punishUserid;
|
|
let entry = saveTable.get(id);
|
|
|
|
if (!entry) {
|
|
entry = {
|
|
keys: [],
|
|
punishType: punishType,
|
|
rest: rest,
|
|
};
|
|
saveTable.set(id, entry);
|
|
}
|
|
|
|
if (userid !== punishUserid) entry.keys.push(userid);
|
|
});
|
|
|
|
let buf = 'Punishment\tRoom ID:User ID\tIPs and alts\tExpires\r\n';
|
|
saveTable.forEach((entry, id) => {
|
|
buf += Punishments.renderEntry(entry, id);
|
|
});
|
|
|
|
FS(ROOM_PUNISHMENT_FILE).write(buf);
|
|
};
|
|
|
|
/**
|
|
* @param {Object} entry
|
|
* @param {string} id
|
|
*/
|
|
Punishments.appendPunishment = function (entry, id, filename) {
|
|
if (id.charAt(0) === '#') return;
|
|
let buf = Punishments.renderEntry(entry, id);
|
|
FS(filename).append(buf);
|
|
};
|
|
|
|
/**
|
|
* @param {Object} entry
|
|
* @param {string} id
|
|
* @return {string}
|
|
*/
|
|
Punishments.renderEntry = function (entry, id) {
|
|
let row = [entry.punishType, id, entry.keys.join(',')].concat(entry.rest);
|
|
return row.join('\t') + '\r\n';
|
|
};
|
|
|
|
Punishments.loadBanlist = async function () {
|
|
const data = await FS('config/ipbans.txt').readTextIfExists();
|
|
if (!data) return;
|
|
let 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.set(ip, ['BAN', '#ipban', Infinity]);
|
|
}
|
|
}
|
|
Punishments.checkRangeBanned = Dnsbl.checker(rangebans);
|
|
};
|
|
|
|
// sharedips.tsv is in the format:
|
|
// IP, type (in this case always SHARED), note
|
|
|
|
Punishments.loadSharedIps = async function () {
|
|
const data = await FS(SHAREDIPS_FILE).readTextIfExists();
|
|
if (!data) return;
|
|
for (const row of data.split("\n")) {
|
|
if (!row || row === '\r') continue;
|
|
const [ip, type, note] = row.trim().split("\t");
|
|
if (!ip.includes('.')) continue;
|
|
if (type !== 'SHARED') continue;
|
|
|
|
Punishments.sharedIps.set(ip, note);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} ip
|
|
* @param {string} note
|
|
*/
|
|
Punishments.appendSharedIp = function (ip, note) {
|
|
let buf = `${ip}\tSHARED\t${note}\r\n`;
|
|
FS(SHAREDIPS_FILE).append(buf);
|
|
};
|
|
|
|
Punishments.saveSharedIps = function () {
|
|
let buf = 'IP\tType\tNote\r\n';
|
|
Punishments.sharedIps.forEach((note, ip) => {
|
|
buf += `${ip}\tSHARED\t${note}\r\n`;
|
|
});
|
|
|
|
FS(SHAREDIPS_FILE).write(buf);
|
|
};
|
|
|
|
setImmediate(() => {
|
|
Punishments.loadPunishments();
|
|
Punishments.loadRoomPunishments();
|
|
Punishments.loadBanlist();
|
|
Punishments.loadSharedIps();
|
|
});
|
|
|
|
/*********************************************************
|
|
* Adding and removing
|
|
*********************************************************/
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {Punishment} punishment
|
|
* @param {?Set<string>} recursionKeys
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.punish = function (user, punishment, recursionKeys) {
|
|
let existingPunishment = Punishments.userids.get(toId(user.name));
|
|
if (existingPunishment) {
|
|
// don't reduce the duration of an existing punishment
|
|
if (existingPunishment[2] > punishment[2]) {
|
|
punishment[2] = existingPunishment[2];
|
|
}
|
|
|
|
// don't override stronger punishment types
|
|
const types = ['LOCK', 'NAMELOCK', 'BAN'];
|
|
if (types.indexOf(existingPunishment[0]) > types.indexOf(punishment[0])) {
|
|
punishment[0] = existingPunishment[0];
|
|
}
|
|
}
|
|
|
|
let keys = recursionKeys || new Set();
|
|
let affected;
|
|
|
|
if (!recursionKeys) {
|
|
affected = user.getAltUsers(PUNISH_TRUSTED, true);
|
|
for (let curUser of affected) {
|
|
this.punish(curUser, punishment, keys);
|
|
}
|
|
}
|
|
|
|
for (let ip in user.ips) {
|
|
Punishments.ips.set(ip, punishment);
|
|
keys.add(ip);
|
|
}
|
|
let lastUserId = user.getLastId();
|
|
if (!lastUserId.startsWith('guest')) {
|
|
Punishments.userids.set(lastUserId, punishment);
|
|
}
|
|
if (user.autoconfirmed) {
|
|
Punishments.userids.set(user.autoconfirmed, punishment);
|
|
keys.add(user.autoconfirmed);
|
|
}
|
|
if (user.trusted) {
|
|
Punishments.userids.set(user.trusted, punishment);
|
|
keys.add(user.trusted);
|
|
if (!PUNISH_TRUSTED) affected.unshift(user);
|
|
}
|
|
if (!recursionKeys) {
|
|
const [punishType, id, ...rest] = punishment;
|
|
keys.delete(id);
|
|
Punishments.appendPunishment({
|
|
keys: Array.from(keys),
|
|
punishType: punishType,
|
|
rest: rest,
|
|
}, id, PUNISHMENT_FILE);
|
|
return affected;
|
|
}
|
|
};
|
|
|
|
Punishments.punishName = function (userid, punishment) {
|
|
let foundKeys = Punishments.search(userid)[0].map(key => key.split(':')[0]);
|
|
let userids = new Set([userid]);
|
|
let ips = new Set();
|
|
for (let key of foundKeys) {
|
|
if (key.includes('.')) {
|
|
ips.add(key);
|
|
} else {
|
|
userids.add(key);
|
|
}
|
|
}
|
|
userids.forEach(id => {
|
|
Punishments.userids.set(id, punishment);
|
|
});
|
|
ips.forEach(ip => {
|
|
Punishments.ips.set(ip, punishment);
|
|
});
|
|
const [punishType, id, ...rest] = punishment;
|
|
let affected = Users.findUsers(Array.from(userids), Array.from(ips), {includeTrusted: PUNISH_TRUSTED, forPunishment: true});
|
|
userids.delete(id);
|
|
Punishments.appendPunishment({
|
|
keys: Array.from(userids).concat(Array.from(ips)),
|
|
punishType: punishType,
|
|
rest: rest,
|
|
}, id, PUNISHMENT_FILE);
|
|
|
|
return affected;
|
|
};
|
|
|
|
/**
|
|
* @param {string} id
|
|
* @param {string} punishType
|
|
*/
|
|
Punishments.unpunish = function (id, punishType) {
|
|
id = toId(id);
|
|
let punishment = Punishments.userids.get(id);
|
|
if (punishment) {
|
|
id = punishment[1];
|
|
}
|
|
// in theory we can stop here if punishment doesn't exist, but
|
|
// in case of inconsistent state, we'll try anyway
|
|
|
|
let success = false;
|
|
Punishments.ips.forEach((punishment, key) => {
|
|
if (punishment[1] === id && punishment[0] === punishType) {
|
|
Punishments.ips.delete(key);
|
|
success = id;
|
|
}
|
|
});
|
|
Punishments.userids.forEach((punishment, key) => {
|
|
if (punishment[1] === id && punishment[0] === punishType) {
|
|
Punishments.userids.delete(key);
|
|
success = id;
|
|
}
|
|
});
|
|
if (success) {
|
|
Punishments.savePunishments();
|
|
}
|
|
return success;
|
|
};
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {Punishment} punishment
|
|
* @param {?Set<string>} recursionKeys
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.roomPunish = function (room, user, punishment, recursionKeys) {
|
|
let keys = recursionKeys || new Set();
|
|
let affected;
|
|
|
|
if (!recursionKeys) {
|
|
affected = user.getAltUsers(PUNISH_TRUSTED, true);
|
|
for (let curUser of affected) {
|
|
this.roomPunish(room, curUser, punishment, keys);
|
|
}
|
|
}
|
|
|
|
for (let ip in user.ips) {
|
|
Punishments.roomIps.nestedSet(room.id, ip, punishment);
|
|
keys.add(ip);
|
|
}
|
|
if (!user.userid.startsWith('guest')) {
|
|
Punishments.roomUserids.nestedSet(room.id, user.userid, punishment);
|
|
}
|
|
if (user.autoconfirmed) {
|
|
Punishments.roomUserids.nestedSet(room.id, user.autoconfirmed, punishment);
|
|
keys.add(user.autoconfirmed);
|
|
}
|
|
if (user.trusted) {
|
|
Punishments.roomUserids.nestedSet(room.id, user.trusted, punishment);
|
|
keys.add(user.trusted);
|
|
if (!PUNISH_TRUSTED) affected.unshift(user);
|
|
}
|
|
if (!recursionKeys) {
|
|
const [punishType, id, ...rest] = punishment;
|
|
keys.delete(id);
|
|
Punishments.appendPunishment({
|
|
keys: Array.from(keys),
|
|
punishType: punishType,
|
|
rest: rest,
|
|
}, room.id + ':' + id, ROOM_PUNISHMENT_FILE);
|
|
|
|
if (!(room.isPrivate === true || room.isPersonal || room.battle)) Punishments.monitorRoomPunishments(user);
|
|
|
|
return affected;
|
|
}
|
|
};
|
|
|
|
Punishments.roomPunishName = function (room, userid, punishment) {
|
|
let foundKeys = Punishments.search(userid)[0].map(key => key.split(':')[0]);
|
|
let userids = new Set([userid]);
|
|
let ips = new Set();
|
|
for (let key of foundKeys) {
|
|
if (key.includes('.')) {
|
|
ips.add(key);
|
|
} else {
|
|
userids.add(key);
|
|
}
|
|
}
|
|
userids.forEach(id => {
|
|
Punishments.roomUserids.nestedSet(room.id, id, punishment);
|
|
});
|
|
ips.forEach(ip => {
|
|
Punishments.roomIps.nestedSet(room.id, ip, punishment);
|
|
});
|
|
const [punishType, id, ...rest] = punishment;
|
|
let affected = Users.findUsers(Array.from(userids), Array.from(ips), {includeTrusted: PUNISH_TRUSTED, forPunishment: true});
|
|
userids.delete(id);
|
|
Punishments.appendPunishment({
|
|
keys: Array.from(userids).concat(Array.from(ips)),
|
|
punishType: punishType,
|
|
rest: rest,
|
|
}, room.id + ':' + id, ROOM_PUNISHMENT_FILE);
|
|
|
|
if (!(room.isPrivate === true || room.isPersonal || room.battle)) Punishments.monitorRoomPunishments(userid);
|
|
return affected;
|
|
};
|
|
|
|
/**
|
|
* @param {Room} room
|
|
* @param {string} userid
|
|
* @param {string} punishType
|
|
* @param {boolean} ignoreWrite Flag to skip persistent storage.
|
|
*/
|
|
Punishments.roomUnpunish = function (room, id, punishType, ignoreWrite) {
|
|
id = toId(id);
|
|
let punishment = Punishments.roomUserids.nestedGet(room, id);
|
|
if (punishment) {
|
|
id = punishment[1];
|
|
}
|
|
// in theory we can stop here if punishment doesn't exist, but
|
|
// in case of inconsistent state, we'll try anyway
|
|
|
|
let success;
|
|
const ipSubMap = Punishments.roomIps.get(room.id);
|
|
if (ipSubMap) {
|
|
ipSubMap.forEach((punishment, key) => {
|
|
if (punishment[1] === id && punishment[0] === punishType) {
|
|
ipSubMap.delete(key);
|
|
success = id;
|
|
}
|
|
});
|
|
}
|
|
const useridSubMap = Punishments.roomUserids.get(room.id);
|
|
if (useridSubMap) {
|
|
useridSubMap.forEach((punishment, key) => {
|
|
if (punishment[1] === id && punishment[0] === punishType) {
|
|
useridSubMap.delete(key);
|
|
success = id;
|
|
}
|
|
});
|
|
}
|
|
if (success && !ignoreWrite) {
|
|
Punishments.saveRoomPunishments();
|
|
}
|
|
return success;
|
|
};
|
|
|
|
/*********************************************************
|
|
* Specific punishments
|
|
*********************************************************/
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {number} expireTime
|
|
* @param {string} id
|
|
* @param {...string} [reason]
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.ban = function (user, expireTime, id, ...reason) {
|
|
if (!id) id = user.getLastId();
|
|
|
|
if (!expireTime) expireTime = Date.now() + BAN_DURATION;
|
|
let punishment = ['BAN', id, expireTime, ...reason];
|
|
|
|
let affected = Punishments.punish(user, punishment);
|
|
for (let curUser of affected) {
|
|
curUser.locked = id;
|
|
curUser.disconnectAll();
|
|
}
|
|
|
|
return affected;
|
|
};
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
Punishments.unban = function (name) {
|
|
return Punishments.unpunish(name, 'BAN');
|
|
};
|
|
/**
|
|
* @param {User} user
|
|
* @param {number} expireTime
|
|
* @param {string} id
|
|
* @param {...string} [reason]
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.lock = function (user, expireTime, id, ...reason) {
|
|
if (!id && user) id = user.getLastId();
|
|
if (!user || typeof user === 'string') user = Users(id);
|
|
|
|
if (!expireTime) expireTime = Date.now() + LOCK_DURATION;
|
|
let punishment = ['LOCK', id, expireTime, ...reason];
|
|
|
|
let affected = [];
|
|
|
|
if (user) {
|
|
affected = Punishments.punish(user, punishment);
|
|
} else {
|
|
affected = Punishments.punishName(id, punishment);
|
|
}
|
|
|
|
for (let curUser of affected) {
|
|
curUser.locked = id;
|
|
curUser.updateIdentity();
|
|
}
|
|
|
|
return affected;
|
|
};
|
|
/**
|
|
* @param {User} user
|
|
* @param {Room} room
|
|
* @param {string} source
|
|
* @param {string} reason
|
|
* @param {?string} message
|
|
* @param {?boolean} week
|
|
*/
|
|
Punishments.autolock = function (user, room, source, reason, message, week) {
|
|
if (!message) message = reason;
|
|
|
|
let punishment = `LOCKED`;
|
|
let expires = null;
|
|
if (week) {
|
|
expires = Date.now() + 7 * 24 * 60 * 60 * 1000;
|
|
punishment = `WEEKLOCKED`;
|
|
}
|
|
Punishments.lock(user, expires, toId(user), `Autolock: ${user.name || toId(user)}: ${reason}`);
|
|
Monitor.log(`[${source}] ${punishment}: ${message}`);
|
|
Rooms.global.modlog(`(${toId(room)}) AUTOLOCK: [${toId(user)}]: ${reason}`);
|
|
};
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
Punishments.unlock = function (name) {
|
|
let user = Users(name);
|
|
let id = toId(name);
|
|
let success = [];
|
|
if (user && user.locked && !user.namelocked) {
|
|
id = user.locked;
|
|
user.locked = false;
|
|
user.namelocked = false;
|
|
user.updateIdentity();
|
|
success.push(user.getLastName());
|
|
if (id.charAt(0) !== '#') {
|
|
Users.users.forEach(curUser => {
|
|
if (curUser.locked === id) {
|
|
curUser.locked = false;
|
|
curUser.namelocked = false;
|
|
curUser.updateIdentity();
|
|
success.push(curUser.getLastName());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (Punishments.unpunish(name, 'LOCK')) {
|
|
if (!success.length) success.push(name);
|
|
}
|
|
if (!success.length) return undefined;
|
|
if (!success.some(v => toId(v) === id)) {
|
|
success.push(id);
|
|
}
|
|
return success;
|
|
};
|
|
/**
|
|
* @param {User} user
|
|
* @param {number} expireTime
|
|
* @param {string} id
|
|
* @param {...string} [reason]
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.namelock = function (user, expireTime, id, ...reason) {
|
|
if (!id) id = user.getLastId();
|
|
|
|
if (!expireTime) expireTime = Date.now() + LOCK_DURATION;
|
|
let punishment = ['NAMELOCK', id, expireTime, ...reason];
|
|
|
|
let affected = Punishments.punish(user, punishment);
|
|
for (let curUser of affected) {
|
|
curUser.locked = id;
|
|
curUser.namelocked = id;
|
|
curUser.resetName();
|
|
curUser.updateIdentity();
|
|
}
|
|
|
|
return affected;
|
|
};
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
Punishments.unnamelock = function (name) {
|
|
let user = Users(name);
|
|
let id = toId(name);
|
|
let success = [];
|
|
let unpunished = Punishments.unpunish(name, 'NAMELOCK');
|
|
if (user && user.locked) {
|
|
id = user.locked;
|
|
user.locked = false;
|
|
user.namelocked = false;
|
|
user.resetName();
|
|
success.push(user.getLastName());
|
|
if (id.charAt(0) !== '#') {
|
|
Users.users.forEach(curUser => {
|
|
if (curUser.locked === id) {
|
|
curUser.locked = false;
|
|
curUser.namelocked = false;
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* @param {string} range
|
|
* @param {string} reason
|
|
*/
|
|
Punishments.lockRange = function (range, reason) {
|
|
let punishment = ['LOCK', '#rangelock', Date.now() + RANGELOCK_DURATION, reason];
|
|
Punishments.ips.set(range, punishment);
|
|
};
|
|
/**
|
|
* @param {string} range
|
|
* @param {string} reason
|
|
*/
|
|
Punishments.banRange = function (range, reason) {
|
|
let punishment = ['BAN', '#rangelock', Date.now() + RANGELOCK_DURATION, reason];
|
|
Punishments.ips.set(range, punishment);
|
|
};
|
|
|
|
/**
|
|
* @param {Room} room
|
|
* @param {User} user
|
|
* @param {number} expireTime
|
|
* @param {string} userId
|
|
* @param {...string} [reason]
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.roomBan = function (room, user, expireTime, userId, ...reason) {
|
|
if (!userId) userId = user.getLastId();
|
|
|
|
if (!expireTime) expireTime = Date.now() + ROOMBAN_DURATION;
|
|
let punishment = ['ROOMBAN', userId, expireTime].concat(reason);
|
|
|
|
let affected = Punishments.roomPunish(room, user, punishment);
|
|
for (let curUser of affected) {
|
|
if (room.game && room.game.removeBannedUser) {
|
|
room.game.removeBannedUser(curUser);
|
|
}
|
|
curUser.leaveRoom(room.id);
|
|
}
|
|
|
|
return affected;
|
|
};
|
|
|
|
/**
|
|
* @param {Room} room
|
|
* @param {User} user
|
|
* @param {number} expireTime
|
|
* @param {string} userId
|
|
* @param {...string} [reason]
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.roomBlacklist = function (room, user, expireTime, userId, ...reason) {
|
|
if (!userId && user) userId = user.getLastId();
|
|
if (!user) user = Users(userId);
|
|
|
|
if (!expireTime) expireTime = Date.now() + BLACKLIST_DURATION;
|
|
let punishment = ['BLACKLIST', userId, expireTime].concat(reason);
|
|
|
|
let affected = [];
|
|
|
|
if (!user || userId && userId !== user.userid) {
|
|
affected = Punishments.roomPunishName(room, userId, punishment);
|
|
}
|
|
|
|
if (user) {
|
|
affected = affected.concat(Punishments.roomPunish(room, user, punishment));
|
|
}
|
|
|
|
for (let curUser of affected) {
|
|
if (room.game && room.game.removeBannedUser) {
|
|
room.game.removeBannedUser(curUser);
|
|
}
|
|
curUser.leaveRoom(room.id);
|
|
}
|
|
|
|
return affected;
|
|
};
|
|
|
|
/**
|
|
* @param {Room} room
|
|
* @param {string} userid
|
|
*/
|
|
Punishments.roomUnban = function (room, userid) {
|
|
const user = Users(userid);
|
|
if (user) {
|
|
let punishment = Punishments.isRoomBanned(user, room.id);
|
|
if (punishment) userid = punishment[1];
|
|
}
|
|
return Punishments.roomUnpunish(room, userid, 'ROOMBAN');
|
|
};
|
|
|
|
/**
|
|
* @param {Room} room
|
|
* @param {string} userid
|
|
* @param {boolean} ignoreWrite Flag to skip persistent storage.
|
|
*/
|
|
Punishments.roomUnblacklist = function (room, userid, ignoreWrite) {
|
|
const user = Users(userid);
|
|
if (user) {
|
|
let punishment = Punishments.isRoomBanned(user, room.id);
|
|
if (punishment) userid = punishment[1];
|
|
}
|
|
return Punishments.roomUnpunish(room, userid, 'BLACKLIST', ignoreWrite);
|
|
};
|
|
|
|
/**
|
|
* @param {Room} room
|
|
*/
|
|
Punishments.roomUnblacklistAll = function (room) {
|
|
const roombans = Punishments.roomUserids.get(room.id);
|
|
if (!roombans) return false;
|
|
|
|
let unblacklisted = [];
|
|
|
|
roombans.forEach((punishment, userid) => {
|
|
if (punishment[0] === 'BLACKLIST') {
|
|
Punishments.roomUnblacklist(room, userid, true);
|
|
unblacklisted.push(userid);
|
|
}
|
|
});
|
|
if (unblacklisted.length === 0) return false;
|
|
Punishments.saveRoomPunishments();
|
|
return unblacklisted;
|
|
};
|
|
|
|
/**
|
|
* @param {string} ip
|
|
* @param {string} note
|
|
*/
|
|
Punishments.addSharedIp = function (ip, note) {
|
|
Punishments.sharedIps.set(ip, note);
|
|
Punishments.appendSharedIp(ip, note);
|
|
|
|
Users.users.forEach(user => {
|
|
if (user.locked && user.locked !== user.userid && ip in user.ips) {
|
|
if (!user.autoconfirmed) {
|
|
user.semilocked = `#sharedip ${user.locked}`;
|
|
}
|
|
user.locked = false;
|
|
|
|
user.updateIdentity();
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @param {string} ip
|
|
*/
|
|
Punishments.removeSharedIp = function (ip) {
|
|
Punishments.sharedIps.delete(ip);
|
|
Punishments.saveSharedIps();
|
|
};
|
|
|
|
/*********************************************************
|
|
* Checking
|
|
*********************************************************/
|
|
|
|
Punishments.search = function (searchId) {
|
|
let foundKeys = [];
|
|
let foundRest = null;
|
|
Punishments.ips.forEach((punishment, ip) => {
|
|
const [, id, ...rest] = punishment;
|
|
|
|
if (searchId === id || searchId === ip) {
|
|
foundKeys.push(ip);
|
|
foundRest = rest;
|
|
}
|
|
});
|
|
Punishments.userids.forEach((punishment, userid) => {
|
|
const [, id, ...rest] = punishment;
|
|
|
|
if (searchId === id || searchId === userid) {
|
|
foundKeys.push(userid);
|
|
foundRest = rest;
|
|
}
|
|
});
|
|
Punishments.roomIps.nestedForEach((punishment, roomid, ip) => {
|
|
const [, punishUserid, ...rest] = punishment;
|
|
|
|
if (searchId === punishUserid || searchId === ip) {
|
|
foundKeys.push(ip + ':' + roomid);
|
|
foundRest = rest;
|
|
}
|
|
});
|
|
Punishments.roomUserids.nestedForEach((punishment, roomid, userid) => {
|
|
const [, punishUserid, ...rest] = punishment;
|
|
|
|
if (searchId === punishUserid || searchId === userid) {
|
|
foundKeys.push(userid + ':' + roomid);
|
|
foundRest = rest;
|
|
}
|
|
});
|
|
|
|
return [foundKeys, foundRest];
|
|
};
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
Punishments.getPunishType = function (name) {
|
|
let punishment = Punishments.userids.get(toId(name));
|
|
if (punishment) return punishment[0];
|
|
let user = Users.get(name);
|
|
if (!user) return;
|
|
punishment = Punishments.ipSearch(user.latestIp);
|
|
if (punishment) return punishment[0];
|
|
return '';
|
|
};
|
|
|
|
/**
|
|
* @param {Room} room
|
|
* @param {string} name
|
|
*/
|
|
Punishments.getRoomPunishType = function (room, name) {
|
|
let punishment = Punishments.roomUserids.nestedGet(room.id, toId(name));
|
|
if (punishment) return punishment[0];
|
|
let user = Users.get(name);
|
|
if (!user) return;
|
|
punishment = Punishments.roomIps.nestedGet(room.id, user.latestIp);
|
|
if (punishment) return punishment[0];
|
|
return '';
|
|
};
|
|
|
|
/**
|
|
* Searches for IP in Punishments.ips
|
|
*
|
|
* For instance, if IP is '1.2.3.4', will return the value corresponding
|
|
* to any of the keys in table match '1.2.3.4', '1.2.3.*', '1.2.*', or '1.*'
|
|
*
|
|
* @param {string} ip
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.ipSearch = function (ip) {
|
|
let punishment = Punishments.ips.get(ip);
|
|
if (punishment) return 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) return punishment;
|
|
dotIndex = ip.lastIndexOf('.');
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* @param {string} userid
|
|
* @return {?Array}
|
|
*/
|
|
Punishments.shortenHost = function (host) {
|
|
if (host.slice(-7) === '-nohost') return host;
|
|
let dotLoc = host.lastIndexOf('.');
|
|
let tld = host.substr(dotLoc);
|
|
if (tld === '.uk' || tld === '.au' || tld === '.br') dotLoc = host.lastIndexOf('.', dotLoc - 1);
|
|
dotLoc = host.lastIndexOf('.', dotLoc - 1);
|
|
return host.substr(dotLoc + 1);
|
|
};
|
|
|
|
// Defined in Punishments.loadBanlist
|
|
Punishments.checkRangeBanned = function () {};
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {boolean} registered
|
|
*/
|
|
Punishments.checkName = function (user, registered) {
|
|
let userid = user.userid;
|
|
let punishment = Punishments.userids.get(userid);
|
|
if (!punishment && user.namelocked) {
|
|
punishment = Punishments.userids.get(user.namelocked);
|
|
if (!punishment) punishment = ['NAMELOCK', user.namelocked, 0];
|
|
}
|
|
if (!punishment && user.locked) {
|
|
punishment = Punishments.userids.get(user.locked);
|
|
if (!punishment) punishment = ['LOCK', user.locked, 0];
|
|
}
|
|
if (!punishment) return;
|
|
|
|
let id = punishment[0];
|
|
let punishUserid = punishment[1];
|
|
let reason = ``;
|
|
if (punishment[3]) reason = `||||Reason: ${punishment[3]}`;
|
|
let appeal = ``;
|
|
if (Config.appealurl) appeal = `||||Or you can appeal at: ${Config.appealurl}`;
|
|
let bannedUnder = ``;
|
|
if (punishUserid !== userid) bannedUnder = ` because you have the same IP as banned user: ${punishUserid}`;
|
|
|
|
if (registered && id === 'BAN') {
|
|
user.send(`|popup|Your username (${user.name}) is banned${bannedUnder}. Your ban will expire in a few days.${reason}${appeal}`);
|
|
user.punishmentNotified = true;
|
|
Punishments.punish(user, punishment);
|
|
user.disconnectAll();
|
|
return;
|
|
}
|
|
if (id === 'NAMELOCK' || user.namelocked) {
|
|
user.send(`|popup|You are namelocked and can't have a username${bannedUnder}. Your namelock will expire in a few days.${reason}${appeal}`);
|
|
if (punishment[2]) Punishments.punish(user, punishment);
|
|
user.locked = punishUserid;
|
|
user.namelocked = punishUserid;
|
|
user.resetName();
|
|
user.updateIdentity();
|
|
} else {
|
|
if (!user.lockNotified) {
|
|
user.send(`|popup|You are locked${bannedUnder}. Your lock will expire in a few days.${reason}${appeal}`);
|
|
user.lockNotified = true;
|
|
}
|
|
Punishments.punish(user, punishment);
|
|
user.locked = punishUserid;
|
|
user.updateIdentity();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {Connection} connection
|
|
*/
|
|
Punishments.checkIp = function (user, connection) {
|
|
let ip = connection.ip;
|
|
let punishment = Punishments.ipSearch(ip);
|
|
|
|
if (punishment) {
|
|
if (Punishments.sharedIps.has(user.latestIp)) {
|
|
if (connection.user && !connection.user.locked && !connection.user.autoconfirmed) {
|
|
connection.user.semilocked = `#sharedip ${punishment[1]}`;
|
|
}
|
|
} else {
|
|
user.locked = punishment[1];
|
|
if (punishment[0] === 'NAMELOCK') {
|
|
user.namelocked = punishment[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
Dnsbl.reverse(ip).then(host => {
|
|
if (host) user.latestHost = host;
|
|
if (Config.hostfilter) Config.hostfilter(host, user, connection);
|
|
});
|
|
|
|
if (Config.dnsbl) {
|
|
Dnsbl.query(connection.ip).then(isBlocked => {
|
|
if (isBlocked) {
|
|
if (connection.user && !connection.user.locked && !connection.user.autoconfirmed) {
|
|
connection.user.semilocked = '#dnsbl';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Connection flood table. Separate table from IP bans.
|
|
* @type Set<string>
|
|
*/
|
|
let cfloods = Punishments.cfloods = new Set();
|
|
|
|
/**
|
|
* IP bans need to be checked separately since we don't even want to
|
|
* make a User object if an IP is banned.
|
|
*
|
|
* @param {Connection} connection
|
|
* @return {?string}
|
|
*/
|
|
Punishments.checkIpBanned = function (connection) {
|
|
let ip = connection.ip;
|
|
if (cfloods.has(ip) || (Monitor.countConnection(ip) && cfloods.add(ip))) {
|
|
connection.send(`|popup||modal|PS is under heavy load and cannot accommodate your connection right now.`);
|
|
return '#cflood';
|
|
}
|
|
|
|
if (Punishments.sharedIps.has(ip)) return false;
|
|
|
|
let banned = false;
|
|
let punishment = Punishments.ipSearch(ip);
|
|
if (punishment && punishment[0] === 'BAN') {
|
|
banned = punishment[1];
|
|
} else if (Punishments.checkRangeBanned(ip)) {
|
|
banned = '#ipban';
|
|
}
|
|
if (!banned) return false;
|
|
|
|
if (banned === '#ipban') {
|
|
connection.send(`|popup||modal|Your IP (${ip}) is not allowed to connect to PS, because it has been used to spam, hack, or otherwise attack our server.||Make sure you are not using any proxies to connect to PS.`);
|
|
} else {
|
|
let appeal = (Config.appealurl ? `||||Or you can appeal at: ${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.${appeal}`);
|
|
}
|
|
Monitor.notice(`CONNECT BLOCKED - IP BANNED: ${ip} (${banned})`);
|
|
|
|
return banned;
|
|
};
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {string} roomid
|
|
* @return {boolean}
|
|
*/
|
|
Punishments.checkNameInRoom = function (user, roomid) {
|
|
let punishment = Punishments.roomUserids.nestedGet(roomid, user.userid);
|
|
if (!punishment && user.autoconfirmed) {
|
|
punishment = Punishments.roomUserids.nestedGet(roomid, user.autoconfirmed);
|
|
}
|
|
if (!punishment) return false;
|
|
if (punishment[0] === 'ROOMBAN' || punishment[0] === 'BLACKLIST') {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {string} userid The name into which the user is renamed.
|
|
* @param {string} roomid
|
|
*/
|
|
Punishments.checkNewNameInRoom = function (user, userid, roomid) {
|
|
const punishment = Punishments.roomUserids.nestedGet(roomid, userid);
|
|
if (punishment) {
|
|
if (punishment[0] !== 'ROOMBAN' && punishment[0] !== 'BLACKLIST') return;
|
|
const room = Rooms(roomid);
|
|
if (room.game && room.game.removeBannedUser) {
|
|
room.game.removeBannedUser(user);
|
|
}
|
|
user.leaveRoom(room.id);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} userid
|
|
* @return {string} Descriptive text for the remaining time until the punishment expires, if any.
|
|
*/
|
|
Punishments.checkLockExpiration = function (userid) {
|
|
const punishment = Punishments.userids.get(userid);
|
|
|
|
if (punishment) {
|
|
let user = Users(userid);
|
|
if (user && user.permalocked) return ` (never expires; you are permalocked)`;
|
|
let expiresIn = new Date(punishment[2]).getTime() - Date.now();
|
|
let expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
|
|
let expiresText = '';
|
|
if (expiresDays >= 1) {
|
|
expiresText = `in around ${expiresDays} day${Chat.plural(expiresDays)}`;
|
|
} else {
|
|
expiresText = `soon`;
|
|
}
|
|
if (expiresIn > 1) return ` (expires ${expiresText})`;
|
|
}
|
|
|
|
return ``;
|
|
};
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {string} roomid
|
|
* @return {?Punishment}
|
|
*/
|
|
Punishments.isRoomBanned = function (user, roomid) {
|
|
if (!user) throw new Error(`Trying to check if a non-existent user is room banned.`);
|
|
|
|
let punishment = Punishments.roomUserids.nestedGet(roomid, user.userid);
|
|
if (punishment && (punishment[0] === 'ROOMBAN' || punishment[0] === 'BLACKLIST')) return punishment;
|
|
|
|
if (user.autoconfirmed) {
|
|
punishment = Punishments.roomUserids.nestedGet(roomid, user.autoconfirmed);
|
|
if (punishment && (punishment[0] === 'ROOMBAN' || punishment[0] === 'BLACKLIST')) return punishment;
|
|
}
|
|
|
|
for (let ip in user.ips) {
|
|
punishment = Punishments.roomIps.nestedGet(roomid, ip);
|
|
if (punishment) {
|
|
if (punishment[0] === 'ROOMBAN') {
|
|
return punishment;
|
|
} else if (punishment[0] === 'BLACKLIST') {
|
|
if (Punishments.sharedIps.has(ip) && user.autoconfirmed) return;
|
|
|
|
return punishment;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param {User | string} user
|
|
* @param {?Object} options
|
|
* @return {Array}
|
|
*/
|
|
Punishments.getRoomPunishments = function (user, options) {
|
|
if (!user) return;
|
|
let userid = toId(user);
|
|
let checkMutes = typeof user !== 'string';
|
|
|
|
let punishments = [];
|
|
|
|
for (let i = 0; i < Rooms.global.chatRooms.length; i++) {
|
|
const curRoom = Rooms.global.chatRooms[i];
|
|
if (!curRoom || curRoom.isPrivate === true || ((options && options.publicOnly) && (curRoom.isPersonal || curRoom.battle))) continue;
|
|
let punishment = Punishments.roomUserids.nestedGet(curRoom.id, userid);
|
|
if (punishment) {
|
|
punishments.push([curRoom, punishment]);
|
|
continue;
|
|
} else if (options && options.checkIps) {
|
|
for (let ip in user.ips) {
|
|
punishment = Punishments.roomIps.nestedGet(curRoom.id, ip);
|
|
if (punishment) {
|
|
punishments.push([curRoom, punishment]);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
if (checkMutes && curRoom.muteQueue) {
|
|
for (let i = 0; i < curRoom.muteQueue.length; i++) {
|
|
let entry = curRoom.muteQueue[i];
|
|
if (userid === entry.userid ||
|
|
user.guestNum === entry.guestNum ||
|
|
(user.autoconfirmed && user.autoconfirmed === entry.autoconfirmed)) {
|
|
punishments.push([curRoom, ['MUTE', entry.userid, curRoom.muteQueue[i].time]]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return punishments;
|
|
};
|
|
|
|
/**
|
|
* Notifies staff if a user has three or more room punishments.
|
|
*
|
|
* @param {User | string} user
|
|
*/
|
|
Punishments.monitorRoomPunishments = function (user) {
|
|
if (user.locked) return;
|
|
|
|
const minPunishments = (typeof Config.monitorminpunishments === 'number' ? Config.monitorminpunishments : 3); // Default to 3 if the Config option is not defined or valid
|
|
if (!minPunishments) return;
|
|
|
|
let punishments = Punishments.getRoomPunishments(user, {publicOnly: true});
|
|
|
|
if (punishments.length >= minPunishments) {
|
|
let points = 0;
|
|
|
|
let punishmentText = punishments.map(([room, punishment]) => {
|
|
const [punishType, punishUserid, , reason] = punishment;
|
|
if (punishType in PUNISHMENT_POINT_VALUES) points += PUNISHMENT_POINT_VALUES[punishType];
|
|
let punishDesc = Punishments.roomPunishmentTypes.get(punishType);
|
|
if (!punishDesc) punishDesc = `punished`;
|
|
if (punishUserid !== user.userid) punishDesc += ` as ${punishUserid}`;
|
|
|
|
if (reason) punishDesc += `: ${reason}`;
|
|
return `<<${room}>> (${punishDesc})`;
|
|
}).join(', ');
|
|
|
|
if (Config.punishmentautolock && points >= AUTOLOCK_POINT_THRESHOLD) {
|
|
let rooms = punishments.map(([room]) => room).join(', ');
|
|
let reason = `Autolocked for having punishments in ${punishments.length} rooms: ${rooms}`;
|
|
let message = `${user.name || `[${toId(user)}]`} was locked for having punishments in ${punishments.length} rooms: ${punishmentText}`;
|
|
|
|
Punishments.autolock(user, 'staff', 'PunishmentMonitor', reason, message);
|
|
if (typeof user !== 'string') user.popup("|modal|You've been locked for breaking the rules in multiple chatrooms.\n\nIf you feel that your lock was unjustified, you can still PM staff members (%, @, &, and ~) to discuss it" + (Config.appealurl ? " or you can appeal:\n" + Config.appealurl : ".") + "\n\nYour lock will expire in a few days.");
|
|
} else {
|
|
Monitor.log(`[PunishmentMonitor] ${user.name} currently has punishments in ${punishments.length} rooms: ${punishmentText}`);
|
|
}
|
|
}
|
|
};
|