feat(automod): init
vi v@vt3e.cat
Tue, 07 Apr 2026 02:26:13 +0100
2 files changed,
105 insertions(+),
0 deletions(-)
A
src/automod/index.ts
@@ -0,0 +1,103 @@
+import { hasManagerPermissions } from "@/utils/discord/permissions"; +import type { Client, Message, Snowflake } from "discord.js"; + +type UserId = Snowflake; + +type Pending = { + channels: Set<string>; + timer: NodeJS.Timeout; +}; + +const pendingReports = new Map<UserId, Pending>(); +const cooldowns = new Map<UserId, NodeJS.Timeout>(); + +async function getLogChannel(client: Client) { + return client.channels.fetch("1477508937287864431"); +} + +export async function handleMessage(client: Client, message: Message) { + if (message.author.bot || !message.guildId) return; + handleSpamLinks(client, message); +} + +async function handleSpamLinks(client: Client, message: Message) { + if (message.author.bot || !message.guildId || !message.member) return; + if (await hasManagerPermissions(message.member)) return; + if (message.channelId === "1378529224192819250") return; + + const { content } = message; + const blacklisted = [ + "discord.gg/", + "discordapp.com/invite/", + "discord.com/invite/", + ]; + + if (!blacklisted.some((link) => content.includes(link))) return; + + if (message.channel.isSendable()) { + message.reply( + `<@${message.author.id}> your message contained an invite link and was deleted to prevent phishing attempts.`, + ); + } + + const logChannel = await getLogChannel(client); + + if (cooldowns.has(message.author.id)) { + if (message.deletable) message.delete(); + return; + } + + const existing = pendingReports.get(message.author.id); + if (!existing) { + if (logChannel?.isSendable()) { + await message.forward(logChannel); + logChannel.send( + `Deleted a message from <@${message.author.id}> in <#${message.channelId}> containing a potential phishing link.`, + ); + message.delete(); + } + + const timer = setTimeout( + () => flushReport(client, message.author.id), + 30_000, + ); + pendingReports.set(message.author.id, { + channels: new Set([message.channelId]), + timer, + }); + + return; + } + + existing.channels.add(message.channelId); + + clearTimeout(existing.timer); + existing.timer = setTimeout( + () => flushReport(client, message.author.id), + 30_000, + ); +} + +async function flushReport(client: Client, authorId: UserId) { + const pending = pendingReports.get(authorId); + if (!pending) return; + + pendingReports.delete(authorId); + clearTimeout(pending.timer); + + const logChannel = await getLogChannel(client); + if (!logChannel?.isSendable()) return; + + const channels = Array.from(pending.channels); + const channelMentions = channels.map((c) => `<#${c}>`).join(", "); + + await logChannel.send( + `<@${authorId}> posted invite/phishing links across ${channels.length} channel(s): ${channelMentions}`, + ); + + const cooldownTimer = setTimeout( + () => cooldowns.delete(authorId), + 5 * 60 * 1000, + ); + cooldowns.set(authorId, cooldownTimer); +}
M
src/events/messageCreate.ts
→
src/events/messageCreate.ts
@@ -17,6 +17,7 @@ import { and, eq } from "drizzle-orm";
import { nanoid } from "nanoid"; import Uwuifier from "uwuifier"; +import { handleMessage } from "@/automod"; import config from "@/config"; import { hasManagerPermissions } from "@/utils/discord/permissions"; import { loggers } from "@/utils/logging";@@ -157,6 +158,7 @@ export default {
event: Events.MessageCreate, async execute(client: Client, message: Message) { if (!client.user) return; + await handleMessage(client, message); const triggers = ["grok is this true", "grok is this false "]; if (