import { arg } from "@/args"; import { Module } from "@/base/ModuleBase"; import { db } from "@/db"; import { warnsTable } from "@/db/schemas"; import { Command } from "@/decorators/command"; import { RequireGuild, RequireNonBot, RequireUserPermission } from "@/decorators/guards"; import type { BotContext, ParsedArgs } from "@/types"; import { ColorPalette } from "@/utils"; import { Client, EmbedBuilder, type Message } from "@fluxerjs/core"; import z from "zod"; // Types const banArgs = { target: arg.string(), duration: z.coerce.number().int().positive().max(604800), delete_messages_day: arg.optional(z.coerce.number().int().min(0).max(7)), reason: arg.restOptional(z.string().min(1).max(500)), } as const; const warnArgs = { target: arg.string(), reason: arg.restOptional(z.coerce.string().max(2000)), } as const; const purgeArgs = { count: z.coerce.number().int().min(2).max(100), } as const; type WarnsInsertType = typeof warnsTable.$inferInsert; // Module @Module({ name: "mod", description: "Moderation stuff", guards: [ RequireNonBot(), RequireUserPermission({ mode: "any", permissions: ["Administrator", "KickMembers", "BanMembers"], }), RequireGuild(), ], }) export class ModerationModule { private readonly palette = new ColorPalette(); @Command({ name: "ban", description: "Ban a member", args: banArgs, }) async ban(_: BotContext, msg: Message, args: ParsedArgs) { const guild = msg.guild; const mentionedUser = msg.mentions[0]; if (!mentionedUser) throw new Error("Please mention an user to ban."); const member = await guild?.fetchMember(mentionedUser.id); if (!member) throw new Error("I couldn't find a member to ban :("); const embed = new EmbedBuilder() .setTitle(`You've been banned from "${msg.guild?.name}"`) .addFields( { name: "Reason", value: args.reason || "No reason provided", }, { name: "Ban Duration", value: `${args.duration > 0 ? args.duration : "Permament"}`, }, { name: "By", value: msg.author.globalName || `${msg.author.username}#${msg.author.discriminator}`, }, ) .setColor(this.palette.get("red")) .setFooter({ text: "This message is automated, you will receive when someone takes action against you", }); await member.user.send({ embeds: [embed], }); await guild?.ban(member.id, { ban_duration_seconds: args.duration, reason: args.reason ?? undefined, delete_message_days: args.delete_messages_day ?? undefined, }); const successMessage = await msg.reply({ content: `Successfully banned **${member.displayName}**`, }); if (successMessage) setTimeout(() => successMessage.delete().catch(() => {}), 5000); } @Command({ name: "kick", description: "Kicks a member", args: warnArgs, }) async kick(_: BotContext, msg: Message, args: ParsedArgs) { const guild = msg.guild; const mentionedUser = msg.mentions[0]; if (!mentionedUser) throw new Error("Please mention a user to kick"); const member = await msg.guild?.fetchMember(mentionedUser.id); if (!member) throw new Error("I couldn't find a member to kick :("); const embed = new EmbedBuilder() .setTitle(`You've been kicked from "${msg.guild?.name}"`) .addFields( { name: "Reason", value: args.reason || "No reason provided", }, { name: "By", value: msg.author.globalName || `${msg.author.username}#${msg.author.discriminator}`, }, ) .setColor(this.palette.get("red")) .setFooter({ text: "This message is automated, you will receive when someone takes action against you", }); await member.user.send({ embeds: [embed], }); await guild?.kick(member.id); msg.reply(`Successfully kicked ${member.displayName} for: ${args.reason}`); } @Command({ name: "warn", description: "Warn a member", args: warnArgs, }) async warn(_: BotContext, msg: Message, args: ParsedArgs) { const guild = msg.guild; if (!guild) throw new Error("This command can only be used in a server"); const mentionedUser = msg.mentions[0]; if (!mentionedUser) throw new Error("Please mention a user to warn"); const member = await msg.guild?.fetchMember(mentionedUser.id); if (!member) throw new Error("I couldn't find a member to warn :("); const embed = new EmbedBuilder() .setTitle(`You've been warned from "${msg.guild?.name}"`) .setDescription( `Reason: ${args.reason}\nBy ${msg.author.globalName || `${msg.author.username}#${msg.author.discriminator}`}`, ) .setColor(this.palette.get("yellow")) .setFooter({ text: "This message is automated, you will receive when someone takes action against you.", }); await member.user.send({ embeds: [embed], }); const insertValues: WarnsInsertType = { guildId: guild.id, userId: member.id, moderator: msg.author.id, reason: args.reason ?? "", createdAt: new Date(Date.now()), }; await db.insert(warnsTable).values(insertValues); msg.reply(`Successfully warned ${member.displayName} for: ${args.reason}`); } @Command({ name: "purge", description: "Purges a number of messages", args: purgeArgs, }) async purge(ctx: BotContext, msg: Message, args: ParsedArgs) { const { client } = ctx; const messages = await client.rest.get>( `${Client.Routes.channelMessages(msg.channelId)}?limit=${args.count}`, ); const messageIds = messages.map((m) => m.id); if (messageIds.length < 2) { await msg.reply("Need at least 2 messages to bulk delete."); return; } const channel = await client.channels.resolve(msg.channelId); await channel.bulkDeleteMessages(messageIds); const confirm = await msg.channel?.send({ content: `Deleted ${args.count}`, }); // Auto-delete the confirmation after 5 seconds if (confirm) setTimeout(() => confirm.delete().catch(() => {}), 5000); } }