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 3f5a71c1..2687778e 100644 --- a/backend/src/plugins/Automod/events/runAutomodOnThreadEvents.ts +++ b/backend/src/plugins/Automod/events/runAutomodOnThreadEvents.ts @@ -53,3 +53,35 @@ 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(() => undefined) + : undefined; + + const changes: AutomodContext["threadChange"] = {}; + if (oldThread.archived !== thread.archived) { + changes.archived = thread.archived ? thread : undefined; + changes.unarchived = !thread.archived ? thread : undefined; + } + if (oldThread.locked !== thread.locked) { + changes.locked = thread.locked ? thread : undefined; + changes.unlocked = !thread.locked ? thread : undefined; + } + + if (Object.keys(changes).length === 0) return; + + const context: AutomodContext = { + timestamp: Date.now(), + threadChange: changes, + user, + channel: thread, + }; + + 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..ea8b6f04 100644 --- a/backend/src/plugins/Automod/triggers/availableTriggers.ts +++ b/backend/src/plugins/Automod/triggers/availableTriggers.ts @@ -32,6 +32,8 @@ import { ThreadDeleteTrigger } from "./threadDelete"; import { UnbanTrigger } from "./unban"; import { UnmuteTrigger } from "./unmute"; import { WarnTrigger } from "./warn"; +import { ThreadArchiveTrigger } from "./threadArchive"; +import { ThreadUnarchiveTrigger } from "./threadUnarchive"; export const availableTriggers: Record> = { any_message: AnyMessageTrigger, @@ -71,6 +73,8 @@ export const availableTriggers: Record thread_create: ThreadCreateTrigger, thread_delete: ThreadDeleteTrigger, + thread_archive: ThreadArchiveTrigger, + thread_unarchive: ThreadUnarchiveTrigger, }; export const AvailableTriggers = t.type({ @@ -112,4 +116,6 @@ export const AvailableTriggers = t.type({ thread_create: ThreadCreateTrigger.configType, thread_delete: ThreadDeleteTrigger.configType, + thread_archive: ThreadArchiveTrigger.configType, + thread_unarchive: ThreadUnarchiveTrigger.configType, }); diff --git a/backend/src/plugins/Automod/triggers/threadArchive.ts b/backend/src/plugins/Automod/triggers/threadArchive.ts new file mode 100644 index 00000000..40cd43df --- /dev/null +++ b/backend/src/plugins/Automod/triggers/threadArchive.ts @@ -0,0 +1,56 @@ +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({ + locked: tNullable(t.boolean), + }), + + defaultConfig: {}, + + async match({ context, triggerConfig }) { + if (!context.threadChange?.archived) { + return; + } + + const thread = context.threadChange.archived; + + if (typeof triggerConfig.locked === "boolean" && thread.locked !== triggerConfig.locked) { + 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/threadUnarchive.ts b/backend/src/plugins/Automod/triggers/threadUnarchive.ts new file mode 100644 index 00000000..05e729f1 --- /dev/null +++ b/backend/src/plugins/Automod/triggers/threadUnarchive.ts @@ -0,0 +1,56 @@ +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({ + locked: tNullable(t.boolean), + }), + + defaultConfig: {}, + + async match({ context, triggerConfig }) { + if (!context.threadChange?.unarchived) { + return; + } + + const thread = context.threadChange.unarchived; + + if (typeof triggerConfig.locked === "boolean" && thread.locked !== triggerConfig.locked) { + 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/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; }