Port Censor and Spam plugins to use GuildSavedMessages events
This commit is contained in:
parent
fbb1ee4719
commit
1a6e680d81
4 changed files with 237 additions and 114 deletions
|
@ -32,7 +32,7 @@ export class QueuedEventEmitter {
|
||||||
const listeners = [...(this.listeners.get(eventName) || []), ...(this.listeners.get("*") || [])];
|
const listeners = [...(this.listeners.get(eventName) || []), ...(this.listeners.get("*") || [])];
|
||||||
|
|
||||||
listeners.forEach(listener => {
|
listeners.forEach(listener => {
|
||||||
this.queue.add(listener.bind(null, args));
|
this.queue.add(listener.bind(null, ...args));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,16 @@ export class GuildSavedMessages extends BaseRepository {
|
||||||
.getOne();
|
.getOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserMessagesByChannelAfterId(userId, channelId, afterId) {
|
||||||
|
return this.messages
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||||
|
.where("user_id = :user_id", { user_id: userId })
|
||||||
|
.where("channel_id = :channel_id", { channel_id: channelId })
|
||||||
|
.where("id > :afterId", { afterId })
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
async create(data) {
|
async create(data) {
|
||||||
try {
|
try {
|
||||||
await this.messages.insert(data);
|
await this.messages.insert(data);
|
||||||
|
@ -128,7 +138,7 @@ export class GuildSavedMessages extends BaseRepository {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.events.emit("edit", [newMessage, oldMessage]);
|
this.events.emit("update", [newMessage, oldMessage]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveEditFromMsg(msg: Message) {
|
async saveEditFromMsg(msg: Message) {
|
||||||
|
|
|
@ -5,9 +5,15 @@ import { GuildLogs } from "../data/GuildLogs";
|
||||||
import { LogType } from "../data/LogType";
|
import { LogType } from "../data/LogType";
|
||||||
import { getInviteCodesInString, getUrlsInString, stripObjectToScalars } from "../utils";
|
import { getInviteCodesInString, getUrlsInString, stripObjectToScalars } from "../utils";
|
||||||
import { ZalgoRegex } from "../data/Zalgo";
|
import { ZalgoRegex } from "../data/Zalgo";
|
||||||
|
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
||||||
|
import { SavedMessage } from "../data/entities/SavedMessage";
|
||||||
|
|
||||||
export class CensorPlugin extends Plugin {
|
export class CensorPlugin extends Plugin {
|
||||||
protected serverLogs: GuildLogs;
|
protected serverLogs: GuildLogs;
|
||||||
|
protected savedMessages: GuildSavedMessages;
|
||||||
|
|
||||||
|
private onMessageCreateFn;
|
||||||
|
private onMessageUpdateFn;
|
||||||
|
|
||||||
getDefaultOptions() {
|
getDefaultOptions() {
|
||||||
return {
|
return {
|
||||||
|
@ -46,58 +52,94 @@ export class CensorPlugin extends Plugin {
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.serverLogs = new GuildLogs(this.guildId);
|
this.serverLogs = new GuildLogs(this.guildId);
|
||||||
|
this.savedMessages = GuildSavedMessages.getInstance(this.guildId);
|
||||||
|
|
||||||
|
this.onMessageCreateFn = this.onMessageCreate.bind(this);
|
||||||
|
this.onMessageUpdateFn = this.onMessageUpdate.bind(this);
|
||||||
|
this.savedMessages.events.on("create", this.onMessageCreateFn);
|
||||||
|
this.savedMessages.events.on("update", this.onMessageUpdateFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
async censorMessage(msg: Message, reason: string) {
|
onUnload() {
|
||||||
this.serverLogs.ignoreLog(LogType.MESSAGE_DELETE, msg.id);
|
this.savedMessages.events.off("create", this.onMessageCreateFn);
|
||||||
|
this.savedMessages.events.off("update", this.onMessageUpdateFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async censorMessage(savedMessage: SavedMessage, reason: string) {
|
||||||
|
this.serverLogs.ignoreLog(LogType.MESSAGE_DELETE, savedMessage.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await msg.delete("Censored");
|
await this.bot.deleteMessage(savedMessage.channel_id, savedMessage.id, "Censored");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const member = this.guild.members.get(savedMessage.user_id);
|
||||||
|
const channel = this.guild.channels.get(savedMessage.channel_id);
|
||||||
|
|
||||||
this.serverLogs.log(LogType.CENSOR, {
|
this.serverLogs.log(LogType.CENSOR, {
|
||||||
member: stripObjectToScalars(msg.member, ["user"]),
|
member: stripObjectToScalars(member, ["user"]),
|
||||||
channel: stripObjectToScalars(msg.channel),
|
channel: stripObjectToScalars(channel),
|
||||||
reason,
|
reason,
|
||||||
messageText: msg.cleanContent
|
messageText: savedMessage.data.content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyFiltersToMsg(msg: Message) {
|
async applyFiltersToMsg(savedMessage: SavedMessage) {
|
||||||
if (!msg.author || msg.author.bot) return;
|
if (!savedMessage.data.content) return;
|
||||||
if (msg.type !== 0) return;
|
|
||||||
if (!msg.content) return;
|
|
||||||
|
|
||||||
// Filter zalgo
|
// Filter zalgo
|
||||||
if (this.configValueForMsg(msg, "filter_zalgo")) {
|
const filterZalgo = this.configValueForMemberIdAndChannelId(
|
||||||
const result = ZalgoRegex.exec(msg.content);
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"filter_zalgo"
|
||||||
|
);
|
||||||
|
if (filterZalgo) {
|
||||||
|
const result = ZalgoRegex.exec(savedMessage.data.content);
|
||||||
if (result) {
|
if (result) {
|
||||||
this.censorMessage(msg, `zalgo detected`);
|
this.censorMessage(savedMessage, "zalgo detected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter invites
|
// Filter invites
|
||||||
if (this.configValueForMsg(msg, "filter_invites")) {
|
const filterInvites = this.configValueForMemberIdAndChannelId(
|
||||||
const inviteGuildWhitelist: string[] = this.configValueForMsg(msg, "invite_guild_whitelist");
|
savedMessage.user_id,
|
||||||
const inviteGuildBlacklist: string[] = this.configValueForMsg(msg, "invite_guild_blacklist");
|
savedMessage.channel_id,
|
||||||
const inviteCodeWhitelist: string[] = this.configValueForMsg(msg, "invite_code_whitelist");
|
"filter_invites"
|
||||||
const inviteCodeBlacklist: string[] = this.configValueForMsg(msg, "invite_code_blacklist");
|
);
|
||||||
|
if (filterInvites) {
|
||||||
const inviteCodes = getInviteCodesInString(msg.content);
|
const inviteGuildWhitelist: string[] = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
let invites: Invite[] = await Promise.all(
|
savedMessage.channel_id,
|
||||||
inviteCodes.map(code => this.bot.getInvite(code).catch(() => null))
|
"invite_guild_whitelist"
|
||||||
);
|
);
|
||||||
|
const inviteGuildBlacklist: string[] = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"invite_guild_blacklist"
|
||||||
|
);
|
||||||
|
const inviteCodeWhitelist: string[] = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"invite_code_whitelist"
|
||||||
|
);
|
||||||
|
const inviteCodeBlacklist: string[] = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"invite_code_blacklist"
|
||||||
|
);
|
||||||
|
|
||||||
|
const inviteCodes = getInviteCodesInString(savedMessage.data.content);
|
||||||
|
|
||||||
|
let invites: Invite[] = await Promise.all(inviteCodes.map(code => this.bot.getInvite(code).catch(() => null)));
|
||||||
|
|
||||||
invites = invites.filter(v => !!v);
|
invites = invites.filter(v => !!v);
|
||||||
|
|
||||||
for (const invite of invites) {
|
for (const invite of invites) {
|
||||||
if (inviteGuildWhitelist && !inviteGuildWhitelist.includes(invite.guild.id)) {
|
if (inviteGuildWhitelist && !inviteGuildWhitelist.includes(invite.guild.id)) {
|
||||||
this.censorMessage(
|
this.censorMessage(
|
||||||
msg,
|
savedMessage,
|
||||||
`invite guild (**${invite.guild.name}** \`${invite.guild.id}\`) not found in whitelist`
|
`invite guild (**${invite.guild.name}** \`${invite.guild.id}\`) not found in whitelist`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
@ -105,80 +147,94 @@ export class CensorPlugin extends Plugin {
|
||||||
|
|
||||||
if (inviteGuildBlacklist && inviteGuildBlacklist.includes(invite.guild.id)) {
|
if (inviteGuildBlacklist && inviteGuildBlacklist.includes(invite.guild.id)) {
|
||||||
this.censorMessage(
|
this.censorMessage(
|
||||||
msg,
|
savedMessage,
|
||||||
`invite guild (**${invite.guild.name}** \`${invite.guild.id}\`) found in blacklist`
|
`invite guild (**${invite.guild.name}** \`${invite.guild.id}\`) found in blacklist`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inviteCodeWhitelist && !inviteCodeWhitelist.includes(invite.code)) {
|
if (inviteCodeWhitelist && !inviteCodeWhitelist.includes(invite.code)) {
|
||||||
this.censorMessage(msg, `invite code (\`${invite.code}\`) not found in whitelist`);
|
this.censorMessage(savedMessage, `invite code (\`${invite.code}\`) not found in whitelist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inviteCodeBlacklist && inviteCodeBlacklist.includes(invite.code)) {
|
if (inviteCodeBlacklist && inviteCodeBlacklist.includes(invite.code)) {
|
||||||
this.censorMessage(msg, `invite code (\`${invite.code}\`) found in blacklist`);
|
this.censorMessage(savedMessage, `invite code (\`${invite.code}\`) found in blacklist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter domains
|
// Filter domains
|
||||||
if (this.configValueForMsg(msg, "filter_domains")) {
|
const filterDomains = this.configValueForMemberIdAndChannelId(
|
||||||
const domainWhitelist: string[] = this.configValueForMsg(msg, "domain_whitelist");
|
savedMessage.user_id,
|
||||||
const domainBlacklist: string[] = this.configValueForMsg(msg, "domain_blacklist");
|
savedMessage.channel_id,
|
||||||
|
"filter_domains"
|
||||||
|
);
|
||||||
|
if (filterDomains) {
|
||||||
|
const domainWhitelist: string[] = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"domain_whitelist"
|
||||||
|
);
|
||||||
|
const domainBlacklist: string[] = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"domain_blacklist"
|
||||||
|
);
|
||||||
|
|
||||||
const urls = getUrlsInString(msg.content);
|
const urls = getUrlsInString(savedMessage.data.content);
|
||||||
for (const thisUrl of urls) {
|
for (const thisUrl of urls) {
|
||||||
if (domainWhitelist && !domainWhitelist.includes(thisUrl.hostname)) {
|
if (domainWhitelist && !domainWhitelist.includes(thisUrl.hostname)) {
|
||||||
this.censorMessage(msg, `domain (\`${thisUrl.hostname}\`) not found in whitelist`);
|
this.censorMessage(savedMessage, `domain (\`${thisUrl.hostname}\`) not found in whitelist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domainBlacklist && domainBlacklist.includes(thisUrl.hostname)) {
|
if (domainBlacklist && domainBlacklist.includes(thisUrl.hostname)) {
|
||||||
this.censorMessage(msg, `domain (\`${thisUrl.hostname}\`) found in blacklist`);
|
this.censorMessage(savedMessage, `domain (\`${thisUrl.hostname}\`) found in blacklist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter tokens
|
// Filter tokens
|
||||||
const blockedTokens = this.configValueForMsg(msg, "blocked_tokens") || [];
|
const blockedTokens =
|
||||||
|
this.configValueForMemberIdAndChannelId(savedMessage.user_id, savedMessage.channel_id, "blocked_tokens") || [];
|
||||||
for (const token of blockedTokens) {
|
for (const token of blockedTokens) {
|
||||||
if (msg.content.toLowerCase().includes(token.toLowerCase())) {
|
if (savedMessage.data.content.toLowerCase().includes(token.toLowerCase())) {
|
||||||
this.censorMessage(msg, `blocked token (\`${token}\`) found`);
|
this.censorMessage(savedMessage, `blocked token (\`${token}\`) found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter words
|
// Filter words
|
||||||
const blockedWords = this.configValueForMsg(msg, "blocked_words") || [];
|
const blockedWords =
|
||||||
|
this.configValueForMemberIdAndChannelId(savedMessage.user_id, savedMessage.channel_id, "blocked_words") || [];
|
||||||
for (const word of blockedWords) {
|
for (const word of blockedWords) {
|
||||||
const regex = new RegExp(`\\b${escapeStringRegexp(word)}\\b`, "i");
|
const regex = new RegExp(`\\b${escapeStringRegexp(word)}\\b`, "i");
|
||||||
if (regex.test(msg.content)) {
|
if (regex.test(savedMessage.data.content)) {
|
||||||
this.censorMessage(msg, `blocked word (\`${word}\`) found`);
|
this.censorMessage(savedMessage, `blocked word (\`${word}\`) found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter regex
|
// Filter regex
|
||||||
const blockedRegex = this.configValueForMsg(msg, "blocked_regex") || [];
|
const blockedRegex =
|
||||||
|
this.configValueForMemberIdAndChannelId(savedMessage.user_id, savedMessage.channel_id, "blocked_regex") || [];
|
||||||
for (const regexStr of blockedRegex) {
|
for (const regexStr of blockedRegex) {
|
||||||
const regex = new RegExp(regexStr);
|
const regex = new RegExp(regexStr);
|
||||||
if (regex.test(msg.content)) {
|
if (regex.test(savedMessage.data.content)) {
|
||||||
this.censorMessage(msg, `blocked regex (\`${regexStr}\`) found`);
|
this.censorMessage(savedMessage, `blocked regex (\`${regexStr}\`) found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@d.event("messageCreate")
|
async onMessageCreate(savedMessage: SavedMessage) {
|
||||||
async onMessageCreate(msg: Message) {
|
this.applyFiltersToMsg(savedMessage);
|
||||||
this.applyFiltersToMsg(msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@d.event("messageUpdate")
|
async onMessageUpdate(savedMessage: SavedMessage) {
|
||||||
async onMessageUpdate(msg: Message) {
|
this.applyFiltersToMsg(savedMessage);
|
||||||
this.applyFiltersToMsg(msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { ModActionsPlugin } from "./ModActions";
|
||||||
import { CaseTypes } from "../data/CaseTypes";
|
import { CaseTypes } from "../data/CaseTypes";
|
||||||
import { GuildArchives } from "../data/GuildArchives";
|
import { GuildArchives } from "../data/GuildArchives";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { SavedMessage } from "../data/entities/SavedMessage";
|
||||||
|
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
||||||
|
|
||||||
enum RecentActionType {
|
enum RecentActionType {
|
||||||
Message = 1,
|
Message = 1,
|
||||||
|
@ -30,7 +32,7 @@ interface IRecentAction {
|
||||||
type: RecentActionType;
|
type: RecentActionType;
|
||||||
userId: string;
|
userId: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
msg: Message;
|
savedMessage: SavedMessage;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +45,7 @@ const ARCHIVE_HEADER_FORMAT = trimLines(`
|
||||||
Channel: #{channel.name} ({channel.id})
|
Channel: #{channel.name} ({channel.id})
|
||||||
User: {user.username}#{user.discriminator} ({user.id})
|
User: {user.username}#{user.discriminator} ({user.id})
|
||||||
`);
|
`);
|
||||||
const ARCHIVE_MESSAGE_FORMAT = "[MSG ID {message.id}] [{timestamp}] {user.username}: {message.content}{attachments}";
|
const ARCHIVE_MESSAGE_FORMAT = "[MSG ID {id}] [{timestamp}] {user.username}: {content}{attachments}";
|
||||||
const ARCHIVE_FOOTER_FORMAT = trimLines(`
|
const ARCHIVE_FOOTER_FORMAT = trimLines(`
|
||||||
Log file generated on {timestamp}
|
Log file generated on {timestamp}
|
||||||
Expires at {expires}
|
Expires at {expires}
|
||||||
|
@ -52,6 +54,9 @@ const ARCHIVE_FOOTER_FORMAT = trimLines(`
|
||||||
export class SpamPlugin extends Plugin {
|
export class SpamPlugin extends Plugin {
|
||||||
protected logs: GuildLogs;
|
protected logs: GuildLogs;
|
||||||
protected archives: GuildArchives;
|
protected archives: GuildArchives;
|
||||||
|
protected savedMessages: GuildSavedMessages;
|
||||||
|
|
||||||
|
private onMessageCreateFn;
|
||||||
|
|
||||||
// Handle spam detection with a queue so we don't have overlapping detections on the same user
|
// Handle spam detection with a queue so we don't have overlapping detections on the same user
|
||||||
protected spamDetectionQueue: Promise<void>;
|
protected spamDetectionQueue: Promise<void>;
|
||||||
|
@ -97,27 +102,32 @@ export class SpamPlugin extends Plugin {
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.logs = new GuildLogs(this.guildId);
|
this.logs = new GuildLogs(this.guildId);
|
||||||
this.archives = GuildArchives.getInstance(this.guildId);
|
this.archives = GuildArchives.getInstance(this.guildId);
|
||||||
|
this.savedMessages = GuildSavedMessages.getInstance(this.guildId);
|
||||||
|
|
||||||
this.recentActions = [];
|
this.recentActions = [];
|
||||||
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
|
this.expiryInterval = setInterval(() => this.clearOldRecentActions(), 1000 * 60);
|
||||||
this.lastHandledMsgIds = new Map();
|
this.lastHandledMsgIds = new Map();
|
||||||
|
|
||||||
this.spamDetectionQueue = Promise.resolve();
|
this.spamDetectionQueue = Promise.resolve();
|
||||||
|
|
||||||
|
this.onMessageCreateFn = this.onMessageCreate.bind(this);
|
||||||
|
this.savedMessages.events.on("create", this.onMessageCreateFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnload() {
|
onUnload() {
|
||||||
clearInterval(this.expiryInterval);
|
clearInterval(this.expiryInterval);
|
||||||
|
this.savedMessages.events.off("create", this.onMessageCreateFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
addRecentAction(
|
addRecentAction(
|
||||||
type: RecentActionType,
|
type: RecentActionType,
|
||||||
userId: string,
|
userId: string,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
msg: Message,
|
savedMessage: SavedMessage,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
count = 1
|
count = 1
|
||||||
) {
|
) {
|
||||||
this.recentActions.push({ type, userId, channelId, msg, timestamp, count });
|
this.recentActions.push({ type, userId, channelId, savedMessage, timestamp, count });
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecentActions(type: RecentActionType, userId: string, channelId: string, since: number) {
|
getRecentActions(type: RecentActionType, userId: string, channelId: string, since: number) {
|
||||||
|
@ -152,7 +162,7 @@ export class SpamPlugin extends Plugin {
|
||||||
this.recentActions = this.recentActions.filter(action => action.timestamp >= expiryTimestamp);
|
this.recentActions = this.recentActions.filter(action => action.timestamp >= expiryTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSpamArchives(messages: Message[], channel: Channel, user: User) {
|
async saveSpamArchives(savedMessages: SavedMessage[], channel: Channel, user: User) {
|
||||||
const expiresAt = moment().add(ARCHIVE_EXPIRY_DAYS, "days");
|
const expiresAt = moment().add(ARCHIVE_EXPIRY_DAYS, "days");
|
||||||
|
|
||||||
const headerStr = formatTemplateString(ARCHIVE_HEADER_FORMAT, {
|
const headerStr = formatTemplateString(ARCHIVE_HEADER_FORMAT, {
|
||||||
|
@ -160,10 +170,11 @@ export class SpamPlugin extends Plugin {
|
||||||
channel,
|
channel,
|
||||||
user
|
user
|
||||||
});
|
});
|
||||||
const msgLines = messages.map(msg => {
|
const msgLines = savedMessages.map(msg => {
|
||||||
return formatTemplateString(ARCHIVE_MESSAGE_FORMAT, {
|
return formatTemplateString(ARCHIVE_MESSAGE_FORMAT, {
|
||||||
message: msg,
|
id: msg.id,
|
||||||
timestamp: moment(msg.timestamp, "x").format("HH:mm:ss"),
|
timestamp: moment(msg.posted_at).format("HH:mm:ss"),
|
||||||
|
content: msg.data.content,
|
||||||
user
|
user
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -180,7 +191,7 @@ export class SpamPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
async logAndDetectSpam(
|
async logAndDetectSpam(
|
||||||
msg: Message,
|
savedMessage: SavedMessage,
|
||||||
type: RecentActionType,
|
type: RecentActionType,
|
||||||
spamConfig: any,
|
spamConfig: any,
|
||||||
actionCount: number,
|
actionCount: number,
|
||||||
|
@ -189,26 +200,34 @@ export class SpamPlugin extends Plugin {
|
||||||
if (actionCount === 0) return;
|
if (actionCount === 0) return;
|
||||||
|
|
||||||
// Make sure we're not handling some messages twice
|
// Make sure we're not handling some messages twice
|
||||||
if (this.lastHandledMsgIds.has(msg.author.id)) {
|
if (this.lastHandledMsgIds.has(savedMessage.user_id)) {
|
||||||
const channelMap = this.lastHandledMsgIds.get(msg.author.id);
|
const channelMap = this.lastHandledMsgIds.get(savedMessage.user_id);
|
||||||
if (channelMap.has(msg.channel.id)) {
|
if (channelMap.has(savedMessage.channel_id)) {
|
||||||
const lastHandledMsgId = channelMap.get(msg.channel.id);
|
const lastHandledMsgId = channelMap.get(savedMessage.channel_id);
|
||||||
if (lastHandledMsgId >= msg.id) return;
|
if (lastHandledMsgId >= savedMessage.id) return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.spamDetectionQueue = this.spamDetectionQueue.then(
|
this.spamDetectionQueue = this.spamDetectionQueue.then(
|
||||||
async () => {
|
async () => {
|
||||||
|
const timestamp = moment(savedMessage.posted_at).valueOf();
|
||||||
|
const member = this.guild.members.get(savedMessage.user_id);
|
||||||
|
|
||||||
// Log this action...
|
// Log this action...
|
||||||
this.addRecentAction(type, msg.author.id, msg.channel.id, msg, msg.timestamp, actionCount);
|
this.addRecentAction(type, savedMessage.user_id, savedMessage.channel_id, savedMessage, timestamp, actionCount);
|
||||||
|
|
||||||
// ...and then check if it trips the spam filters
|
// ...and then check if it trips the spam filters
|
||||||
const since = msg.timestamp - 1000 * spamConfig.interval;
|
const since = timestamp - 1000 * spamConfig.interval;
|
||||||
const recentActionsCount = this.getRecentActionCount(type, msg.author.id, msg.channel.id, since);
|
const recentActionsCount = this.getRecentActionCount(
|
||||||
|
type,
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
since
|
||||||
|
);
|
||||||
|
|
||||||
// If the user tripped the spam filter...
|
// If the user tripped the spam filter...
|
||||||
if (recentActionsCount > spamConfig.count) {
|
if (recentActionsCount > spamConfig.count) {
|
||||||
const recentActions = this.getRecentActions(type, msg.author.id, msg.channel.id, since);
|
const recentActions = this.getRecentActions(type, savedMessage.user_id, savedMessage.channel_id, since);
|
||||||
let modActionsPlugin;
|
let modActionsPlugin;
|
||||||
|
|
||||||
// Start by muting them, if enabled
|
// Start by muting them, if enabled
|
||||||
|
@ -221,43 +240,52 @@ export class SpamPlugin extends Plugin {
|
||||||
|
|
||||||
const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000;
|
const muteTime = spamConfig.mute_time ? spamConfig.mute_time * 60 * 1000 : 120 * 1000;
|
||||||
|
|
||||||
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, msg.member.id);
|
if (member) {
|
||||||
modActionsPlugin.muteMember(msg.member, muteTime, "Automatic spam detection");
|
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, savedMessage.user_id);
|
||||||
|
modActionsPlugin.muteMember(member, muteTime, "Automatic spam detection");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 messages = recentActions.map(a => a.msg);
|
const savedMessages = recentActions.map(a => a.savedMessage);
|
||||||
const msgIds = messages.map(m => m.id);
|
const msgIds = savedMessages.map(m => m.id);
|
||||||
const lastDetectedMsgId = msgIds[msgIds.length - 1];
|
const lastDetectedMsgId = msgIds[msgIds.length - 1];
|
||||||
const additionalMessages = await this.bot.getMessages(msg.channel.id, 100, null, lastDetectedMsgId);
|
|
||||||
|
const additionalMessages = await this.savedMessages.getUserMessagesByChannelAfterId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
lastDetectedMsgId
|
||||||
|
);
|
||||||
additionalMessages.forEach(m => msgIds.push(m.id));
|
additionalMessages.forEach(m => msgIds.push(m.id));
|
||||||
|
|
||||||
// Then, if enabled, remove the spam messages
|
// Then, if enabled, remove the spam messages
|
||||||
if (spamConfig.clean !== false) {
|
if (spamConfig.clean !== false) {
|
||||||
msgIds.forEach(id => this.logs.ignoreLog(LogType.MESSAGE_DELETE, id));
|
msgIds.forEach(id => this.logs.ignoreLog(LogType.MESSAGE_DELETE, id));
|
||||||
this.bot.deleteMessages(msg.channel.id, msgIds);
|
this.bot.deleteMessages(savedMessage.channel_id, msgIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the ID of the last handled message
|
// Store the ID of the last handled message
|
||||||
const uniqueMessages = Array.from(new Set([...messages, ...additionalMessages]));
|
const uniqueMessages = Array.from(new Set([...savedMessages, ...additionalMessages]));
|
||||||
uniqueMessages.sort((a, b) => (a.id > b.id ? 1 : -1));
|
uniqueMessages.sort((a, b) => (a.id > b.id ? 1 : -1));
|
||||||
const lastHandledMsgId = uniqueMessages.reduce((last: string, m: Message): string => {
|
const lastHandledMsgId = uniqueMessages.reduce((last: string, m: SavedMessage): string => {
|
||||||
return !last || m.id > last ? m.id : last;
|
return !last || m.id > last ? m.id : last;
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
if (!this.lastHandledMsgIds.has(msg.author.id)) {
|
if (!this.lastHandledMsgIds.has(savedMessage.user_id)) {
|
||||||
this.lastHandledMsgIds.set(msg.author.id, new Map());
|
this.lastHandledMsgIds.set(savedMessage.user_id, new Map());
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelMap = this.lastHandledMsgIds.get(msg.author.id);
|
const channelMap = this.lastHandledMsgIds.get(savedMessage.user_id);
|
||||||
channelMap.set(msg.channel.id, lastHandledMsgId);
|
channelMap.set(savedMessage.channel_id, lastHandledMsgId);
|
||||||
|
|
||||||
// Clear the handled actions from recentActions
|
// Clear the handled actions from recentActions
|
||||||
this.clearRecentUserActions(type, msg.author.id, msg.channel.id);
|
this.clearRecentUserActions(type, savedMessage.user_id, savedMessage.channel_id);
|
||||||
|
|
||||||
// Generate a log from the detected messages
|
// Generate a log from the detected messages
|
||||||
const logUrl = await this.saveSpamArchives(uniqueMessages, msg.channel, msg.author);
|
const channel = this.guild.channels.get(savedMessage.channel_id);
|
||||||
|
const user = this.bot.users.get(savedMessage.user_id);
|
||||||
|
const logUrl = await this.saveSpamArchives(uniqueMessages, channel, user);
|
||||||
|
|
||||||
// Create a case and log the actions taken above
|
// Create a case and log the actions taken above
|
||||||
const caseType = spamConfig.mute ? CaseTypes.Mute : CaseTypes.Note;
|
const caseType = spamConfig.mute ? CaseTypes.Mute : CaseTypes.Note;
|
||||||
|
@ -267,8 +295,8 @@ export class SpamPlugin extends Plugin {
|
||||||
`);
|
`);
|
||||||
|
|
||||||
this.logs.log(LogType.SPAM_DETECTED, {
|
this.logs.log(LogType.SPAM_DETECTED, {
|
||||||
member: stripObjectToScalars(msg.member, ["user"]),
|
member: stripObjectToScalars(member, ["user"]),
|
||||||
channel: stripObjectToScalars(msg.channel),
|
channel: stripObjectToScalars(channel),
|
||||||
description,
|
description,
|
||||||
limit: spamConfig.count,
|
limit: spamConfig.count,
|
||||||
interval: spamConfig.interval,
|
interval: spamConfig.interval,
|
||||||
|
@ -276,7 +304,7 @@ export class SpamPlugin extends Plugin {
|
||||||
});
|
});
|
||||||
|
|
||||||
const caseId = await modActionsPlugin.createCase(
|
const caseId = await modActionsPlugin.createCase(
|
||||||
msg.member.id,
|
savedMessage.user_id,
|
||||||
this.bot.user.id,
|
this.bot.user.id,
|
||||||
caseType,
|
caseType,
|
||||||
null,
|
null,
|
||||||
|
@ -285,8 +313,8 @@ export class SpamPlugin extends Plugin {
|
||||||
);
|
);
|
||||||
|
|
||||||
// For mutes, also set the mute's case id (for !mutes)
|
// For mutes, also set the mute's case id (for !mutes)
|
||||||
if (spamConfig.mute) {
|
if (spamConfig.mute && member) {
|
||||||
await modActionsPlugin.mutes.setCaseId(msg.member.id, caseId);
|
await modActionsPlugin.mutes.setCaseId(savedMessage.user_id, caseId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -298,55 +326,84 @@ export class SpamPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For interoperability with the Censor plugin
|
// For interoperability with the Censor plugin
|
||||||
async logCensor(msg: Message) {
|
async logCensor(savedMessage: SavedMessage) {
|
||||||
const spamConfig = this.configValueForMsg(msg, "max_censor");
|
const spamConfig = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"max_censor"
|
||||||
|
);
|
||||||
if (spamConfig) {
|
if (spamConfig) {
|
||||||
this.logAndDetectSpam(msg, RecentActionType.Censor, spamConfig, 1, "too many censored messages");
|
this.logAndDetectSpam(savedMessage, RecentActionType.Censor, spamConfig, 1, "too many censored messages");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@d.event("messageCreate")
|
async onMessageCreate(savedMessage: SavedMessage) {
|
||||||
async onMessageCreate(msg: Message) {
|
if (savedMessage.is_bot) return;
|
||||||
if (msg.author.bot) return;
|
|
||||||
|
|
||||||
const maxMessages = this.configValueForMsg(msg, "max_messages");
|
const maxMessages = this.configValueForMemberIdAndChannelId(
|
||||||
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"max_messages"
|
||||||
|
);
|
||||||
if (maxMessages) {
|
if (maxMessages) {
|
||||||
this.logAndDetectSpam(msg, RecentActionType.Message, maxMessages, 1, "too many messages");
|
this.logAndDetectSpam(savedMessage, RecentActionType.Message, maxMessages, 1, "too many messages");
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxMentions = this.configValueForMsg(msg, "max_mentions");
|
const maxMentions = this.configValueForMemberIdAndChannelId(
|
||||||
const mentions = msg.content ? [...getUserMentions(msg.content), ...getRoleMentions(msg.content)] : [];
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"max_mentions"
|
||||||
|
);
|
||||||
|
const mentions = savedMessage.data.content
|
||||||
|
? [...getUserMentions(savedMessage.data.content), ...getRoleMentions(savedMessage.data.content)]
|
||||||
|
: [];
|
||||||
if (maxMentions && mentions.length) {
|
if (maxMentions && mentions.length) {
|
||||||
this.logAndDetectSpam(msg, RecentActionType.Mention, maxMentions, mentions.length, "too many mentions");
|
this.logAndDetectSpam(savedMessage, RecentActionType.Mention, maxMentions, mentions.length, "too many mentions");
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxLinks = this.configValueForMsg(msg, "max_links");
|
const maxLinks = this.configValueForMemberIdAndChannelId(
|
||||||
if (maxLinks && msg.content) {
|
savedMessage.user_id,
|
||||||
const links = getUrlsInString(msg.content);
|
savedMessage.channel_id,
|
||||||
this.logAndDetectSpam(msg, RecentActionType.Link, maxLinks, links.length, "too many links");
|
"max_links"
|
||||||
|
);
|
||||||
|
if (maxLinks && savedMessage.data.content) {
|
||||||
|
const links = getUrlsInString(savedMessage.data.content);
|
||||||
|
this.logAndDetectSpam(savedMessage, RecentActionType.Link, maxLinks, links.length, "too many links");
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxAttachments = this.configValueForMsg(msg, "max_attachments");
|
const maxAttachments = this.configValueForMemberIdAndChannelId(
|
||||||
if (maxAttachments && msg.attachments.length) {
|
savedMessage.user_id,
|
||||||
|
savedMessage.channel_id,
|
||||||
|
"max_attachments"
|
||||||
|
);
|
||||||
|
if (maxAttachments && savedMessage.data.attachments) {
|
||||||
this.logAndDetectSpam(
|
this.logAndDetectSpam(
|
||||||
msg,
|
savedMessage,
|
||||||
RecentActionType.Attachment,
|
RecentActionType.Attachment,
|
||||||
maxAttachments,
|
maxAttachments,
|
||||||
msg.attachments.length,
|
savedMessage.data.attachments.length,
|
||||||
"too many attachments"
|
"too many attachments"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxEmoji = this.configValueForMsg(msg, "max_emoji");
|
const maxEmoji = this.configValueForMemberIdAndChannelId(
|
||||||
if (maxEmoji && msg.content) {
|
savedMessage.user_id,
|
||||||
const emojiCount = getEmojiInString(msg.content).length;
|
savedMessage.channel_id,
|
||||||
this.logAndDetectSpam(msg, RecentActionType.Emoji, maxEmoji, emojiCount, "too many emoji");
|
"max_emoji"
|
||||||
|
);
|
||||||
|
if (maxEmoji && savedMessage.data.content) {
|
||||||
|
const emojiCount = getEmojiInString(savedMessage.data.content).length;
|
||||||
|
this.logAndDetectSpam(savedMessage, RecentActionType.Emoji, maxEmoji, emojiCount, "too many emoji");
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxNewlines = this.configValueForMsg(msg, "max_newlines");
|
const maxNewlines = this.configValueForMemberIdAndChannelId(
|
||||||
if (maxNewlines && msg.content) {
|
savedMessage.user_id,
|
||||||
const newlineCount = (msg.content.match(/\n/g) || []).length;
|
savedMessage.channel_id,
|
||||||
this.logAndDetectSpam(msg, RecentActionType.Newline, maxNewlines, newlineCount, "too many newlines");
|
"max_newlines"
|
||||||
|
);
|
||||||
|
if (maxNewlines && savedMessage.data.content) {
|
||||||
|
const newlineCount = (savedMessage.data.content.match(/\n/g) || []).length;
|
||||||
|
this.logAndDetectSpam(savedMessage, RecentActionType.Newline, maxNewlines, newlineCount, "too many newlines");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Max duplicates
|
// TODO: Max duplicates
|
||||||
|
|
Loading…
Add table
Reference in a new issue