pokemon-showdown/server/chat-commands/moderation.js
Guangcong Luo 23f9bfa1b7
Split up server/chat-commands/ (#5943)
`server/chat-commands.js` is now a directory. It's been split into
`core`, `moderation`, and `admin`. `info` and `roomsettings` from
`chat-plugins` have also moved to `chat-commands`.

Some cleanup:

- Bot commands for inserting HTML into rooms like `/adduhtml` have been
  moved from `info` into `admin`.

- `/a` has been renamed `/addline`, for clarity (and also moved from
  `info` into `admin`).

- Room management commands like `/createroom` and `/roomintro` were
  moved to `room-settings`

- `chat-commands/admin` has been TypeScripted
2019-11-15 11:12:54 +13:00

1747 lines
74 KiB
JavaScript

/**
* Moderation commands
* Pokemon Showdown - http://pokemonshowdown.com/
*
* These are commands for staff.
*
* For the API, see chat-plugins/COMMANDS.md
*
* @license MIT
*/
'use strict';
/* eslint no-else-return: "error" */
const MAX_REASON_LENGTH = 300;
const MUTE_LENGTH = 7 * 60 * 1000;
const HOURMUTE_LENGTH = 60 * 60 * 1000;
/** Require reasons for punishment commands */
const REQUIRE_REASONS = true;
exports.commands = {
roomowner(target, room, user) {
if (!room.chatRoomData) {
return this.sendReply("/roomowner - This room isn't designed for per-room moderation to be added");
}
if (!target) return this.parse('/help roomowner');
target = this.splitTarget(target, true);
if (target) return this.errorReply(`This command does not support specifying a reason.`);
let targetUser = this.targetUser;
let name = this.targetUsername;
let userid = toID(name);
if (!Users.isUsernameKnown(userid)) {
return this.errorReply(`User '${this.targetUsername}' is offline and unrecognized, and so can't be promoted.`);
}
if (!this.can('makeroom')) return false;
if (!room.auth) room.auth = room.chatRoomData.auth = {};
room.auth[userid] = '#';
this.addModAction(`${name} was appointed Room Owner by ${user.name}.`);
this.modlog('ROOMOWNER', userid);
if (targetUser) {
targetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.roomid}.`);
room.onUpdateIdentity(targetUser);
if (room.subRooms) {
for (const subRoom of room.subRooms.values()) {
subRoom.onUpdateIdentity(targetUser);
}
}
}
Rooms.global.writeChatRoomData();
},
roomownerhelp: [`/roomowner [username] - Appoints [username] as a room owner. Requires: & ~`],
'!roompromote': true,
roomdemote: 'roompromote',
roompromote(target, room, user, connection, cmd) {
if (!room) {
// this command isn't marked as room-only because it's usable in PMs through /invite
return this.errorReply("This command is only available in rooms");
}
if (!room.auth) {
this.sendReply("/roompromote - This room isn't designed for per-room moderation.");
return this.sendReply("Before setting room staff, you need to set a room owner with /roomowner");
}
if (!this.canTalk()) return;
if (!target) return this.parse('/help roompromote');
const force = target.startsWith('!!!');
if (force) target = target.slice(3);
target = this.splitTarget(target, true);
let targetUser = this.targetUser;
let userid = toID(this.targetUsername);
let name = targetUser ? targetUser.name : this.filter(this.targetUsername);
if (!name) return;
name = name.slice(0, 18);
if (!userid) return this.parse('/help roompromote');
if (!targetUser && !Users.isUsernameKnown(userid) && !force) {
return this.errorReply(`User '${name}' is offline and unrecognized, and so can't be promoted.`);
}
if (targetUser && !targetUser.registered) {
return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
}
let currentGroup = room.getAuth({id: userid, group: (Users.usergroups[userid] || ' ').charAt(0)});
let nextGroup = target;
if (target === 'deauth') nextGroup = Config.groupsranking[0];
if (!nextGroup) {
return this.errorReply("Please specify a group such as /roomvoice or /roomdeauth");
}
if (!Config.groups[nextGroup]) {
return this.errorReply(`Group '${nextGroup}' does not exist.`);
}
if (Config.groups[nextGroup].globalonly || (Config.groups[nextGroup].battleonly && !room.battle)) {
return this.errorReply(`Group 'room${Config.groups[nextGroup].id}' does not exist as a room rank.`);
}
let groupName = Config.groups[nextGroup].name || "regular user";
if ((room.auth[userid] || Config.groupsranking[0]) === nextGroup) {
return this.errorReply(`User '${name}' is already a ${groupName} in this room.`);
}
if (!user.can('makeroom')) {
if (currentGroup !== ' ' && !user.can('room' + (Config.groups[currentGroup] ? Config.groups[currentGroup].id : 'voice'), null, room)) {
return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${(Config.groups[currentGroup] ? Config.groups[currentGroup].name : "an undefined group")}.`);
}
if (nextGroup !== ' ' && !user.can('room' + Config.groups[nextGroup].id, null, room)) {
return this.errorReply(`/${cmd} - Access denied for promoting/demoting to ${Config.groups[nextGroup].name}.`);
}
}
let nextGroupIndex = Config.groupsranking.indexOf(nextGroup) || 1; // assume voice if not defined (although it should be by now)
if (targetUser && targetUser.locked && !room.isPrivate && !room.battle && !room.isPersonal && nextGroupIndex >= 2) {
return this.errorReply("Locked users can't be promoted.");
}
if (nextGroup === Config.groupsranking[0]) {
delete room.auth[userid];
} else {
room.auth[userid] = nextGroup;
}
// Only show popup if: user is online and in the room, the room is public, and not a groupchat or a battle.
let needsPopup = targetUser && room.users[targetUser.id] && !room.isPrivate && !room.isPersonal && !room.battle;
if (this.pmTarget && targetUser) {
const text = `${targetUser.name} was invited (and promoted to Room ${groupName}) by ${user.name}.`;
room.add(`|c|${user.getIdentity(room)}|/log ${text}`).update();
this.modlog('INVITE', targetUser, null, {noip: 1, noalts: 1});
} else if (nextGroup in Config.groups && currentGroup in Config.groups && Config.groups[nextGroup].rank < Config.groups[currentGroup].rank) {
if (targetUser && room.users[targetUser.id] && !Config.groups[nextGroup].modlog) {
// if the user can't see the demotion message (i.e. rank < %), it is shown in the chat
targetUser.send(`>${room.roomid}\n(You were demoted to Room ${groupName} by ${user.name}.)`);
}
this.privateModAction(`(${name} was demoted to Room ${groupName} by ${user.name}.)`);
this.modlog(`ROOM${groupName.toUpperCase()}`, userid, '(demote)');
if (needsPopup) targetUser.popup(`You were demoted to Room ${groupName} by ${user.name} in ${room.roomid}.`);
} else if (nextGroup === '#') {
this.addModAction(`${'' + name} was promoted to ${groupName} by ${user.name}.`);
this.modlog('ROOM OWNER', userid);
if (needsPopup) targetUser.popup(`You were promoted to ${groupName} by ${user.name} in ${room.roomid}.`);
} else {
this.addModAction(`${'' + name} was promoted to Room ${groupName} by ${user.name}.`);
this.modlog(`ROOM${groupName.toUpperCase()}`, userid);
if (needsPopup) targetUser.popup(`You were promoted to Room ${groupName} by ${user.name} in ${room.roomid}.`);
}
if (targetUser) {
targetUser.updateIdentity(room.roomid);
if (room.subRooms) {
for (const subRoom of room.subRooms.values()) {
targetUser.updateIdentity(subRoom.roomid);
}
}
}
if (room.chatRoomData) Rooms.global.writeChatRoomData();
},
roompromotehelp: [
`/roompromote OR /roomdemote [username], [group symbol] - Promotes/demotes the user to the specified room rank. Requires: @ # & ~`,
`/room[group] [username] - Promotes/demotes the user to the specified room rank. Requires: @ # & ~`,
`/roomdeauth [username] - Removes all room rank from the user. Requires: @ # & ~`,
],
'!roomauth': true,
roomstaff: 'roomauth',
roomauth1: 'roomauth',
roomauth(target, room, user, connection, cmd) {
let userLookup = '';
if (cmd === 'roomauth1') userLookup = `\n\nTo look up auth for a user, use /userauth ${target}`;
let targetRoom = room;
if (target) targetRoom = Rooms.search(target);
if (!targetRoom || targetRoom.roomid === 'global' || !targetRoom.checkModjoin(user)) return this.errorReply(`The room "${target}" does not exist.`);
if (!targetRoom.auth) return this.sendReply(`/roomauth - The room '${targetRoom.title || target}' isn't designed for per-room moderation and therefore has no auth list.${userLookup}`);
let rankLists = {};
for (let u in targetRoom.auth) {
if (!rankLists[targetRoom.auth[u]]) rankLists[targetRoom.auth[u]] = [];
rankLists[targetRoom.auth[u]].push(u);
}
let buffer = Object.keys(rankLists).sort((a, b) =>
(Config.groups[b] || {rank: 0}).rank - (Config.groups[a] || {rank: 0}).rank
).map(r => {
let roomRankList = rankLists[r].sort();
roomRankList = roomRankList.map(s => {
const u = Users.get(s);
const isAway = u && u.statusType !== 'online';
return s in targetRoom.users && !isAway ? `**${s}**` : s;
});
return `${Config.groups[r] ? `${Config.groups[r].name}s (${r})` : r}:\n${roomRankList.join(", ")}`;
});
let curRoom = targetRoom;
while (curRoom.parent) {
const modjoinSetting = curRoom.modjoin === true ? curRoom.modchat : curRoom.modjoin;
const roomType = (modjoinSetting ? `modjoin ${modjoinSetting} ` : '');
const inheritedUserType = (modjoinSetting ? ` of rank ${modjoinSetting} and above` : '');
if (curRoom.parent) {
const also = buffer.length === 0 ? `` : ` also`;
buffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType}${also} have authority in this room.`);
}
curRoom = curRoom.parent;
}
if (!buffer.length) {
connection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);
return;
}
if (!curRoom.isPrivate) {
buffer.push(`${curRoom.title} is a public room, so global auth with no relevant roomauth will have authority in this room.`);
} else if (curRoom.isPrivate === 'hidden' || curRoom.isPrivate === 'voice') {
buffer.push(`${curRoom.title} is a hidden room, so global auth with no relevant roomauth will have authority in this room.`);
}
if (targetRoom !== room) buffer.unshift(`${targetRoom.title} room auth:`);
connection.popup(`${buffer.join("\n\n")}${userLookup}`);
},
'!userauth': true,
userauth(target, room, user, connection) {
let targetId = toID(target) || user.id;
let targetUser = Users.getExact(targetId);
let targetUsername = (targetUser ? targetUser.name : target);
let buffer = [];
let innerBuffer = [];
let group = Users.usergroups[targetId];
if (group) {
group = group.charAt(0);
if (group === ' ') group = 'trusted';
buffer.push(`Global auth: ${group}`);
}
for (const curRoom of Rooms.rooms.values()) {
if (!curRoom.auth || curRoom.isPrivate) continue;
group = curRoom.auth[targetId];
if (!group) continue;
innerBuffer.push(group + curRoom.roomid);
}
if (innerBuffer.length) {
buffer.push(`Room auth: ${innerBuffer.join(', ')}`);
}
if (targetId === user.id || user.can('lock')) {
innerBuffer = [];
for (const curRoom of Rooms.rooms.values()) {
if (!curRoom.auth || !curRoom.isPrivate) continue;
if (curRoom.isPrivate === true) continue;
let auth = curRoom.auth[targetId];
if (!auth) continue;
innerBuffer.push(auth + curRoom.roomid);
}
if (innerBuffer.length) {
buffer.push(`Hidden room auth: ${innerBuffer.join(', ')}`);
}
}
if (targetId === user.id || user.can('makeroom')) {
innerBuffer = [];
for (const chatRoom of Rooms.global.chatRooms) {
if (!chatRoom.auth || !chatRoom.isPrivate) continue;
if (chatRoom.isPrivate !== true) continue;
let auth = chatRoom.auth[targetId];
if (!auth) continue;
innerBuffer.push(auth + chatRoom.roomid);
}
if (innerBuffer.length) {
buffer.push(`Private room auth: ${innerBuffer.join(', ')}`);
}
}
if (!buffer.length) {
buffer.push("No global or room auth.");
}
buffer.unshift(`${targetUsername} user auth:`);
connection.popup(buffer.join("\n\n"));
},
'!autojoin': true,
autojoin(target, room, user, connection) {
let targets = target.split(',');
if (targets.length > 11 || connection.inRooms.size > 1) return;
Rooms.global.autojoinRooms(user, connection);
let autojoins = [];
const promises = targets.map(target =>
user.tryJoinRoom(target, connection).then(ret => {
if (ret === Rooms.RETRY_AFTER_LOGIN) {
autojoins.push(target);
}
})
);
Promise.all(promises).then(() => {
connection.autojoins = autojoins.join(',');
});
},
'!join': true,
joim: 'join',
j: 'join',
join(target, room, user, connection) {
if (!target) return this.parse('/help join');
if (target.startsWith('http://')) target = target.slice(7);
if (target.startsWith('https://')) target = target.slice(8);
if (target.startsWith(`${Config.routes.client}/`)) target = target.slice(Config.routes.client.length + 1);
if (target.startsWith('psim.us/')) target = target.slice(8);
user.tryJoinRoom(target, connection).then(ret => {
if (ret === Rooms.RETRY_AFTER_LOGIN) {
connection.sendTo(target, `|noinit|namerequired|The room '${target}' does not exist or requires a login to join.`);
}
});
},
joinhelp: [`/join [roomname] - Attempt to join the room [roomname].`],
'!part': true,
leave: 'part',
part(target, room, user, connection) {
let targetRoom = target ? Rooms.search(target) : room;
if (!targetRoom || targetRoom === Rooms.global) {
if (target.startsWith('view-')) return;
return this.errorReply(`The room '${target}' does not exist.`);
}
user.leaveRoom(targetRoom, connection);
},
/*********************************************************
* Moderating: Punishments
*********************************************************/
kick: 'warn',
k: 'warn',
warn(target, room, user) {
if (!target) return this.parse('/help warn');
if (!this.canTalk()) return;
if (room.isPersonal && !user.can('warn')) return this.errorReply("Warning is unavailable in group chats.");
// If used in staff, help tickets or battles, log the warn to the global modlog.
const globalWarn = room.roomid === 'staff' || room.roomid.startsWith('help-') || (room.battle && !room.parent);
target = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) {
if (!targetUser || !globalWarn) return this.errorReply(`User '${this.targetUsername}' not found.`);
this.addModAction(`${targetUser.name} would be warned by ${user.name} but is offline.${(target ? ` (${target})` : ``)}`);
this.globalModlog('WARN OFFLINE', targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
return;
}
if (!(targetUser in room.users) && !globalWarn) {
return this.errorReply(`User ${this.targetUsername} is not in the room ${room.roomid}.`);
}
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!this.can('warn', targetUser, room)) return false;
if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to warn upper staff members.");
const now = Date.now();
const timeout = now - targetUser.lastWarnedAt;
if (timeout < 15 * 1000) {
const remainder = (15 - (timeout / 1000)).toFixed(2);
return this.errorReply(`You must wait ${remainder} more seconds before you can warn ${targetUser.name} again.`);
}
this.addModAction(`${targetUser.name} was warned by ${user.name}.${(target ? ` (${target})` : ``)}`);
if (globalWarn) {
this.globalModlog('WARN', targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
} else {
this.modlog('WARN', targetUser, target, {noalts: 1});
}
targetUser.send(`|c|~|/warn ${target}`);
const userid = targetUser.getLastId();
this.add(`|unlink|${userid}`);
if (userid !== toID(this.inputUsername)) this.add(`|unlink|${toID(this.inputUsername)}`);
targetUser.lastWarnedAt = now;
// Automatically upload replays as evidence/reference to the punishment
if (globalWarn && room.battle) this.parse('/savereplay forpunishment');
},
warnhelp: [`/warn OR /k [username], [reason] - Warns a user showing them the Pok\u00e9mon Showdown Rules and [reason] in an overlay. Requires: % @ # & ~`],
redirect: 'redir',
redir(target, room, user, connection) {
if (!target) return this.parse('/help redirect');
if (room.isPrivate || room.isPersonal) return this.errorReply("Users cannot be redirected from private or personal rooms.");
target = this.splitTarget(target);
let targetUser = this.targetUser;
let targetRoom = Rooms.search(target);
if (!targetRoom || targetRoom.modjoin || targetRoom.staffRoom) {
return this.errorReply(`The room "${target}" does not exist.`);
}
if (!this.can('warn', targetUser, room) || !this.can('warn', targetUser, targetRoom)) return false;
if (!this.can('rangeban', targetUser)) {
this.errorReply(`Redirects have been deprecated. Instead of /redirect, use <<room links>> or /invite to guide users to the correct room, and punish if users don't cooperate.`);
return;
}
if (!targetUser || !targetUser.connected) {
return this.errorReply(`User ${this.targetUsername} not found.`);
}
if (targetRoom.roomid === "global") return this.errorReply(`Users cannot be redirected to the global room.`);
if (targetRoom.isPrivate || targetRoom.isPersonal) {
return this.errorReply(`The room "${target}" is not public.`);
}
if (targetUser.inRooms.has(targetRoom.roomid)) {
return this.errorReply(`User ${targetUser.name} is already in the room ${targetRoom.title}!`);
}
if (!targetUser.inRooms.has(room.roomid)) {
return this.errorReply(`User ${this.targetUsername} is not in the room ${room.roomid}.`);
}
targetUser.leaveRoom(room.roomid);
targetUser.popup(`You are in the wrong room; please go to <<${targetRoom.roomid}>> instead`);
this.addModAction(`${targetUser.name} was redirected to room ${targetRoom.title} by ${user.name}.`);
this.modlog('REDIRECT', targetUser, `to ${targetRoom.title}`, {noip: 1, noalts: 1});
targetUser.leaveRoom(room);
},
redirhelp: [`/redirect OR /redir [username], [roomname] - [DEPRECATED] Attempts to redirect the user [username] to the room [roomname]. Requires: & ~`],
m: 'mute',
mute(target, room, user, connection, cmd) {
if (!target) return this.parse('/help mute');
if (!this.canTalk()) return;
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.`);
}
let muteDuration = ((cmd === 'hm' || cmd === 'hourmute') ? HOURMUTE_LENGTH : MUTE_LENGTH);
if (!this.can('mute', targetUser, room)) return false;
if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to mute upper staff members.");
let canBeMutedFurther = ((room.getMuteTime(targetUser) || 0) <= (muteDuration * 5 / 6));
if (targetUser.locked || (room.isMuted(targetUser) && !canBeMutedFurther) || Punishments.isRoomBanned(targetUser, room.roomid)) {
let problem = ` but was already ${(targetUser.locked ? "locked" : room.isMuted(targetUser) ? "muted" : "room banned")}`;
if (!target) {
return this.privateModAction(`(${targetUser.name} would be muted by ${user.name} ${problem}.)`);
}
return this.addModAction(`${targetUser.name} would be muted by ${user.name} ${problem}. (${target})`);
}
if (targetUser in room.users) targetUser.popup(`|modal|${user.name} has muted you in ${room.roomid} for ${Chat.toDurationString(muteDuration)}. ${target}`);
this.addModAction(`${targetUser.name} was muted by ${user.name} for ${Chat.toDurationString(muteDuration)}.${(target ? ` (${target})` : ``)}`);
this.modlog(`${cmd.includes('h') ? 'HOUR' : ''}MUTE`, targetUser, target);
if (targetUser.autoconfirmed && targetUser.autoconfirmed !== targetUser.id) {
let displayMessage = `(${targetUser.name}'s ac account: ${targetUser.autoconfirmed})`;
this.privateModAction(displayMessage);
}
let userid = targetUser.getLastId();
this.add(`|unlink|${userid}`);
if (userid !== toID(this.inputUsername)) this.add(`|unlink|${toID(this.inputUsername)}`);
room.mute(targetUser, muteDuration, false);
},
mutehelp: [`/mute OR /m [username], [reason] - Mutes a user with reason for 7 minutes. Requires: % @ # & ~`],
hm: 'hourmute',
hourmute(target) {
if (!target) return this.parse('/help hourmute');
this.run('mute');
},
hourmutehelp: [`/hourmute OR /hm [username], [reason] - Mutes a user with reason for an hour. Requires: % @ # & ~`],
um: 'unmute',
unmute(target, room, user) {
if (!target) return this.parse('/help unmute');
target = this.splitTarget(target);
if (target) return this.errorReply(`This command does not support specifying a reason.`);
if (!this.canTalk()) return;
if (!this.can('mute', null, room)) return false;
let targetUser = this.targetUser;
let successfullyUnmuted = room.unmute(targetUser ? targetUser.id : toID(this.targetUsername), `Your mute in '${room.title}' has been lifted.`);
if (successfullyUnmuted) {
this.addModAction(`${(targetUser ? targetUser.name : successfullyUnmuted)} was unmuted by ${user.name}.`);
this.modlog('UNMUTE', (targetUser || successfullyUnmuted), null, {noip: 1, noalts: 1});
} else {
this.errorReply(`${(targetUser ? targetUser.name : this.targetUsername)} is not muted.`);
}
},
unmutehelp: [`/unmute [username] - Removes mute from user. Requires: % @ # & ~`],
rb: 'ban',
roomban: 'ban',
b: 'ban',
ban(target, room, user, connection) {
if (!target) return this.parse('/help ban');
if (!this.canTalk()) return;
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('ban', targetUser, room)) return false;
if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to ban upper staff members.");
if (Punishments.getRoomPunishType(room, this.targetUsername) === 'BLACKLIST') return this.errorReply(`This user is already blacklisted from ${room.roomid}.`);
let name = targetUser.getLastName();
let userid = targetUser.getLastId();
if (Punishments.isRoomBanned(targetUser, room.roomid) && !target) {
let problem = " but was already banned";
return this.privateModAction(`(${name} would be banned by ${user.name} ${problem}.)`);
}
if (targetUser.trusted && room.isPrivate !== true && !room.isPersonal) {
Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} ${(targetUser.trusted !== targetUser.id ? ` (${targetUser.trusted})` : ``)} was roombanned from ${room.roomid} by ${user.name}, and should probably be demoted.`);
}
if (targetUser in room.users || user.can('lock')) {
targetUser.popup(
`|modal||html|<p>${Chat.escapeHTML(user.name)} has banned you from the room ${room.roomid} ${(room.subRooms ? ` and its subrooms` : ``)}.</p>${(target ? `<p>Reason: ${Chat.escapeHTML(target)}</p>` : ``)}<p>To appeal the ban, PM the staff member that banned you${(!room.battle && room.auth ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.roomid}">List Room Staff</button></p>` : `.</p>`)}`
);
}
const reason = (target ? ` (${target})` : ``);
this.addModAction(`${name} was banned from ${room.title} by ${user.name}.${reason}`);
let affected = Punishments.roomBan(room, targetUser, null, null, target);
if (!room.isPrivate && room.chatRoomData) {
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : ``)} banned alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
this.privateModAction(displayMessage);
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
}
room.hideText([userid, toID(this.inputUsername)]);
if (room.isPrivate !== true && room.chatRoomData) {
this.globalModlog("ROOMBAN", targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
} else {
this.modlog("ROOMBAN", targetUser, ` by ${user.id}${(target ? `: ${target}` : ``)}`);
}
return true;
},
banhelp: [`/ban [username], [reason] - Bans the user from the room you are in. Requires: @ # & ~`],
unroomban: 'unban',
roomunban: 'unban',
unban(target, room, user, connection) {
if (!target) return this.parse('/help unban');
if (!this.can('ban', null, room)) return false;
let name = Punishments.roomUnban(room, target);
if (name) {
this.addModAction(`${name} was unbanned from ${room.title} by ${user.name}.`);
if (room.isPrivate !== true && room.chatRoomData) {
this.globalModlog("UNROOMBAN", name, ` by ${user.id}`);
}
} else {
this.errorReply(`User '${target}' is not banned from this room.`);
}
},
unbanhelp: [`/unban [username] - Unbans the user from the room you are in. Requires: @ # & ~`],
forcelock: 'lock',
l: 'lock',
ipmute: 'lock',
wl: 'lock',
weeklock: 'lock',
lock(target, room, user, connection, cmd) {
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 && !Punishments.search(toID(this.targetUsername)).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.`);
}
if (!this.can('lock', targetUser)) return false;
let name, userid;
if (targetUser) {
name = targetUser.getLastName();
userid = targetUser.getLastId();
if (targetUser.locked && !week) {
return this.privateModAction(`(${name} would be locked by ${user.name} but was already locked.)`);
}
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.`);
}
} else {
name = this.targetUsername;
userid = toID(this.targetUsername);
}
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();
}
// Use default time for locks.
let duration = week ? Date.now() + 7 * 24 * 60 * 60 * 1000 : null;
let affected = [];
if (targetUser) {
const ignoreAlts = Punishments.sharedIps.has(targetUser.latestIP);
affected = Punishments.lock(targetUser, duration, targetUser.locked, ignoreAlts, userReason);
} else {
affected = Punishments.lock(null, duration, userid, false, userReason);
}
const globalReason = (target ? `: ${userReason} ${proof}` : '');
this.globalModlog((week ? "WEEKLOCK" : "LOCK"), targetUser || userid, ` by ${user.id}${globalReason}`);
let weekMsg = week ? ' for a week' : '';
let lockMessage = `${name} was locked from talking${weekMsg} by ${user.name}.` + (userReason ? ` (${userReason})` : "");
this.addModAction(lockMessage);
// Notify staff room when a user is locked outside of it.
if (room.roomid !== 'staff' && Rooms.get('staff')) {
Rooms.get('staff').addByUser(user, `<<${room.roomid}>> ${lockMessage}`);
}
room.hideText([userid, toID(this.inputUsername)]);
let acAccount = (targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : "")} locked alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
this.privateModAction(displayMessage);
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
if (targetUser) {
let message = `|popup||html|${user.name} has locked you from talking in chats, battles, and PMing regular users${weekMsg}`;
if (userReason) message += `\n\nReason: ${userReason}`;
let appeal = '';
if (Chat.pages.help) {
appeal += `<a href="view-help-request--appeal"><button class="button"><strong>Appeal your punishment</strong></button></a>`;
} else if (Config.appealurl) {
appeal += `appeal: <a href="${Config.appealurl}">${Config.appealurl}</a>`;
}
if (appeal) message += `\n\nIf you feel that your lock was unjustified, you can ${appeal}.`;
message += `\n\nYour lock will expire in a few days.`;
targetUser.send(message);
const roomauth = Rooms.global.destroyPersonalRooms(userid);
if (roomauth.length) Monitor.log(`[CrisisMonitor] Locked user ${name} has public roomauth (${roomauth.join(', ')}), and should probably be demoted.`);
}
// Automatically upload replays as evidence/reference to the punishment
if (room.battle) this.parse('/savereplay forpunishment');
return true;
},
lockhelp: [
`/lock OR /l [username], [reason] - Locks the user from talking in all chats. Requires: % @ & ~`,
`/weeklock OR /wl [username], [reason] - Same as /lock, but locks users for a week.`,
`/lock OR /l [username], [reason] spoiler: [proof] - Marks proof in modlog only.`,
],
unlock(target, room, user) {
if (!target) return this.parse('/help unlock');
if (!this.can('lock')) return false;
let targetUser = Users.get(target);
if (targetUser && targetUser.namelocked) {
return this.errorReply(`User ${targetUser.name} is namelocked, not locked. Use /unnamelock to unnamelock them.`);
}
let reason = '';
if (targetUser && targetUser.locked && targetUser.locked.charAt(0) === '#') {
reason = ` (${targetUser.locked})`;
}
let unlocked = Punishments.unlock(target);
if (unlocked) {
const unlockMessage = `${unlocked.join(", ")} ${((unlocked.length > 1) ? "were" : "was")} unlocked by ${user.name}.${reason}`;
this.addModAction(unlockMessage);
// Notify staff room when a user is unlocked outside of it.
if (!reason && room.roomid !== 'staff' && Rooms.get('staff')) {
Rooms.get('staff').addByUser(user, `<<${room.roomid}>> ${unlockMessage}`);
}
if (!reason) this.globalModlog("UNLOCK", toID(target), ` by ${user.id}`);
if (targetUser) targetUser.popup(`${user.name} has unlocked you.`);
} else {
this.errorReply(`User '${target}' is not locked.`);
}
},
unlockname(target, room, user) {
if (!target) return this.parse('/help unlock');
if (!this.can('lock')) return false;
const userid = toID(target);
const punishment = Punishments.userids.get(userid);
if (!punishment) return this.errorReply("This name isn't locked.");
if (punishment[1] === userid) return this.errorReply(`"${userid}" was specifically locked by a staff member (check the global modlog). Use /unlock if you really want to unlock this name.`);
Punishments.userids.delete(userid);
Punishments.savePunishments();
for (const curUser of Users.findUsers([userid], [])) {
if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.id)) {
curUser.locked = false;
curUser.namelocked = false;
curUser.updateIdentity();
}
}
this.globalModlog("UNLOCKNAME", userid, ` by ${user.name}`);
const unlockMessage = `The name '${target}' was unlocked by ${user.name}.`;
this.addModAction(unlockMessage);
if (room.roomid !== 'staff' && Rooms.get('staff')) {
Rooms.get('staff').addByUser(user, `<<${room.roomid}>> ${unlockMessage}`);
}
},
unlockip(target, room, user) {
target = target.trim();
if (!target) return this.parse('/help unlock');
if (!this.can('ban')) return false;
const range = target.charAt(target.length - 1) === '*';
if (range && !this.can('rangeban')) return false;
if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address.");
const punishment = Punishments.ips.get(target);
if (!punishment) return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
Punishments.ips.delete(target);
Punishments.savePunishments();
for (const curUser of Users.findUsers([], [target])) {
if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.id)) {
curUser.locked = false;
curUser.namelocked = false;
curUser.updateIdentity();
}
}
this.globalModlog(`UNLOCK${range ? 'RANGE' : 'IP'}`, target, ` by ${user.name}`);
const broadcastRoom = Rooms.get('staff') || room;
broadcastRoom.addByUser(user, `${user.name} unlocked the ${range ? "IP range" : "IP"}: ${target}`);
},
unlockiphelp: [`/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ & ~`],
unlocknamehelp: [`/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ & ~`],
unlockhelp: [
`/unlock [username] - Unlocks the user. Requires: % @ & ~`,
`/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ & ~`,
`/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ & ~`,
],
forceglobalban: 'globalban',
gban: 'globalban',
globalban(target, room, user, connection, cmd) {
if (!target) return this.parse('/help globalban');
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 (!target && REQUIRE_REASONS) {
return this.errorReply("Global bans require a reason.");
}
if (!this.can('ban', targetUser)) return false;
let name = targetUser.getLastName();
let userid = targetUser.getLastId();
if (targetUser.trusted) {
if (cmd === 'forceglobalban') {
let from = targetUser.distrust();
Monitor.log(`[CrisisMonitor] ${name} was globally banned by ${user.name} and demoted from ${from.join(", ")}.`);
this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from.join(", ")}`);
} else {
return this.sendReply(`${name} is a trusted user. If you are sure you would like to ban them use /forceglobalban.`);
}
} else if (cmd === 'forceglobalban') {
return this.errorReply(`Use /globalban; ${name} is not a trusted user.`);
}
const roomauth = Rooms.global.destroyPersonalRooms(userid);
if (roomauth.length) Monitor.log(`[CrisisMonitor] Globally banned user ${name} has public roomauth (${roomauth.join(', ')}), and should probably be demoted.`);
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 globally banned you.${(userReason ? `\n\nReason: ${userReason}` : ``)} ${(Config.appealurl ? `\n\nIf you feel that your ban was unjustified, you can appeal:\n${Config.appealurl}` : ``)}\n\nYour ban will expire in a few days.`);
let banMessage = `${name} was globally banned by ${user.name}.${(userReason ? ` (${userReason})` : ``)}`;
this.addModAction(banMessage);
// Notify staff room when a user is banned outside of it.
if (room.roomid !== 'staff' && Rooms.get('staff')) {
Rooms.get('staff').addByUser(user, `<<${room.roomid}>> ${banMessage}`);
}
let affected = Punishments.ban(targetUser, null, null, false, userReason);
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
let guests = affected.length - 1;
affected = affected.slice(1).map(user => user.getLastName()).filter(alt => alt.substr(0, 7) !== '[Guest ');
guests -= affected.length;
displayMessage = `(${name}'s ${(acAccount ? `ac account: ${acAccount}, ` : ``)} banned alts: ${affected.join(", ")} ${(guests ? ` [${guests} guests]` : ``)})`;
this.privateModAction(displayMessage);
for (const user of affected) {
this.add(`|unlink|${toID(user)}`);
}
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
room.hideText([userid, toID(this.inputUsername)]);
const globalReason = (target ? `: ${userReason} ${proof}` : '');
this.globalModlog("BAN", targetUser, ` by ${user.id}${globalReason}`);
return true;
},
globalbanhelp: [
`/globalban OR /gban [username], [reason] - Kick user from all rooms and ban user's IP address with reason. Requires: @ & ~`,
`/globalban OR /gban [username], [reason] spoiler: [proof] - Marks proof in modlog only.`,
],
globalunban: 'unglobalban',
unglobalban(target, room, user) {
if (!target) return this.parse(`/help unglobalban`);
if (!this.can('ban')) return false;
let name = Punishments.unban(target);
let unbanMessage = `${name} was globally unbanned by ${user.name}.`;
if (name) {
this.addModAction(unbanMessage);
// Notify staff room when a user is unbanned outside of it.
if (room.roomid !== 'staff' && Rooms.get('staff')) {
Rooms.get('staff').addByUser(user, `<<${room.roomid}>> ${unbanMessage}`);
}
this.globalModlog("UNBAN", name, ` by ${user.id}`);
} else {
this.errorReply(`User '${target}' is not globally banned.`);
}
},
unglobalbanhelp: [`/unglobalban [username] - Unban a user. Requires: @ & ~`],
unbanall(target, room, user) {
if (!this.can('rangeban')) return false;
if (!target) {
user.lastCommand = '/unbanall';
this.errorReply("THIS WILL UNBAN AND UNLOCK ALL USERS.");
this.errorReply("To confirm, use: /unbanall confirm");
return;
}
if (user.lastCommand !== '/unbanall' || target !== 'confirm') {
return this.parse('/help unbanall');
}
user.lastCommand = '';
Punishments.userids.clear();
Punishments.ips.clear();
Punishments.savePunishments();
this.addModAction(`All bans and locks have been lifted by ${user.name}.`);
this.modlog('UNBANALL');
},
unbanallhelp: [`/unbanall - Unban all IP addresses. Requires: & ~`],
deroomvoiceall(target, room, user) {
if (!this.can('editroom', null, room)) return false;
if (!room.auth) return this.errorReply("Room does not have roomauth.");
if (!target) {
user.lastCommand = '/deroomvoiceall';
this.errorReply("THIS WILL DEROOMVOICE ALL ROOMVOICED USERS.");
this.errorReply("To confirm, use: /deroomvoiceall confirm");
return;
}
if (user.lastCommand !== '/deroomvoiceall' || target !== 'confirm') {
return this.parse('/help deroomvoiceall');
}
user.lastCommand = '';
let count = 0;
for (let userid in room.auth) {
if (room.auth[userid] === '+') {
delete room.auth[userid];
if (userid in room.users) room.users[userid].updateIdentity(room.roomid);
count++;
}
}
if (!count) {
return this.sendReply("(This room has zero roomvoices)");
}
if (room.chatRoomData) {
Rooms.global.writeChatRoomData();
}
this.addModAction(`All ${count} roomvoices have been cleared by ${user.name}.`);
this.modlog('DEROOMVOICEALL');
},
deroomvoiceallhelp: [`/deroomvoiceall - Devoice all roomvoiced users. Requires: # & ~`],
rangeban: 'banip',
banip(target, room, user) {
const [ip, reason] = this.splitOne(target);
if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip)) return this.parse('/help banip');
if (!reason) return this.errorReply("/banip requires a ban reason");
if (!this.can('rangeban')) return false;
const ipDesc = `IP ${(ip.endsWith('*') ? `range ` : ``)}${ip}`;
const curPunishment = Punishments.ipSearch(ip);
if (curPunishment && curPunishment[0] === 'BAN') {
return this.errorReply(`The ${ipDesc} is already temporarily banned.`);
}
Punishments.banRange(ip, reason);
this.addModAction(`${user.name} hour-banned the ${ipDesc}: ${reason}`);
this.modlog('RANGEBAN', null, reason);
},
baniphelp: [`/banip [ip] - Globally bans this IP or IP range for an hour. Accepts wildcards to ban ranges. Existing users on the IP will not be banned. Requires: & ~`],
unrangeban: 'unbanip',
unbanip(target, room, user) {
target = target.trim();
if (!target) {
return this.parse('/help unbanip');
}
if (!this.can('rangeban')) return false;
if (!Punishments.ips.has(target)) {
return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
}
Punishments.ips.delete(target);
this.addModAction(`${user.name} unbanned the ${(target.charAt(target.length - 1) === '*' ? "IP range" : "IP")}: ${target}`);
this.modlog('UNRANGEBAN', null, target);
},
unbaniphelp: [`/unbanip [ip] - Unbans. Accepts wildcards to ban ranges. Requires: & ~`],
rangelock: 'lockip',
lockip(target, room, user) {
const [ip, reason] = this.splitOne(target);
if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip)) return this.parse('/help lockip');
if (!reason) return this.errorReply("/lockip requires a lock reason");
if (!this.can('rangeban')) return false;
const ipDesc = `IP ${(ip.endsWith('*') ? `range ` : ``)}${ip}`;
const curPunishment = Punishments.ipSearch(ip);
if (curPunishment && (curPunishment[0] === 'BAN' || curPunishment[0] === 'LOCK')) {
const punishDesc = curPunishment[0] === 'BAN' ? `temporarily banned` : `temporarily locked`;
return this.errorReply(`The ${ipDesc} is already ${punishDesc}.`);
}
Punishments.lockRange(ip, reason);
this.addModAction(`${user.name} hour-locked the ${ipDesc}: ${reason}`);
this.modlog('RANGELOCK', null, reason);
},
lockiphelp: [`/lockip [ip] - Globally locks this IP or IP range for an hour. Accepts wildcards to ban ranges. Existing users on the IP will not be banned. Requires: & ~`],
unrangelock: 'unlockip',
rangeunlock: 'unlockip',
/*********************************************************
* Moderating: Other
*********************************************************/
mn: 'modnote',
modnote(target, room, user, connection) {
if (!target) return this.parse('/help modnote');
if (!this.canTalk()) return;
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The note is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
if (!this.can('receiveauthmessages', null, room)) return false;
target = target.replace(/\n/g, "; ");
if (room.roomid === 'staff' || room.roomid === 'upperstaff') {
this.globalModlog('NOTE', null, ` by ${user.id}: ${target}`);
} else {
this.modlog('NOTE', null, target);
}
return this.privateModAction(`(${user.name} notes: ${target})`);
},
modnotehelp: [`/modnote [note] - Adds a moderator note that can be read through modlog. Requires: % @ # & ~`],
globalpromote: 'promote',
promote(target, room, user, connection, cmd) {
if (!target) return this.parse('/help promote');
target = this.splitTarget(target, true);
let targetUser = this.targetUser;
let userid = toID(this.targetUsername);
let name = targetUser ? targetUser.name : this.targetUsername;
if (!userid) return this.parse('/help promote');
let currentGroup = ((targetUser && targetUser.group) || Users.usergroups[userid] || ' ')[0];
let nextGroup = target;
if (target === 'deauth') nextGroup = Config.groupsranking[0];
if (!nextGroup) {
return this.errorReply("Please specify a group such as /globalvoice or /globaldeauth");
}
if (!Config.groups[nextGroup]) {
return this.errorReply(`Group '${nextGroup}' does not exist.`);
}
if (!cmd.startsWith('global')) {
let groupid = Config.groups[nextGroup].id;
if (!groupid && nextGroup === Config.groupsranking[0]) groupid = 'deauth';
if (Config.groups[nextGroup].globalonly) return this.errorReply(`Did you mean "/global${groupid}"?`);
if (Config.groups[nextGroup].roomonly) return this.errorReply(`Did you mean "/room${groupid}"?`);
return this.errorReply(`Did you mean "/room${groupid}" or "/global${groupid}"?`);
}
if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) {
return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
}
let groupName = Config.groups[nextGroup].name || "regular user";
if (currentGroup === nextGroup) {
return this.errorReply(`User '${name}' is already a ${groupName}`);
}
if (!user.canPromote(currentGroup, nextGroup)) {
return this.errorReply(`/${cmd} - Access denied.`);
}
if (!Users.isUsernameKnown(userid)) {
return this.errorReply(`/globalpromote - WARNING: '${name}' is offline and unrecognized. The username might be misspelled (either by you or the person who told you) or unregistered. Use /forcepromote if you're sure you want to risk it.`);
}
if (targetUser && !targetUser.registered) {
return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
}
Users.setOfflineGroup(name, nextGroup);
if (Config.groups[nextGroup].rank < Config.groups[currentGroup].rank) {
this.privateModAction(`(${name} was demoted to ${groupName} by ${user.name}.)`);
this.modlog(`GLOBAL ${groupName.toUpperCase()}`, userid, '(demote)');
if (targetUser) targetUser.popup(`You were demoted to ${groupName} by ${user.name}.`);
} else {
this.addModAction(`${name} was promoted to ${groupName} by ${user.name}.`);
this.modlog(`GLOBAL ${groupName.toUpperCase()}`, userid);
if (targetUser) targetUser.popup(`You were promoted to ${groupName} by ${user.name}.`);
}
if (targetUser) targetUser.updateIdentity();
},
promotehelp: [`/promote [username], [group] - Promotes the user to the specified group. Requires: & ~`],
untrustuser: 'trustuser',
unconfirmuser: 'trustuser',
confirmuser: 'trustuser',
trustuser(target, room, user, connection, cmd) {
if (!target) return this.parse('/help trustuser');
if (!this.can('promote')) return;
target = this.splitTarget(target, true);
if (target) return this.errorReply(`This command does not support specifying a reason.`);
let targetUser = this.targetUser;
let userid = toID(this.targetUsername);
let name = targetUser ? targetUser.name : this.targetUsername;
if (!userid) return this.parse('/help trustuser');
if (!targetUser) return this.errorReply(`User '${name}' is not online.`);
if (cmd.startsWith('un')) {
if (!targetUser.trusted) return this.errorReply(`User '${name}' is not trusted.`);
if (targetUser.group !== Config.groupsranking[0]) {
return this.errorReply(`User '${name}' has a global rank higher than trusted.`);
}
targetUser.setGroup(' ');
this.sendReply(`User '${name}' is no longer trusted.`);
this.privateModAction(`${name} was set to no longer be a trusted user by ${user.name}.`);
this.modlog('UNTRUSTUSER', userid);
} else {
if (targetUser.trusted) return this.errorReply(`User '${name}' is already trusted.`);
targetUser.setGroup(Config.groupsranking[0], true);
this.sendReply(`User '${name}' is now trusted.`);
this.privateModAction(`${name} was set as a trusted user by ${user.name}.`);
this.modlog('TRUSTUSER', userid);
}
},
trustuserhelp: [
`/trustuser [username] - Trusts the user (makes them immune to locks). Requires: & ~`,
`/untrustuser [username] - Removes the trusted user status from the user. Requires: & ~`,
],
globaldemote: 'demote',
demote(target) {
if (!target) return this.parse('/help demote');
this.run('promote');
},
demotehelp: [`/demote [username], [group] - Demotes the user to the specified group. Requires: & ~`],
forcepromote(target, room, user, connection) {
// warning: never document this command in /help
if (!this.can('forcepromote')) return false;
target = this.splitTarget(target, true);
let name = this.filter(this.targetUsername);
if (!name) return;
name = name.slice(0, 18);
let nextGroup = target;
if (!Config.groups[nextGroup]) return this.errorReply(`Group '${nextGroup}' does not exist.`);
if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
if (Users.isUsernameKnown(name)) {
return this.errorReply("/forcepromote - Don't forcepromote unless you have to.");
}
Users.setOfflineGroup(name, nextGroup);
this.addModAction(`${name} was promoted to ${(Config.groups[nextGroup].name || "regular user")} by ${user.name}.`);
this.modlog(`GLOBAL${(Config.groups[nextGroup].name || "regular").toUpperCase()}`, toID(name));
},
devoice: 'deauth',
deauth(target, room, user) {
return this.parse(`/demote ${target}, deauth`);
},
deglobalvoice: 'globaldeauth',
deglobalauth: 'globaldeauth',
globaldevoice: 'globaldeauth',
globaldeauth(target, room, user) {
return this.parse(`/globaldemote ${target}, deauth`);
},
deroomvoice: 'roomdeauth',
roomdevoice: 'roomdeauth',
deroomauth: 'roomdeauth',
roomdeauth(target, room, user) {
return this.parse(`/roomdemote ${target}, deauth`);
},
declare(target, room, user) {
target = target.trim();
if (!target) return this.parse('/help declare');
if (!this.can('declare', null, room)) return false;
if (!this.canTalk()) return;
if (target.length > 2000) return this.errorReply("Declares should not exceed 2000 characters.");
for (let u in room.users) {
if (Users.get(u).connected) Users.get(u).sendTo(room, `|notify|${room.title} announcement!|${target}`);
}
this.add(Chat.html`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
this.modlog('DECLARE', null, target);
},
declarehelp: [`/declare [message] - Anonymously announces a message. Requires: # * & ~`],
htmldeclare(target, room, user) {
if (!target) return this.parse('/help htmldeclare');
if (!this.can('gdeclare', null, room)) return false;
if (!this.canTalk()) return;
target = this.canHTML(target);
if (!target) return;
for (let u in room.users) {
if (Users.get(u).connected) Users.get(u).sendTo(room, `|notify|${room.title} announcement!|${Chat.stripHTML(target)}`);
}
this.add(`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
this.modlog(`HTMLDECLARE`, null, target);
},
htmldeclarehelp: [`/htmldeclare [message] - Anonymously announces a message using safe HTML. Requires: ~`],
gdeclare: 'globaldeclare',
globaldeclare(target, room, user) {
if (!target) return this.parse('/help globaldeclare');
if (!this.can('gdeclare')) return false;
target = this.canHTML(target);
if (!target) return;
for (const u of Users.users.values()) {
if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw <div class="broadcast-blue"><b>${target}</b></div>`);
}
this.modlog(`GLOBALDECLARE`, null, target);
},
globaldeclarehelp: [`/globaldeclare [message] - Anonymously announces a message to every room on the server. Requires: ~`],
cdeclare: 'chatdeclare',
chatdeclare(target, room, user) {
if (!target) return this.parse('/help chatdeclare');
if (!this.can('gdeclare')) return false;
target = this.canHTML(target);
if (!target) return;
for (const curRoom of Rooms.rooms.values()) {
if (curRoom.roomid !== 'global' && curRoom.type !== 'battle') {
curRoom.addRaw(`<div class="broadcast-blue"><b>${target}</b></div>`).update();
}
}
this.modlog(`CHATDECLARE`, null, target);
},
chatdeclarehelp: [`/cdeclare [message] - Anonymously announces a message to all chatrooms on the server. Requires: ~`],
'!announce': true,
wall: 'announce',
announce(target, room, user) {
if (!target) return this.parse('/help announce');
if (room && !this.can('announce', null, room)) return false;
target = this.canTalk(target);
if (!target) return;
return `/announce ${target}`;
},
announcehelp: [`/announce OR /wall [message] - Makes an announcement. Requires: % @ # & ~`],
notifyoffrank: 'notifyrank',
notifyrank(target, room, user, connection, cmd) {
if (!target) return this.parse(`/help notifyrank`);
if (!this.can('addhtml', null, room)) return false;
if (!this.canTalk()) return;
let [rank, titleNotification] = this.splitOne(target);
if (rank === 'all') rank = ` `;
if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`);
const id = `${room.roomid}-rank-${(Config.groups[rank].id || `all`)}`;
if (cmd === 'notifyoffrank') {
if (rank === ' ') {
room.send(`|tempnotifyoff|${id}`);
} else {
room.sendRankedUsers(`|tempnotifyoff|${id}`, rank);
}
} else {
let [title, notificationHighlight] = this.splitOne(titleNotification);
if (!title) title = `${room.title} ${(Config.groups[rank].name ? `${Config.groups[rank].name}+ ` : ``)}message!`;
if (!user.can('addhtml')) {
title += ` (notification from ${user.name})`;
}
const [notification, highlight] = this.splitOne(notificationHighlight);
if (notification.length > 300) return this.errorReply(`Notifications should not exceed 300 characters.`);
const message = `|tempnotify|${id}|${title}|${notification}${(highlight ? `|${highlight}` : ``)}`;
if (rank === ' ') {
room.send(message);
} else {
room.sendRankedUsers(message, rank);
}
this.modlog(`NOTIFYRANK`, null, target);
}
},
notifyrankhelp: [
`/notifyrank [rank], [title], [message], [highlight] - Sends a notification to users who are [rank] or higher (and highlight on [highlight], if specified). Requires: # * & ~`,
`/notifyoffrank [rank] - Closes the notification previously sent with /notifyrank [rank]. Requires: # * & ~`,
],
fr: 'forcerename',
forcerename(target, room, user) {
if (!target) return this.parse('/help forcerename');
let reason = this.splitTarget(target, true);
let targetUser = this.targetUser;
if (!targetUser) {
this.splitTarget(target);
if (this.targetUser) {
return this.errorReply(`User has already changed their name to '${this.targetUser.name}'.`);
}
return this.errorReply(`User '${target}' not found.`);
}
if (!this.can('forcerename', targetUser)) return false;
let forceRenameMessage;
if (targetUser.connected) {
forceRenameMessage = `was forced to choose a new name by ${user.name}${(reason ? `: ${reason}` : ``)}`;
this.globalModlog('FORCERENAME', targetUser, ` by ${user.name}${(reason ? `: ${reason}` : ``)}`);
Chat.forceRenames.set(targetUser.id, (Chat.forceRenames.get(targetUser.id) || 0) + 1);
Ladders.cancelSearches(targetUser);
targetUser.send(`|nametaken||${user.name} considers your name inappropriate${(reason ? `: ${reason}` : ".")}`);
} else {
forceRenameMessage = `would be forced to choose a new name by ${user.name} but is offline${(reason ? `: ${reason}` : ``)}`;
this.globalModlog('FORCERENAME OFFLINE', targetUser, ` by ${user.name}${(reason ? `: ${reason}` : ``)}`);
if (!Chat.forceRenames.has(targetUser.id)) Chat.forceRenames.set(targetUser.id, 0);
}
if (room.roomid !== 'staff') this.privateModAction(`(${targetUser.name} ${forceRenameMessage})`);
const roomMessage = room.roomid !== 'staff' ? `«<a href="/${room.roomid}" target="_blank">${room.roomid}</a>» ` : '';
const rankMessage = targetUser.getAccountStatusString();
Rooms.global.notifyRooms(['staff'], `|html|${roomMessage}` + Chat.html`<span class="username">${targetUser.name}</span> ${rankMessage} ${forceRenameMessage}`);
targetUser.resetName(true);
return true;
},
forcerenamehelp: [
`/forcerename OR /fr [username], [reason] - Forcibly change a user's name and shows them the [reason]. Requires: % @ & ~`,
`/allowname [username] - Unmarks a forcerenamed username, stopping staff from being notified when it is used. Requires % @ & ~`,
],
nl: 'namelock',
forcenamelock: 'namelock',
namelock(target, room, user, connection, cmd) {
if (!target) return this.parse('/help namelock');
let reason = this.splitTarget(target);
let targetUser = this.targetUser;
if (!targetUser) {
return this.errorReply(`User '${this.targetUsername}' not found.`);
}
if (targetUser.id !== toID(this.inputUsername) && cmd !== 'forcenamelock') {
return this.errorReply(`${this.inputUsername} has already changed their name to ${targetUser.name}. To namelock anyway, use /forcenamelock.`);
}
if (!this.can('forcerename', targetUser)) return false;
if (targetUser.namelocked) return this.errorReply(`User '${targetUser.name}' is already namelocked.`);
const reasonText = reason ? ` (${reason})` : `.`;
const lockMessage = `${targetUser.name} was namelocked by ${user.name}${reasonText}`;
this.privateModAction(`(${lockMessage})`);
// Notify staff room when a user is locked outside of it.
if (room.roomid !== 'staff' && Rooms.get('staff')) {
Rooms.get('staff').addByUser(user, `<<${room.roomid}>> ${lockMessage}`);
}
const roomauth = Rooms.global.destroyPersonalRooms(targetUser.id);
if (roomauth.length) Monitor.log(`[CrisisMonitor] Namelocked user ${targetUser.name} has public roomauth (${roomauth.join(', ')}), and should probably be demoted.`);
this.globalModlog("NAMELOCK", targetUser, ` by ${user.id}${reasonText}`);
Ladders.cancelSearches(targetUser);
Punishments.namelock(targetUser, null, null, false, reason);
targetUser.popup(`|modal|${user.name} has locked your name and you can't change names anymore${reasonText}`);
// Automatically upload replays as evidence/reference to the punishment
if (room.battle) this.parse('/savereplay forpunishment');
return true;
},
namelockhelp: [`/namelock OR /nl [username], [reason] - Name locks a user and shows them the [reason]. Requires: % @ & ~`],
unl: 'unnamelock',
unnamelock(target, room, user) {
if (!target) return this.parse('/help unnamelock');
if (!this.can('forcerename')) return false;
let targetUser = Users.get(target);
let reason = '';
if (targetUser && targetUser.namelocked) {
reason = ` (${targetUser.namelocked})`;
}
let unlocked = Punishments.unnamelock(target);
if (unlocked) {
this.addModAction(`${unlocked} was unnamelocked by ${user.name}.${reason}`);
if (!reason) this.globalModlog("UNNAMELOCK", toID(target), ` by ${user.id}`);
if (targetUser) targetUser.popup(`${user.name} has unnamelocked you.`);
} else {
this.errorReply(`User '${target}' is not namelocked.`);
}
},
unnamelockhelp: [`/unnamelock [username] - Unnamelocks the user. Requires: % @ & ~`],
hidetextalts: 'hidetext',
hidealttext: 'hidetext',
hidealtstext: 'hidetext',
htext: 'hidetext',
forcehidetext: 'hidetext',
forcehtext: 'hidetext',
hidetext(target, room, user, connection, cmd) {
if (!target) return this.parse(`/help hidetext`);
this.splitTarget(target);
let targetUser = this.targetUser;
let name = this.targetUsername;
if (!targetUser && !room.log.hasUsername(target)) return this.errorReply(`User ${target} not found or has no roomlogs.`);
let userid = toID(this.inputUsername);
if (!this.can('mute', null, room)) return;
if (targetUser && targetUser.trusted && targetUser !== user && !cmd.includes('force')) {
return this.errorReply(`${target} is a trusted user, are you sure you want to hide their messages? Use /forcehidetext if you're sure.`);
}
if (targetUser && cmd.includes('alt')) {
room.send(`|c|~|${name}'s alts messages were cleared from ${room.title} by ${user.name}.`);
this.modlog('HIDEALTSTEXT', targetUser, null, {noip: 1});
room.hideText([
userid,
...Object.keys(targetUser.prevNames),
...targetUser.getAltUsers(true).map(user => user.getLastId()),
]);
} else {
room.send(`|c|~|${name}'s messages were cleared from ${room.title} by ${user.name}.`);
this.modlog('HIDETEXT', targetUser || userid, null, {noip: 1, noalts: 1});
room.hideText([userid]);
}
},
hidetexthelp: [
`/hidetext [username] - Removes a user's messages from chat. Requires: % @ # & ~`,
`/hidealtstext [username] - Removes a user's messages, and their alternate account's messages from the chat. Requires: % @ # & ~`,
],
ab: 'blacklist',
blacklist(target, room, user) {
if (!target) return this.parse('/help blacklist');
if (!this.canTalk()) return;
if (toID(target) === 'show') return this.errorReply(`You're looking for /showbl`);
target = this.splitTarget(target);
const targetUser = this.targetUser;
if (!targetUser) {
this.errorReply(`User ${this.targetUsername} not found.`);
return this.errorReply(`If you want to blacklist an offline account by name (not IP), consider /blacklistname`);
}
if (!this.can('editroom', targetUser, room)) return false;
if (!room.chatRoomData) {
return this.errorReply(`This room is not going to last long enough for a blacklist to matter - just ban the user`);
}
let punishment = Punishments.isRoomBanned(targetUser, room.roomid);
if (punishment && punishment[0] === 'BLACKLIST') {
return this.errorReply(`This user is already blacklisted from this room.`);
}
if (!target && REQUIRE_REASONS) {
return this.errorReply(`Blacklists require a reason.`);
}
if (target.length > MAX_REASON_LENGTH) {
return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
}
const name = targetUser.getLastName();
const userid = targetUser.getLastId();
if (targetUser.trusted && room.isPrivate !== true) {
Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name}${(targetUser.trusted !== targetUser.id ? ` (${targetUser.trusted})` : '')} was blacklisted from ${room.roomid} by ${user.name}, and should probably be demoted.`);
}
if (targetUser in room.users || user.can('lock')) {
targetUser.popup(
`|modal||html|<p>${Chat.escapeHTML(user.name)} has blacklisted you from the room ${room.roomid}${(room.subRooms ? ` and its subrooms` : '')}. Reason: ${Chat.escapeHTML(target)}</p>` +
`<p>To appeal the ban, PM the staff member that blacklisted you${(!room.battle && room.auth ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.roomid}">List Room Staff</button></p>` : `.</p>`)}`
);
}
this.privateModAction(`(${name} was blacklisted from ${room.title} by ${user.name}. ${(target ? ` (${target})` : '')})`);
let affected = Punishments.roomBlacklist(room, targetUser, null, null, target);
if (!room.isPrivate && room.chatRoomData) {
let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
let displayMessage = '';
if (affected.length > 1) {
displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount},` : '')} blacklisted alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
this.privateModAction(displayMessage);
} else if (acAccount) {
displayMessage = `(${name}'s ac account: ${acAccount})`;
this.privateModAction(displayMessage);
}
}
if (!room.isPrivate && room.chatRoomData) {
this.globalModlog("BLACKLIST", targetUser, ` by ${user.id}${(target ? `: ${target}` : '')}`);
} else {
// Room modlog only
this.modlog("BLACKLIST", targetUser, ` by ${user.id}${(target ? `: ${target}` : '')}`);
}
return true;
},
blacklisthelp: [
`/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~`,
`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`,
`/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`,
`/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`,
],
forcebattleban: 'battleban',
battleban(target, room, user, connection, cmd) {
if (!target) return this.parse(`/help battleban`);
const reason = this.splitTarget(target);
const 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 (!reason) {
return this.errorReply(`Battle bans require a reason.`);
}
const includesUrl = reason.includes(`.${Config.routes.root}/`); // lgtm [js/incomplete-url-substring-sanitization]
if (!room.battle && !includesUrl && cmd !== 'forcebattleban') {
return this.errorReply(`Battle bans require a battle replay if used outside of a battle; if the battle has expired, use /forcebattleban.`);
}
if (!this.can('rangeban', targetUser)) {
this.errorReply(`Battlebans have been deprecated. Alternatives:`);
this.errorReply(`- timerstalling and bragging about it: lock`);
this.errorReply(`- other timerstalling: they're not timerstalling, leave them alone`);
this.errorReply(`- bad nicknames: lock, locks prevent nicknames from appearing; you should always have been locking for this`);
this.errorReply(`- ladder cheating: gban, get a moderator if necessary`);
this.errorReply(`- serious ladder cheating: permaban, get a leader`);
this.errorReply(`- other: get a leader`);
return;
}
if (Punishments.isBattleBanned(targetUser)) return this.errorReply(`User '${targetUser.name}' is already banned from battling.`);
const reasonText = reason ? ` (${reason})` : `.`;
const battlebanMessage = `${targetUser.name} was banned from starting new battles by ${user.name}${reasonText}`;
this.privateModAction(`(${battlebanMessage})`);
// Notify staff room when a user is banned from battling outside of it.
if (room.roomid !== 'staff' && Rooms.get('staff')) {
Rooms.get('staff').addByUser(user, `<<${room.roomid}>> ${battlebanMessage}`);
}
if (targetUser.trusted) {
Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} was banned from battling by ${user.name}, and should probably be demoted.`);
}
this.globalModlog("BATTLEBAN", targetUser, ` by ${user.id}${reasonText}`);
Ladders.cancelSearches(targetUser);
Punishments.battleban(targetUser, null, null, reason);
targetUser.popup(`|modal|${user.name} has prevented you from starting new battles for 2 days${reasonText}`);
// Automatically upload replays as evidence/reference to the punishment
if (room.battle) this.parse('/savereplay forpunishment');
return true;
},
battlebanhelp: [`/battleban [username], [reason] - [DEPRECATED] Prevents the user from starting new battles for 2 days and shows them the [reason]. Requires: & ~`],
unbattleban(target, room, user) {
if (!target) return this.parse('/help unbattleban');
if (!this.can('lock')) return;
const targetUser = Users.get(target);
const unbanned = Punishments.unbattleban(target);
if (unbanned) {
this.addModAction(`${unbanned} was allowed to battle again by ${user.name}.`);
this.globalModlog("UNBATTLEBAN", toID(target), ` by ${user.name}`);
if (targetUser) targetUser.popup(`${user.name} has allowed you to battle again.`);
} else {
this.errorReply(`User ${target} is not banned from battling.`);
}
},
unbattlebanhelp: [`/unbattleban [username] - [DEPRECATED] Allows a user to battle again. Requires: % @ & ~`],
nameblacklist: 'blacklistname',
blacklistname(target, room, user) {
if (!target) return this.parse('/help blacklistname');
if (!this.canTalk()) return;
if (!this.can('editroom', null, room)) return false;
if (!room.chatRoomData) {
return this.errorReply("This room is not going to last long enough for a blacklist to matter - just ban the user");
}
let [targetStr, reason] = target.split('|').map(val => val.trim());
if (!targetStr || (!reason && REQUIRE_REASONS)) {
return this.errorReply("Usage: /blacklistname name1, name2, ... | reason");
}
let targets = targetStr.split(',').map(s => toID(s));
let duplicates = targets.filter(userid => {
let punishment = Punishments.roomUserids.nestedGet(room.roomid, userid);
return punishment && punishment[0] === 'BLACKLIST';
});
if (duplicates.length) {
return this.errorReply(`[${duplicates.join(', ')}] ${Chat.plural(duplicates, "are", "is")} already blacklisted.`);
}
const userRank = Config.groupsranking.indexOf(room.getAuth(user));
for (const userid of targets) {
if (!userid) return this.errorReply(`User '${userid}' is not a valid userid.`);
const targetRank = Config.groupsranking.indexOf(room.getAuth({id: userid}));
if (targetRank >= userRank) return this.errorReply(`/blacklistname - Access denied: ${userid} is of equal or higher authority than you.`);
Punishments.roomBlacklist(room, null, null, userid, reason);
const trusted = Users.isTrusted(userid);
if (trusted && room.isPrivate !== true) {
Monitor.log(`[CrisisMonitor] Trusted user ${userid}${(trusted !== userid ? ` (${trusted})` : ``)} was nameblacklisted from ${room.roomid} by ${user.name}, and should probably be demoted.`);
}
if (!room.isPrivate && room.chatRoomData) {
this.globalModlog("NAMEBLACKLIST", userid, ` by ${user.id}${(reason ? `: ${reason}` : '')}`);
}
}
this.privateModAction(`(${targets.join(', ')}${Chat.plural(targets, " were", " was")} nameblacklisted from ${room.title} by ${user.name}.)`);
return true;
},
blacklistnamehelp: [`/blacklistname OR /nameblacklist [username1, username2, etc.] | reason - Blacklists the given username(s) from the room you are in for a year. Requires: # & ~`],
unab: 'unblacklist',
unblacklist(target, room, user) {
if (!target) return this.parse('/help unblacklist');
if (!this.can('editroom', null, room)) return false;
const name = Punishments.roomUnblacklist(room, target);
if (name) {
this.privateModAction(`(${name} was unblacklisted by ${user.name}.)`);
if (!room.isPrivate && room.chatRoomData) {
this.globalModlog("UNBLACKLIST", name, ` by ${user.id}`);
}
} else {
this.errorReply(`User '${target}' is not blacklisted.`);
}
},
unblacklisthelp: [`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`],
unblacklistall(target, room, user) {
if (!this.can('editroom', null, room)) return false;
if (!target) {
user.lastCommand = '/unblacklistall';
this.errorReply("THIS WILL UNBLACKLIST ALL BLACKLISTED USERS IN THIS ROOM.");
this.errorReply("To confirm, use: /unblacklistall confirm");
return;
}
if (user.lastCommand !== '/unblacklistall' || target !== 'confirm') {
return this.parse('/help unblacklistall');
}
user.lastCommand = '';
let unblacklisted = Punishments.roomUnblacklistAll(room);
if (!unblacklisted) return this.errorReply("No users are currently blacklisted in this room to unblacklist.");
this.addModAction(`All blacklists in this room have been lifted by ${user.name}.`);
this.modlog('UNBLACKLISTALL');
this.roomlog(`Unblacklisted users: ${unblacklisted.join(', ')}`);
},
unblacklistallhelp: [`/unblacklistall - Unblacklists all blacklisted users in the current room. Requires #, &, ~`],
expiringbls: 'showblacklist',
expiringblacklists: 'showblacklist',
blacklists: 'showblacklist',
showbl: 'showblacklist',
showblacklist(target, room, user, connection, cmd) {
if (target) room = Rooms.search(target);
if (!room) return this.errorReply(`The room "${target}" was not found.`);
if (!this.can('mute', null, room)) return false;
const SOON_EXPIRING_TIME = 3 * 30 * 24 * 60 * 60 * 1000; // 3 months
if (!room.chatRoomData) return this.errorReply("This room does not support blacklists.");
const subMap = Punishments.roomUserids.get(room.roomid);
if (!subMap || subMap.size === 0) {
return this.sendReply("This room has no blacklisted users.");
}
let blMap = new Map();
let ips = '';
for (const [userid, punishment] of subMap) {
const [punishType, id, expireTime] = punishment;
if (punishType === 'BLACKLIST') {
if (!blMap.has(id)) blMap.set(id, [expireTime]);
if (id !== userid) blMap.get(id).push(userid);
}
}
if (user.can('ban')) {
const subMap = Punishments.roomIps.get(room.roomid);
if (subMap) {
ips = '/ips';
for (const [ip, punishment] of subMap) {
const [punishType, id] = punishment;
if (punishType === 'BLACKLIST') {
if (!blMap.has(id)) blMap.set(id, []);
blMap.get(id).push(ip);
}
}
}
}
let soonExpiring = (cmd === 'expiringblacklists' || cmd === 'expiringbls');
let buf = Chat.html`Blacklist for ${room.title}${soonExpiring ? ` (expiring within 3 months)` : ''}:<br />`;
for (const [userid, data] of blMap) {
const [expireTime, ...alts] = data;
if (soonExpiring && expireTime > Date.now() + SOON_EXPIRING_TIME) continue;
const expiresIn = new Date(expireTime).getTime() - Date.now();
const expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
buf += `- <strong>${userid}</strong>, for ${Chat.count(expiresDays, "days")}`;
if (alts.length) buf += `, alts${ips}: ${alts.join(', ')}`;
buf += `<br />`;
}
this.sendReplyBox(buf);
},
showblacklisthelp: [
`/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`,
`/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`,
],
markshared(target, room, user) {
if (!target) return this.parse('/help markshared');
if (!this.can('ban')) return false;
let [ip, note] = this.splitOne(target);
if (!/^[0-9.*]+$/.test(ip)) return this.errorReply("Please enter a valid IP address.");
if (Punishments.sharedIps.has(ip)) return this.errorReply("This IP is already marked as shared.");
if (!note) {
this.errorReply(`You must specify who owns this shared IP.`);
this.parse(`/help markshared`);
return;
}
Punishments.addSharedIp(ip, note);
note = ` (${note})`;
this.globalModlog('SHAREDIP', ip, ` by ${user.name}${note}`);
const message = `The IP '${ip}' was marked as shared by ${user.name}.${note}`;
const staffRoom = Rooms.get('staff');
if (staffRoom) return staffRoom.addByUser(user, message);
return this.addModAction(message);
},
marksharedhelp: [`/markshared [IP], [owner/organization of IP] - Marks an IP address as shared. Note: the owner/organization (i.e., University of Minnesota) of the shared IP is required. Requires @, &, ~`],
unmarkshared(target, room, user) {
if (!target) return this.parse('/help unmarkshared');
if (!this.can('ban')) return false;
if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address.");
if (!Punishments.sharedIps.has(target)) return this.errorReply("This IP isn't marked as shared.");
Punishments.removeSharedIp(target);
this.globalModlog('UNSHAREIP', target, ` by ${user.name}`);
return this.addModAction(`The IP '${target}' was unmarked as shared by ${user.name}.`);
},
unmarksharedhelp: [`/unmarkshared [ip] - Unmarks a shared IP address. Requires @, &, ~`],
};