2018-07-31 02:42:45 +03:00
|
|
|
import { decorators as d, Plugin } from "knub";
|
|
|
|
import { Message, TextChannel } from "eris";
|
|
|
|
import {
|
|
|
|
getEmojiInString,
|
2018-08-01 19:13:32 +03:00
|
|
|
getRoleMentions,
|
2018-07-31 02:42:45 +03:00
|
|
|
getUrlsInString,
|
2018-08-01 19:13:32 +03:00
|
|
|
getUserMentions,
|
2018-08-01 20:21:55 +03:00
|
|
|
stripObjectToScalars,
|
|
|
|
trimLines
|
2018-07-31 02:42:45 +03:00
|
|
|
} from "../utils";
|
|
|
|
import { LogType } from "../data/LogType";
|
|
|
|
import { GuildLogs } from "../data/GuildLogs";
|
|
|
|
import { ModActionsPlugin } from "./ModActions";
|
|
|
|
import { CaseType } from "../data/CaseType";
|
2018-08-01 20:09:51 +03:00
|
|
|
import { GuildSpamLogs } from "../data/GuildSpamLogs";
|
2018-07-31 02:42:45 +03:00
|
|
|
|
|
|
|
enum RecentActionType {
|
|
|
|
Message = 1,
|
|
|
|
Mention,
|
|
|
|
Link,
|
|
|
|
Attachment,
|
|
|
|
Emoji,
|
|
|
|
Newline
|
|
|
|
}
|
|
|
|
|
|
|
|
interface IRecentAction {
|
|
|
|
type: RecentActionType;
|
|
|
|
userId: string;
|
|
|
|
channelId: string;
|
2018-08-01 19:13:32 +03:00
|
|
|
msg: Message;
|
2018-07-31 02:42:45 +03:00
|
|
|
timestamp: number;
|
|
|
|
count: number;
|
|
|
|
}
|
|
|
|
|
2018-08-01 19:13:32 +03:00
|
|
|
const MAX_INTERVAL = 300;
|
|
|
|
|
2018-07-31 02:42:45 +03:00
|
|
|
export class SpamPlugin extends Plugin {
|
|
|
|
protected logs: GuildLogs;
|
2018-08-01 20:09:51 +03:00
|
|
|
protected spamLogs: GuildSpamLogs;
|
2018-07-31 02:42:45 +03:00
|
|
|
|
|
|
|
protected recentActions: IRecentAction[];
|
|
|
|
|
|
|
|
private expiryInterval;
|
|
|
|
|
|
|
|
getDefaultOptions() {
|
|
|
|
return {
|
|
|
|
config: {
|
|
|
|
max_messages: null,
|
|
|
|
max_mentions: null,
|
|
|
|
max_links: null,
|
|
|
|
max_attachments: null,
|
|
|
|
max_emojis: null,
|
|
|
|
max_newlines: null,
|
|
|
|
max_duplicates: null
|
2018-08-02 22:22:51 +03:00
|
|
|
},
|
|
|
|
overrides: [
|
|
|
|
{
|
|
|
|
level: ">=50",
|
|
|
|
config: {
|
|
|
|
max_messages: null,
|
|
|
|
max_mentions: null,
|
|
|
|
max_links: null,
|
|
|
|
max_attachments: null,
|
|
|
|
max_emojis: null,
|
|
|
|
max_newlines: null,
|
|
|
|
max_duplicates: null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
2018-07-31 02:42:45 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
onLoad() {
|
|
|
|
this.logs = new GuildLogs(this.guildId);
|
2018-08-01 20:09:51 +03:00
|
|
|
this.spamLogs = new GuildSpamLogs(this.guildId);
|
2018-07-31 02:42:45 +03:00
|
|
|
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
|
|
|
|
this.recentActions = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
onUnload() {
|
|
|
|
clearInterval(this.expiryInterval);
|
|
|
|
}
|
|
|
|
|
|
|
|
addRecentAction(
|
|
|
|
type: RecentActionType,
|
|
|
|
userId: string,
|
|
|
|
channelId: string,
|
2018-08-01 19:13:32 +03:00
|
|
|
msg: Message,
|
2018-07-31 02:42:45 +03:00
|
|
|
timestamp: number,
|
|
|
|
count = 1
|
|
|
|
) {
|
|
|
|
this.recentActions.push({
|
|
|
|
type,
|
|
|
|
userId,
|
|
|
|
channelId,
|
2018-08-01 19:13:32 +03:00
|
|
|
msg,
|
2018-07-31 02:42:45 +03:00
|
|
|
timestamp,
|
|
|
|
count
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-08-01 19:13:32 +03:00
|
|
|
getRecentActions(type: RecentActionType, userId: string, channelId: string, since: number) {
|
|
|
|
return this.recentActions.filter(action => {
|
|
|
|
if (action.timestamp < since) return false;
|
|
|
|
if (action.type !== type) return false;
|
|
|
|
if (action.channelId !== channelId) return false;
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-31 02:42:45 +03:00
|
|
|
getRecentActionCount(type: RecentActionType, userId: string, channelId: string, since: number) {
|
|
|
|
return this.recentActions.reduce((count, action) => {
|
|
|
|
if (action.timestamp < since) return count;
|
|
|
|
if (action.type !== type) return count;
|
|
|
|
if (action.channelId !== channelId) return count;
|
|
|
|
return count + action.count;
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearRecentUserActions(type: RecentActionType, userId: string, channelId: string) {
|
|
|
|
this.recentActions = this.recentActions.filter(action => {
|
|
|
|
return action.type !== type || action.userId !== userId || action.channelId !== channelId;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
clearOldRecentActions() {
|
|
|
|
// TODO: Figure out expiry time from longest interval in the config?
|
2018-08-01 19:13:32 +03:00
|
|
|
const expiryTimestamp = Date.now() - 1000 * MAX_INTERVAL;
|
2018-07-31 02:42:45 +03:00
|
|
|
this.recentActions = this.recentActions.filter(action => action.timestamp >= expiryTimestamp);
|
|
|
|
}
|
|
|
|
|
2018-08-01 20:09:51 +03:00
|
|
|
async saveSpamLogs(messages: Message[]) {
|
|
|
|
const channel = messages[0].channel as TextChannel;
|
|
|
|
const header = `Server: ${this.guild.name} (${this.guild.id}), channel: #${channel.name} (${
|
|
|
|
channel.id
|
|
|
|
})`;
|
|
|
|
const logId = await this.spamLogs.createFromMessages(messages, header);
|
|
|
|
|
|
|
|
const url = this.knub.getGlobalConfig().url;
|
|
|
|
return url ? `${url}/spam-logs/${logId}` : `Log ID: ${logId}`;
|
|
|
|
}
|
|
|
|
|
2018-07-31 02:42:45 +03:00
|
|
|
async detectSpam(
|
|
|
|
msg: Message,
|
|
|
|
type: RecentActionType,
|
|
|
|
spamConfig: any,
|
|
|
|
actionCount: number,
|
|
|
|
description: string
|
|
|
|
) {
|
|
|
|
if (actionCount === 0) return;
|
|
|
|
|
2018-08-01 19:13:32 +03:00
|
|
|
const since = msg.timestamp - 1000 * spamConfig.interval;
|
|
|
|
|
|
|
|
this.addRecentAction(type, msg.author.id, msg.channel.id, msg, msg.timestamp, actionCount);
|
|
|
|
const recentActionsCount = this.getRecentActionCount(
|
2018-07-31 02:42:45 +03:00
|
|
|
type,
|
|
|
|
msg.author.id,
|
|
|
|
msg.channel.id,
|
2018-08-01 19:13:32 +03:00
|
|
|
since
|
2018-07-31 02:42:45 +03:00
|
|
|
);
|
|
|
|
|
2018-08-01 19:13:32 +03:00
|
|
|
if (recentActionsCount > spamConfig.count) {
|
2018-08-01 20:21:55 +03:00
|
|
|
const recentActions = this.getRecentActions(type, msg.author.id, msg.channel.id, since);
|
|
|
|
const logUrl = await this.saveSpamLogs(recentActions.map(a => a.msg));
|
|
|
|
|
2018-07-31 02:42:45 +03:00
|
|
|
if (spamConfig.clean !== false) {
|
2018-08-01 19:13:32 +03:00
|
|
|
const msgIds = recentActions.map(a => a.msg.id);
|
|
|
|
await this.bot.deleteMessages(msg.channel.id, msgIds);
|
|
|
|
|
2018-07-31 02:42:45 +03:00
|
|
|
this.logs.log(LogType.SPAM_DELETE, {
|
|
|
|
member: stripObjectToScalars(msg.member, ["user"]),
|
|
|
|
channel: stripObjectToScalars(msg.channel),
|
|
|
|
description,
|
|
|
|
limit: spamConfig.count,
|
2018-08-01 20:09:51 +03:00
|
|
|
interval: spamConfig.interval,
|
|
|
|
logUrl
|
2018-07-31 02:42:45 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (spamConfig.mute) {
|
2018-08-01 19:13:32 +03:00
|
|
|
// For muting the user, we use the ModActions plugin
|
|
|
|
// This means that spam mute functionality requires the ModActions plugin to be loaded
|
2018-07-31 02:42:45 +03:00
|
|
|
const guildData = this.knub.getGuildData(this.guildId);
|
|
|
|
const modActionsPlugin = guildData.loadedPlugins.get("mod_actions") as ModActionsPlugin;
|
|
|
|
if (!modActionsPlugin) return;
|
|
|
|
|
|
|
|
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, msg.member.id);
|
|
|
|
await modActionsPlugin.muteMember(
|
|
|
|
msg.member,
|
2018-08-02 17:53:50 +03:00
|
|
|
spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000,
|
2018-07-31 02:42:45 +03:00
|
|
|
"Automatic spam detection"
|
|
|
|
);
|
|
|
|
await modActionsPlugin.createCase(
|
|
|
|
msg.member.id,
|
|
|
|
this.bot.user.id,
|
|
|
|
CaseType.Mute,
|
|
|
|
null,
|
2018-08-01 20:21:55 +03:00
|
|
|
trimLines(`
|
|
|
|
Automatic spam detection: ${description} (over ${spamConfig.count} in ${
|
|
|
|
spamConfig.interval
|
|
|
|
}s)
|
|
|
|
${logUrl}
|
|
|
|
`),
|
2018-07-31 02:42:45 +03:00
|
|
|
true
|
|
|
|
);
|
|
|
|
this.logs.log(LogType.MEMBER_MUTE_SPAM, {
|
|
|
|
member: stripObjectToScalars(msg.member, ["user"]),
|
|
|
|
channel: stripObjectToScalars(msg.channel),
|
|
|
|
description,
|
|
|
|
limit: spamConfig.count,
|
|
|
|
interval: spamConfig.interval
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@d.event("messageCreate")
|
|
|
|
onMessageCreate(msg: Message) {
|
|
|
|
if (msg.author.bot) return;
|
|
|
|
|
|
|
|
const maxMessages = this.configValueForMsg(msg, "max_messages");
|
|
|
|
if (maxMessages) {
|
|
|
|
this.detectSpam(msg, RecentActionType.Message, maxMessages, 1, "too many messages");
|
|
|
|
}
|
|
|
|
|
|
|
|
const maxMentions = this.configValueForMsg(msg, "max_mentions");
|
2018-08-01 19:13:32 +03:00
|
|
|
const mentions = msg.content
|
|
|
|
? [...getUserMentions(msg.content), ...getRoleMentions(msg.content)]
|
|
|
|
: [];
|
|
|
|
if (maxMentions && mentions.length) {
|
2018-07-31 02:42:45 +03:00
|
|
|
this.detectSpam(
|
|
|
|
msg,
|
|
|
|
RecentActionType.Mention,
|
|
|
|
maxMentions,
|
2018-08-01 19:13:32 +03:00
|
|
|
mentions.length,
|
2018-07-31 02:42:45 +03:00
|
|
|
"too many mentions"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const maxLinks = this.configValueForMsg(msg, "max_links");
|
|
|
|
if (maxLinks && msg.content) {
|
|
|
|
const links = getUrlsInString(msg.content);
|
|
|
|
this.detectSpam(msg, RecentActionType.Link, maxLinks, links.length, "too many links");
|
|
|
|
}
|
|
|
|
|
|
|
|
const maxAttachments = this.configValueForMsg(msg, "max_attachments");
|
|
|
|
if (maxAttachments && msg.attachments.length) {
|
|
|
|
this.detectSpam(
|
|
|
|
msg,
|
|
|
|
RecentActionType.Attachment,
|
|
|
|
maxAttachments,
|
|
|
|
msg.attachments.length,
|
|
|
|
"too many attachments"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const maxEmoji = this.configValueForMsg(msg, "max_emoji");
|
|
|
|
if (maxEmoji && msg.content) {
|
|
|
|
const emojiCount = getEmojiInString(msg.content).length;
|
|
|
|
this.detectSpam(msg, RecentActionType.Emoji, maxEmoji, emojiCount, "too many emoji");
|
|
|
|
}
|
|
|
|
|
|
|
|
const maxNewlines = this.configValueForMsg(msg, "max_newlines");
|
|
|
|
if (maxNewlines && msg.content) {
|
|
|
|
const newlineCount = (msg.content.match(/\n/g) || []).length;
|
|
|
|
this.detectSpam(
|
|
|
|
msg,
|
|
|
|
RecentActionType.Newline,
|
|
|
|
maxNewlines,
|
|
|
|
newlineCount,
|
|
|
|
"too many newlines"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Max duplicates
|
|
|
|
}
|
|
|
|
}
|