blob: 05a9ec2e283adb3ce446617f4d9325a5f6120664 [file] [log] [blame]
PineaFan5bea7e12023-01-05 21:20:04 +00001import { LinkWarningFooter, LoadingEmbed } from "./../../utils/defaultEmbeds.js";
2import Discord, { CommandInteraction, GuildMember, 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
PineaFan5bea7e12023-01-05 21:20:04 +000051
Skyler Greyc634e2b2022-08-06 17:50:48 +010052const callback = async (interaction: CommandInteraction): Promise<unknown> => {
PineaFana00db1b2023-01-02 15:32:54 +000053 if (!interaction.guild) return;
Skyler Grey11236ba2022-08-08 21:13:33 +010054 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger;
PineaFan5bea7e12023-01-05 21:20:04 +000055 const member = interaction.options.getMember("user") as GuildMember;
56 const time: {days: number, hours: number, minutes: number, seconds: number} = {
57 days: (interaction.options.get("days")?.value as number | null) ?? 0,
58 hours: (interaction.options.get("hours")?.value as number | null) ?? 0,
59 minutes: (interaction.options.get("minutes")?.value as number | null) ?? 0,
60 seconds: (interaction.options.get("seconds")?.value as number | null) ?? 0
pineafan63fc5e22022-08-04 22:04:10 +010061 };
PineaFana00db1b2023-01-02 15:32:54 +000062 const config = await client.database.guilds.read(interaction.guild.id);
Skyler Grey11236ba2022-08-08 21:13:33 +010063 let serverSettingsDescription = config.moderation.mute.timeout ? "given a timeout" : "";
Skyler Grey75ea9172022-08-06 10:22:23 +010064 if (config.moderation.mute.role)
65 serverSettingsDescription +=
Skyler Grey11236ba2022-08-08 21:13:33 +010066 (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010067
Skyler Grey11236ba2022-08-08 21:13:33 +010068 let muteTime = time.days * 24 * 60 * 60 + time.hours * 60 * 60 + time.minutes * 60 + time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +010069 if (muteTime === 0) {
PineaFan5bea7e12023-01-05 21:20:04 +000070 const m = await interaction.reply({
Skyler Grey75ea9172022-08-06 10:22:23 +010071 embeds: [
72 new EmojiEmbed()
73 .setEmoji("PUNISH.MUTE.GREEN")
74 .setTitle("Mute")
PineaFan5bea7e12023-01-05 21:20:04 +000075 .setDescription("How long should the user be muted for?")
Skyler Grey75ea9172022-08-06 10:22:23 +010076 .setStatus("Success")
77 ],
78 components: [
PineaFan5bea7e12023-01-05 21:20:04 +000079 new ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040080 new Discord.ButtonBuilder().setCustomId("1m").setLabel("1 Minute").setStyle(ButtonStyle.Secondary),
81 new Discord.ButtonBuilder().setCustomId("10m").setLabel("10 Minutes").setStyle(ButtonStyle.Secondary),
82 new Discord.ButtonBuilder().setCustomId("30m").setLabel("30 Minutes").setStyle(ButtonStyle.Secondary),
83 new Discord.ButtonBuilder().setCustomId("1h").setLabel("1 Hour").setStyle(ButtonStyle.Secondary)
Skyler Grey75ea9172022-08-06 10:22:23 +010084 ]),
PineaFan5bea7e12023-01-05 21:20:04 +000085 new ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040086 new Discord.ButtonBuilder().setCustomId("6h").setLabel("6 Hours").setStyle(ButtonStyle.Secondary),
87 new Discord.ButtonBuilder().setCustomId("12h").setLabel("12 Hours").setStyle(ButtonStyle.Secondary),
88 new Discord.ButtonBuilder().setCustomId("1d").setLabel("1 Day").setStyle(ButtonStyle.Secondary),
89 new Discord.ButtonBuilder().setCustomId("1w").setLabel("1 Week").setStyle(ButtonStyle.Secondary)
Skyler Grey75ea9172022-08-06 10:22:23 +010090 ]),
PineaFan5bea7e12023-01-05 21:20:04 +000091 new ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040092 new Discord.ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +010093 .setCustomId("cancel")
94 .setLabel("Cancel")
TheCodedProf21c08592022-09-13 14:14:43 -040095 .setStyle(ButtonStyle.Danger)
Skyler Grey75ea9172022-08-06 10:22:23 +010096 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
97 ])
98 ],
99 ephemeral: true,
100 fetchReply: true
PineaFan5bea7e12023-01-05 21:20:04 +0000101 });
pineafan8b4b17f2022-02-27 20:42:52 +0000102 let component;
103 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100104 component = await m.awaitMessageComponent({
105 filter: (m) => m.user.id === interaction.user.id,
106 time: 300000
107 });
108 } catch {
109 return;
110 }
pineafan8b4b17f2022-02-27 20:42:52 +0000111 component.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100112 if (component.customId === "cancel")
113 return interaction.editReply({
114 embeds: [
115 new EmojiEmbed()
116 .setEmoji("PUNISH.MUTE.RED")
117 .setTitle("Mute")
118 .setDescription("Mute cancelled")
119 .setStatus("Danger")
120 ]
121 });
pineafan8b4b17f2022-02-27 20:42:52 +0000122 switch (component.customId) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100123 case "1m": {
124 muteTime = 60;
125 break;
126 }
127 case "10m": {
128 muteTime = 60 * 10;
129 break;
130 }
131 case "30m": {
132 muteTime = 60 * 30;
133 break;
134 }
135 case "1h": {
136 muteTime = 60 * 60;
137 break;
138 }
139 case "6h": {
140 muteTime = 60 * 60 * 6;
141 break;
142 }
143 case "12h": {
144 muteTime = 60 * 60 * 12;
145 break;
146 }
147 case "1d": {
148 muteTime = 60 * 60 * 24;
149 break;
150 }
151 case "1w": {
152 muteTime = 60 * 60 * 24 * 7;
153 break;
154 }
pineafan8b4b17f2022-02-27 20:42:52 +0000155 }
156 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100157 await interaction.reply({
158 embeds: LoadingEmbed,
159 ephemeral: true,
160 fetchReply: true
161 });
pineafan8b4b17f2022-02-27 20:42:52 +0000162 }
pineafan5d1908e2022-02-28 21:34:47 +0000163 // TODO:[Modals] Replace this with a modal
pineafan62ce1922022-08-25 20:34:45 +0100164 let reason: string | null = null;
pineafan02ba0232022-07-24 22:16:15 +0100165 let notify = true;
166 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100167 let confirmation;
Skyler Greyad002172022-08-16 18:48:26 +0100168 let timedOut = false;
169 let success = false;
pineafan62ce1922022-08-25 20:34:45 +0100170 do {
pineafan73a7c4a2022-07-24 10:38:04 +0100171 confirmation = await new confirmationMessage(interaction)
172 .setEmoji("PUNISH.MUTE.RED")
173 .setTitle("Mute")
Skyler Grey75ea9172022-08-06 10:22:23 +0100174 .setDescription(
175 keyValueList({
PineaFan5bea7e12023-01-05 21:20:04 +0000176 user: renderUser(member.user),
Skyler Grey75ea9172022-08-06 10:22:23 +0100177 time: `${humanizeDuration(muteTime * 1000, {
178 round: true
179 })}`,
pineafan62ce1922022-08-25 20:34:45 +0100180 reason: reason ? "\n> " + (reason).replaceAll("\n", "\n> ") : "*No reason provided*"
Skyler Grey75ea9172022-08-06 10:22:23 +0100181 }) +
182 "The user will be " +
183 serverSettingsDescription +
PineaFan5bea7e12023-01-05 21:20:04 +0000184 "\n\n" +
185 `Are you sure you want to mute <@!${member.id}>?`
Skyler Grey75ea9172022-08-06 10:22:23 +0100186 )
pineafan73a7c4a2022-07-24 10:38:04 +0100187 .setColor("Danger")
188 .addCustomBoolean(
Skyler Grey75ea9172022-08-06 10:22:23 +0100189 "appeal",
190 "Create appeal ticket",
PineaFana00db1b2023-01-02 15:32:54 +0000191 !(await areTicketsEnabled(interaction.guild.id)),
PineaFan5bea7e12023-01-05 21:20:04 +0000192 async () => 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",
PineaFana34d04b2023-01-03 22:05:42 +0000194 null,
Skyler Grey75ea9172022-08-06 10:22:23 +0100195 "CONTROL.TICKET",
196 createAppealTicket
197 )
198 .addCustomBoolean(
199 "notify",
200 "Notify user",
201 false,
Skyler Grey75ea9172022-08-06 10:22:23 +0100202 null,
PineaFan5bea7e12023-01-05 21:20:04 +0000203 "The user will be sent a DM",
PineaFana34d04b2023-01-03 22:05:42 +0000204 null,
Skyler Grey75ea9172022-08-06 10:22:23 +0100205 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
206 notify
207 )
pineafan73a7c4a2022-07-24 10:38:04 +0100208 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100209 .send(true);
210 reason = reason ?? "";
Skyler Greyad002172022-08-16 18:48:26 +0100211 if (confirmation.cancelled) timedOut = true;
PineaFan5bea7e12023-01-05 21:20:04 +0000212 else if (confirmation.success) success = true;
213 else if (confirmation.newReason) reason = confirmation.newReason;
214 else if (confirmation.components) {
pineafan62ce1922022-08-25 20:34:45 +0100215 notify = confirmation.components["notify"]!.active;
216 createAppealTicket = confirmation.components["appeal"]!.active;
pineafan02ba0232022-07-24 22:16:15 +0100217 }
pineafan62ce1922022-08-25 20:34:45 +0100218 } while (!timedOut && !success)
Skyler Greyad002172022-08-16 18:48:26 +0100219 if (timedOut) return;
PineaFan5bea7e12023-01-05 21:20:04 +0000220 if (!confirmation.success) {
pineafan62ce1922022-08-25 20:34:45 +0100221 await interaction.editReply({
222 embeds: [
223 new EmojiEmbed()
224 .setEmoji("PUNISH.BAN.GREEN")
225 .setTitle("Softban")
226 .setDescription("No changes were made")
227 .setStatus("Success")
228 ],
229 components: []
230 });
PineaFan5bea7e12023-01-05 21:20:04 +0000231 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000232 }
PineaFan5bea7e12023-01-05 21:20:04 +0000233 const status: {timeout: boolean | null, role: boolean | null, dm: boolean | null} = {timeout: null, role: null, dm: null};
234 let dmMessage;
235 try {
236 if (notify) {
237 const messageData: {
238 embeds: EmojiEmbed[];
239 components: ActionRowBuilder<ButtonBuilder>[];
240 } = {
241 embeds: [
242 new EmojiEmbed()
243 .setEmoji("PUNISH.MUTE.RED")
244 .setTitle("Muted")
245 .setDescription(
246 `You have been muted in ${interaction.guild.name}` +
247 (reason ? ` for:\n> ${reason}` : ".\n*No reason was provided*") + "\n\n" +
248 `You will be unmuted at: <t:${Math.round(new Date().getTime() / 1000) + muteTime}:D> at ` +
249 `<t:${Math.round(new Date().getTime() / 1000) + muteTime}:T> (<t:${Math.round(new Date().getTime() / 1000) + muteTime
250 }:R>)` + "\n\n" +
251 (createAppealTicket
252 ? `You can appeal this in the ticket created in <#${confirmation.components!["appeal"]!.response}>`
253 : "")
254 )
255 .setStatus("Danger")
256 ],
257 components: []
258 }
259 if (config.moderation.mute.text && config.moderation.mute.link) {
260 messageData.embeds[0]!.setFooter(LinkWarningFooter);
261 messageData.components.push(new ActionRowBuilder<Discord.ButtonBuilder>()
262 .addComponents(new ButtonBuilder()
263 .setStyle(ButtonStyle.Link)
264 .setLabel(config.moderation.mute.text)
265 .setURL(config.moderation.mute.link)
266 )
267 )
268 };
269 dmMessage = await member.send(messageData);
270 status.dm = true;
271 }
272 } catch {
273 status.dm = false;
274 }
275 try {
276 if (config.moderation.mute.timeout) {
277 await member.timeout(muteTime * 1000, reason || "*No reason provided*");
278 if (config.moderation.mute.role !== null) {
279 await member.roles.add(config.moderation.mute.role);
280 await client.database.eventScheduler.schedule("naturalUnmute", (new Date().getTime() + muteTime * 1000).toString(), {
281 guild: interaction.guild.id,
282 user: member.id,
283 expires: new Date().getTime() + muteTime * 1000
284 });
285 }
286 } else {
287 status.timeout = true;
288 }
289 } catch {
290 status.timeout = false;
291 }
292 try {
293 if (config.moderation.mute.role !== null) {
294 await member.roles.add(config.moderation.mute.role);
295 await client.database.eventScheduler.schedule("unmuteRole", (new Date().getTime() + muteTime * 1000).toString(), {
296 guild: interaction.guild.id,
297 user: member.id,
298 role: config.moderation.mute.role
299 });
300 } else {
301 status.role = true;
302 }
303 } catch {
304 status.role = false;
305 }
306 const countTrue = (items: (boolean | null)[]) => items.filter(item => item === true).length;
307 const requiredPunishments = countTrue([config.moderation.mute.timeout, config.moderation.mute.role !== null]);
308 const actualPunishments = countTrue([status.timeout, status.role]);
309
310 await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason);
311 if (requiredPunishments !== actualPunishments) {
312 const messages = [];
313 if (config.moderation.mute.timeout) messages.push(`The member was ${status.timeout ? "" : "not "}timed out`);
314 if (config.moderation.mute.role !== null) messages.push(`The member was ${status.role ? "" : "not "}given the mute role`);
315 messages.push(`The member was not sent a DM`);
316 if (dmMessage && actualPunishments === 0) await dmMessage.delete();
317 await interaction.editReply({
318 embeds: [
319 new EmojiEmbed()
320 .setEmoji("PUNISH.MUTE." + (actualPunishments > 0 ? "YELLOW" : "RED"))
321 .setTitle("Mute")
322 .setDescription(
323 "Mute " + (actualPunishments > 0 ? "partially" : "failed") + ":\n" +
324 messages.map(message => `> ${message}`).join("\n")
325 )
326 .setStatus(actualPunishments > 0 ? "Warning" : "Danger")
327 ],
328 components: []
329 });
330 }
331 const data = {
332 meta: {
333 type: "memberMute",
334 displayName: "Member Muted",
335 calculateType: "guildMemberPunish",
336 color: NucleusColors.yellow,
337 emoji: "PUNISH.WARN.YELLOW",
338 timestamp: new Date().getTime()
339 },
340 list: {
341 memberId: entry(member.user.id, `\`${member.user.id}\``),
342 name: entry(member.user.id, renderUser(member.user)),
343 mutedUntil: entry(
344 (new Date().getTime() + muteTime * 1000).toString(),
345 renderDelta(new Date().getTime() + muteTime * 1000)
346 ),
347 muted: entry(new Date().getTime.toString(), renderDelta(new Date().getTime() - 1000)),
348 mutedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)),
349 reason: entry(reason, reason ? reason : "*No reason provided*")
350 },
351 hidden: {
352 guild: interaction.guild.id
353 }
354 };
355 log(data);
356 const failed = !status.dm && notify;
357 await interaction.editReply({
358 embeds: [
359 new EmojiEmbed()
360 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
361 .setTitle("Mute")
362 .setDescription(
363 "The member was muted" + (failed ? ", but could not be notified" : "") +
364 (createAppealTicket
365 ? ` and an appeal ticket was opened in <#${confirmation.components!["appeal"]!.response}>`
366 : "")
367 )
368 .setStatus(failed ? "Warning" : "Success")
369 ],
370 components: []
371 });
pineafan63fc5e22022-08-04 22:04:10 +0100372};
pineafan8b4b17f2022-02-27 20:42:52 +0000373
pineafanbd02b4a2022-08-05 22:01:38 +0100374const check = (interaction: CommandInteraction) => {
PineaFana00db1b2023-01-02 15:32:54 +0000375 if (!interaction.guild) return;
Skyler Grey75ea9172022-08-06 10:22:23 +0100376 const member = interaction.member as GuildMember;
PineaFan5bea7e12023-01-05 21:20:04 +0000377 const me = interaction.guild.members.me!;
Skyler Grey75ea9172022-08-06 10:22:23 +0100378 const apply = interaction.options.getMember("user") as GuildMember;
pineafan62ce1922022-08-25 20:34:45 +0100379 const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
380 const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0;
381 const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100382 // Do not allow muting the owner
PineaFana00db1b2023-01-02 15:32:54 +0000383 if (member.id === interaction.guild.ownerId) throw new Error("You cannot mute the owner of the server");
pineafan8b4b17f2022-02-27 20:42:52 +0000384 // Check if Nucleus can mute the member
pineafan3a02ea32022-08-11 21:35:04 +0100385 if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000386 // Check if Nucleus has permission to mute
PineaFan5bea7e12023-01-05 21:20:04 +0000387 if (!me.permissions.has("ModerateMembers")) throw new Error("I do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000388 // Do not allow muting Nucleus
pineafan3a02ea32022-08-11 21:35:04 +0100389 if (member.id === me.id) throw new Error("I cannot mute myself");
pineafan8b4b17f2022-02-27 20:42:52 +0000390 // Allow the owner to mute anyone
PineaFana00db1b2023-01-02 15:32:54 +0000391 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000392 // Check if the user has moderate_members permission
PineaFan5bea7e12023-01-05 21:20:04 +0000393 if (!member.permissions.has("ModerateMembers"))
pineafan3a02ea32022-08-11 21:35:04 +0100394 throw new Error("You do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000395 // Check if the user is below on the role list
pineafan3a02ea32022-08-11 21:35:04 +0100396 if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000397 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100398 return true;
399};
pineafan8b4b17f2022-02-27 20:42:52 +0000400
Skyler Grey75ea9172022-08-06 10:22:23 +0100401export { command, callback, check };