From 48505187329dd8ca49ba5cf1326a95bb8f519b5b Mon Sep 17 00:00:00 2001 From: Kath Date: Thu, 1 Aug 2024 21:43:57 +0800 Subject: [PATCH] Automod System --- a.ts | 52 +++++++++ index.ts | 6 +- src/commands/tag.ts | 2 +- src/commands/user.ts | 15 +-- src/events/interactionCreate.ts | 10 +- src/events/messageCreate.ts | 117 +++++++++++++++++++++ src/events/ready.ts | 11 +- src/{functions => utils}/CheckPermits.ts | 0 src/{functions => utils}/DeployCommands.ts | 0 src/{functions => utils}/DeployEvents.ts | 2 +- src/{functions => utils}/Infraction.ts | 40 ++++++- src/{functions => utils}/logger.ts | 0 src/{functions => utils}/mongo.ts | 0 src/utils/regex.ts | 5 + 14 files changed, 234 insertions(+), 26 deletions(-) create mode 100644 a.ts create mode 100644 src/events/messageCreate.ts rename src/{functions => utils}/CheckPermits.ts (100%) rename src/{functions => utils}/DeployCommands.ts (100%) rename src/{functions => utils}/DeployEvents.ts (93%) rename src/{functions => utils}/Infraction.ts (73%) rename src/{functions => utils}/logger.ts (100%) rename src/{functions => utils}/mongo.ts (100%) create mode 100644 src/utils/regex.ts diff --git a/a.ts b/a.ts new file mode 100644 index 0000000..67a95c2 --- /dev/null +++ b/a.ts @@ -0,0 +1,52 @@ +const urlRegex = + /https?:\/\/(www\.)?([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b)([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; + +const allowedDomains: string[] = [ + 'hypixel.net', + '*.hypixel.net', + 'discord.com', + '*.discord.com', + 'kath.lol', + '*.kathund.wtf', + 'kathund.wtf', + 'hypixel-api-reborn.github.io' +]; + +function matchWildcard(domain: string, pattern: string): boolean { + const regexPattern = pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(domain); +} + +function isUrlAllowed(url: string): boolean { + const isValidUrl = urlRegex.test(url); + if (!isValidUrl) { + return false; + } + + const match = url.match(urlRegex); + const domain = match ? match[2] : null; + if (!domain) { + return false; + } + + return allowedDomains.some((pattern) => matchWildcard(domain, pattern)); +} + +const testUrls = [ + 'https://example.com', + 'https://sub.example.com', + 'https://blocked.com', + 'http://sub.allowed.com', + 'http://another-allowed-url.org', + 'https://not-allowed-url.net', + 'https://sub.allow.net', + 'https://another.sub.allow.net', + 'https://kathund.wtf', + 'https://test.kathund.wtf' +]; + +testUrls.forEach((url) => { + const result = isUrlAllowed(url); + console.log(`Is URL allowed: ${url} -> ${result}`); +}); diff --git a/index.ts b/index.ts index f88e837..d1e1ba3 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ import { Client, Events, GatewayIntentBits } from 'discord.js'; -import DeployCommands from './src/functions/DeployCommands'; -import { execute } from './src/events/ready'; +import DeployCommands from './src/utils/DeployCommands'; +import ready from './src/events/ready'; import { token } from './config.json'; const client: Client = new Client({ @@ -15,7 +15,7 @@ const client: Client = new Client({ DeployCommands(client); client.on(Events.ClientReady, () => { - execute(client); + ready(client); }); client.login(token); diff --git a/src/commands/tag.ts b/src/commands/tag.ts index 62dba3a..da8c31c 100644 --- a/src/commands/tag.ts +++ b/src/commands/tag.ts @@ -11,7 +11,7 @@ import { EmbedBuilder, ChannelType } from 'discord.js'; -import { deleteTag, getTag, getTagNames } from '../functions/mongo'; +import { deleteTag, getTag, getTagNames } from '../utils/mongo'; import { supportCategory } from '../../config.json'; export const data = new SlashCommandBuilder() diff --git a/src/commands/user.ts b/src/commands/user.ts index 054bffe..7ccbcd7 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -7,7 +7,7 @@ import { EmbedBuilder, ButtonStyle } from 'discord.js'; -import Infraction, { getUserInfractions } from '../functions/Infraction'; +import Infraction, { getUserInfractions } from '../utils/Infraction'; import ms from 'ms'; export const data = new SlashCommandBuilder() @@ -58,7 +58,6 @@ export const data = new SlashCommandBuilder() .setName('unmute') .setDescription('unmute a user') .addUserOption((option) => option.setName('user').setDescription('The user to mute').setRequired(true)) - .addStringOption((option) => option.setName('time').setDescription('How long to mute').setRequired(true)) .addStringOption((option) => option.setName('reason').setDescription('The reason for the mute').setRequired(false) ) @@ -136,7 +135,8 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise long: null, user: { id: commandUser.id, staff: false, bot: commandUser.bot }, staff: { id: interaction.user.id, staff: true, bot: interaction.user.bot }, - timestamp: Date.now() + timestamp: Date.now(), + extraInfo: '' }) .log() .save(); @@ -152,7 +152,8 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise long: null, user: { id: commandUser.id, staff: false, bot: commandUser.bot }, staff: { id: interaction.user.id, staff: true, bot: interaction.user.bot }, - timestamp: Date.now() + timestamp: Date.now(), + extraInfo: '' }) .log() .save(); @@ -179,7 +180,8 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise long, user: { id: commandUser.id, staff: false, bot: commandUser.bot }, staff: { id: interaction.user.id, staff: true, bot: interaction.user.bot }, - timestamp: Date.now() + timestamp: Date.now(), + extraInfo: '' }) .log() .save(); @@ -196,7 +198,8 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise long: null, user: { id: commandUser.id, staff: false, bot: commandUser.bot }, staff: { id: interaction.user.id, staff: true, bot: interaction.user.bot }, - timestamp: Date.now() + timestamp: Date.now(), + extraInfo: '' }) .log() .save(); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 189a790..2b1d8b6 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,9 +1,9 @@ -import { Interaction, Events, InteractionType, GuildMemberRoleManager } from 'discord.js'; +import { Interaction, InteractionType, GuildMemberRoleManager } from 'discord.js'; import { teamRole, devRole } from '../../config.json'; -import { Tag, modifyTag } from '../functions/mongo'; -import { eventMessage } from '../functions/logger'; -export const name = Events.InteractionCreate; -export async function execute(interaction: Interaction): Promise { +import { Tag, modifyTag } from '../utils/mongo'; +import { eventMessage } from '../utils/logger'; + +export default async function (interaction: Interaction): Promise { try { if (!interaction.member || !interaction.channel || !interaction.guild) return; const memberRoles = (interaction.member.roles as GuildMemberRoleManager).cache.map((role) => role.id); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts new file mode 100644 index 0000000..8c65385 --- /dev/null +++ b/src/events/messageCreate.ts @@ -0,0 +1,117 @@ +import { ChannelType, GuildMemberRoleManager, Message, TextChannel, Webhook, WebhookType } from 'discord.js'; +import { HypixelAPIKeyRegex, IPAddressPattern, URLRegex, DiscordInviteRegex } from '../utils/regex'; +import { autoModBypassRole } from '../../config.json'; +import Infraction from '../utils/Infraction'; + +const allowedDomains: string[] = [ + 'hypixel.net', + '*.hypixel.net', + 'discord.com', + '*.discord.com', + 'kath.lol', + '*.kathund.wtf', + 'kathund.wtf', + 'hypixel-api-reborn.github.io' +]; + +function matchWildcard(domain: string, pattern: string): boolean { + const regexPattern = pattern.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(domain); +} + +function isUrlAllowed(url: string): boolean { + const isValidUrl = URLRegex.test(url); + if (!isValidUrl) return false; + const match = url.match(URLRegex); + const domain = match ? match[2] : null; + if (!domain) return false; + return !allowedDomains.some((pattern) => matchWildcard(domain, pattern)); +} + +async function getWebhook( + channel: TextChannel +): Promise | undefined> { + const webhooks = await channel.fetchWebhooks(); + + if (0 === webhooks.size) { + channel.createWebhook({ + name: channel.client.user.globalName ?? channel.client.user.username, + avatar: channel.client.user.avatarURL() + }); + + await getWebhook(channel); + } + + return webhooks.first(); +} + +export default async function (message: Message): Promise { + try { + if ( + message.channel.type !== ChannelType.GuildText || + message.author.bot || + !message.channel || + !message.member || + !message.guild + ) { + return; + } + const memberRoles = (message.member.roles as GuildMemberRoleManager).cache.map((role) => role.id); + if (memberRoles.includes(autoModBypassRole)) return; + const hypixelKeyTest = HypixelAPIKeyRegex.test(message.content); + const ipTest = IPAddressPattern.test(message.content); + const urlTest = isUrlAllowed(message.content); + const discordTest = DiscordInviteRegex.test(message.content); + const webhook = await getWebhook(message.channel); + if (!webhook) return; + if (hypixelKeyTest) { + const filteredContent = message.content.replace(HypixelAPIKeyRegex, '[API Key Removed]'); + webhook.send({ + username: message.member.nickname ?? message.author.globalName ?? message.author.username, + avatarURL: message.member.avatarURL() ?? message.author.avatarURL() ?? undefined, + content: filteredContent + }); + const alert = await message.reply({ content: 'Hey thats your Hypixel API Key. Please dont post that' }); + message.delete(); + new Infraction({ + automatic: true, + reason: 'Automod Pickup', + long: null, + type: 'AutoMod', + user: { id: message.author.id, staff: false, bot: message.author.bot }, + staff: { id: message.client.user.id, staff: true, bot: message.client.user.bot }, + timestamp: Date.now(), + extraInfo: `Message: ${message.content}\[Jump to Message](${message.url})` + }) + .log() + .save(); + setTimeout(() => alert.delete(), 10000); + } else if (ipTest || urlTest || discordTest) { + const filteredContent = message.content + .replace(IPAddressPattern, '[Content Removed]') + .replace(URLRegex, '[Content Removed]') + .replace(DiscordInviteRegex, '[Content Removed]'); + webhook.send({ + username: message.member.nickname ?? message.author.globalName ?? message.author.username, + avatarURL: message.member.avatarURL() ?? message.author.avatarURL() ?? undefined, + content: filteredContent + }); + message.delete(); + new Infraction({ + automatic: true, + reason: 'Automod Pickup', + long: null, + type: 'AutoMod', + user: { id: message.author.id, staff: false, bot: message.author.bot }, + staff: { id: message.client.user.id, staff: true, bot: message.client.user.bot }, + timestamp: Date.now(), + extraInfo: `**Message:**\n\`\`\`\n${message.content}\n\`\`\`\n[Jump to Message](${message.url})` + }) + .log() + .save(); + } + } catch (error) { + console.log(error); + } +} diff --git a/src/events/ready.ts b/src/events/ready.ts index bac8544..d16d5e1 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,11 +1,12 @@ -import CheckPermits from '../functions/CheckPermits'; -import DeployEvents from '../functions/DeployEvents'; -import { eventMessage } from '../functions/logger'; -import { connectDB } from '../functions/mongo'; +import CheckPermits from '../utils/CheckPermits'; +import DeployEvents from '../utils/DeployEvents'; +import { eventMessage } from '../utils/logger'; +import { connectDB } from '../utils/mongo'; import { serverId } from '../../config.json'; import { Client } from 'discord.js'; import cron from 'node-cron'; -export async function execute(client: Client): Promise { + +export default async function (client: Client): Promise { try { eventMessage(`Logged in as ${client.user?.username} (${client.user?.id})!`); DeployEvents(client); diff --git a/src/functions/CheckPermits.ts b/src/utils/CheckPermits.ts similarity index 100% rename from src/functions/CheckPermits.ts rename to src/utils/CheckPermits.ts diff --git a/src/functions/DeployCommands.ts b/src/utils/DeployCommands.ts similarity index 100% rename from src/functions/DeployCommands.ts rename to src/utils/DeployCommands.ts diff --git a/src/functions/DeployEvents.ts b/src/utils/DeployEvents.ts similarity index 93% rename from src/functions/DeployEvents.ts rename to src/utils/DeployEvents.ts index 957e726..e21e34e 100644 --- a/src/functions/DeployEvents.ts +++ b/src/utils/DeployEvents.ts @@ -13,7 +13,7 @@ export default async function (client: Client): Promise { } const event = await import(`../events/${file}`); const name = file.split('.')[0]; - client.on(name, event.execute.bind(null)); + client.on(name, event.default.bind(null)); eventMessage(`Successfully loaded ${name}`); } eventMessage(`Successfully loaded ${count} event(s).`); diff --git a/src/functions/Infraction.ts b/src/utils/Infraction.ts similarity index 73% rename from src/functions/Infraction.ts rename to src/utils/Infraction.ts index 03854f4..7a2ea77 100644 --- a/src/functions/Infraction.ts +++ b/src/utils/Infraction.ts @@ -1,6 +1,7 @@ import { infractionLogchannel } from '../../config.json'; -import { ChannelType } from 'discord.js'; +import { ChannelType, EmbedBuilder } from 'discord.js'; import { model, Schema } from 'mongoose'; +import ms from 'ms'; export interface InfractionUser { id: string; @@ -8,7 +9,7 @@ export interface InfractionUser { bot: boolean; } -export type InfractionType = 'EVENT' | 'WARN' | 'KICK' | 'BAN' | 'MUTE' | 'UNMUTE'; +export type InfractionType = 'AutoMod' | 'WARN' | 'KICK' | 'BAN' | 'MUTE' | 'UNMUTE'; export interface InfractionInfomation { automatic: boolean; @@ -18,6 +19,7 @@ export interface InfractionInfomation { user: InfractionUser; staff: InfractionUser; timestamp: number; + extraInfo: string; } const InteractionUserSchema = new Schema({ id: String, staff: Boolean, bot: Boolean }); @@ -31,7 +33,8 @@ const InfractionSchema = new Schema({ type: String, user: InteractionUserSchema, staff: InteractionUserSchema, - timestamp: Number + timestamp: Number, + extraInfo: String }); const InfractionModel = model('infraction', InfractionSchema); @@ -49,7 +52,8 @@ class Infraction { type: this.infraction.type, user: this.infraction.user, staff: this.infraction.staff, - timestamp: this.infraction.timestamp + timestamp: this.infraction.timestamp, + extraInfo: this.infraction.extraInfo }).save(); } @@ -88,6 +92,11 @@ class Infraction { return this; } + public setExtraInfo(extraInfo: string): this { + this.infraction.extraInfo = extraInfo; + return this; + } + public getAutomatic(): boolean { return this.infraction.automatic; } @@ -116,6 +125,10 @@ class Infraction { return this.infraction.timestamp; } + public getExtraInfo(): string { + return this.infraction.extraInfo; + } + public toString(): string { return `Infraction: ${this.infraction.reason}\nType: ${this.infraction.type}\nLong: ${ this.infraction.long @@ -129,7 +142,24 @@ class Infraction { public log(): this { const channel = guild.channels.cache.get(infractionLogchannel); if (!channel || channel.type !== ChannelType.GuildText) return this; - channel.send(this.toString()); + const embed = new EmbedBuilder() + .setColor('#FF8C00') + .setTitle(`Infraction | ${this.infraction.type}`) + .addFields( + { name: 'User', value: `<@${this.infraction.user.id}>` }, + { name: 'Reason', value: this.infraction.reason }, + { name: 'Staff', value: `<@${this.infraction.staff.id}>` }, + { name: 'Automatic', value: this.infraction.automatic ? 'Yes' : 'No' }, + { + name: 'Timestamp', + value: ` ()` + } + ); + if (0 < this.infraction.extraInfo.length) embed.addFields({ name: 'Info', value: this.infraction.extraInfo }); + if (this.infraction.long) embed.addFields({ name: 'How long', value: `${ms(86400000, { long: true })}` }); + channel.send({ embeds: [embed] }); return this; } } diff --git a/src/functions/logger.ts b/src/utils/logger.ts similarity index 100% rename from src/functions/logger.ts rename to src/utils/logger.ts diff --git a/src/functions/mongo.ts b/src/utils/mongo.ts similarity index 100% rename from src/functions/mongo.ts rename to src/utils/mongo.ts diff --git a/src/utils/regex.ts b/src/utils/regex.ts new file mode 100644 index 0000000..f036223 --- /dev/null +++ b/src/utils/regex.ts @@ -0,0 +1,5 @@ +export const HypixelAPIKeyRegex = /[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}/g; +export const IPAddressPattern = /((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}/g; +export const URLRegex = + /https?:\/\/(www\.)?([-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b)([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; +export const DiscordInviteRegex = /(?:https:\/\/)?discord\.gg\/[a-zA-Z0-9]+/g;