');
}
popupReply(message) {
this.connection.popup(message);
}
add(data) {
if (this.pmTarget) {
data = this.pmTransform(data);
this.user.send(data);
if (this.pmTarget !== this.user) this.pmTarget.send(data);
return;
}
this.room.add(data);
}
send(data) {
if (this.pmTarget) {
data = this.pmTransform(data);
this.user.send(data);
if (this.pmTarget !== this.user) this.pmTarget.send(data);
return;
}
this.room.send(data);
}
sendModCommand(data) {
this.room.sendModCommand(data);
}
privateModCommand(data) {
this.room.sendModCommand(data);
this.logEntry(data);
this.room.modlog(data);
}
globalModlog(action, user, text) {
let buf = "(" + this.room.id + ") " + action + ": ";
if (typeof user === 'string') {
buf += "[" + toId(user) + "]";
} else {
let userid = user.getLastId();
buf += "[" + userid + "]";
if (user.autoconfirmed && user.autoconfirmed !== userid) buf += " ac:[" + user.autoconfirmed + "]";
}
buf += text;
Rooms.global.modlog(buf);
}
logEntry(data) {
if (this.pmTarget) return;
this.room.logEntry(data);
}
addModCommand(text, logOnlyText) {
this.room.addLogMessage(this.user, text);
this.room.modlog(text + (logOnlyText || ""));
}
logModCommand(text) {
this.room.modlog(text);
}
update() {
if (this.room) this.room.update();
}
can(permission, target, room) {
if (!this.user.can(permission, target, room)) {
this.errorReply(this.cmdToken + this.fullCmd + " - Access denied.");
return false;
}
return true;
}
canBroadcast(suppressMessage) {
if (!this.broadcasting && this.cmdToken === BROADCAST_TOKEN) {
let message = this.canTalk(suppressMessage || this.message);
if (!message) return false;
if (!this.pmTarget && !this.user.can('broadcast', null, this.room)) {
this.errorReply("You need to be voiced to broadcast this command's information.");
this.errorReply("To see it for yourself, use: /" + this.message.substr(1));
return false;
}
// broadcast cooldown
let broadcastMessage = message.toLowerCase().replace(/[^a-z0-9\s!,]/g, '');
if (this.room && this.room.lastBroadcast === this.broadcastMessage &&
this.room.lastBroadcastTime >= Date.now() - BROADCAST_COOLDOWN) {
this.errorReply("You can't broadcast this because it was just broadcasted.");
return false;
}
this.message = message;
this.broadcastMessage = broadcastMessage;
}
return true;
}
runBroadcast(suppressMessage) {
if (this.broadcasting || this.cmdToken !== BROADCAST_TOKEN) {
// Already being broadcast, or the user doesn't intend to broadcast.
return true;
}
if (!this.broadcastMessage) {
// Permission hasn't been checked yet. Do it now.
if (!this.canBroadcast(suppressMessage)) return false;
}
if (this.pmTarget) {
this.add('|c~|' + (suppressMessage || this.message));
} else {
this.add('|c|' + this.user.getIdentity(this.room.id) + '|' + (suppressMessage || this.message));
}
if (!this.pmTarget) {
this.room.lastBroadcast = this.broadcastMessage;
this.room.lastBroadcastTime = Date.now();
}
this.broadcasting = true;
return true;
}
canTalk(message, room, targetUser) {
if (room === undefined) room = this.room;
if (targetUser === undefined && this.pmTarget) {
room = undefined;
targetUser = this.pmTarget;
}
let user = this.user;
let connection = this.connection;
if (room && room.id === 'global') {
// should never happen
// console.log(`Command tried to write to global: ${user.name}: ${message}`);
return false;
}
if (!user.named) {
connection.popup(`You must choose a name before you can talk.`);
return false;
}
if (!user.can('bypassall')) {
let lockType = (user.namelocked ? `namelocked` : user.locked ? `locked` : ``);
let lockExpiration = Punishments.checkLockExpiration(user.namelocked || user.locked);
if (room) {
if (lockType) {
this.errorReply(`You are ${lockType} and can't talk in chat. ${lockExpiration}`);
return false;
}
if (room.isMuted(user)) {
this.errorReply(`You are muted and cannot talk in this room.`);
return false;
}
if (room.modchat && !user.authAtLeast(room.modchat, room)) {
if (room.modchat === 'autoconfirmed') {
this.errorReply(`Because moderated chat is set, your account must be at least one week old and you must have won at least one ladder game to speak in this room.`);
return false;
}
const groupName = Config.groups[room.modchat] && Config.groups[room.modchat].name || room.modchat;
this.errorReply(`Because moderated chat is set, you must be of rank ${groupName} or higher to speak in this room.`);
return false;
}
if (!(user.userid in room.users)) {
connection.popup("You can't send a message to this room without being in it.");
return false;
}
}
if (targetUser) {
if (lockType && !targetUser.can('lock')) {
return this.errorReply(`You are ${lockType} and can only private message members of the global moderation team (users marked by @ or above in the Help room). ${lockExpiration}`);
}
if (targetUser.locked && !user.can('lock')) {
return this.errorReply(`The user "${targetUser.name}" is locked and cannot be PMed.`);
}
if (Config.pmmodchat && !user.authAtLeast(Config.pmmodchat)) {
let groupName = Config.groups[Config.pmmodchat] && Config.groups[Config.pmmodchat].name || Config.pmmodchat;
return this.errorReply(`Because moderated chat is set, you must be of rank ${groupName} or higher to PM users.`);
}
if (targetUser.ignorePMs && targetUser.ignorePMs !== user.group && !user.can('lock')) {
if (!targetUser.can('lock')) {
return this.errorReply(`This user is blocking private messages right now.`);
} else if (targetUser.can('bypassall')) {
return this.errorReply(`This admin is too busy to answer private messages right now. Please contact a different staff member.`);
}
}
if (user.ignorePMs && user.ignorePMs !== targetUser.group && !targetUser.can('lock')) {
return this.errorReply(`You are blocking private messages right now.`);
}
}
}
if (typeof message === 'string') {
if (!message) {
connection.popup("Your message can't be blank.");
return false;
}
let length = message.length;
length += 10 * message.replace(/[^\ufdfd]*/g, '').length;
if (length > MAX_MESSAGE_LENGTH && !user.can('ignorelimits')) {
this.errorReply("Your message is too long: " + message);
return false;
}
// remove zalgo
message = message.replace(/[\u0300-\u036f\u0483-\u0489\u0610-\u0615\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06ED\u0E31\u0E34-\u0E3A\u0E47-\u0E4E]{3,}/g, '');
if (/[\u239b-\u23b9]/.test(message)) {
this.errorReply("Your message contains banned characters.");
return false;
}
if (!this.checkFormat(room, user, message)) {
return false;
}
if (!this.checkSlowchat(room, user) && !user.can('mute', null, room)) {
this.errorReply("This room has slow-chat enabled. You can only talk once every " + room.slowchat + " seconds.");
return false;
}
if (!this.checkBanwords(room, user.name) && !user.can('bypassall')) {
this.errorReply(`Your username contains a phrase banned by this room.`);
return false;
}
if (!this.checkBanwords(room, message) && !user.can('mute', null, room)) {
this.errorReply("Your message contained banned words.");
return false;
}
let gameFilter = this.checkGameFilter();
if (gameFilter && !user.can('bypassall')) {
this.errorReply(gameFilter);
return false;
}
if (room) {
let normalized = message.trim();
if ((room.id === 'lobby' || room.id === 'help') && (normalized === user.lastMessage) &&
((Date.now() - user.lastMessageTime) < MESSAGE_COOLDOWN)) {
this.errorReply("You can't send the same message again so soon.");
return false;
}
user.lastMessage = message;
user.lastMessageTime = Date.now();
}
if (Config.chatfilter) {
return Config.chatfilter.call(this, message, user, room, connection, targetUser);
}
return message;
}
return true;
}
canEmbedURI(uri, isRelative) {
if (uri.startsWith('https://')) return uri;
if (uri.startsWith('//')) return uri;
if (uri.startsWith('data:')) return uri;
if (!uri.startsWith('http://')) {
if (/^[a-z]+\:\/\//.test(uri) || isRelative) {
return this.errorReply("URIs must begin with 'https://' or 'http://' or 'data:'");
}
} else {
uri = uri.slice(7);
}
let slashIndex = uri.indexOf('/');
let domain = (slashIndex >= 0 ? uri.slice(0, slashIndex) : uri);
// heuristic that works for all the domains we care about
let secondLastDotIndex = domain.lastIndexOf('.', domain.length - 5);
if (secondLastDotIndex >= 0) domain = domain.slice(secondLastDotIndex + 1);
let approvedDomains = {
'imgur.com': 1,
'gyazo.com': 1,
'puu.sh': 1,
'rotmgtool.com': 1,
'pokemonshowdown.com': 1,
'nocookie.net': 1,
'blogspot.com': 1,
'imageshack.us': 1,
'deviantart.net': 1,
'd.pr': 1,
'pokefans.net': 1,
};
if (domain in approvedDomains) {
return '//' + uri;
}
if (domain === 'bit.ly') {
return this.errorReply("Please don't use URL shorteners.");
}
// unknown URI, allow HTTP to be safe
return 'http://' + uri;
}
canHTML(html) {
html = ('' + (html || '')).trim();
if (!html) return '';
let images = /]*/ig;
let match;
while ((match = images.exec(html))) {
if (this.room.isPersonal && !this.user.can('announce')) {
this.errorReply("Images are not allowed in personal rooms.");
return false;
}
if (!/width=([0-9]+|"[0-9]+")/i.test(match[0]) || !/height=([0-9]+|"[0-9]+")/i.test(match[0])) {
// Width and height are required because most browsers insert the
// element before width and height are known, and when the
// image is loaded, this changes the height of the chat area, which
// messes up autoscrolling.
this.errorReply('All images must have a width and height attribute');
return false;
}
let srcMatch = /src\s*\=\s*"?([^ "]+)(\s*")?/i.exec(match[0]);
if (srcMatch) {
let uri = this.canEmbedURI(srcMatch[1], true);
if (!uri) return false;
html = html.slice(0, match.index + srcMatch.index) + 'src="' + uri + '"' + html.slice(match.index + srcMatch.index + srcMatch[0].length);
// lastIndex is inaccurate since html was changed
images.lastIndex = match.index + 11;
}
}
if ((this.room.isPersonal || this.room.isPrivate === true) && !this.user.can('lock') && html.replace(/\s*style\s*=\s*\"?[^\"]*\"\s*>/g, '>').match(/