Skip to content
This repository has been archived by the owner on Apr 6, 2021. It is now read-only.

Commit

Permalink
Implement basic chat moderation
Browse files Browse the repository at this point in the history
  • Loading branch information
sirDonovan committed Jul 21, 2017
1 parent b1f5cad commit 319a2a6
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 3 deletions.
30 changes: 29 additions & 1 deletion config-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ exports.groups = {
/**@type {Array<string>} */
exports.developers = [];

// Custom function
// Custom functions
/**@type {?Function} */
exports.parseMessage = null;
/**@type {?Function} */
exports.moderate = null;

/**@type {boolean | {[k: string]: boolean}} */
exports.allowModeration = false;

let punishmentPoints = {
'verbalwarn': 0,
'warn': 1,
'mute': 2,
'hourmute': 3,
'roomban': 4,
};

let punishmentActions = {};
for (let i in punishmentPoints) {
punishmentActions['' + punishmentPoints[i]] = i;
}

exports.punishmentPoints = punishmentPoints;
exports.punishmentActions = punishmentActions;

// Reasons used when Cassius punishes a user for
// flooding, stretching, caps, etc.
// example: punishmentReasons = {'flooding': 'please do not flood the chat'}

/**@type {?{[k: string]: string}} */
exports.punishmentReasons = null;
96 changes: 94 additions & 2 deletions message-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
const Room = require('./rooms').Room; // eslint-disable-line no-unused-vars
const User = require('./users').User; // eslint-disable-line no-unused-vars

const whitespaceRegex = new RegExp('\\s+', 'g');
const nullCharactersRegex = new RegExp('[\u0000\u200B-\u200F]+', 'g');
const capsRegex = new RegExp('[A-Z]', 'g');
const stretchRegex = new RegExp('(.+)\\1+', 'g');

const FLOOD_MINIMUM_MESSAGES = 5;
const FLOOD_MAXIMUM_TIME = 5 * 1000;
const STRETCHING_MINIMUM = 20;
const CAPS_MINIMUM = 30;
const PUNISHMENT_COOLDOWN = 5 * 1000;

class Context {
/**
* @param {string} target
Expand Down Expand Up @@ -164,7 +175,9 @@ class MessageParser {
if (message in room.listeners) room.listeners[message]();
return;
}
this.parseCommand(message, room, user);
let time = Date.now();
this.parseCommand(message, room, user, time);
if (!user.hasRank(room, '+')) this.moderate(message, room, user, time);
break;
}
case 'c:': {
Expand All @@ -178,7 +191,9 @@ class MessageParser {
if (message in room.listeners) room.listeners[message]();
return;
}
this.parseCommand(message, room, user, parseInt(splitMessage[0]) * 1000);
let time = parseInt(splitMessage[0]) * 1000;
this.parseCommand(message, room, user, time);
if (!user.hasRank(room, '+')) this.moderate(message, room, user, time);
break;
}
case 'pm': {
Expand Down Expand Up @@ -229,6 +244,83 @@ class MessageParser {

new Context(target, room, user, command, time).run();
}

/**
* @param {string} message
* @param {Room} room
* @param {User} user
* @param {number} time
*/
moderate(message, room, user, time) {
if (!Users.self.hasRank(room, '%')) return;
if (typeof Config.allowModeration === 'object') {
if (!Config.allowModeration[room.id]) return;
} else {
if (!Config.allowModeration) return;
}
if (!Config.punishmentPoints || !Config.punishmentActions) return;

message = message.trim().replace(whitespaceRegex, '').replace(nullCharactersRegex, '');

let data = user.roomData.get(room);
if (!data) {
data = {messages: [], points: 0, lastAction: 0};
user.roomData.set(room, data);
}

data.messages.unshift({message: message, time: time});

// avoid escalating punishments for the same message(s) due to lag or the message queue
if (data.lastAction && time - data.lastAction < PUNISHMENT_COOLDOWN) return;

/**@type {Array<{action: string, rule: string, reason: string}>} */
let punishments = [];

if (typeof Config.moderate === 'function') {
let result = Config.moderate(message, room, user, time);
if (result instanceof Array) punishments = punishments.concat(result);
}

// flooding
if (data.messages.length >= FLOOD_MINIMUM_MESSAGES && time - data.messages[FLOOD_MINIMUM_MESSAGES - 1].time <= FLOOD_MAXIMUM_TIME) {
punishments.push({action: 'mute', rule: 'flooding', reason: 'please do not flood the chat'});
}

// stretching
let stretching = message.match(stretchRegex);
if (stretching) {
stretching.sort((a, b) => b.length - a.length);
if (stretching[0].length >= STRETCHING_MINIMUM) {
punishments.push({action: 'verbalwarn', rule: 'stretching', reason: 'please do not stretch'});
}
}

// caps
let caps = message.match(capsRegex);
if (caps && caps.length >= CAPS_MINIMUM) {
punishments.push({action: 'verbalwarn', rule: 'caps', reason: 'please do not abuse caps'});
}

if (!punishments.length) return;

punishments.sort((a, b) => Config.punishmentPoints[b.action] - Config.punishmentPoints[a.action]);
let punishment = punishments[0];
let points = Config.punishmentPoints[punishment.action];
let reason = punishment.reason;
if (Config.punishmentReasons && Config.punishmentReasons[punishment.rule]) reason = Config.punishmentReasons[punishment.rule];
let action = punishment.action;
if (data.points >= points) {
data.points++;
points = data.points;
if (Config.punishmentActions['' + points]) action = Config.punishmentActions['' + points];
} else {
data.points = points;
}
if (action === 'verbalwarn') return room.say(user.name + ", " + reason);
if (action === 'roomban' && !Users.self.hasRank(room, '@')) action = 'hourmute';
room.say("/" + action + " " + user.name + ", " + reason);
data.lastAction = time;
}
}

exports.MessageParser = new MessageParser();
2 changes: 2 additions & 0 deletions users.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class User {
this.id = id;
/**@type {Map<Room, string>} */
this.rooms = new Map();
/**@type {Map<Room, {messages: Array<{time: number, message: string}>, points: number, lastAction: number}>} */
this.roomData = new Map();
/**@type {?Game} */
this.game = null;
}
Expand Down

0 comments on commit 319a2a6

Please sign in to comment.