Yet another Fluxer bot built with TypeScript and Bun
at develop 211 lines 6.3 kB view raw
1import { arg } from "@/args"; 2import { Module } from "@/base/ModuleBase"; 3import { db } from "@/db"; 4import { warnsTable } from "@/db/schemas"; 5import { Command } from "@/decorators/command"; 6import { RequireGuild, RequireNonBot, RequireUserPermission } from "@/decorators/guards"; 7import type { BotContext, ParsedArgs } from "@/types"; 8import { ColorPalette } from "@/utils"; 9import { Client, EmbedBuilder, type Message } from "@fluxerjs/core"; 10import z from "zod"; 11 12// Types 13 14const banArgs = { 15 target: arg.string(), 16 duration: z.coerce.number().int().positive().max(604800), 17 delete_messages_day: arg.optional(z.coerce.number().int().min(0).max(7)), 18 reason: arg.restOptional(z.string().min(1).max(500)), 19} as const; 20 21const warnArgs = { 22 target: arg.string(), 23 reason: arg.restOptional(z.coerce.string().max(2000)), 24} as const; 25 26const purgeArgs = { 27 count: z.coerce.number().int().min(2).max(100), 28} as const; 29 30type WarnsInsertType = typeof warnsTable.$inferInsert; 31 32// Module 33 34@Module({ 35 name: "mod", 36 description: "Moderation stuff", 37 guards: [ 38 RequireNonBot(), 39 RequireUserPermission({ 40 mode: "any", 41 permissions: ["Administrator", "KickMembers", "BanMembers"], 42 }), 43 RequireGuild(), 44 ], 45}) 46export class ModerationModule { 47 private readonly palette = new ColorPalette(); 48 49 @Command({ 50 name: "ban", 51 description: "Ban a member", 52 args: banArgs, 53 }) 54 async ban(_: BotContext, msg: Message, args: ParsedArgs<typeof banArgs>) { 55 const guild = msg.guild; 56 const mentionedUser = msg.mentions[0]; 57 if (!mentionedUser) throw new Error("Please mention an user to ban."); 58 59 const member = await guild?.fetchMember(mentionedUser.id); 60 if (!member) throw new Error("I couldn't find a member to ban :("); 61 62 const embed = new EmbedBuilder() 63 .setTitle(`You've been banned from "${msg.guild?.name}"`) 64 .addFields( 65 { 66 name: "Reason", 67 value: args.reason || "No reason provided", 68 }, 69 { 70 name: "Ban Duration", 71 value: `${args.duration > 0 ? args.duration : "Permament"}`, 72 }, 73 { 74 name: "By", 75 value: msg.author.globalName || `${msg.author.username}#${msg.author.discriminator}`, 76 }, 77 ) 78 .setColor(this.palette.get("red")) 79 .setFooter({ 80 text: "This message is automated, you will receive when someone takes action against you", 81 }); 82 83 await member.user.send({ 84 embeds: [embed], 85 }); 86 87 await guild?.ban(member.id, { 88 ban_duration_seconds: args.duration, 89 reason: args.reason ?? undefined, 90 delete_message_days: args.delete_messages_day ?? undefined, 91 }); 92 93 const successMessage = await msg.reply({ 94 content: `Successfully banned **${member.displayName}**`, 95 }); 96 97 if (successMessage) setTimeout(() => successMessage.delete().catch(() => {}), 5000); 98 } 99 100 @Command({ 101 name: "kick", 102 description: "Kicks a member", 103 args: warnArgs, 104 }) 105 async kick(_: BotContext, msg: Message, args: ParsedArgs<typeof warnArgs>) { 106 const guild = msg.guild; 107 108 const mentionedUser = msg.mentions[0]; 109 if (!mentionedUser) throw new Error("Please mention a user to kick"); 110 111 const member = await msg.guild?.fetchMember(mentionedUser.id); 112 if (!member) throw new Error("I couldn't find a member to kick :("); 113 114 const embed = new EmbedBuilder() 115 .setTitle(`You've been kicked from "${msg.guild?.name}"`) 116 .addFields( 117 { 118 name: "Reason", 119 value: args.reason || "No reason provided", 120 }, 121 { 122 name: "By", 123 value: msg.author.globalName || `${msg.author.username}#${msg.author.discriminator}`, 124 }, 125 ) 126 .setColor(this.palette.get("red")) 127 .setFooter({ 128 text: "This message is automated, you will receive when someone takes action against you", 129 }); 130 131 await member.user.send({ 132 embeds: [embed], 133 }); 134 135 await guild?.kick(member.id); 136 137 msg.reply(`Successfully kicked ${member.displayName} for: ${args.reason}`); 138 } 139 140 @Command({ 141 name: "warn", 142 description: "Warn a member", 143 args: warnArgs, 144 }) 145 async warn(_: BotContext, msg: Message, args: ParsedArgs<typeof warnArgs>) { 146 const guild = msg.guild; 147 if (!guild) throw new Error("This command can only be used in a server"); 148 149 const mentionedUser = msg.mentions[0]; 150 if (!mentionedUser) throw new Error("Please mention a user to warn"); 151 152 const member = await msg.guild?.fetchMember(mentionedUser.id); 153 if (!member) throw new Error("I couldn't find a member to warn :("); 154 155 const embed = new EmbedBuilder() 156 .setTitle(`You've been warned from "${msg.guild?.name}"`) 157 .setDescription( 158 `Reason: ${args.reason}\nBy ${msg.author.globalName || `${msg.author.username}#${msg.author.discriminator}`}`, 159 ) 160 .setColor(this.palette.get("yellow")) 161 .setFooter({ 162 text: "This message is automated, you will receive when someone takes action against you.", 163 }); 164 165 await member.user.send({ 166 embeds: [embed], 167 }); 168 169 const insertValues: WarnsInsertType = { 170 guildId: guild.id, 171 userId: member.id, 172 moderator: msg.author.id, 173 reason: args.reason ?? "", 174 createdAt: new Date(Date.now()), 175 }; 176 177 await db.insert(warnsTable).values(insertValues); 178 179 msg.reply(`Successfully warned ${member.displayName} for: ${args.reason}`); 180 } 181 182 @Command({ 183 name: "purge", 184 description: "Purges a number of messages", 185 args: purgeArgs, 186 }) 187 async purge(ctx: BotContext, msg: Message, args: ParsedArgs<typeof purgeArgs>) { 188 const { client } = ctx; 189 190 const messages = await client.rest.get<Array<{ id: string }>>( 191 `${Client.Routes.channelMessages(msg.channelId)}?limit=${args.count}`, 192 ); 193 194 const messageIds = messages.map((m) => m.id); 195 196 if (messageIds.length < 2) { 197 await msg.reply("Need at least 2 messages to bulk delete."); 198 return; 199 } 200 201 const channel = await client.channels.resolve(msg.channelId); 202 await channel.bulkDeleteMessages(messageIds); 203 204 const confirm = await msg.channel?.send({ 205 content: `Deleted ${args.count}`, 206 }); 207 208 // Auto-delete the confirmation after 5 seconds 209 if (confirm) setTimeout(() => confirm.delete().catch(() => {}), 5000); 210 } 211}