diff --git a/backend/src/plugins/Automod/events/RunAutomodOnJoinEvt.ts b/backend/src/plugins/Automod/events/RunAutomodOnJoinEvt.ts index 83786f12..596feda6 100644 --- a/backend/src/plugins/Automod/events/RunAutomodOnJoinEvt.ts +++ b/backend/src/plugins/Automod/events/RunAutomodOnJoinEvt.ts @@ -1,7 +1,7 @@ -import { SavedMessage } from "../../../data/entities/SavedMessage"; -import { eventListener, PluginData } from "knub"; +import { eventListener } from "knub"; import { AutomodContext, AutomodPluginType } from "../types"; import { runAutomod } from "../functions/runAutomod"; +import { RecentActionType } from "../constants"; export const RunAutomodOnJoinEvt = eventListener()( "guildMemberAdd", @@ -9,8 +9,19 @@ export const RunAutomodOnJoinEvt = eventListener()( const context: AutomodContext = { timestamp: Date.now(), user: member.user, + member, + joined: true, }; - pluginData.state.queue.add(() => runAutomod(pluginData, context)); + pluginData.state.queue.add(() => { + pluginData.state.recentActions.push({ + type: RecentActionType.MemberJoin, + context, + count: 1, + identifier: null, + }); + + runAutomod(pluginData, context); + }); }, ); diff --git a/backend/src/plugins/Automod/events/runAutomodOnMessage.ts b/backend/src/plugins/Automod/events/runAutomodOnMessage.ts index d90872b4..2fb7e632 100644 --- a/backend/src/plugins/Automod/events/runAutomodOnMessage.ts +++ b/backend/src/plugins/Automod/events/runAutomodOnMessage.ts @@ -6,9 +6,14 @@ import { addRecentActionsFromMessage } from "../functions/addRecentActionsFromMe import moment from "moment-timezone"; export function runAutomodOnMessage(pluginData: PluginData, message: SavedMessage, isEdit: boolean) { + const user = pluginData.client.users.get(message.user_id); + const member = pluginData.guild.members.get(message.user_id); + const context: AutomodContext = { timestamp: moment.utc(message.posted_at).valueOf(), message, + user, + member, }; pluginData.state.queue.add(async () => { diff --git a/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts b/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts index ad88d00d..91f4ada5 100644 --- a/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts +++ b/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts @@ -1,4 +1,3 @@ -import moment from "moment-timezone"; import { AutomodContext, AutomodPluginType } from "../types"; import { PluginData } from "knub"; import { RECENT_ACTION_EXPIRY_TIME, RecentActionType } from "../constants"; diff --git a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts index 557736f6..29222c41 100644 --- a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts +++ b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts @@ -52,7 +52,7 @@ export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: pluginData.state.recentSpam.push({ type: spamType, - userId: context.message.user_id, + userIds: [context.message.user_id], archiveId, timestamp: Date.now(), }); diff --git a/backend/src/plugins/Automod/functions/findRecentSpam.ts b/backend/src/plugins/Automod/functions/findRecentSpam.ts index a22b91b5..1531574f 100644 --- a/backend/src/plugins/Automod/functions/findRecentSpam.ts +++ b/backend/src/plugins/Automod/functions/findRecentSpam.ts @@ -2,8 +2,8 @@ import { PluginData } from "knub"; import { AutomodPluginType } from "../types"; import { RecentActionType } from "../constants"; -export function findRecentSpam(pluginData: PluginData, type: RecentActionType, userId: string) { +export function findRecentSpam(pluginData: PluginData, type: RecentActionType, userId?: string) { return pluginData.state.recentSpam.find(spam => { - return spam.type === type && spam.userId === userId; + return spam.type === type && (!userId || spam.userIds.includes(userId)); }); } diff --git a/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts b/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts index 9197e24d..70ed5472 100644 --- a/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts +++ b/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts @@ -7,8 +7,10 @@ export function getMatchingRecentActions( type: RecentActionType, identifier: string | null, since: number, - to: number, + to?: number, ) { + to = to || Date.now(); + return pluginData.state.recentActions.filter(action => { return ( action.type === type && diff --git a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts index 3ac81fee..16fb72d9 100644 --- a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts +++ b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts @@ -1,4 +1,3 @@ -import * as t from "io-ts"; import { SavedMessage } from "../../../data/entities/SavedMessage"; import { resolveMember } from "../../../utils"; import { PluginData } from "knub"; diff --git a/backend/src/plugins/Automod/functions/runAutomod.ts b/backend/src/plugins/Automod/functions/runAutomod.ts index 41ae909a..f3bcde40 100644 --- a/backend/src/plugins/Automod/functions/runAutomod.ts +++ b/backend/src/plugins/Automod/functions/runAutomod.ts @@ -1,5 +1,5 @@ import { PluginData } from "knub"; -import { AutomodContext, AutomodPluginType, TRule } from "../types"; +import { AutomodContext, AutomodPluginType } from "../types"; import { availableTriggers } from "../triggers/availableTriggers"; import { availableActions } from "../actions/availableActions"; import { AutomodTriggerMatchResult } from "../helpers"; diff --git a/backend/src/plugins/Automod/functions/sumRecentActionCounts.ts b/backend/src/plugins/Automod/functions/sumRecentActionCounts.ts new file mode 100644 index 00000000..021e44e6 --- /dev/null +++ b/backend/src/plugins/Automod/functions/sumRecentActionCounts.ts @@ -0,0 +1,5 @@ +import { RecentAction } from "../types"; + +export function sumRecentActionCounts(actions: RecentAction[]) { + return actions.reduce((total, action) => total + action.count, 0); +} diff --git a/backend/src/plugins/Automod/helpers.ts b/backend/src/plugins/Automod/helpers.ts index ebec79c2..e654e1ef 100644 --- a/backend/src/plugins/Automod/helpers.ts +++ b/backend/src/plugins/Automod/helpers.ts @@ -7,7 +7,7 @@ export interface AutomodTriggerMatchResult { extraContexts?: AutomodContext[]; extra?: TExtra; - silentClean?: boolean; + silentClean?: boolean; // TODO: Maybe generalize to a "silent" value in general, which mutes alert/log } type AutomodTriggerMatchFn = (meta: { diff --git a/backend/src/plugins/Automod/triggers/availableTriggers.ts b/backend/src/plugins/Automod/triggers/availableTriggers.ts index 6f858ed6..85e8cb6b 100644 --- a/backend/src/plugins/Automod/triggers/availableTriggers.ts +++ b/backend/src/plugins/Automod/triggers/availableTriggers.ts @@ -12,6 +12,8 @@ import { MatchRegexTrigger } from "./matchRegex"; import { MatchInvitesTrigger } from "./matchInvites"; import { MatchLinksTrigger } from "./matchLinks"; import { MatchAttachmentTypeTrigger } from "./matchAttachmentType"; +import { MemberJoinSpamTrigger } from "./memberJoinSpam"; +import { MemberJoinTrigger } from "./memberJoin"; export const availableTriggers: Record> = { match_words: MatchWordsTrigger, @@ -19,6 +21,7 @@ export const availableTriggers: Record match_invites: MatchInvitesTrigger, match_links: MatchLinksTrigger, match_attachment_type: MatchAttachmentTypeTrigger, + member_join: MemberJoinTrigger, message_spam: MessageSpamTrigger, mention_spam: MentionSpamTrigger, @@ -27,6 +30,7 @@ export const availableTriggers: Record emoji_spam: EmojiSpamTrigger, line_spam: LineSpamTrigger, character_spam: CharacterSpamTrigger, + member_join_spam: MemberJoinSpamTrigger, }; export const AvailableTriggers = t.type({ @@ -35,6 +39,7 @@ export const AvailableTriggers = t.type({ match_invites: MatchInvitesTrigger.configType, match_links: MatchLinksTrigger.configType, match_attachment_type: MatchAttachmentTypeTrigger.configType, + member_join: MemberJoinTrigger.configType, message_spam: MessageSpamTrigger.configType, mention_spam: MentionSpamTrigger.configType, @@ -43,4 +48,5 @@ export const AvailableTriggers = t.type({ emoji_spam: EmojiSpamTrigger.configType, line_spam: LineSpamTrigger.configType, character_spam: CharacterSpamTrigger.configType, + member_join_spam: MemberJoinSpamTrigger.configType, }); diff --git a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts index 68f96fcf..4fdf8887 100644 --- a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts +++ b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts @@ -1,19 +1,6 @@ import * as t from "io-ts"; -import { transliterate } from "transliteration"; -import escapeStringRegexp from "escape-string-regexp"; -import { AnyInvite, Attachment, GuildInvite } from "eris"; import { automodTrigger } from "../helpers"; -import { - asSingleLine, - disableCodeBlocks, - disableInlineCode, - getInviteCodesInString, - isGuildInvite, - resolveInvite, - tNullable, - verboseChannelMention, -} from "../../../utils"; -import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; +import { asSingleLine, disableCodeBlocks, disableInlineCode, verboseChannelMention } from "../../../utils"; interface MatchResultType { matchedType: string; diff --git a/backend/src/plugins/Automod/triggers/matchInvites.ts b/backend/src/plugins/Automod/triggers/matchInvites.ts index 5617b58e..0f1d3d94 100644 --- a/backend/src/plugins/Automod/triggers/matchInvites.ts +++ b/backend/src/plugins/Automod/triggers/matchInvites.ts @@ -1,12 +1,8 @@ import * as t from "io-ts"; -import { transliterate } from "transliteration"; -import escapeStringRegexp from "escape-string-regexp"; -import { AnyInvite, GuildInvite } from "eris"; +import { GuildInvite } from "eris"; import { automodTrigger } from "../helpers"; import { - asSingleLine, disableCodeBlocks, - disableInlineCode, getInviteCodesInString, isGuildInvite, resolveInvite, diff --git a/backend/src/plugins/Automod/triggers/matchLinks.ts b/backend/src/plugins/Automod/triggers/matchLinks.ts index c18ad190..c5235e3e 100644 --- a/backend/src/plugins/Automod/triggers/matchLinks.ts +++ b/backend/src/plugins/Automod/triggers/matchLinks.ts @@ -1,16 +1,11 @@ import * as t from "io-ts"; -import { transliterate } from "transliteration"; import escapeStringRegexp from "escape-string-regexp"; -import { AnyInvite, GuildInvite } from "eris"; import { automodTrigger } from "../helpers"; import { asSingleLine, disableCodeBlocks, disableInlineCode, - getInviteCodesInString, getUrlsInString, - isGuildInvite, - resolveInvite, tNullable, verboseChannelMention, } from "../../../utils"; diff --git a/backend/src/plugins/Automod/triggers/matchRegex.ts b/backend/src/plugins/Automod/triggers/matchRegex.ts index f76457ee..fdfd3f06 100644 --- a/backend/src/plugins/Automod/triggers/matchRegex.ts +++ b/backend/src/plugins/Automod/triggers/matchRegex.ts @@ -1,6 +1,5 @@ import * as t from "io-ts"; import { transliterate } from "transliteration"; -import escapeStringRegexp from "escape-string-regexp"; import { automodTrigger } from "../helpers"; import { disableInlineCode, verboseChannelMention } from "../../../utils"; import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage"; diff --git a/backend/src/plugins/Automod/triggers/memberJoin.ts b/backend/src/plugins/Automod/triggers/memberJoin.ts new file mode 100644 index 00000000..3efca615 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/memberJoin.ts @@ -0,0 +1,34 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; +import { convertDelayStringToMS, tDelayString } from "../../../utils"; + +export const MemberJoinTrigger = automodTrigger()({ + configType: t.type({ + only_new: t.boolean, + new_threshold: tDelayString, + }), + + defaultConfig: { + only_new: false, + new_threshold: "1h", + }, + + async match({ pluginData, context, triggerConfig }) { + if (!context.joined || !context.member) { + return; + } + + if (triggerConfig.only_new) { + const threshold = Date.now() - convertDelayStringToMS(triggerConfig.new_threshold); + if (context.member.createdAt >= threshold) { + return {}; + } + } + + return {}; + }, + + renderMatchInformation({ pluginData, contexts, triggerConfig }) { + return null; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/memberJoinSpam.ts b/backend/src/plugins/Automod/triggers/memberJoinSpam.ts new file mode 100644 index 00000000..bd678fbe --- /dev/null +++ b/backend/src/plugins/Automod/triggers/memberJoinSpam.ts @@ -0,0 +1,55 @@ +import * as t from "io-ts"; +import { automodTrigger } from "../helpers"; +import { convertDelayStringToMS, tDelayString } from "../../../utils"; +import { getMatchingRecentActions } from "../functions/getMatchingRecentActions"; +import { RecentActionType } from "../constants"; +import { sumRecentActionCounts } from "../functions/sumRecentActionCounts"; +import { findRecentSpam } from "../functions/findRecentSpam"; + +export const MemberJoinSpamTrigger = automodTrigger()({ + configType: t.type({ + amount: t.number, + within: tDelayString, + }), + + defaultConfig: {}, + + async match({ pluginData, context, triggerConfig }) { + if (!context.joined || !context.member) { + return; + } + + const recentSpam = findRecentSpam(pluginData, RecentActionType.MemberJoin); + if (recentSpam) { + context.actioned = true; + return {}; + } + + const since = Date.now() - convertDelayStringToMS(triggerConfig.within); + const matchingActions = getMatchingRecentActions(pluginData, RecentActionType.MemberJoin, null, since); + const totalCount = sumRecentActionCounts(matchingActions); + + if (totalCount >= triggerConfig.amount) { + const contexts = [context, ...matchingActions.map(a => a.context).filter(c => c !== context)]; + + for (const _context of contexts) { + _context.actioned = true; + } + + pluginData.state.recentSpam.push({ + type: RecentActionType.MemberJoin, + timestamp: Date.now(), + archiveId: null, + userIds: [], + }); + + return { + extraContexts: contexts, + }; + } + }, + + renderMatchInformation({ pluginData, contexts, triggerConfig }) { + return null; + }, +}); diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index e8b51ad2..43c95031 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -4,7 +4,7 @@ import { BasePluginType } from "knub"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { GuildLogs } from "../../data/GuildLogs"; import { SavedMessage } from "../../data/entities/SavedMessage"; -import { User } from "eris"; +import { Member, User } from "eris"; import { AvailableTriggers } from "./triggers/availableTriggers"; import { AvailableActions } from "./actions/availableActions"; import { Queue } from "../../Queue"; @@ -71,6 +71,8 @@ export interface AutomodContext { user?: User | UnknownUser; message?: SavedMessage; + member?: Member; + joined?: boolean; } export interface RecentAction { @@ -83,6 +85,6 @@ export interface RecentAction { export interface RecentSpam { archiveId: string; type: RecentActionType; - userId: string; + userIds: string[]; timestamp: number; }