3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-16 14:11:50 +00:00

spam: add support for non-message spam; add max_voice_move spam detection

This commit is contained in:
Dragory 2019-02-17 17:09:49 +02:00
parent 4f5eb0689d
commit 30db4d58b4
3 changed files with 122 additions and 29 deletions

View file

@ -37,7 +37,8 @@
"COMMAND": "🤖 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) used command in **#{channel.name}**:\n`{command}`", "COMMAND": "🤖 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) used command in **#{channel.name}**:\n`{command}`",
"SPAM_DETECTED": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) spam detected in **#{channel.name}**: {description} (more than {limit} in {interval}s)\n{archiveUrl}", "MESSAGE_SPAM_DETECTED": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) spam detected in **#{channel.name}**: {description} (more than {limit} in {interval}s)\n{archiveUrl}",
"OTHER_SPAM_DETECTED": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) spam detected: {description} (more than {limit} in {interval}s)",
"CENSOR": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) censored message in **#{channel.name}** (`{channel.id}`) {reason}:\n```{messageText}```", "CENSOR": "🛑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) censored message in **#{channel.name}** (`{channel.id}`) {reason}:\n```{messageText}```",
"CLEAN": "🚿 **{mod.username}#{mod.discriminator}** (`{mod.id}`) cleaned **{count}** message(s) in **#{channel.name}**\n{archiveUrl}", "CLEAN": "🚿 **{mod.username}#{mod.discriminator}** (`{mod.id}`) cleaned **{count}** message(s) in **#{channel.name}**\n{archiveUrl}",

View file

@ -33,7 +33,7 @@ export enum LogType {
COMMAND, COMMAND,
SPAM_DETECTED, MESSAGE_SPAM_DETECTED,
CENSOR, CENSOR,
CLEAN, CLEAN,
@ -44,5 +44,6 @@ export enum LogType {
MEMBER_TIMED_MUTE, MEMBER_TIMED_MUTE,
MEMBER_TIMED_UNMUTE, MEMBER_TIMED_UNMUTE,
MEMBER_JOIN_WITH_PRIOR_RECORDS MEMBER_JOIN_WITH_PRIOR_RECORDS,
OTHER_SPAM_DETECTED,
} }

View file

@ -1,5 +1,5 @@
import { Plugin } from "knub"; import { decorators as d, Plugin } from "knub";
import { Channel, User } from "eris"; import { Channel, Member, User } from "eris";
import { import {
getEmojiInString, getEmojiInString,
getRoleMentions, getRoleMentions,
@ -28,13 +28,14 @@ enum RecentActionType {
Newline, Newline,
Censor, Censor,
Character, Character,
VoiceChannelMove,
} }
interface IRecentAction { interface IRecentAction<T> {
type: RecentActionType; type: RecentActionType;
userId: string; userId: string;
channelId: string; actionGroupId: string;
savedMessage: SavedMessage; extraData: T;
timestamp: number; timestamp: number;
count: number; count: number;
} }
@ -58,7 +59,7 @@ export class SpamPlugin extends Plugin {
protected spamDetectionQueue: Promise<void>; protected spamDetectionQueue: Promise<void>;
// List of recent potentially-spammy actions // List of recent potentially-spammy actions
protected recentActions: IRecentAction[]; protected recentActions: Array<IRecentAction<any>>;
// A map of userId => channelId => msgId // A map of userId => channelId => msgId
// Keeps track of the last handled (= spam detected and acted on) message ID per user, per channel // Keeps track of the last handled (= spam detected and acted on) message ID per user, per channel
@ -124,37 +125,37 @@ export class SpamPlugin extends Plugin {
addRecentAction( addRecentAction(
type: RecentActionType, type: RecentActionType,
userId: string, userId: string,
channelId: string, actionGroupId: string,
savedMessage: SavedMessage, extraData: any,
timestamp: number, timestamp: number,
count = 1, count = 1,
) { ) {
this.recentActions.push({ type, userId, channelId, savedMessage, timestamp, count }); this.recentActions.push({ type, userId, actionGroupId, extraData, timestamp, count });
} }
getRecentActions(type: RecentActionType, userId: string, channelId: string, since: number) { getRecentActions(type: RecentActionType, userId: string, actionGroupId: string, since: number) {
return this.recentActions.filter(action => { return this.recentActions.filter(action => {
if (action.timestamp < since) return false; if (action.timestamp < since) return false;
if (action.type !== type) return false; if (action.type !== type) return false;
if (action.channelId !== channelId) return false; if (action.actionGroupId !== actionGroupId) return false;
if (action.userId !== userId) return false; if (action.userId !== userId) return false;
return true; return true;
}); });
} }
getRecentActionCount(type: RecentActionType, userId: string, channelId: string, since: number) { getRecentActionCount(type: RecentActionType, userId: string, actionGroupId: string, since: number) {
return this.recentActions.reduce((count, action) => { return this.recentActions.reduce((count, action) => {
if (action.timestamp < since) return count; if (action.timestamp < since) return count;
if (action.type !== type) return count; if (action.type !== type) return count;
if (action.channelId !== channelId) return count; if (action.actionGroupId !== actionGroupId) return count;
if (action.userId !== userId) return false; if (action.userId !== userId) return false;
return count + action.count; return count + action.count;
}, 0); }, 0);
} }
clearRecentUserActions(type: RecentActionType, userId: string, channelId: string) { clearRecentUserActions(type: RecentActionType, userId: string, actionGroupId: string) {
this.recentActions = this.recentActions.filter(action => { this.recentActions = this.recentActions.filter(action => {
return action.type !== type || action.userId !== userId || action.channelId !== channelId; return action.type !== type || action.userId !== userId || action.actionGroupId !== actionGroupId;
}); });
} }
@ -172,7 +173,7 @@ export class SpamPlugin extends Plugin {
return this.archives.getUrl(baseUrl, archiveId); return this.archives.getUrl(baseUrl, archiveId);
} }
async logAndDetectSpam( async logAndDetectMessageSpam(
savedMessage: SavedMessage, savedMessage: SavedMessage,
type: RecentActionType, type: RecentActionType,
spamConfig: any, spamConfig: any,
@ -220,7 +221,7 @@ export class SpamPlugin extends Plugin {
// Get the offending message IDs // Get the offending message IDs
// We also get the IDs of any messages after the last offending message, to account for lag before detection // We also get the IDs of any messages after the last offending message, to account for lag before detection
const savedMessages = recentActions.map(a => a.savedMessage); const savedMessages = recentActions.map(a => a.extraData as SavedMessage);
const msgIds = savedMessages.map(m => m.id); const msgIds = savedMessages.map(m => m.id);
const lastDetectedMsgId = msgIds[msgIds.length - 1]; const lastDetectedMsgId = msgIds[msgIds.length - 1];
@ -265,7 +266,7 @@ export class SpamPlugin extends Plugin {
${archiveUrl} ${archiveUrl}
`); `);
this.logs.log(LogType.SPAM_DETECTED, { this.logs.log(LogType.MESSAGE_SPAM_DETECTED, {
member: stripObjectToScalars(member, ["user"]), member: stripObjectToScalars(member, ["user"]),
channel: stripObjectToScalars(channel), channel: stripObjectToScalars(channel),
description, description,
@ -295,6 +296,66 @@ export class SpamPlugin extends Plugin {
); );
} }
async logAndDetectOtherSpam(
type: RecentActionType,
spamConfig: any,
userId: string,
actionCount: number,
actionGroupId: string,
timestamp: number,
extraData = null,
description: string,
) {
this.spamDetectionQueue = this.spamDetectionQueue.then(async () => {
// Log this action...
this.addRecentAction(type, userId, actionGroupId, extraData, timestamp, actionCount);
// ...and then check if it trips the spam filters
const since = timestamp - 1000 * spamConfig.interval;
const recentActionsCount = this.getRecentActionCount(type, userId, actionGroupId, since);
if (recentActionsCount > spamConfig.count) {
const member = this.guild.members.get(userId);
// Start by muting them, if enabled
if (spamConfig.mute && member) {
const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000;
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, userId);
this.actions.fire("mute", { member, muteTime, reason: "Automatic spam detection" });
}
// Clear recent cases
this.clearRecentUserActions(RecentActionType.VoiceChannelMove, userId, actionGroupId);
// Create a case and log the actions taken above
const caseType = spamConfig.mute ? CaseTypes.Mute : CaseTypes.Note;
const caseText = trimLines(`
Automatic spam detection: ${description} (over ${spamConfig.count} in ${spamConfig.interval}s)
`);
this.logs.log(LogType.OTHER_SPAM_DETECTED, {
member: stripObjectToScalars(member, ["user"]),
description,
limit: spamConfig.count,
interval: spamConfig.interval,
});
const theCase: Case = await this.actions.fire("createCase", {
userId,
modId: this.bot.user.id,
type: caseType,
reason: caseText,
automatic: true,
});
// For mutes, also set the mute's case id (for !mutes)
if (spamConfig.mute && member) {
await this.mutes.setCaseId(userId, theCase.id);
}
}
});
}
// For interoperability with the Censor plugin // For interoperability with the Censor plugin
async logCensor(savedMessage: SavedMessage) { async logCensor(savedMessage: SavedMessage) {
const spamConfig = this.configValueForMemberIdAndChannelId( const spamConfig = this.configValueForMemberIdAndChannelId(
@ -303,7 +364,7 @@ export class SpamPlugin extends Plugin {
"max_censor", "max_censor",
); );
if (spamConfig) { if (spamConfig) {
this.logAndDetectSpam(savedMessage, RecentActionType.Censor, spamConfig, 1, "too many censored messages"); this.logAndDetectMessageSpam(savedMessage, RecentActionType.Censor, spamConfig, 1, "too many censored messages");
} }
} }
@ -316,7 +377,7 @@ export class SpamPlugin extends Plugin {
"max_messages", "max_messages",
); );
if (maxMessages) { if (maxMessages) {
this.logAndDetectSpam(savedMessage, RecentActionType.Message, maxMessages, 1, "too many messages"); this.logAndDetectMessageSpam(savedMessage, RecentActionType.Message, maxMessages, 1, "too many messages");
} }
const maxMentions = this.configValueForMemberIdAndChannelId( const maxMentions = this.configValueForMemberIdAndChannelId(
@ -328,7 +389,13 @@ export class SpamPlugin extends Plugin {
? [...getUserMentions(savedMessage.data.content), ...getRoleMentions(savedMessage.data.content)] ? [...getUserMentions(savedMessage.data.content), ...getRoleMentions(savedMessage.data.content)]
: []; : [];
if (maxMentions && mentions.length) { if (maxMentions && mentions.length) {
this.logAndDetectSpam(savedMessage, RecentActionType.Mention, maxMentions, mentions.length, "too many mentions"); this.logAndDetectMessageSpam(
savedMessage,
RecentActionType.Mention,
maxMentions,
mentions.length,
"too many mentions",
);
} }
const maxLinks = this.configValueForMemberIdAndChannelId( const maxLinks = this.configValueForMemberIdAndChannelId(
@ -338,7 +405,7 @@ export class SpamPlugin extends Plugin {
); );
if (maxLinks && savedMessage.data.content) { if (maxLinks && savedMessage.data.content) {
const links = getUrlsInString(savedMessage.data.content); const links = getUrlsInString(savedMessage.data.content);
this.logAndDetectSpam(savedMessage, RecentActionType.Link, maxLinks, links.length, "too many links"); this.logAndDetectMessageSpam(savedMessage, RecentActionType.Link, maxLinks, links.length, "too many links");
} }
const maxAttachments = this.configValueForMemberIdAndChannelId( const maxAttachments = this.configValueForMemberIdAndChannelId(
@ -347,7 +414,7 @@ export class SpamPlugin extends Plugin {
"max_attachments", "max_attachments",
); );
if (maxAttachments && savedMessage.data.attachments) { if (maxAttachments && savedMessage.data.attachments) {
this.logAndDetectSpam( this.logAndDetectMessageSpam(
savedMessage, savedMessage,
RecentActionType.Attachment, RecentActionType.Attachment,
maxAttachments, maxAttachments,
@ -363,7 +430,7 @@ export class SpamPlugin extends Plugin {
); );
if (maxEmoji && savedMessage.data.content) { if (maxEmoji && savedMessage.data.content) {
const emojiCount = getEmojiInString(savedMessage.data.content).length; const emojiCount = getEmojiInString(savedMessage.data.content).length;
this.logAndDetectSpam(savedMessage, RecentActionType.Emoji, maxEmoji, emojiCount, "too many emoji"); this.logAndDetectMessageSpam(savedMessage, RecentActionType.Emoji, maxEmoji, emojiCount, "too many emoji");
} }
const maxNewlines = this.configValueForMemberIdAndChannelId( const maxNewlines = this.configValueForMemberIdAndChannelId(
@ -373,7 +440,13 @@ export class SpamPlugin extends Plugin {
); );
if (maxNewlines && savedMessage.data.content) { if (maxNewlines && savedMessage.data.content) {
const newlineCount = (savedMessage.data.content.match(/\n/g) || []).length; const newlineCount = (savedMessage.data.content.match(/\n/g) || []).length;
this.logAndDetectSpam(savedMessage, RecentActionType.Newline, maxNewlines, newlineCount, "too many newlines"); this.logAndDetectMessageSpam(
savedMessage,
RecentActionType.Newline,
maxNewlines,
newlineCount,
"too many newlines",
);
} }
const maxCharacters = this.configValueForMemberIdAndChannelId( const maxCharacters = this.configValueForMemberIdAndChannelId(
@ -383,7 +456,7 @@ export class SpamPlugin extends Plugin {
); );
if (maxCharacters && savedMessage.data.content) { if (maxCharacters && savedMessage.data.content) {
const characterCount = [...savedMessage.data.content.trim()].length; const characterCount = [...savedMessage.data.content.trim()].length;
this.logAndDetectSpam( this.logAndDetectMessageSpam(
savedMessage, savedMessage,
RecentActionType.Character, RecentActionType.Character,
maxCharacters, maxCharacters,
@ -394,4 +467,22 @@ export class SpamPlugin extends Plugin {
// TODO: Max duplicates // TODO: Max duplicates
} }
@d.event("voiceChannelJoin")
@d.event("voiceChannelSwitch")
onVoiceChannelSwitch(member: Member, channel: Channel) {
const spamConfig = this.configValueForMemberIdAndChannelId(member.id, channel.id, "max_voice_move");
if (spamConfig) {
this.logAndDetectOtherSpam(
RecentActionType.VoiceChannelMove,
spamConfig,
member.id,
1,
"0",
Date.now(),
null,
"too many voice channel moves",
);
}
}
} }