Refactor both locks and punishments (#3698)

This both allows staff to lock users that are offline, given they have a previous punishment (globally or in any room)
This also will utilize existing punishment information for handling nameblacklists, making a nameblacklist of a user with a current punishment equivalent to having used /blacklist at the moment of that punishment
This commit is contained in:
Bär Halberkamp 2017-07-27 18:36:34 -04:00 committed by Guangcong Luo
parent 67cf1e2453
commit 464768e7d8
3 changed files with 179 additions and 151 deletions

View File

@ -1514,44 +1514,66 @@ exports.commands = {
forcelock: 'lock',
l: 'lock',
ipmute: 'lock',
wl: 'lock',
weeklock: 'lock',
lock: function (target, room, user, connection, cmd) {
if (!target) return this.parse('/help lock');
let week = cmd === 'wl' || cmd === 'weeklock';
if (!target) {
if (week) return this.parse('/help weeklock');
return this.parse('/help lock');
}
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser) return this.errorReply("User '" + this.targetUsername + "' not found.");
if (!targetUser && !Punishments.search(toId(this.targetUsername))[0].length) return this.errorReply(`User '${this.targetUsername}' not found.`);
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply("The reason is too long. It cannot exceed " + MAX_REASON_LENGTH + " characters.");
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!this.can('lock', targetUser)) return false;
let name = targetUser.getLastName();
let userid = targetUser.getLastId();
if (targetUser.locked && !target) {
let problem = " but was already locked";
return this.privateModCommand("(" + name + " would be locked by " + user.name + problem + ".)");
}
let name, userid;
if (targetUser.trusted) {
if (cmd === 'forcelock') {
let from = targetUser.distrust();
Monitor.log("[CrisisMonitor] " + name + " was locked by " + user.name + " and demoted from " + from.join(", ") + ".");
this.globalModlog("CRISISDEMOTE", targetUser, " from " + from.join(", "));
} else {
return this.sendReply("" + name + " is a trusted user. If you are sure you would like to lock them use /forcelock.");
if (targetUser) {
name = targetUser.getLastName();
userid = targetUser.getLastId();
if (targetUser.locked && !target) {
return this.privateModCommand(`(${name} would be locked by ${user.name} but was already locked.)`);
}
} else if (cmd === 'forcelock') {
return this.errorReply("Use /lock; " + name + " is not a trusted user.");
}
// Destroy personal rooms of the locked user.
targetUser.inRooms.forEach(roomid => {
if (roomid === 'global') return;
let targetRoom = Rooms.get(roomid);
if (targetRoom.isPersonal && targetRoom.auth[userid] === '#') {
targetRoom.destroy();
if (targetUser.trusted) {
if (cmd === 'forcelock') {
let from = targetUser.distrust();
Monitor.log(`[CrisisMonitor] ${name} was locked by ${user.name} and demoted from ${from.join(", ")}.`);
this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from.join(", ")}`);
} else {
return this.sendReply(`${name} is a trusted user. If you are sure you would like to lock them use /forcelock.`);
}
} else if (cmd === 'forcelock') {
return this.errorReply(`Use /lock; ${name} is not a trusted user.`);
}
});
let roomauth = [];
Rooms.rooms.forEach((curRoom, id) => {
if (id === 'global' || !curRoom.auth) return;
// Destroy personal rooms of the locked user.
if (curRoom.isPersonal && curRoom.auth[userid] === '#') {
curRoom.destroy();
} else {
if (curRoom.isPrivate || curRoom.battle) return;
let group = curRoom.auth[userid];
if (group) roomauth.push(`${group}${id}`);
}
});
if (roomauth.length) Monitor.log(`[CrisisMonitor] Locked user ${name} has public roomauth (${roomauth.join(', ')}), and should probably be demoted.`);
} else {
name = this.targetUsername;
userid = toId(this.targetUsername);
}
let proof = '';
let userReason = target;
@ -1563,117 +1585,50 @@ exports.commands = {
userReason = target.substr(0, proofIndex).trim();
}
targetUser.popup("|modal|" + user.name + " has locked you from talking in chats, battles, and PMing regular users." + (userReason ? "\n\nReason: " + userReason : "") + "\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.");
let weekMsg = week ? ' for a week' : '';
let lockMessage = "" + name + " was locked from talking by " + user.name + "." + (userReason ? " (" + userReason + ")" : "");
if (targetUser) {
targetUser.popup(`|modal|${user.name} has locked you from talking in chats, battles, and PMing regular users${weekMsg}.` + (userReason ? `\n\nReason: ${userReason}` : "") + `\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.`);
}
let lockMessage = `${name} was locked from talking${weekMsg} by ${user.name}.` + (userReason ? ` (${userReason})` : "");
this.addModCommand(lockMessage, ` ${proof}(${targetUser.latestIp})`);
this.addModCommand(lockMessage, ` ${proof}` + (targetUser ? `(${targetUser.latestIp})` : ''));
// Notify staff room when a user is locked outside of it.
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addLogMessage(user, "<<" + room.id + ">> " + lockMessage);
Rooms('staff').addLogMessage(user, `<<${room.id}>> ${lockMessage}`);
}
let affected = Punishments.lock(targetUser, null, null, userReason);
// Use default time for locks.
let duration = week ? Date.now() + 7 * 24 * 60 * 60 * 1000 : null;
let affected = [];
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
if (targetUser) {
affected = Punishments.lock(targetUser, duration, null, userReason);
} else {
affected = Punishments.lock(null, duration, userid, userReason);
}
let acAccount = (targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
if (affected.length > 1) {
this.privateModCommand("(" + name + "'s " + (acAccount ? " ac account: " + acAccount + ", " : "") + "locked alts: " + affected.slice(1).map(user => user.getLastName()).join(", ") + ")");
this.privateModCommand(`(${name}'s ` + (acAccount ? ` ac account: ${acAccount}, ` : "") + `locked alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`);
} else if (acAccount) {
this.privateModCommand("(" + name + "'s ac account: " + acAccount + ")");
this.privateModCommand(`(${name}'s ac account: ${acAccount})`);
}
this.add('|unlink|hide|' + userid);
if (userid !== toId(this.inputUsername)) this.add('|unlink|hide|' + toId(this.inputUsername));
this.add(`|unlink|hide|${userid}`);
if (userid !== toId(this.inputUsername)) this.add(`|unlink|hide|${toId(this.inputUsername)}`);
const globalReason = (target ? `: ${userReason} ${proof}` : ``);
this.globalModlog("LOCK", targetUser, ` by ${user.name}${globalReason}`);
this.globalModlog((week ? "WEEKLOCK" : "LOCK"), targetUser || userid, ` by ${user.name}${globalReason}`);
return true;
},
lockhelp: [
"/lock OR /l [username], [reason] - Locks the user from talking in all chats. Requires: % @ * & ~",
"/weeklock OR /wl [username], [reason] - Same as /lock, but locks users for a week.",
"/lock OR /l [username], [reason] spoiler: [proof] - Marks proof in modlog only.",
],
wl: 'weeklock',
weeklock: function (target, room, user, connection, cmd) {
if (!target) return this.parse('/help weeklock');
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser) return this.errorReply("User '" + this.targetUsername + "' not found.");
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply("The reason is too long. It cannot exceed " + MAX_REASON_LENGTH + " characters.");
}
if (!this.can('lock', targetUser)) return false;
let name = targetUser.getLastName();
let userid = targetUser.getLastId();
if (targetUser.locked && !target) {
let problem = " but was already locked";
return this.privateModCommand("(" + name + " would be locked by " + user.name + problem + ".)");
}
if (targetUser.trusted) {
if (cmd === 'forcelock') {
let from = targetUser.distrust();
Monitor.log("[CrisisMonitor] " + name + " was locked by " + user.name + " and demoted from " + from.join(", ") + ".");
this.globalModlog("CRISISDEMOTE", targetUser, " from " + from.join(", "));
} else {
return this.sendReply("" + name + " is a trusted user. If you are sure you would like to lock them use /forcelock.");
}
} else if (cmd === 'forcelock') {
return this.errorReply("Use /lock; " + name + " is not a trusted user.");
}
// Destroy personal rooms of the locked user.
targetUser.inRooms.forEach(roomid => {
if (roomid === 'global') return;
let targetRoom = Rooms.get(roomid);
if (targetRoom.isPersonal && targetRoom.auth[userid] === '#') {
targetRoom.destroy();
}
});
let proof = '';
let userReason = target;
let targetLowercase = target.toLowerCase();
if (target && (targetLowercase.includes('spoiler:') || targetLowercase.includes('spoilers:'))) {
let proofIndex = (targetLowercase.includes('spoilers:') ? targetLowercase.indexOf('spoilers:') : targetLowercase.indexOf('spoiler:'));
let bump = (targetLowercase.includes('spoilers:') ? 9 : 8);
proof = `(PROOF: ${target.substr(proofIndex + bump, target.length).trim()}) `;
userReason = target.substr(0, proofIndex).trim();
}
targetUser.popup("|modal|" + user.name + " has locked you from talking in chats, battles, and PMing regular users for a week." + (userReason ? "\n\nReason: " + userReason : "") + "\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.");
let lockMessage = "" + name + " was locked from talking for a week by " + user.name + "." + (userReason ? " (" + userReason + ")" : "");
this.addModCommand(lockMessage, ` ${proof}(${targetUser.latestIp})`);
// Notify staff room when a user is locked outside of it.
if (room.id !== 'staff' && Rooms('staff')) {
Rooms('staff').addLogMessage(user, "<<" + room.id + ">> " + lockMessage);
}
let affected = Punishments.lock(targetUser, Date.now() + 7 * 24 * 60 * 60 * 1000, null, userReason);
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
if (affected.length > 1) {
this.privateModCommand("(" + name + "'s " + (acAccount ? " ac account: " + acAccount + ", " : "") + "locked alts: " + affected.slice(1).map(user => user.getLastName()).join(", ") + ")");
} else if (acAccount) {
this.privateModCommand("(" + name + "'s ac account: " + acAccount + ")");
}
this.add('|unlink|hide|' + userid);
if (userid !== toId(this.inputUsername)) this.add('|unlink|hide|' + toId(this.inputUsername));
const globalReason = (target ? `: ${userReason} ${proof}` : ``);
this.globalModlog("WEEKLOCK", targetUser, ` by ${user.name}${globalReason}`);
return true;
},
weeklockhelp: [
"/weeklock OR /wl [username], [reason] - Locks the user from talking in all chats for one week. Requires: % @ * & ~",
"/weeklock OR /wl [username], [reason] spoiler: [proof] - Marks proof in modlog only.",
],
unlock: function (target, room, user) {
if (!target) return this.parse('/help unlock');
if (!this.can('lock')) return false;

View File

@ -432,6 +432,36 @@ Punishments.punish = function (user, punishment, recursionKeys) {
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
@ -512,15 +542,33 @@ Punishments.roomPunish = function (room, user, punishment, recursionKeys) {
};
Punishments.roomPunishName = function (room, userid, punishment) {
Punishments.roomUserids.nestedSet(room.id, 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: [userid],
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;
};
/**
@ -602,12 +650,20 @@ Punishments.unban = function (name) {
* @return {?Array}
*/
Punishments.lock = function (user, expireTime, id, ...reason) {
if (!id) id = user.getLastId();
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 = Punishments.punish(user, punishment);
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();
@ -632,9 +688,9 @@ Punishments.autolock = function (user, room, source, reason, message, week) {
expires = Date.now() + 7 * 24 * 60 * 60 * 1000;
punishment = `WEEKLOCKED`;
}
Punishments.lock(user, expires, user.userid, `Autolock: ${user.name}: ${reason}`);
Punishments.lock(user, expires, toId(user), `Autolock: ${user.name || toId(user)}: ${reason}`);
Monitor.log(`[${source}] ${punishment}: ${message}`);
Rooms.global.modlog(`(${toId(room)}) AUTOLOCK: [${user.userid}]: ${reason}`);
Rooms.global.modlog(`(${toId(room)}) AUTOLOCK: [${toId(user)}]: ${reason}`);
};
/**
* @param {string} name
@ -782,21 +838,24 @@ Punishments.roomBlacklist = function (room, user, expireTime, userId, ...reason)
if (!expireTime) expireTime = Date.now() + BLACKLIST_DURATION;
let punishment = ['BLACKLIST', userId, expireTime].concat(reason);
let affected = [];
if (!user || userId && userId !== user.userid) {
Punishments.roomPunishName(room, userId, punishment);
affected = Punishments.roomPunishName(room, userId, punishment);
}
if (user) {
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;
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;
};
/**
@ -1205,7 +1264,7 @@ Punishments.isRoomBanned = function (user, roomid) {
* 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} user
* @param {User | string} user
* @param {?Object} options
* @return {Array}
*/
@ -1250,7 +1309,7 @@ Punishments.getRoomPunishments = function (user, options) {
/**
* Notifies staff if a user has three or more room punishments.
*
* @param {User} user
* @param {User | string} user
*/
Punishments.monitorRoomPunishments = function (user) {
if (user.locked) return;
@ -1277,10 +1336,10 @@ Punishments.monitorRoomPunishments = function (user) {
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} was locked for having punishments in ${punishments.length} rooms: ${punishmentText}`;
let message = `${user.name || `[${toId(user)}]`} was locked for having punishments in ${punishments.length} rooms: ${punishmentText}`;
Punishments.autolock(user, 'staff', 'PunishmentMonitor', reason, message);
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.");
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}`);
}

View File

@ -129,6 +129,33 @@ let getExactUser = Users.getExact = function (name) {
return getUser(name, true);
};
/**
* Get a list of all users matching a list of userids and ips.
*
* Usage:
* Users.findUsers([userids], [ips])
*
*/
let findUsers = Users.findUsers = function (userids, ips, options) {
let matches = [];
if (options && options.forPunishment) ips = ips.filter(ip => !Punishments.sharedIps.has(ip));
users.forEach(user => {
if (!(options && options.forPunishment) && !user.named && !user.connected) return;
if (!(options && options.includeTrusted) && user.trusted) return;
if (userids.includes(user.userid)) {
matches.push(user);
return;
}
for (let myIp of ips) {
if (myIp in user.ips) {
matches.push(user);
return;
}
}
});
return matches;
};
/*********************************************************
* User groups
*********************************************************/
@ -1110,21 +1137,8 @@ class User {
this.inRooms.clear();
}
getAltUsers(includeTrusted, forPunishment) {
let alts = [];
if (forPunishment) alts.push(this);
let ips = Object.keys(this.ips);
if (forPunishment) ips = ips.filter(ip => !Punishments.sharedIps.has(ip));
users.forEach(user => {
if (user === this) return;
if (!forPunishment && !user.named && !user.connected) return;
if (!includeTrusted && user.trusted) return;
for (let myIp of ips) {
if (myIp in user.ips) {
alts.push(user);
return;
}
}
});
let alts = findUsers([this.getLastId()], Object.keys(this.ips), {includeTrusted: includeTrusted, forPunishment: forPunishment});
if (!forPunishment) alts.filter(user => user !== this);
return alts;
}
getLastName() {