|
|
@ -1,4 +1,4 @@
|
|
|
|
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
|
|
|
import { PluginInfo, trimPluginDescription, ZeppelinPlugin } from "./ZeppelinPlugin";
|
|
|
|
import * as t from "io-ts";
|
|
|
|
import * as t from "io-ts";
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
convertDelayStringToMS,
|
|
|
|
convertDelayStringToMS,
|
|
|
@ -7,8 +7,11 @@ import {
|
|
|
|
getRoleMentions,
|
|
|
|
getRoleMentions,
|
|
|
|
getUrlsInString,
|
|
|
|
getUrlsInString,
|
|
|
|
getUserMentions,
|
|
|
|
getUserMentions,
|
|
|
|
|
|
|
|
messageSummary,
|
|
|
|
MINUTES,
|
|
|
|
MINUTES,
|
|
|
|
noop,
|
|
|
|
noop,
|
|
|
|
|
|
|
|
SECONDS,
|
|
|
|
|
|
|
|
stripObjectToScalars,
|
|
|
|
tNullable,
|
|
|
|
tNullable,
|
|
|
|
} from "../utils";
|
|
|
|
} from "../utils";
|
|
|
|
import { decorators as d } from "knub";
|
|
|
|
import { decorators as d } from "knub";
|
|
|
@ -22,12 +25,19 @@ import { ModActionsPlugin } from "./ModActions";
|
|
|
|
import { MutesPlugin } from "./Mutes";
|
|
|
|
import { MutesPlugin } from "./Mutes";
|
|
|
|
import { LogsPlugin } from "./Logs";
|
|
|
|
import { LogsPlugin } from "./Logs";
|
|
|
|
import { LogType } from "../data/LogType";
|
|
|
|
import { LogType } from "../data/LogType";
|
|
|
|
|
|
|
|
import { TSafeRegex } from "../validatorUtils";
|
|
|
|
|
|
|
|
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
|
|
|
|
|
|
|
import { GuildArchives } from "../data/GuildArchives";
|
|
|
|
|
|
|
|
import { GuildLogs } from "../data/GuildLogs";
|
|
|
|
|
|
|
|
import { SavedMessage } from "../data/entities/SavedMessage";
|
|
|
|
|
|
|
|
import moment from "moment-timezone";
|
|
|
|
|
|
|
|
|
|
|
|
type MessageInfo = { channelId: string; messageId: string };
|
|
|
|
type MessageInfo = { channelId: string; messageId: string };
|
|
|
|
|
|
|
|
|
|
|
|
type TextTriggerWithMultipleMatchTypes = {
|
|
|
|
type TextTriggerWithMultipleMatchTypes = {
|
|
|
|
match_messages: boolean;
|
|
|
|
match_messages: boolean;
|
|
|
|
match_embeds: boolean;
|
|
|
|
match_embeds: boolean;
|
|
|
|
|
|
|
|
match_visible_names: boolean;
|
|
|
|
match_usernames: boolean;
|
|
|
|
match_usernames: boolean;
|
|
|
|
match_nicknames: boolean;
|
|
|
|
match_nicknames: boolean;
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -44,7 +54,7 @@ interface MessageTextTriggerMatchResult extends TriggerMatchResult {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface OtherTextTriggerMatchResult extends TriggerMatchResult {
|
|
|
|
interface OtherTextTriggerMatchResult extends TriggerMatchResult {
|
|
|
|
type: "username" | "nickname";
|
|
|
|
type: "username" | "nickname" | "visiblename";
|
|
|
|
str: string;
|
|
|
|
str: string;
|
|
|
|
userId: string;
|
|
|
|
userId: string;
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -89,6 +99,7 @@ const MatchWordsTrigger = t.type({
|
|
|
|
only_full_words: t.boolean,
|
|
|
|
only_full_words: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
|
|
|
|
match_visible_names: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
});
|
|
|
|
});
|
|
|
@ -99,15 +110,17 @@ const defaultMatchWordsTrigger: TMatchWordsTrigger = {
|
|
|
|
only_full_words: true,
|
|
|
|
only_full_words: true,
|
|
|
|
match_messages: true,
|
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
|
|
|
match_embeds: true,
|
|
|
|
|
|
|
|
match_visible_names: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const MatchRegexTrigger = t.type({
|
|
|
|
const MatchRegexTrigger = t.type({
|
|
|
|
patterns: t.array(t.string),
|
|
|
|
patterns: t.array(TSafeRegex),
|
|
|
|
case_sensitive: t.boolean,
|
|
|
|
case_sensitive: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
|
|
|
|
match_visible_names: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
});
|
|
|
|
});
|
|
|
@ -116,6 +129,7 @@ const defaultMatchRegexTrigger: Partial<TMatchRegexTrigger> = {
|
|
|
|
case_sensitive: false,
|
|
|
|
case_sensitive: false,
|
|
|
|
match_messages: true,
|
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
|
|
|
match_embeds: true,
|
|
|
|
|
|
|
|
match_visible_names: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -128,6 +142,7 @@ const MatchInvitesTrigger = t.type({
|
|
|
|
allow_group_dm_invites: t.boolean,
|
|
|
|
allow_group_dm_invites: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
|
|
|
|
match_visible_names: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
});
|
|
|
|
});
|
|
|
@ -136,6 +151,7 @@ const defaultMatchInvitesTrigger: Partial<TMatchInvitesTrigger> = {
|
|
|
|
allow_group_dm_invites: false,
|
|
|
|
allow_group_dm_invites: false,
|
|
|
|
match_messages: true,
|
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
|
|
|
match_embeds: true,
|
|
|
|
|
|
|
|
match_visible_names: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -146,6 +162,7 @@ const MatchLinksTrigger = t.type({
|
|
|
|
include_subdomains: t.boolean,
|
|
|
|
include_subdomains: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_messages: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
match_embeds: t.boolean,
|
|
|
|
|
|
|
|
match_visible_names: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_usernames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
match_nicknames: t.boolean,
|
|
|
|
});
|
|
|
|
});
|
|
|
@ -154,6 +171,7 @@ const defaultMatchLinksTrigger: Partial<TMatchLinksTrigger> = {
|
|
|
|
include_subdomains: true,
|
|
|
|
include_subdomains: true,
|
|
|
|
match_messages: true,
|
|
|
|
match_messages: true,
|
|
|
|
match_embeds: true,
|
|
|
|
match_embeds: true,
|
|
|
|
|
|
|
|
match_visible_names: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_usernames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
match_nicknames: false,
|
|
|
|
};
|
|
|
|
};
|
|
|
@ -173,22 +191,22 @@ const defaultTextSpamTrigger: Partial<t.TypeOf<typeof BaseTextSpamTrigger>> = {
|
|
|
|
per_channel: true,
|
|
|
|
per_channel: true,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const MaxMessagesTrigger = BaseTextSpamTrigger;
|
|
|
|
const MessageSpamTrigger = BaseTextSpamTrigger;
|
|
|
|
type TMaxMessagesTrigger = t.TypeOf<typeof MaxMessagesTrigger>;
|
|
|
|
type TMessageSpamTrigger = t.TypeOf<typeof MessageSpamTrigger>;
|
|
|
|
const MaxMentionsTrigger = BaseTextSpamTrigger;
|
|
|
|
const MentionSpamTrigger = BaseTextSpamTrigger;
|
|
|
|
type TMaxMentionsTrigger = t.TypeOf<typeof MaxMentionsTrigger>;
|
|
|
|
type TMentionSpamTrigger = t.TypeOf<typeof MentionSpamTrigger>;
|
|
|
|
const MaxLinksTrigger = BaseTextSpamTrigger;
|
|
|
|
const LinkSpamTrigger = BaseTextSpamTrigger;
|
|
|
|
type TMaxLinksTrigger = t.TypeOf<typeof MaxLinksTrigger>;
|
|
|
|
type TLinkSpamTrigger = t.TypeOf<typeof LinkSpamTrigger>;
|
|
|
|
const MaxAttachmentsTrigger = BaseTextSpamTrigger;
|
|
|
|
const AttachmentSpamTrigger = BaseTextSpamTrigger;
|
|
|
|
type TMaxAttachmentsTrigger = t.TypeOf<typeof MaxAttachmentsTrigger>;
|
|
|
|
type TAttachmentSpamTrigger = t.TypeOf<typeof AttachmentSpamTrigger>;
|
|
|
|
const MaxEmojisTrigger = BaseTextSpamTrigger;
|
|
|
|
const EmojiSpamTrigger = BaseTextSpamTrigger;
|
|
|
|
type TMaxEmojisTrigger = t.TypeOf<typeof MaxEmojisTrigger>;
|
|
|
|
type TEmojiSpamTrigger = t.TypeOf<typeof EmojiSpamTrigger>;
|
|
|
|
const MaxLinesTrigger = BaseTextSpamTrigger;
|
|
|
|
const LineSpamTrigger = BaseTextSpamTrigger;
|
|
|
|
type TMaxLinesTrigger = t.TypeOf<typeof MaxLinesTrigger>;
|
|
|
|
type TLineSpamTrigger = t.TypeOf<typeof LineSpamTrigger>;
|
|
|
|
const MaxCharactersTrigger = BaseTextSpamTrigger;
|
|
|
|
const CharacterSpamTrigger = BaseTextSpamTrigger;
|
|
|
|
type TMaxCharactersTrigger = t.TypeOf<typeof MaxCharactersTrigger>;
|
|
|
|
type TCharacterSpamTrigger = t.TypeOf<typeof CharacterSpamTrigger>;
|
|
|
|
const MaxVoiceMovesTrigger = BaseSpamTrigger;
|
|
|
|
const VoiceMoveSpamTrigger = BaseSpamTrigger;
|
|
|
|
type TMaxVoiceMovesTrigger = t.TypeOf<typeof MaxVoiceMovesTrigger>;
|
|
|
|
type TVoiceMoveSpamTrigger = t.TypeOf<typeof VoiceMoveSpamTrigger>;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* ACTIONS
|
|
|
|
* ACTIONS
|
|
|
@ -217,6 +235,10 @@ const AlertAction = t.type({
|
|
|
|
text: t.string,
|
|
|
|
text: t.string,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const ChangeNicknameAction = t.type({
|
|
|
|
|
|
|
|
name: t.string,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* FULL CONFIG SCHEMA
|
|
|
|
* FULL CONFIG SCHEMA
|
|
|
|
*/
|
|
|
|
*/
|
|
|
@ -231,14 +253,14 @@ const Rule = t.type({
|
|
|
|
match_regex: tNullable(MatchRegexTrigger),
|
|
|
|
match_regex: tNullable(MatchRegexTrigger),
|
|
|
|
match_invites: tNullable(MatchInvitesTrigger),
|
|
|
|
match_invites: tNullable(MatchInvitesTrigger),
|
|
|
|
match_links: tNullable(MatchLinksTrigger),
|
|
|
|
match_links: tNullable(MatchLinksTrigger),
|
|
|
|
max_messages: tNullable(MaxMessagesTrigger),
|
|
|
|
message_spam: tNullable(MessageSpamTrigger),
|
|
|
|
max_mentions: tNullable(MaxMentionsTrigger),
|
|
|
|
mention_spam: tNullable(MentionSpamTrigger),
|
|
|
|
max_links: tNullable(MaxLinksTrigger),
|
|
|
|
link_spam: tNullable(LinkSpamTrigger),
|
|
|
|
max_attachments: tNullable(MaxAttachmentsTrigger),
|
|
|
|
attachment_spam: tNullable(AttachmentSpamTrigger),
|
|
|
|
max_emojis: tNullable(MaxEmojisTrigger),
|
|
|
|
emoji_spam: tNullable(EmojiSpamTrigger),
|
|
|
|
max_lines: tNullable(MaxLinesTrigger),
|
|
|
|
line_spam: tNullable(LineSpamTrigger),
|
|
|
|
max_characters: tNullable(MaxCharactersTrigger),
|
|
|
|
character_spam: tNullable(CharacterSpamTrigger),
|
|
|
|
max_voice_moves: tNullable(MaxVoiceMovesTrigger),
|
|
|
|
// voice_move_spam: tNullable(VoiceMoveSpamTrigger), // TODO
|
|
|
|
// TODO: Duplicates trigger
|
|
|
|
// TODO: Duplicates trigger
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
),
|
|
|
@ -249,6 +271,7 @@ const Rule = t.type({
|
|
|
|
kick: tNullable(KickAction),
|
|
|
|
kick: tNullable(KickAction),
|
|
|
|
ban: tNullable(BanAction),
|
|
|
|
ban: tNullable(BanAction),
|
|
|
|
alert: tNullable(AlertAction),
|
|
|
|
alert: tNullable(AlertAction),
|
|
|
|
|
|
|
|
change_nickname: tNullable(ChangeNicknameAction),
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
type TRule = t.TypeOf<typeof Rule>;
|
|
|
|
type TRule = t.TypeOf<typeof Rule>;
|
|
|
@ -264,6 +287,16 @@ type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
|
|
|
|
|
|
|
|
|
|
|
const defaultTriggers = {
|
|
|
|
const defaultTriggers = {
|
|
|
|
match_words: defaultMatchWordsTrigger,
|
|
|
|
match_words: defaultMatchWordsTrigger,
|
|
|
|
|
|
|
|
match_regex: defaultMatchRegexTrigger,
|
|
|
|
|
|
|
|
match_invites: defaultMatchInvitesTrigger,
|
|
|
|
|
|
|
|
match_links: defaultMatchLinksTrigger,
|
|
|
|
|
|
|
|
message_spam: defaultTextSpamTrigger,
|
|
|
|
|
|
|
|
mention_spam: defaultTextSpamTrigger,
|
|
|
|
|
|
|
|
link_spam: defaultTextSpamTrigger,
|
|
|
|
|
|
|
|
attachment_spam: defaultTextSpamTrigger,
|
|
|
|
|
|
|
|
emoji_spam: defaultTextSpamTrigger,
|
|
|
|
|
|
|
|
line_spam: defaultTextSpamTrigger,
|
|
|
|
|
|
|
|
character_spam: defaultTextSpamTrigger,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
@ -303,9 +336,12 @@ type OtherRecentAction = BaseRecentAction & {
|
|
|
|
type: RecentActionType.VoiceChannelMove;
|
|
|
|
type: RecentActionType.VoiceChannelMove;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type RecentAction = TextRecentAction | OtherRecentAction;
|
|
|
|
type RecentAction = (TextRecentAction | OtherRecentAction) & { expiresAt: number };
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_SPAM_CHECK_TIMESPAN = 5 * MINUTES;
|
|
|
|
const SPAM_GRACE_PERIOD_LENGTH = 10 * SECONDS;
|
|
|
|
|
|
|
|
const RECENT_ACTION_EXPIRY_TIME = 2 * MINUTES;
|
|
|
|
|
|
|
|
const MAX_RECENTLY_DELETED_MESSAGES = 10;
|
|
|
|
|
|
|
|
const RECENT_NICKNAME_CHANGE_EXPIRY_TIME = 5 * MINUTES;
|
|
|
|
|
|
|
|
|
|
|
|
const inviteCache = new SimpleCache(10 * MINUTES);
|
|
|
|
const inviteCache = new SimpleCache(10 * MINUTES);
|
|
|
|
|
|
|
|
|
|
|
@ -314,12 +350,95 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
public static configSchema = ConfigSchema;
|
|
|
|
public static configSchema = ConfigSchema;
|
|
|
|
public static dependencies = ["mod_actions", "mutes", "logs"];
|
|
|
|
public static dependencies = ["mod_actions", "mutes", "logs"];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static pluginInfo: PluginInfo = {
|
|
|
|
|
|
|
|
prettyName: "Automod",
|
|
|
|
|
|
|
|
description: trimPluginDescription(`
|
|
|
|
|
|
|
|
This plugin allows you to specify automated actions in response to triggers. Example use cases include word filtering and spam prevention.
|
|
|
|
|
|
|
|
`),
|
|
|
|
|
|
|
|
configurationGuide: trimPluginDescription(`
|
|
|
|
|
|
|
|
The automod plugin is very customizable. For a full list of available triggers, actions, and their options, see Config schema at the bottom of this page.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Simple word filter
|
|
|
|
|
|
|
|
Removes any messages that contain the word 'banana' and sends a warning to the user.
|
|
|
|
|
|
|
|
Moderators (level >= 50) are ignored by the filter based on the override.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
~~~yml
|
|
|
|
|
|
|
|
automod:
|
|
|
|
|
|
|
|
config:
|
|
|
|
|
|
|
|
rules:
|
|
|
|
|
|
|
|
my_filter:
|
|
|
|
|
|
|
|
triggers:
|
|
|
|
|
|
|
|
- match_words:
|
|
|
|
|
|
|
|
words: ['banana']
|
|
|
|
|
|
|
|
case_sensitive: false
|
|
|
|
|
|
|
|
only_full_words: true
|
|
|
|
|
|
|
|
actions:
|
|
|
|
|
|
|
|
clean: true
|
|
|
|
|
|
|
|
warn:
|
|
|
|
|
|
|
|
reason: 'Do not talk about bananas!'
|
|
|
|
|
|
|
|
overrides:
|
|
|
|
|
|
|
|
- level: '>=50'
|
|
|
|
|
|
|
|
config:
|
|
|
|
|
|
|
|
rules:
|
|
|
|
|
|
|
|
my_filter:
|
|
|
|
|
|
|
|
enabled: false
|
|
|
|
|
|
|
|
~~~
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Spam detection
|
|
|
|
|
|
|
|
This example includes 2 filters:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- The first one is triggered if a user sends 5 messages within 10 seconds OR 3 attachments within 60 seconds.
|
|
|
|
|
|
|
|
The messages are deleted and the user is muted for 5 minutes.
|
|
|
|
|
|
|
|
- The second filter is triggered if a user sends more than 2 emoji within 5 seconds.
|
|
|
|
|
|
|
|
The messages are deleted but the user is not muted.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Moderators are ignored by both filters based on the override.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
~~~yml
|
|
|
|
|
|
|
|
automod:
|
|
|
|
|
|
|
|
config:
|
|
|
|
|
|
|
|
rules:
|
|
|
|
|
|
|
|
my_spam_filter:
|
|
|
|
|
|
|
|
triggers:
|
|
|
|
|
|
|
|
- message_spam:
|
|
|
|
|
|
|
|
amount: 5
|
|
|
|
|
|
|
|
within: 10s
|
|
|
|
|
|
|
|
- attachment_spam:
|
|
|
|
|
|
|
|
amount: 3
|
|
|
|
|
|
|
|
within: 60s
|
|
|
|
|
|
|
|
actions:
|
|
|
|
|
|
|
|
clean: true
|
|
|
|
|
|
|
|
mute:
|
|
|
|
|
|
|
|
duration: 5m
|
|
|
|
|
|
|
|
reason: 'Auto-muted for spam'
|
|
|
|
|
|
|
|
my_second_filter:
|
|
|
|
|
|
|
|
triggers:
|
|
|
|
|
|
|
|
- message_spam:
|
|
|
|
|
|
|
|
amount: 5
|
|
|
|
|
|
|
|
within: 10s
|
|
|
|
|
|
|
|
actions:
|
|
|
|
|
|
|
|
clean: true
|
|
|
|
|
|
|
|
mute:
|
|
|
|
|
|
|
|
duration: 5m
|
|
|
|
|
|
|
|
reason: 'Auto-muted for spam'
|
|
|
|
|
|
|
|
overrides:
|
|
|
|
|
|
|
|
- level: '>=50'
|
|
|
|
|
|
|
|
config:
|
|
|
|
|
|
|
|
rules:
|
|
|
|
|
|
|
|
my_spam_filter:
|
|
|
|
|
|
|
|
enabled: false
|
|
|
|
|
|
|
|
my_second_filter:
|
|
|
|
|
|
|
|
enabled: false
|
|
|
|
|
|
|
|
~~~
|
|
|
|
|
|
|
|
`),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
protected unloaded = false;
|
|
|
|
protected unloaded = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Handle automod checks/actions in a queue so we don't get overlap on the same user
|
|
|
|
// Handle automod checks/actions in a queue so we don't get overlap on the same user
|
|
|
|
protected automodQueue: Queue;
|
|
|
|
protected automodQueue: Queue;
|
|
|
|
|
|
|
|
|
|
|
|
// Recent actions are used to detect "max_*" type of triggers, i.e. spam triggers
|
|
|
|
// Recent actions are used to detect spam triggers
|
|
|
|
protected recentActions: RecentAction[];
|
|
|
|
protected recentActions: RecentAction[];
|
|
|
|
protected recentActionClearInterval: Timeout;
|
|
|
|
protected recentActionClearInterval: Timeout;
|
|
|
|
|
|
|
|
|
|
|
@ -327,13 +446,24 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
// During this grace period, if the user repeats the same type of recent action that tripped the rule, that message will
|
|
|
|
// During this grace period, if the user repeats the same type of recent action that tripped the rule, that message will
|
|
|
|
// be deleted and no further action will be carried out. This is mainly to account for the delay between the spam message
|
|
|
|
// be deleted and no further action will be carried out. This is mainly to account for the delay between the spam message
|
|
|
|
// being posted and the bot reacting to it, during which the user could keep posting more spam.
|
|
|
|
// being posted and the bot reacting to it, during which the user could keep posting more spam.
|
|
|
|
protected spamGracePeriods: Array<{ key: string; type: RecentActionType; expiresAt: number }>;
|
|
|
|
protected spamGracePeriods: Map<string, { expiresAt: number; deletedMessages: string[] }>; // Key = identifier-actionType
|
|
|
|
protected spamGracePriodClearInterval: Timeout;
|
|
|
|
protected spamGracePriodClearInterval: Timeout;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected recentlyDeletedMessages: string[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected recentNicknameChanges: Map<string, { expiresAt: number }>;
|
|
|
|
|
|
|
|
protected recentNicknameChangesClearInterval: Timeout;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected onMessageCreateFn;
|
|
|
|
|
|
|
|
|
|
|
|
protected modActions: ModActionsPlugin;
|
|
|
|
protected modActions: ModActionsPlugin;
|
|
|
|
protected mutes: MutesPlugin;
|
|
|
|
protected mutes: MutesPlugin;
|
|
|
|
protected logs: LogsPlugin;
|
|
|
|
protected logs: LogsPlugin;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected savedMessages: GuildSavedMessages;
|
|
|
|
|
|
|
|
protected archives: GuildArchives;
|
|
|
|
|
|
|
|
protected guildLogs: GuildLogs;
|
|
|
|
|
|
|
|
|
|
|
|
protected static preprocessStaticConfig(config) {
|
|
|
|
protected static preprocessStaticConfig(config) {
|
|
|
|
if (config.rules && typeof config.rules === "object") {
|
|
|
|
if (config.rules && typeof config.rules === "object") {
|
|
|
|
// Loop through each rule
|
|
|
|
// Loop through each rule
|
|
|
@ -351,7 +481,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
if (rule["triggers"] != null && Array.isArray(rule["triggers"])) {
|
|
|
|
if (rule["triggers"] != null && Array.isArray(rule["triggers"])) {
|
|
|
|
for (const trigger of rule["triggers"]) {
|
|
|
|
for (const trigger of rule["triggers"]) {
|
|
|
|
if (trigger == null || typeof trigger !== "object") continue;
|
|
|
|
if (trigger == null || typeof trigger !== "object") continue;
|
|
|
|
// Apply default triggers to the triggers used in this rule
|
|
|
|
// Apply default config to the triggers used in this rule
|
|
|
|
for (const [defaultTriggerName, defaultTrigger] of Object.entries(defaultTriggers)) {
|
|
|
|
for (const [defaultTriggerName, defaultTrigger] of Object.entries(defaultTriggers)) {
|
|
|
|
if (trigger[defaultTriggerName] && typeof trigger[defaultTriggerName] === "object") {
|
|
|
|
if (trigger[defaultTriggerName] && typeof trigger[defaultTriggerName] === "object") {
|
|
|
|
trigger[defaultTriggerName] = mergeConfig({}, defaultTrigger, trigger[defaultTriggerName]);
|
|
|
|
trigger[defaultTriggerName] = mergeConfig({}, defaultTrigger, trigger[defaultTriggerName]);
|
|
|
@ -373,31 +503,60 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
|
|
|
|
|
|
|
|
protected onLoad() {
|
|
|
|
protected onLoad() {
|
|
|
|
this.automodQueue = new Queue();
|
|
|
|
this.automodQueue = new Queue();
|
|
|
|
this.modActions = this.getPlugin("mod_actions");
|
|
|
|
|
|
|
|
this.logs = this.getPlugin("logs");
|
|
|
|
this.recentActions = [];
|
|
|
|
|
|
|
|
this.spamGracePeriods = new Map();
|
|
|
|
|
|
|
|
this.spamGracePriodClearInterval = setInterval(() => this.clearExpiredGracePeriods(), 1 * SECONDS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.recentlyDeletedMessages = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.recentNicknameChanges = new Map();
|
|
|
|
|
|
|
|
this.recentNicknameChangesClearInterval = setInterval(() => this.clearExpiredRecentNicknameChanges(), 30 * SECONDS);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.savedMessages = GuildSavedMessages.getGuildInstance(this.guildId);
|
|
|
|
|
|
|
|
this.archives = GuildArchives.getGuildInstance(this.guildId);
|
|
|
|
|
|
|
|
this.guildLogs = new GuildLogs(this.guildId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.onMessageCreateFn = msg => this.onMessageCreate(msg);
|
|
|
|
|
|
|
|
this.savedMessages.events.on("create", this.onMessageCreateFn);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected getModActions(): ModActionsPlugin {
|
|
|
|
|
|
|
|
return this.getPlugin("mod_actions");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected getLogs(): LogsPlugin {
|
|
|
|
|
|
|
|
return this.getPlugin("logs");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected onUnload() {
|
|
|
|
protected onUnload() {
|
|
|
|
this.unloaded = true;
|
|
|
|
this.unloaded = true;
|
|
|
|
|
|
|
|
this.savedMessages.events.off("create", this.onMessageCreateFn);
|
|
|
|
clearInterval(this.recentActionClearInterval);
|
|
|
|
clearInterval(this.recentActionClearInterval);
|
|
|
|
clearInterval(this.spamGracePriodClearInterval);
|
|
|
|
clearInterval(this.spamGracePriodClearInterval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected evaluateMatchWordsTrigger(trigger: TMatchWordsTrigger, str: string): boolean {
|
|
|
|
protected evaluateMatchWordsTrigger(trigger: TMatchWordsTrigger, str: string): boolean {
|
|
|
|
for (const word of trigger.words) {
|
|
|
|
for (const word of trigger.words) {
|
|
|
|
const pattern = trigger.only_full_words ? `\b${escapeStringRegexp(word)}\b` : escapeStringRegexp(word);
|
|
|
|
const pattern = trigger.only_full_words ? `\\b${escapeStringRegexp(word)}\\b` : escapeStringRegexp(word);
|
|
|
|
|
|
|
|
|
|
|
|
const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
|
|
|
|
const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
|
|
|
|
return regex.test(str);
|
|
|
|
const test = regex.test(str);
|
|
|
|
|
|
|
|
if (test) return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected evaluateMatchRegexTrigger(trigger: TMatchRegexTrigger, str: string): boolean {
|
|
|
|
protected evaluateMatchRegexTrigger(trigger: TMatchRegexTrigger, str: string): boolean {
|
|
|
|
// TODO: Time limit regexes
|
|
|
|
// TODO: Time limit regexes
|
|
|
|
for (const pattern of trigger.patterns) {
|
|
|
|
for (const pattern of trigger.patterns) {
|
|
|
|
const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
|
|
|
|
const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
|
|
|
|
return regex.test(str);
|
|
|
|
const test = regex.test(str);
|
|
|
|
|
|
|
|
if (test) return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected async evaluateMatchInvitesTrigger(trigger: TMatchInvitesTrigger, str: string): Promise<boolean> {
|
|
|
|
protected async evaluateMatchInvitesTrigger(trigger: TMatchInvitesTrigger, str: string): Promise<boolean> {
|
|
|
@ -478,19 +637,23 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
protected matchTextSpamTrigger(
|
|
|
|
protected matchTextSpamTrigger(
|
|
|
|
recentActionType: RecentActionType,
|
|
|
|
recentActionType: RecentActionType,
|
|
|
|
trigger: TBaseTextSpamTrigger,
|
|
|
|
trigger: TBaseTextSpamTrigger,
|
|
|
|
msg: Message,
|
|
|
|
msg: SavedMessage,
|
|
|
|
): TextSpamTriggerMatchResult {
|
|
|
|
): TextSpamTriggerMatchResult {
|
|
|
|
const since = msg.timestamp - convertDelayStringToMS(trigger.within);
|
|
|
|
const since = moment.utc(msg.posted_at).valueOf() - convertDelayStringToMS(trigger.within);
|
|
|
|
const recentActions = trigger.per_channel
|
|
|
|
const recentActions = trigger.per_channel
|
|
|
|
? this.getMatchingRecentActions(recentActionType, `${msg.channel.id}-${msg.author.id}`, since)
|
|
|
|
? this.getMatchingRecentActions(recentActionType, `${msg.channel_id}-${msg.user_id}`, since)
|
|
|
|
: this.getMatchingRecentActions(recentActionType, msg.author.id, since);
|
|
|
|
: this.getMatchingRecentActions(recentActionType, msg.user_id, since);
|
|
|
|
if (recentActions.length > trigger.amount) {
|
|
|
|
const totalCount = recentActions.reduce((total, action) => {
|
|
|
|
|
|
|
|
return total + action.count;
|
|
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (totalCount >= trigger.amount) {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
type: "textspam",
|
|
|
|
type: "textspam",
|
|
|
|
actionType: recentActionType,
|
|
|
|
actionType: recentActionType,
|
|
|
|
channelId: trigger.per_channel ? msg.channel.id : null,
|
|
|
|
channelId: trigger.per_channel ? msg.channel_id : null,
|
|
|
|
messageInfos: recentActions.map(action => (action as TextRecentAction).messageInfo),
|
|
|
|
messageInfos: recentActions.map(action => (action as TextRecentAction).messageInfo),
|
|
|
|
userId: msg.author.id,
|
|
|
|
userId: msg.user_id,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -499,33 +662,40 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
|
|
|
|
|
|
|
|
protected async matchMultipleTextTypesOnMessage(
|
|
|
|
protected async matchMultipleTextTypesOnMessage(
|
|
|
|
trigger: TextTriggerWithMultipleMatchTypes,
|
|
|
|
trigger: TextTriggerWithMultipleMatchTypes,
|
|
|
|
msg: Message,
|
|
|
|
msg: SavedMessage,
|
|
|
|
cb,
|
|
|
|
cb,
|
|
|
|
): Promise<TextTriggerMatchResult> {
|
|
|
|
): Promise<TextTriggerMatchResult> {
|
|
|
|
const messageInfo: MessageInfo = { channelId: msg.channel.id, messageId: msg.id };
|
|
|
|
const messageInfo: MessageInfo = { channelId: msg.channel_id, messageId: msg.id };
|
|
|
|
|
|
|
|
const member = this.guild.members.get(msg.user_id);
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.match_messages) {
|
|
|
|
if (trigger.match_messages) {
|
|
|
|
const str = msg.content;
|
|
|
|
const str = msg.data.content;
|
|
|
|
const match = await cb(str);
|
|
|
|
const match = await cb(str);
|
|
|
|
if (match) return { type: "message", str, userId: msg.author.id, messageInfo };
|
|
|
|
if (match) return { type: "message", str, userId: msg.user_id, messageInfo };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.match_embeds && msg.embeds.length) {
|
|
|
|
if (trigger.match_embeds && msg.data.embeds && msg.data.embeds.length) {
|
|
|
|
const str = JSON.stringify(msg.embeds[0]);
|
|
|
|
const str = JSON.stringify(msg.data.embeds[0]);
|
|
|
|
const match = await cb(str);
|
|
|
|
const match = await cb(str);
|
|
|
|
if (match) return { type: "embed", str, userId: msg.author.id, messageInfo };
|
|
|
|
if (match) return { type: "embed", str, userId: msg.user_id, messageInfo };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.match_visible_names) {
|
|
|
|
|
|
|
|
const str = member.nick || msg.data.author.username;
|
|
|
|
|
|
|
|
const match = await cb(str);
|
|
|
|
|
|
|
|
if (match) return { type: "visiblename", str, userId: msg.user_id };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.match_usernames) {
|
|
|
|
if (trigger.match_usernames) {
|
|
|
|
const str = `${msg.author.username}#${msg.author.discriminator}`;
|
|
|
|
const str = `${msg.data.author.username}#${msg.data.author.discriminator}`;
|
|
|
|
const match = await cb(str);
|
|
|
|
const match = await cb(str);
|
|
|
|
if (match) return { type: "username", str, userId: msg.author.id };
|
|
|
|
if (match) return { type: "username", str, userId: msg.user_id };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.match_nicknames && msg.member.nick) {
|
|
|
|
if (trigger.match_nicknames && member.nick) {
|
|
|
|
const str = msg.member.nick;
|
|
|
|
const str = member.nick;
|
|
|
|
const match = await cb(str);
|
|
|
|
const match = await cb(str);
|
|
|
|
if (match) return { type: "nickname", str, userId: msg.author.id };
|
|
|
|
if (match) return { type: "nickname", str, userId: msg.user_id };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
@ -556,8 +726,10 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
protected async matchRuleToMessage(
|
|
|
|
protected async matchRuleToMessage(
|
|
|
|
rule: TRule,
|
|
|
|
rule: TRule,
|
|
|
|
msg: Message,
|
|
|
|
msg: SavedMessage,
|
|
|
|
): Promise<TextTriggerMatchResult | TextSpamTriggerMatchResult> {
|
|
|
|
): Promise<TextTriggerMatchResult | TextSpamTriggerMatchResult> {
|
|
|
|
|
|
|
|
if (!rule.enabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
for (const trigger of rule.triggers) {
|
|
|
|
for (const trigger of rule.triggers) {
|
|
|
|
if (trigger.match_words) {
|
|
|
|
if (trigger.match_words) {
|
|
|
|
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_words, msg, str => {
|
|
|
|
const match = await this.matchMultipleTextTypesOnMessage(trigger.match_words, msg, str => {
|
|
|
@ -587,38 +759,38 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.max_messages) {
|
|
|
|
if (trigger.message_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Message, trigger.max_messages, msg);
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Message, trigger.message_spam, msg);
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.max_mentions) {
|
|
|
|
if (trigger.mention_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Mention, trigger.max_mentions, msg);
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Mention, trigger.mention_spam, msg);
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.max_links) {
|
|
|
|
if (trigger.link_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Link, trigger.max_links, msg);
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Link, trigger.link_spam, msg);
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.max_attachments) {
|
|
|
|
if (trigger.attachment_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Attachment, trigger.max_attachments, msg);
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Attachment, trigger.attachment_spam, msg);
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.max_emojis) {
|
|
|
|
if (trigger.emoji_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Emoji, trigger.max_emojis, msg);
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Emoji, trigger.emoji_spam, msg);
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.max_lines) {
|
|
|
|
if (trigger.line_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Line, trigger.max_lines, msg);
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Line, trigger.line_spam, msg);
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (trigger.max_characters) {
|
|
|
|
if (trigger.character_spam) {
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Character, trigger.max_characters, msg);
|
|
|
|
const match = this.matchTextSpamTrigger(RecentActionType.Character, trigger.character_spam, msg);
|
|
|
|
if (match) return match;
|
|
|
|
if (match) return match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -626,23 +798,45 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
return null;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected async addRecentMessageAction(action: TextRecentAction) {
|
|
|
|
|
|
|
|
const gracePeriodKey = `${action.identifier}-${action.type}`;
|
|
|
|
|
|
|
|
if (this.spamGracePeriods.has(gracePeriodKey)) {
|
|
|
|
|
|
|
|
// If we're on spam detection grace period, just delete the message
|
|
|
|
|
|
|
|
if (!this.recentlyDeletedMessages.includes(action.messageInfo.messageId)) {
|
|
|
|
|
|
|
|
this.bot.deleteMessage(action.messageInfo.channelId, action.messageInfo.messageId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.recentlyDeletedMessages.push(action.messageInfo.messageId);
|
|
|
|
|
|
|
|
if (this.recentlyDeletedMessages.length > MAX_RECENTLY_DELETED_MESSAGES) {
|
|
|
|
|
|
|
|
this.recentlyDeletedMessages.splice(0, this.recentlyDeletedMessages.length - MAX_RECENTLY_DELETED_MESSAGES);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.recentActions.push({
|
|
|
|
|
|
|
|
...action,
|
|
|
|
|
|
|
|
expiresAt: Date.now() + RECENT_ACTION_EXPIRY_TIME,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Logs recent actions for spam detection purposes
|
|
|
|
* Logs recent actions for spam detection purposes
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
protected async logRecentActionsForMessage(msg: Message) {
|
|
|
|
protected async logRecentActionsForMessage(msg: SavedMessage) {
|
|
|
|
const timestamp = msg.timestamp;
|
|
|
|
const timestamp = moment.utc(msg.posted_at).valueOf();
|
|
|
|
const globalIdentifier = msg.author.id;
|
|
|
|
const globalIdentifier = msg.user_id;
|
|
|
|
const perChannelIdentifier = `${msg.channel.id}-${msg.author.id}`;
|
|
|
|
const perChannelIdentifier = `${msg.channel_id}-${msg.user_id}`;
|
|
|
|
const messageInfo: MessageInfo = { channelId: msg.channel.id, messageId: msg.id };
|
|
|
|
const messageInfo: MessageInfo = { channelId: msg.channel_id, messageId: msg.id };
|
|
|
|
|
|
|
|
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Message,
|
|
|
|
type: RecentActionType.Message,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
count: 1,
|
|
|
|
count: 1,
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Message,
|
|
|
|
type: RecentActionType.Message,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
@ -650,16 +844,17 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const mentionCount = getUserMentions(msg.content || "").length + getRoleMentions(msg.content || "").length;
|
|
|
|
const mentionCount =
|
|
|
|
|
|
|
|
getUserMentions(msg.data.content || "").length + getRoleMentions(msg.data.content || "").length;
|
|
|
|
if (mentionCount) {
|
|
|
|
if (mentionCount) {
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Mention,
|
|
|
|
type: RecentActionType.Mention,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
count: mentionCount,
|
|
|
|
count: mentionCount,
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Mention,
|
|
|
|
type: RecentActionType.Mention,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
@ -668,16 +863,16 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const linkCount = getUrlsInString(msg.content || "").length;
|
|
|
|
const linkCount = getUrlsInString(msg.data.content || "").length;
|
|
|
|
if (linkCount) {
|
|
|
|
if (linkCount) {
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Link,
|
|
|
|
type: RecentActionType.Link,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
count: linkCount,
|
|
|
|
count: linkCount,
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Link,
|
|
|
|
type: RecentActionType.Link,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
@ -686,16 +881,16 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const attachmentCount = msg.attachments.length;
|
|
|
|
const attachmentCount = msg.data.attachments && msg.data.attachments.length;
|
|
|
|
if (attachmentCount) {
|
|
|
|
if (attachmentCount) {
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Attachment,
|
|
|
|
type: RecentActionType.Attachment,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
count: attachmentCount,
|
|
|
|
count: attachmentCount,
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Attachment,
|
|
|
|
type: RecentActionType.Attachment,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
@ -704,16 +899,16 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const emojiCount = getEmojiInString(msg.content || "").length;
|
|
|
|
const emojiCount = getEmojiInString(msg.data.content || "").length;
|
|
|
|
if (emojiCount) {
|
|
|
|
if (emojiCount) {
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Emoji,
|
|
|
|
type: RecentActionType.Emoji,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
count: emojiCount,
|
|
|
|
count: emojiCount,
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Emoji,
|
|
|
|
type: RecentActionType.Emoji,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
@ -723,16 +918,16 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// + 1 is for the first line of the message (which doesn't have a line break)
|
|
|
|
// + 1 is for the first line of the message (which doesn't have a line break)
|
|
|
|
const lineCount = msg.content ? msg.content.match(/\n/g).length + 1 : 0;
|
|
|
|
const lineCount = msg.data.content ? (msg.data.content.match(/\n/g) || []).length + 1 : 0;
|
|
|
|
if (lineCount) {
|
|
|
|
if (lineCount) {
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Line,
|
|
|
|
type: RecentActionType.Line,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
count: lineCount,
|
|
|
|
count: lineCount,
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Line,
|
|
|
|
type: RecentActionType.Line,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
@ -741,16 +936,16 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const characterCount = [...(msg.content || "")].length;
|
|
|
|
const characterCount = [...(msg.data.content || "")].length;
|
|
|
|
if (characterCount) {
|
|
|
|
if (characterCount) {
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Character,
|
|
|
|
type: RecentActionType.Character,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
identifier: globalIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
count: characterCount,
|
|
|
|
count: characterCount,
|
|
|
|
messageInfo,
|
|
|
|
messageInfo,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
this.recentActions.push({
|
|
|
|
this.addRecentMessageAction({
|
|
|
|
type: RecentActionType.Character,
|
|
|
|
type: RecentActionType.Character,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
identifier: perChannelIdentifier,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
@ -766,37 +961,132 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected async applyActionsOnMatch(rule: TRule, matchResult: AnyTriggerMatchResult) {
|
|
|
|
protected async activateGracePeriod(matchResult: TextSpamTriggerMatchResult) {
|
|
|
|
if (rule.actions.clean) {
|
|
|
|
const expiresAt = Date.now() + SPAM_GRACE_PERIOD_LENGTH;
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed") {
|
|
|
|
|
|
|
|
await this.bot.deleteMessage(matchResult.messageInfo.channelId, matchResult.messageInfo.messageId).catch(noop);
|
|
|
|
// Global identifier
|
|
|
|
} else if (matchResult.type === "textspam" || matchResult.type === "raidspam") {
|
|
|
|
this.spamGracePeriods.set(`${matchResult.userId}-${matchResult.actionType}`, { expiresAt, deletedMessages: [] });
|
|
|
|
for (const { channelId, messageId } of matchResult.messageInfos) {
|
|
|
|
// Per-channel identifier
|
|
|
|
await this.bot.deleteMessage(channelId, messageId).catch(noop);
|
|
|
|
this.spamGracePeriods.set(`${matchResult.channelId}-${matchResult.userId}-${matchResult.actionType}`, {
|
|
|
|
}
|
|
|
|
expiresAt,
|
|
|
|
|
|
|
|
deletedMessages: [],
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected async clearExpiredGracePeriods() {
|
|
|
|
|
|
|
|
for (const [key, info] of this.spamGracePeriods.entries()) {
|
|
|
|
|
|
|
|
if (info.expiresAt <= Date.now()) {
|
|
|
|
|
|
|
|
this.spamGracePeriods.delete(key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected async clearOldRecentActions() {
|
|
|
|
|
|
|
|
this.recentActions = this.recentActions.filter(info => {
|
|
|
|
|
|
|
|
return info.expiresAt <= Date.now();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected async clearExpiredRecentNicknameChanges() {
|
|
|
|
|
|
|
|
for (const [key, info] of this.recentNicknameChanges.entries()) {
|
|
|
|
|
|
|
|
if (info.expiresAt <= Date.now()) {
|
|
|
|
|
|
|
|
this.recentNicknameChanges.delete(key);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected async clearSpecificRecentActions(type: RecentActionType, identifier: string) {
|
|
|
|
|
|
|
|
this.recentActions = this.recentActions.filter(info => {
|
|
|
|
|
|
|
|
return !(info.type === type && info.identifier === identifier);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected async applyActionsOnMatch(rule: TRule, matchResult: AnyTriggerMatchResult) {
|
|
|
|
|
|
|
|
const actionsTaken = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let matchSummary = null;
|
|
|
|
|
|
|
|
let caseExtraNote = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type === "textspam") {
|
|
|
|
|
|
|
|
this.activateGracePeriod(matchResult);
|
|
|
|
|
|
|
|
this.clearSpecificRecentActions(
|
|
|
|
|
|
|
|
matchResult.actionType,
|
|
|
|
|
|
|
|
matchResult.channelId ? `${matchResult.channelId}-${matchResult.userId}` : matchResult.userId,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Match summary
|
|
|
|
|
|
|
|
let matchedMessageIds = [];
|
|
|
|
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed") {
|
|
|
|
|
|
|
|
matchedMessageIds = [matchResult.messageInfo.messageId];
|
|
|
|
|
|
|
|
} else if (matchResult.type === "textspam" || matchResult.type === "raidspam") {
|
|
|
|
|
|
|
|
matchedMessageIds = matchResult.messageInfos.map(m => m.messageId);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (matchedMessageIds.length > 1) {
|
|
|
|
|
|
|
|
const savedMessages = await this.savedMessages.getMultiple(matchedMessageIds);
|
|
|
|
|
|
|
|
const archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild);
|
|
|
|
|
|
|
|
const baseUrl = this.knub.getGlobalConfig().url;
|
|
|
|
|
|
|
|
const archiveUrl = this.archives.getUrl(baseUrl, archiveId);
|
|
|
|
|
|
|
|
matchSummary = `Deleted messages: <${archiveUrl}>`;
|
|
|
|
|
|
|
|
} else if (matchedMessageIds.length === 1) {
|
|
|
|
|
|
|
|
const message = await this.savedMessages.find(matchedMessageIds[0]);
|
|
|
|
|
|
|
|
matchSummary = `Deleted message:\n${messageSummary(message)}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type === "username") {
|
|
|
|
|
|
|
|
matchSummary = `Matched username: ${matchResult.str}`;
|
|
|
|
|
|
|
|
} else if (matchResult.type === "nickname") {
|
|
|
|
|
|
|
|
matchSummary = `Matched nickname: ${matchResult.str}`;
|
|
|
|
|
|
|
|
} else if (matchResult.type === "visiblename") {
|
|
|
|
|
|
|
|
matchSummary = `Matched visible name: ${matchResult.str}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
caseExtraNote = `Matched automod rule "${rule.name}"`;
|
|
|
|
|
|
|
|
if (matchSummary) {
|
|
|
|
|
|
|
|
caseExtraNote += `\n${matchSummary}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
|
|
|
if (rule.actions.clean) {
|
|
|
|
|
|
|
|
const messagesToDelete: Array<{ channelId: string; messageId: string }> = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed") {
|
|
|
|
|
|
|
|
messagesToDelete.push(matchResult.messageInfo);
|
|
|
|
|
|
|
|
} else if (matchResult.type === "textspam" || matchResult.type === "raidspam") {
|
|
|
|
|
|
|
|
messagesToDelete.push(...matchResult.messageInfos);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const { channelId, messageId } of messagesToDelete) {
|
|
|
|
|
|
|
|
await this.bot.deleteMessage(channelId, messageId).catch(noop);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actionsTaken.push("clean");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.actions.warn) {
|
|
|
|
if (rule.actions.warn) {
|
|
|
|
const reason = rule.actions.mute.reason || "Warned automatically";
|
|
|
|
const reason = rule.actions.warn.reason || "Warned automatically";
|
|
|
|
|
|
|
|
|
|
|
|
const caseArgs = {
|
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
extraNotes: [`Matched automod rule "${rule.name}"`],
|
|
|
|
extraNotes: [caseExtraNote],
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
|
const member = await this.getMember(matchResult.userId);
|
|
|
|
const member = await this.getMember(matchResult.userId);
|
|
|
|
if (member) {
|
|
|
|
if (member) {
|
|
|
|
await this.modActions.warnMember(member, reason, caseArgs);
|
|
|
|
await this.getModActions().warnMember(member, reason, caseArgs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (matchResult.type === "raidspam") {
|
|
|
|
} else if (matchResult.type === "raidspam") {
|
|
|
|
for (const userId of matchResult.userIds) {
|
|
|
|
for (const userId of matchResult.userIds) {
|
|
|
|
const member = await this.getMember(userId);
|
|
|
|
const member = await this.getMember(userId);
|
|
|
|
if (member) {
|
|
|
|
if (member) {
|
|
|
|
await this.modActions.warnMember(member, reason, caseArgs);
|
|
|
|
await this.getModActions().warnMember(member, reason, caseArgs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actionsTaken.push("warn");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.actions.mute) {
|
|
|
|
if (rule.actions.mute) {
|
|
|
@ -804,7 +1094,7 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
const reason = rule.actions.mute.reason || "Muted automatically";
|
|
|
|
const reason = rule.actions.mute.reason || "Muted automatically";
|
|
|
|
const caseArgs = {
|
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
extraNotes: [`Matched automod rule "${rule.name}"`],
|
|
|
|
extraNotes: [caseExtraNote],
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
@ -814,60 +1104,115 @@ export class AutomodPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|
|
|
await this.mutes.muteUser(userId, duration, reason, caseArgs);
|
|
|
|
await this.mutes.muteUser(userId, duration, reason, caseArgs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actionsTaken.push("mute");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.actions.kick) {
|
|
|
|
if (rule.actions.kick) {
|
|
|
|
const reason = rule.actions.kick.reason || "Kicked automatically";
|
|
|
|
const reason = rule.actions.kick.reason || "Kicked automatically";
|
|
|
|
const caseArgs = {
|
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
extraNotes: [`Matched automod rule "${rule.name}"`],
|
|
|
|
extraNotes: [caseExtraNote],
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
|
const member = await this.getMember(matchResult.userId);
|
|
|
|
const member = await this.getMember(matchResult.userId);
|
|
|
|
if (member) {
|
|
|
|
if (member) {
|
|
|
|
await this.modActions.kickMember(member, reason, caseArgs);
|
|
|
|
await this.getModActions().kickMember(member, reason, caseArgs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (matchResult.type === "raidspam") {
|
|
|
|
} else if (matchResult.type === "raidspam") {
|
|
|
|
for (const userId of matchResult.userIds) {
|
|
|
|
for (const userId of matchResult.userIds) {
|
|
|
|
const member = await this.getMember(userId);
|
|
|
|
const member = await this.getMember(userId);
|
|
|
|
if (member) {
|
|
|
|
if (member) {
|
|
|
|
await this.modActions.kickMember(member, reason, caseArgs);
|
|
|
|
await this.getModActions().kickMember(member, reason, caseArgs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actionsTaken.push("kick");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.actions.ban) {
|
|
|
|
if (rule.actions.ban) {
|
|
|
|
const reason = rule.actions.ban.reason || "Banned automatically";
|
|
|
|
const reason = rule.actions.ban.reason || "Banned automatically";
|
|
|
|
const caseArgs = {
|
|
|
|
const caseArgs = {
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
modId: this.bot.user.id,
|
|
|
|
extraNotes: [`Matched automod rule "${rule.name}"`],
|
|
|
|
extraNotes: [caseExtraNote],
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
|
if (matchResult.type === "message" || matchResult.type === "embed" || matchResult.type === "textspam") {
|
|
|
|
await this.modActions.banUserId(matchResult.userId, reason, caseArgs);
|
|
|
|
await this.getModActions().banUserId(matchResult.userId, reason, caseArgs);
|
|
|
|
} else if (matchResult.type === "raidspam") {
|
|
|
|
} else if (matchResult.type === "raidspam") {
|
|
|
|
for (const userId of matchResult.userIds) {
|
|
|
|
for (const userId of matchResult.userIds) {
|
|
|
|
await this.modActions.banUserId(userId, reason, caseArgs);
|
|
|
|
await this.getModActions().banUserId(userId, reason, caseArgs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actionsTaken.push("ban");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.actions.alert) {
|
|
|
|
if (rule.actions.change_nickname) {
|
|
|
|
const text = rule.actions.alert.text;
|
|
|
|
const userIdsToChange =
|
|
|
|
this.logs.log(LogType.AUTOMOD_ALERT, { text });
|
|
|
|
matchResult.type === "raidspam" || matchResult.type === "otherspam"
|
|
|
|
|
|
|
|
? matchResult.userIds
|
|
|
|
|
|
|
|
: [matchResult.userId];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const userId of userIdsToChange) {
|
|
|
|
|
|
|
|
if (this.recentNicknameChanges.has(userId)) continue;
|
|
|
|
|
|
|
|
this.guild
|
|
|
|
|
|
|
|
.editMember(userId, {
|
|
|
|
|
|
|
|
nick: rule.actions.change_nickname.name,
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
|
|
this.getLogs().log(LogType.BOT_ALERT, {
|
|
|
|
|
|
|
|
body: `Failed to change the nickname of \`${userId}\``,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
this.recentNicknameChanges.set(userId, { expiresAt: RECENT_NICKNAME_CHANGE_EXPIRY_TIME });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actionsTaken.push("nickname");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.actions.alert || matchResult.type !== "raidspam") {
|
|
|
|
|
|
|
|
const user = await this.resolveUser((matchResult as any).userId || "0");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.actions.alert) {
|
|
|
|
|
|
|
|
const text = rule.actions.alert.text;
|
|
|
|
|
|
|
|
this.getLogs().log(LogType.AUTOMOD_ALERT, {
|
|
|
|
|
|
|
|
rule: rule.name,
|
|
|
|
|
|
|
|
user: stripObjectToScalars(user),
|
|
|
|
|
|
|
|
text,
|
|
|
|
|
|
|
|
matchSummary,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actionsTaken.push("alert");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (matchResult.type !== "raidspam") {
|
|
|
|
|
|
|
|
this.getLogs().log(LogType.AUTOMOD_ACTION, {
|
|
|
|
|
|
|
|
rule: rule.name,
|
|
|
|
|
|
|
|
user: stripObjectToScalars(user),
|
|
|
|
|
|
|
|
actionsTaken: actionsTaken.length ? actionsTaken.join(", ") : "<none>",
|
|
|
|
|
|
|
|
matchSummary,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@d.event("messageCreate")
|
|
|
|
protected onMessageCreate(msg: SavedMessage) {
|
|
|
|
protected onMessageCreate(msg: Message) {
|
|
|
|
if (msg.is_bot) return;
|
|
|
|
|
|
|
|
|
|
|
|
this.automodQueue.add(async () => {
|
|
|
|
this.automodQueue.add(async () => {
|
|
|
|
if (this.unloaded) return;
|
|
|
|
if (this.unloaded) return;
|
|
|
|
|
|
|
|
|
|
|
|
await this.logRecentActionsForMessage(msg);
|
|
|
|
await this.logRecentActionsForMessage(msg);
|
|
|
|
|
|
|
|
|
|
|
|
const config = this.getMatchingConfig({ message: msg });
|
|
|
|
const member = this.guild.members.get(msg.user_id);
|
|
|
|
|
|
|
|
const config = this.getMatchingConfig({
|
|
|
|
|
|
|
|
member,
|
|
|
|
|
|
|
|
userId: msg.user_id,
|
|
|
|
|
|
|
|
channelId: msg.channel_id,
|
|
|
|
|
|
|
|
});
|
|
|
|
for (const [name, rule] of Object.entries(config.rules)) {
|
|
|
|
for (const [name, rule] of Object.entries(config.rules)) {
|
|
|
|
const matchResult = await this.matchRuleToMessage(rule, msg);
|
|
|
|
const matchResult = await this.matchRuleToMessage(rule, msg);
|
|
|
|
if (matchResult) {
|
|
|
|
if (matchResult) {
|
|
|
|