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:
parent
4f5eb0689d
commit
30db4d58b4
3 changed files with 122 additions and 29 deletions
|
@ -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}",
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue