Yet another Fluxer bot built with TypeScript and Bun
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}