blob: db0d68b4334fed20939eb79519b33a644852f348 [file] [log] [blame]
pineafan63fc5e22022-08-04 22:04:10 +01001import { LoadingEmbed } from "./../../utils/defaultEmbeds.js";
TheCodedProf21c08592022-09-13 14:14:43 -04002import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
pineafan62ce1922022-08-25 20:34:45 +01003import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
pineafan4edb7762022-06-26 19:21:04 +01004import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +00005import getEmojiByName from "../../utils/getEmojiByName.js";
6import confirmationMessage from "../../utils/confirmationMessage.js";
7import keyValueList from "../../utils/generateKeyValueList.js";
pineafan62ce1922022-08-25 20:34:45 +01008// @ts-expect-error
pineafan8b4b17f2022-02-27 20:42:52 +00009import humanizeDuration from "humanize-duration";
pineafan6702cef2022-06-13 17:52:37 +010010import client from "../../utils/client.js";
Skyler Grey11236ba2022-08-08 21:13:33 +010011import { areTicketsEnabled, create } from "../../actions/createModActionTicket.js";
pineafan8b4b17f2022-02-27 20:42:52 +000012
13const command = (builder: SlashCommandSubcommandBuilder) =>
14 builder
pineafan63fc5e22022-08-04 22:04:10 +010015 .setName("mute")
Skyler Grey11236ba2022-08-08 21:13:33 +010016 .setDescription("Mutes a member, stopping them from talking in the server")
17 .addUserOption((option) => option.setName("user").setDescription("The user to mute").setRequired(true))
Skyler Grey75ea9172022-08-06 10:22:23 +010018 .addIntegerOption((option) =>
19 option
20 .setName("days")
Skyler Grey11236ba2022-08-08 21:13:33 +010021 .setDescription("The number of days to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010022 .setMinValue(0)
23 .setMaxValue(27)
24 .setRequired(false)
25 )
26 .addIntegerOption((option) =>
27 option
28 .setName("hours")
Skyler Grey11236ba2022-08-08 21:13:33 +010029 .setDescription("The number of hours to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010030 .setMinValue(0)
31 .setMaxValue(23)
32 .setRequired(false)
33 )
34 .addIntegerOption((option) =>
35 option
36 .setName("minutes")
Skyler Grey11236ba2022-08-08 21:13:33 +010037 .setDescription("The number of minutes to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010038 .setMinValue(0)
39 .setMaxValue(59)
40 .setRequired(false)
41 )
42 .addIntegerOption((option) =>
43 option
44 .setName("seconds")
Skyler Grey11236ba2022-08-08 21:13:33 +010045 .setDescription("The number of seconds to mute the user for | Default: 0")
Skyler Grey75ea9172022-08-06 10:22:23 +010046 .setMinValue(0)
47 .setMaxValue(59)
48 .setRequired(false)
49 );
pineafan8b4b17f2022-02-27 20:42:52 +000050
Skyler Greyc634e2b2022-08-06 17:50:48 +010051const callback = async (interaction: CommandInteraction): Promise<unknown> => {
Skyler Grey11236ba2022-08-08 21:13:33 +010052 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +010053 const user = interaction.options.getMember("user") as GuildMember;
pineafan8b4b17f2022-02-27 20:42:52 +000054 const time = {
Skyler Greyc634e2b2022-08-06 17:50:48 +010055 days: interaction.options.getInteger("days") ?? 0,
56 hours: interaction.options.getInteger("hours") ?? 0,
57 minutes: interaction.options.getInteger("minutes") ?? 0,
58 seconds: interaction.options.getInteger("seconds") ?? 0
pineafan63fc5e22022-08-04 22:04:10 +010059 };
pineafan62ce1922022-08-25 20:34:45 +010060 const config = await client.database.guilds.read(interaction.guild!.id);
Skyler Grey11236ba2022-08-08 21:13:33 +010061 let serverSettingsDescription = config.moderation.mute.timeout ? "given a timeout" : "";
Skyler Grey75ea9172022-08-06 10:22:23 +010062 if (config.moderation.mute.role)
63 serverSettingsDescription +=
Skyler Grey11236ba2022-08-08 21:13:33 +010064 (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010065
Skyler Grey11236ba2022-08-08 21:13:33 +010066 let muteTime = time.days * 24 * 60 * 60 + time.hours * 60 * 60 + time.minutes * 60 + time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +010067 if (muteTime === 0) {
Skyler Grey75ea9172022-08-06 10:22:23 +010068 const m = (await interaction.reply({
69 embeds: [
70 new EmojiEmbed()
71 .setEmoji("PUNISH.MUTE.GREEN")
72 .setTitle("Mute")
73 .setDescription("How long should the user be muted")
74 .setStatus("Success")
75 ],
76 components: [
TheCodedProf21c08592022-09-13 14:14:43 -040077 new ActionRowBuilder().addComponents([
78 new Discord.ButtonBuilder().setCustomId("1m").setLabel("1 Minute").setStyle(ButtonStyle.Secondary),
79 new Discord.ButtonBuilder().setCustomId("10m").setLabel("10 Minutes").setStyle(ButtonStyle.Secondary),
80 new Discord.ButtonBuilder().setCustomId("30m").setLabel("30 Minutes").setStyle(ButtonStyle.Secondary),
81 new Discord.ButtonBuilder().setCustomId("1h").setLabel("1 Hour").setStyle(ButtonStyle.Secondary)
Skyler Grey75ea9172022-08-06 10:22:23 +010082 ]),
TheCodedProf21c08592022-09-13 14:14:43 -040083 new ActionRowBuilder().addComponents([
84 new Discord.ButtonBuilder().setCustomId("6h").setLabel("6 Hours").setStyle(ButtonStyle.Secondary),
85 new Discord.ButtonBuilder().setCustomId("12h").setLabel("12 Hours").setStyle(ButtonStyle.Secondary),
86 new Discord.ButtonBuilder().setCustomId("1d").setLabel("1 Day").setStyle(ButtonStyle.Secondary),
87 new Discord.ButtonBuilder().setCustomId("1w").setLabel("1 Week").setStyle(ButtonStyle.Secondary)
Skyler Grey75ea9172022-08-06 10:22:23 +010088 ]),
TheCodedProf21c08592022-09-13 14:14:43 -040089 new ActionRowBuilder().addComponents([
90 new Discord.ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +010091 .setCustomId("cancel")
92 .setLabel("Cancel")
TheCodedProf21c08592022-09-13 14:14:43 -040093 .setStyle(ButtonStyle.Danger)
Skyler Grey75ea9172022-08-06 10:22:23 +010094 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
95 ])
96 ],
97 ephemeral: true,
98 fetchReply: true
99 })) as Message;
pineafan8b4b17f2022-02-27 20:42:52 +0000100 let component;
101 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100102 component = await m.awaitMessageComponent({
103 filter: (m) => m.user.id === interaction.user.id,
104 time: 300000
105 });
106 } catch {
107 return;
108 }
pineafan8b4b17f2022-02-27 20:42:52 +0000109 component.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100110 if (component.customId === "cancel")
111 return interaction.editReply({
112 embeds: [
113 new EmojiEmbed()
114 .setEmoji("PUNISH.MUTE.RED")
115 .setTitle("Mute")
116 .setDescription("Mute cancelled")
117 .setStatus("Danger")
118 ]
119 });
pineafan8b4b17f2022-02-27 20:42:52 +0000120 switch (component.customId) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100121 case "1m": {
122 muteTime = 60;
123 break;
124 }
125 case "10m": {
126 muteTime = 60 * 10;
127 break;
128 }
129 case "30m": {
130 muteTime = 60 * 30;
131 break;
132 }
133 case "1h": {
134 muteTime = 60 * 60;
135 break;
136 }
137 case "6h": {
138 muteTime = 60 * 60 * 6;
139 break;
140 }
141 case "12h": {
142 muteTime = 60 * 60 * 12;
143 break;
144 }
145 case "1d": {
146 muteTime = 60 * 60 * 24;
147 break;
148 }
149 case "1w": {
150 muteTime = 60 * 60 * 24 * 7;
151 break;
152 }
pineafan8b4b17f2022-02-27 20:42:52 +0000153 }
154 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100155 await interaction.reply({
156 embeds: LoadingEmbed,
157 ephemeral: true,
158 fetchReply: true
159 });
pineafan8b4b17f2022-02-27 20:42:52 +0000160 }
pineafan5d1908e2022-02-28 21:34:47 +0000161 // TODO:[Modals] Replace this with a modal
pineafan62ce1922022-08-25 20:34:45 +0100162 let reason: string | null = null;
pineafan02ba0232022-07-24 22:16:15 +0100163 let notify = true;
164 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100165 let confirmation;
Skyler Greyad002172022-08-16 18:48:26 +0100166 let timedOut = false;
167 let success = false;
pineafan62ce1922022-08-25 20:34:45 +0100168 do {
pineafan73a7c4a2022-07-24 10:38:04 +0100169 confirmation = await new confirmationMessage(interaction)
170 .setEmoji("PUNISH.MUTE.RED")
171 .setTitle("Mute")
Skyler Grey75ea9172022-08-06 10:22:23 +0100172 .setDescription(
173 keyValueList({
174 user: renderUser(user.user),
175 time: `${humanizeDuration(muteTime * 1000, {
176 round: true
177 })}`,
pineafan62ce1922022-08-25 20:34:45 +0100178 reason: reason ? "\n> " + (reason).replaceAll("\n", "\n> ") : "*No reason provided*"
Skyler Grey75ea9172022-08-06 10:22:23 +0100179 }) +
180 "The user will be " +
181 serverSettingsDescription +
182 "\n" +
183 `The user **will${notify ? "" : " not"}** be notified\n\n` +
184 `Are you sure you want to mute <@!${user.id}>?`
185 )
pineafan73a7c4a2022-07-24 10:38:04 +0100186 .setColor("Danger")
187 .addCustomBoolean(
Skyler Grey75ea9172022-08-06 10:22:23 +0100188 "appeal",
189 "Create appeal ticket",
pineafan62ce1922022-08-25 20:34:45 +0100190 !(await areTicketsEnabled(interaction.guild!.id)),
Skyler Grey75ea9172022-08-06 10:22:23 +0100191 async () =>
pineafan62ce1922022-08-25 20:34:45 +0100192 await create(interaction.guild!, interaction.options.getUser("user")!, interaction.user, reason),
Skyler Grey75ea9172022-08-06 10:22:23 +0100193 "An appeal ticket will be created when Confirm is clicked",
194 "CONTROL.TICKET",
195 createAppealTicket
196 )
197 .addCustomBoolean(
198 "notify",
199 "Notify user",
200 false,
pineafan62ce1922022-08-25 20:34:45 +0100201 undefined,
Skyler Grey75ea9172022-08-06 10:22:23 +0100202 null,
203 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
204 notify
205 )
pineafan73a7c4a2022-07-24 10:38:04 +0100206 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100207 .send(true);
208 reason = reason ?? "";
Skyler Greyad002172022-08-16 18:48:26 +0100209 if (confirmation.cancelled) timedOut = true;
210 if (confirmation.success) success = true;
pineafan63fc5e22022-08-04 22:04:10 +0100211 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100212 if (confirmation.components) {
pineafan62ce1922022-08-25 20:34:45 +0100213 notify = confirmation.components["notify"]!.active;
214 createAppealTicket = confirmation.components["appeal"]!.active;
pineafan02ba0232022-07-24 22:16:15 +0100215 }
pineafan62ce1922022-08-25 20:34:45 +0100216 } while (!timedOut && !success)
Skyler Greyad002172022-08-16 18:48:26 +0100217 if (timedOut) return;
218 let dmd = false;
219 let dm;
pineafan62ce1922022-08-25 20:34:45 +0100220 if (confirmation.success) {
221 try {
222 if (notify) {
223 dm = await user.send({
224 embeds: [
225 new EmojiEmbed()
226 .setEmoji("PUNISH.MUTE.RED")
227 .setTitle("Muted")
228 .setDescription(
229 `You have been muted in ${interaction.guild!.name}` +
230 (reason
231 ? ` for:\n> ${reason}`
232 : ".\n\n" +
233 `You will be unmuted at: <t:${
234 Math.round(new Date().getTime() / 1000) + muteTime
235 }:D> at <t:${Math.round(new Date().getTime() / 1000) + muteTime}:T> (<t:${
236 Math.round(new Date().getTime() / 1000) + muteTime
237 }:R>)`) +
238 (confirmation.components!["appeal"]!.response
239 ? `You can appeal this here: <#${confirmation.components!["appeal"]!.response}>`
240 : "")
241 )
242 .setStatus("Danger")
243 ],
244 components: [
TheCodedProf21c08592022-09-13 14:14:43 -0400245 new ActionRowBuilder().addComponents(
pineafan62ce1922022-08-25 20:34:45 +0100246 config.moderation.mute.text
247 ? [
TheCodedProf21c08592022-09-13 14:14:43 -0400248 new ButtonBuilder()
249 .setStyle(ButtonStyle.Link)
pineafan62ce1922022-08-25 20:34:45 +0100250 .setLabel(config.moderation.mute.text)
251 .setURL(config.moderation.mute.link)
252 ]
253 : []
254 )
255 ]
256 });
257 dmd = true;
258 }
259 } catch {
260 dmd = false;
261 }
262 const member = user;
263 let errors = 0;
264 try {
265 if (config.moderation.mute.timeout) {
PineaFan100df682023-01-02 13:26:08 +0000266 await member.timeout(muteTime * 1000, reason || "*No reason provided*");
pineafan62ce1922022-08-25 20:34:45 +0100267 if (config.moderation.mute.role !== null) {
268 await member.roles.add(config.moderation.mute.role);
269 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
270 guild: interaction.guild!.id,
271 user: user.id,
272 expires: new Date().getTime() + muteTime * 1000
273 });
274 }
275 }
276 } catch {
277 errors++;
278 }
279 try {
280 if (config.moderation.mute.role !== null) {
281 await member.roles.add(config.moderation.mute.role);
282 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
283 guild: interaction.guild!.id,
284 user: user.id,
285 role: config.moderation.mute.role
286 });
287 }
288 } catch (e) {
289 console.log(e);
290 errors++;
291 }
292 if (errors === 2) {
293 await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100294 embeds: [
295 new EmojiEmbed()
296 .setEmoji("PUNISH.MUTE.RED")
pineafan62ce1922022-08-25 20:34:45 +0100297 .setTitle("Mute")
298 .setDescription("Something went wrong and the user was not muted")
Skyler Grey75ea9172022-08-06 10:22:23 +0100299 .setStatus("Danger")
300 ],
pineafan62ce1922022-08-25 20:34:45 +0100301 components: []
302 }); // TODO: make this clearer
303 if (dmd && dm) await dm.delete();
304 return;
Skyler Greyad002172022-08-16 18:48:26 +0100305 }
pineafan62ce1922022-08-25 20:34:45 +0100306 await client.database.history.create("mute", interaction.guild!.id, member.user, interaction.user, reason);
307 const failed = !dmd && notify;
Skyler Grey75ea9172022-08-06 10:22:23 +0100308 await interaction.editReply({
309 embeds: [
310 new EmojiEmbed()
pineafan62ce1922022-08-25 20:34:45 +0100311 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
Skyler Grey75ea9172022-08-06 10:22:23 +0100312 .setTitle("Mute")
pineafan62ce1922022-08-25 20:34:45 +0100313 .setDescription(
314 "The member was muted" +
315 (failed ? ", but could not be notified" : "") +
316 (confirmation.components!["appeal"]!.response
317 ? ` and an appeal ticket was opened in <#${confirmation.components!["appeal"]!.response}>`
318 : "")
319 )
320 .setStatus(failed ? "Warning" : "Success")
Skyler Grey75ea9172022-08-06 10:22:23 +0100321 ],
322 components: []
pineafan62ce1922022-08-25 20:34:45 +0100323 });
324 const data = {
325 meta: {
326 type: "memberMute",
327 displayName: "Member Muted",
328 calculateType: "guildMemberPunish",
329 color: NucleusColors.yellow,
330 emoji: "PUNISH.WARN.YELLOW",
331 timestamp: new Date().getTime()
332 },
333 list: {
334 memberId: entry(member.user.id, `\`${member.user.id}\``),
335 name: entry(member.user.id, renderUser(member.user)),
336 mutedUntil: entry(
337 new Date().getTime() + muteTime * 1000,
338 renderDelta(new Date().getTime() + muteTime * 1000)
339 ),
340 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
341 mutedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user)),
342 reason: entry(reason, reason ? reason : "*No reason provided*")
343 },
344 hidden: {
345 guild: interaction.guild!.id
346 }
347 };
348 log(data);
349 } else {
350 await interaction.editReply({
351 embeds: [
352 new EmojiEmbed()
353 .setEmoji("PUNISH.BAN.GREEN")
354 .setTitle("Softban")
355 .setDescription("No changes were made")
356 .setStatus("Success")
357 ],
358 components: []
359 });
pineafan8b4b17f2022-02-27 20:42:52 +0000360 }
pineafan63fc5e22022-08-04 22:04:10 +0100361};
pineafan8b4b17f2022-02-27 20:42:52 +0000362
pineafanbd02b4a2022-08-05 22:01:38 +0100363const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100364 const member = interaction.member as GuildMember;
pineafan62ce1922022-08-25 20:34:45 +0100365 const me = interaction.guild!.me!;
Skyler Grey75ea9172022-08-06 10:22:23 +0100366 const apply = interaction.options.getMember("user") as GuildMember;
pineafan62ce1922022-08-25 20:34:45 +0100367 const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
368 const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0;
369 const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100370 // Do not allow muting the owner
pineafan62ce1922022-08-25 20:34:45 +0100371 if (member.id === interaction.guild!.ownerId) throw new Error("You cannot mute the owner of the server");
pineafan8b4b17f2022-02-27 20:42:52 +0000372 // Check if Nucleus can mute the member
pineafan3a02ea32022-08-11 21:35:04 +0100373 if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000374 // Check if Nucleus has permission to mute
pineafan3a02ea32022-08-11 21:35:04 +0100375 if (!me.permissions.has("MODERATE_MEMBERS")) throw new Error("I do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000376 // Do not allow muting Nucleus
pineafan3a02ea32022-08-11 21:35:04 +0100377 if (member.id === me.id) throw new Error("I cannot mute myself");
pineafan8b4b17f2022-02-27 20:42:52 +0000378 // Allow the owner to mute anyone
pineafan62ce1922022-08-25 20:34:45 +0100379 if (member.id === interaction.guild!.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000380 // Check if the user has moderate_members permission
pineafan3a02ea32022-08-11 21:35:04 +0100381 if (!member.permissions.has("MODERATE_MEMBERS"))
382 throw new Error("You do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000383 // Check if the user is below on the role list
pineafan3a02ea32022-08-11 21:35:04 +0100384 if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000385 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100386 return true;
387};
pineafan8b4b17f2022-02-27 20:42:52 +0000388
Skyler Grey75ea9172022-08-06 10:22:23 +0100389export { command, callback, check };