diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index 6c8a2605..8d9e466b 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -24,7 +24,11 @@ import { RunAutomodOnJoinEvt, RunAutomodOnLeaveEvt } from "./events/RunAutomodOn import { RunAutomodOnMemberUpdate } from "./events/RunAutomodOnMemberUpdate"; import { runAutomodOnMessage } from "./events/runAutomodOnMessage"; import { runAutomodOnModAction } from "./events/runAutomodOnModAction"; -import { RunAutomodOnThreadCreate, RunAutomodOnThreadDelete } from "./events/runAutomodOnThreadEvents"; +import { + RunAutomodOnThreadCreate, + RunAutomodOnThreadDelete, + RunAutomodOnThreadUpdate, +} from "./events/runAutomodOnThreadEvents"; import { clearOldRecentNicknameChanges } from "./functions/clearOldNicknameChanges"; import { clearOldRecentActions } from "./functions/clearOldRecentActions"; import { clearOldRecentSpam } from "./functions/clearOldRecentSpam"; @@ -208,6 +212,7 @@ export const AutomodPlugin = zeppelinGuildPlugin()({ RunAutomodOnLeaveEvt, RunAutomodOnThreadCreate, RunAutomodOnThreadDelete, + RunAutomodOnThreadUpdate // Messages use message events from SavedMessages, see onLoad below ], diff --git a/backend/src/plugins/Automod/events/runAutomodOnThreadEvents.ts b/backend/src/plugins/Automod/events/runAutomodOnThreadEvents.ts index 7e7889e9..449b8ba5 100644 --- a/backend/src/plugins/Automod/events/runAutomodOnThreadEvents.ts +++ b/backend/src/plugins/Automod/events/runAutomodOnThreadEvents.ts @@ -2,6 +2,7 @@ import { typedGuildEventListener } from "knub"; import { RecentActionType } from "../constants"; import { GuildSavedMessages } from "../../../data/GuildSavedMessages"; import { runAutomod } from "../functions/runAutomod"; +import diff from "lodash.difference"; import { AutomodContext, AutomodPluginType } from "../types"; export const RunAutomodOnThreadCreate = typedGuildEventListener()({ @@ -67,3 +68,31 @@ export const RunAutomodOnThreadDelete = typedGuildEventListener()({ + event: "threadUpdate", + async listener({ pluginData, args: { oldThread, newThread: thread } }) { + const user = thread.ownerId ? await pluginData.client.users.fetch(thread.ownerId).catch(() => void 0) : void 0; + const changes: AutomodContext["threadChange"] = {}; + if (oldThread.archived !== thread.archived) { + changes.archived = thread.archived ? thread : void 0; + changes.unarchived = !thread.archived ? thread : void 0; + } + if (oldThread.locked !== thread.locked) { + changes.locked = thread.locked ? thread : void 0; + changes.unlocked = !thread.locked ? thread : void 0; + } + + if (Object.keys(changes).length === 0) return; + + const context: AutomodContext = { + timestamp: Date.now(), + threadChange: changes, + user, + }; + + pluginData.state.queue.add(() => { + runAutomod(pluginData, context); + }); + }, +}); diff --git a/backend/src/plugins/Automod/triggers/availableTriggers.ts b/backend/src/plugins/Automod/triggers/availableTriggers.ts index ece5949f..a3c7b709 100644 --- a/backend/src/plugins/Automod/triggers/availableTriggers.ts +++ b/backend/src/plugins/Automod/triggers/availableTriggers.ts @@ -32,6 +32,10 @@ import { ThreadDeleteTrigger } from "./threadDelete"; import { UnbanTrigger } from "./unban"; import { UnmuteTrigger } from "./unmute"; import { WarnTrigger } from "./warn"; +import { ThreadArchiveTrigger } from "./threadArchive"; +import { ThreadUnarchiveTrigger } from "./threadUnarchive"; +import { ThreadLockTrigger } from "./threadLock"; +import { ThreadUnlockTrigger } from "./threadUnlock"; export const availableTriggers: Record> = { any_message: AnyMessageTrigger, @@ -71,6 +75,10 @@ export const availableTriggers: Record thread_create: ThreadCreateTrigger, thread_delete: ThreadDeleteTrigger, + thread_archive: ThreadArchiveTrigger, + thread_unarchive: ThreadUnarchiveTrigger, + thread_lock: ThreadLockTrigger, + thread_unlock: ThreadUnlockTrigger, }; export const AvailableTriggers = t.type({ diff --git a/backend/src/plugins/Automod/triggers/threadArchive.ts b/backend/src/plugins/Automod/triggers/threadArchive.ts new file mode 100644 index 00000000..2f088ad8 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/threadArchive.ts @@ -0,0 +1,57 @@ +import { Snowflake } from "discord-api-types"; +import { User, Util } from "discord.js"; +import * as t from "io-ts"; +import { tNullable } from "../../../utils"; +import { automodTrigger } from "../helpers"; + +interface ThreadArchiveResult { + matchedThreadId: Snowflake; + matchedThreadName: string; + matchedThreadParentId: Snowflake; + matchedThreadParentName: string; + matchedThreadOwner: User | undefined; +} + +export const ThreadArchiveTrigger = automodTrigger()({ + configType: t.type({ + parent: tNullable(t.union([t.string, t.array(t.string)])), + }), + + defaultConfig: {}, + + async match({ context, triggerConfig }) { + if (!context.threadChange?.archived) { + return; + } + + const thread = context.threadChange.archived; + + if (triggerConfig.parent) { + const parentIds = Array.isArray(triggerConfig.parent) ? triggerConfig.parent : [triggerConfig.parent]; + if (thread.parentId && !parentIds.includes(thread.parentId)) return; + } + + return { + extra: { + matchedThreadId: thread.id, + matchedThreadName: thread.name, + matchedThreadParentId: thread.parentId ?? "Unknown", + matchedThreadParentName: thread.parent?.name ?? "Unknown", + matchedThreadOwner: context.user, + }, + }; + }, + + async renderMatchInformation({ matchResult }) { + const threadId = matchResult.extra.matchedThreadId; + const threadName = matchResult.extra.matchedThreadName; + const threadOwner = matchResult.extra.matchedThreadOwner; + const parentId = matchResult.extra.matchedThreadParentId; + const parentName = matchResult.extra.matchedThreadParentName; + const base = `Thread **#${threadName}** (\`${threadId}\`) has been archived in the **#${parentName}** (\`${parentId}\`) channel`; + if (threadOwner) { + return `${base} by **${Util.escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`; + } + return base; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/threadLock.ts b/backend/src/plugins/Automod/triggers/threadLock.ts new file mode 100644 index 00000000..e2497584 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/threadLock.ts @@ -0,0 +1,57 @@ +import { Snowflake } from "discord-api-types"; +import { User, Util } from "discord.js"; +import * as t from "io-ts"; +import { tNullable } from "../../../utils"; +import { automodTrigger } from "../helpers"; + +interface ThreadLockResult { + matchedThreadId: Snowflake; + matchedThreadName: string; + matchedThreadParentId: Snowflake; + matchedThreadParentName: string; + matchedThreadOwner: User | undefined; +} + +export const ThreadLockTrigger = automodTrigger()({ + configType: t.type({ + parent: tNullable(t.union([t.string, t.array(t.string)])), + }), + + defaultConfig: {}, + + async match({ context, triggerConfig }) { + if (!context.threadChange?.locked) { + return; + } + + const thread = context.threadChange.locked; + + if (triggerConfig.parent) { + const parentIds = Array.isArray(triggerConfig.parent) ? triggerConfig.parent : [triggerConfig.parent]; + if (thread.parentId && !parentIds.includes(thread.parentId)) return; + } + + return { + extra: { + matchedThreadId: thread.id, + matchedThreadName: thread.name, + matchedThreadParentId: thread.parentId ?? "Unknown", + matchedThreadParentName: thread.parent?.name ?? "Unknown", + matchedThreadOwner: context.user, + }, + }; + }, + + async renderMatchInformation({ matchResult }) { + const threadId = matchResult.extra.matchedThreadId; + const threadName = matchResult.extra.matchedThreadName; + const threadOwner = matchResult.extra.matchedThreadOwner; + const parentId = matchResult.extra.matchedThreadParentId; + const parentName = matchResult.extra.matchedThreadParentName; + const base = `Thread **#${threadName}** (\`${threadId}\`) has been locked in the **#${parentName}** (\`${parentId}\`) channel`; + if (threadOwner) { + return `${base} by **${Util.escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`; + } + return base; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/threadUnarchive.ts b/backend/src/plugins/Automod/triggers/threadUnarchive.ts new file mode 100644 index 00000000..eb5577c9 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/threadUnarchive.ts @@ -0,0 +1,57 @@ +import { Snowflake } from "discord-api-types"; +import { User, Util } from "discord.js"; +import * as t from "io-ts"; +import { tNullable } from "../../../utils"; +import { automodTrigger } from "../helpers"; + +interface ThreadUnarchiveResult { + matchedThreadId: Snowflake; + matchedThreadName: string; + matchedThreadParentId: Snowflake; + matchedThreadParentName: string; + matchedThreadOwner: User | undefined; +} + +export const ThreadUnarchiveTrigger = automodTrigger()({ + configType: t.type({ + parent: tNullable(t.union([t.string, t.array(t.string)])), + }), + + defaultConfig: {}, + + async match({ context, triggerConfig }) { + if (!context.threadChange?.unarchived) { + return; + } + + const thread = context.threadChange.unarchived; + + if (triggerConfig.parent) { + const parentIds = Array.isArray(triggerConfig.parent) ? triggerConfig.parent : [triggerConfig.parent]; + if (thread.parentId && !parentIds.includes(thread.parentId)) return; + } + + return { + extra: { + matchedThreadId: thread.id, + matchedThreadName: thread.name, + matchedThreadParentId: thread.parentId ?? "Unknown", + matchedThreadParentName: thread.parent?.name ?? "Unknown", + matchedThreadOwner: context.user, + }, + }; + }, + + async renderMatchInformation({ matchResult }) { + const threadId = matchResult.extra.matchedThreadId; + const threadName = matchResult.extra.matchedThreadName; + const threadOwner = matchResult.extra.matchedThreadOwner; + const parentId = matchResult.extra.matchedThreadParentId; + const parentName = matchResult.extra.matchedThreadParentName; + const base = `Thread **#${threadName}** (\`${threadId}\`) has been unarchived in the **#${parentName}** (\`${parentId}\`) channel`; + if (threadOwner) { + return `${base} by **${Util.escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`; + } + return base; + }, +}); diff --git a/backend/src/plugins/Automod/triggers/threadUnlock.ts b/backend/src/plugins/Automod/triggers/threadUnlock.ts new file mode 100644 index 00000000..96311246 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/threadUnlock.ts @@ -0,0 +1,57 @@ +import { Snowflake } from "discord-api-types"; +import { User, Util } from "discord.js"; +import * as t from "io-ts"; +import { tNullable } from "../../../utils"; +import { automodTrigger } from "../helpers"; + +interface ThreadUnlockResult { + matchedThreadId: Snowflake; + matchedThreadName: string; + matchedThreadParentId: Snowflake; + matchedThreadParentName: string; + matchedThreadOwner: User | undefined; +} + +export const ThreadUnlockTrigger = automodTrigger()({ + configType: t.type({ + parent: tNullable(t.union([t.string, t.array(t.string)])), + }), + + defaultConfig: {}, + + async match({ context, triggerConfig }) { + if (!context.threadChange?.unlocked) { + return; + } + + const thread = context.threadChange.unlocked; + + if (triggerConfig.parent) { + const parentIds = Array.isArray(triggerConfig.parent) ? triggerConfig.parent : [triggerConfig.parent]; + if (thread.parentId && !parentIds.includes(thread.parentId)) return; + } + + return { + extra: { + matchedThreadId: thread.id, + matchedThreadName: thread.name, + matchedThreadParentId: thread.parentId ?? "Unknown", + matchedThreadParentName: thread.parent?.name ?? "Unknown", + matchedThreadOwner: context.user, + }, + }; + }, + + async renderMatchInformation({ matchResult }) { + const threadId = matchResult.extra.matchedThreadId; + const threadName = matchResult.extra.matchedThreadName; + const threadOwner = matchResult.extra.matchedThreadOwner; + const parentId = matchResult.extra.matchedThreadParentId; + const parentName = matchResult.extra.matchedThreadParentName; + const base = `Thread **#${threadName}** (\`${threadId}\`) has been locked in the **#${parentName}** (\`${parentId}\`) channel`; + if (threadOwner) { + return `${base} by **${Util.escapeBold(threadOwner.tag)}** (\`${threadOwner.id}\`)`; + } + return base; + }, +}); diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index 7ae40f30..ed7cf56e 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -134,6 +134,10 @@ export interface AutomodContext { threadChange?: { created?: ThreadChannel; deleted?: ThreadChannel; + archived?: ThreadChannel; + unarchived?: ThreadChannel; + locked?: ThreadChannel; + unlocked?: ThreadChannel; }; channel?: TextChannel | ThreadChannel; }