blob: 95c8c5a14e242d71ec23f4951b45fe8ac037a968 [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> => {
PineaFana00db1b2023-01-02 15:32:54 +000052 if (!interaction.guild) return;
Skyler Grey11236ba2022-08-08 21:13:33 +010053 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +010054 const user = interaction.options.getMember("user") as GuildMember;
pineafan8b4b17f2022-02-27 20:42:52 +000055 const time = {
Skyler Greyc634e2b2022-08-06 17:50:48 +010056 days: interaction.options.getInteger("days") ?? 0,
57 hours: interaction.options.getInteger("hours") ?? 0,
58 minutes: interaction.options.getInteger("minutes") ?? 0,
59 seconds: interaction.options.getInteger("seconds") ?? 0
pineafan63fc5e22022-08-04 22:04:10 +010060 };
PineaFana00db1b2023-01-02 15:32:54 +000061 const config = await client.database.guilds.read(interaction.guild.id);
Skyler Grey11236ba2022-08-08 21:13:33 +010062 let serverSettingsDescription = config.moderation.mute.timeout ? "given a timeout" : "";
Skyler Grey75ea9172022-08-06 10:22:23 +010063 if (config.moderation.mute.role)
64 serverSettingsDescription +=
Skyler Grey11236ba2022-08-08 21:13:33 +010065 (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010066
Skyler Grey11236ba2022-08-08 21:13:33 +010067 let muteTime = time.days * 24 * 60 * 60 + time.hours * 60 * 60 + time.minutes * 60 + time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +010068 if (muteTime === 0) {
Skyler Grey75ea9172022-08-06 10:22:23 +010069 const m = (await interaction.reply({
70 embeds: [
71 new EmojiEmbed()
72 .setEmoji("PUNISH.MUTE.GREEN")
73 .setTitle("Mute")
74 .setDescription("How long should the user be muted")
75 .setStatus("Success")
76 ],
77 components: [
TheCodedProf21c08592022-09-13 14:14:43 -040078 new ActionRowBuilder().addComponents([
79 new Discord.ButtonBuilder().setCustomId("1m").setLabel("1 Minute").setStyle(ButtonStyle.Secondary),
80 new Discord.ButtonBuilder().setCustomId("10m").setLabel("10 Minutes").setStyle(ButtonStyle.Secondary),
81 new Discord.ButtonBuilder().setCustomId("30m").setLabel("30 Minutes").setStyle(ButtonStyle.Secondary),
82 new Discord.ButtonBuilder().setCustomId("1h").setLabel("1 Hour").setStyle(ButtonStyle.Secondary)
Skyler Grey75ea9172022-08-06 10:22:23 +010083 ]),
TheCodedProf21c08592022-09-13 14:14:43 -040084 new ActionRowBuilder().addComponents([
85 new Discord.ButtonBuilder().setCustomId("6h").setLabel("6 Hours").setStyle(ButtonStyle.Secondary),
86 new Discord.ButtonBuilder().setCustomId("12h").setLabel("12 Hours").setStyle(ButtonStyle.Secondary),
87 new Discord.ButtonBuilder().setCustomId("1d").setLabel("1 Day").setStyle(ButtonStyle.Secondary),
88 new Discord.ButtonBuilder().setCustomId("1w").setLabel("1 Week").setStyle(ButtonStyle.Secondary)
Skyler Grey75ea9172022-08-06 10:22:23 +010089 ]),
TheCodedProf21c08592022-09-13 14:14:43 -040090 new ActionRowBuilder().addComponents([
91 new Discord.ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +010092 .setCustomId("cancel")
93 .setLabel("Cancel")
TheCodedProf21c08592022-09-13 14:14:43 -040094 .setStyle(ButtonStyle.Danger)
Skyler Grey75ea9172022-08-06 10:22:23 +010095 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
96 ])
97 ],
98 ephemeral: true,
99 fetchReply: true
100 })) as Message;
pineafan8b4b17f2022-02-27 20:42:52 +0000101 let component;
102 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100103 component = await m.awaitMessageComponent({
104 filter: (m) => m.user.id === interaction.user.id,
105 time: 300000
106 });
107 } catch {
108 return;
109 }
pineafan8b4b17f2022-02-27 20:42:52 +0000110 component.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100111 if (component.customId === "cancel")
112 return interaction.editReply({
113 embeds: [
114 new EmojiEmbed()
115 .setEmoji("PUNISH.MUTE.RED")
116 .setTitle("Mute")
117 .setDescription("Mute cancelled")
118 .setStatus("Danger")
119 ]
120 });
pineafan8b4b17f2022-02-27 20:42:52 +0000121 switch (component.customId) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100122 case "1m": {
123 muteTime = 60;
124 break;
125 }
126 case "10m": {
127 muteTime = 60 * 10;
128 break;
129 }
130 case "30m": {
131 muteTime = 60 * 30;
132 break;
133 }
134 case "1h": {
135 muteTime = 60 * 60;
136 break;
137 }
138 case "6h": {
139 muteTime = 60 * 60 * 6;
140 break;
141 }
142 case "12h": {
143 muteTime = 60 * 60 * 12;
144 break;
145 }
146 case "1d": {
147 muteTime = 60 * 60 * 24;
148 break;
149 }
150 case "1w": {
151 muteTime = 60 * 60 * 24 * 7;
152 break;
153 }
pineafan8b4b17f2022-02-27 20:42:52 +0000154 }
155 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100156 await interaction.reply({
157 embeds: LoadingEmbed,
158 ephemeral: true,
159 fetchReply: true
160 });
pineafan8b4b17f2022-02-27 20:42:52 +0000161 }
pineafan5d1908e2022-02-28 21:34:47 +0000162 // TODO:[Modals] Replace this with a modal
pineafan62ce1922022-08-25 20:34:45 +0100163 let reason: string | null = null;
pineafan02ba0232022-07-24 22:16:15 +0100164 let notify = true;
165 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100166 let confirmation;
Skyler Greyad002172022-08-16 18:48:26 +0100167 let timedOut = false;
168 let success = false;
pineafan62ce1922022-08-25 20:34:45 +0100169 do {
pineafan73a7c4a2022-07-24 10:38:04 +0100170 confirmation = await new confirmationMessage(interaction)
171 .setEmoji("PUNISH.MUTE.RED")
172 .setTitle("Mute")
Skyler Grey75ea9172022-08-06 10:22:23 +0100173 .setDescription(
174 keyValueList({
175 user: renderUser(user.user),
176 time: `${humanizeDuration(muteTime * 1000, {
177 round: true
178 })}`,
pineafan62ce1922022-08-25 20:34:45 +0100179 reason: reason ? "\n> " + (reason).replaceAll("\n", "\n> ") : "*No reason provided*"
Skyler Grey75ea9172022-08-06 10:22:23 +0100180 }) +
181 "The user will be " +
182 serverSettingsDescription +
183 "\n" +
184 `The user **will${notify ? "" : " not"}** be notified\n\n` +
185 `Are you sure you want to mute <@!${user.id}>?`
186 )
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)),
Skyler Grey75ea9172022-08-06 10:22:23 +0100192 async () =>
PineaFana00db1b2023-01-02 15:32:54 +0000193 await create(interaction.guild, interaction.options.getUser("user")!, interaction.user, reason),
Skyler Grey75ea9172022-08-06 10:22:23 +0100194 "An appeal ticket will be created when Confirm is clicked",
PineaFana34d04b2023-01-03 22:05:42 +0000195 null,
Skyler Grey75ea9172022-08-06 10:22:23 +0100196 "CONTROL.TICKET",
197 createAppealTicket
198 )
199 .addCustomBoolean(
200 "notify",
201 "Notify user",
202 false,
pineafan62ce1922022-08-25 20:34:45 +0100203 undefined,
Skyler Grey75ea9172022-08-06 10:22:23 +0100204 null,
PineaFana34d04b2023-01-03 22:05:42 +0000205 null,
Skyler Grey75ea9172022-08-06 10:22:23 +0100206 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
207 notify
208 )
pineafan73a7c4a2022-07-24 10:38:04 +0100209 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100210 .send(true);
211 reason = reason ?? "";
Skyler Greyad002172022-08-16 18:48:26 +0100212 if (confirmation.cancelled) timedOut = true;
213 if (confirmation.success) success = true;
pineafan63fc5e22022-08-04 22:04:10 +0100214 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100215 if (confirmation.components) {
pineafan62ce1922022-08-25 20:34:45 +0100216 notify = confirmation.components["notify"]!.active;
217 createAppealTicket = confirmation.components["appeal"]!.active;
pineafan02ba0232022-07-24 22:16:15 +0100218 }
pineafan62ce1922022-08-25 20:34:45 +0100219 } while (!timedOut && !success)
Skyler Greyad002172022-08-16 18:48:26 +0100220 if (timedOut) return;
221 let dmd = false;
222 let dm;
pineafan62ce1922022-08-25 20:34:45 +0100223 if (confirmation.success) {
224 try {
225 if (notify) {
226 dm = await user.send({
227 embeds: [
228 new EmojiEmbed()
229 .setEmoji("PUNISH.MUTE.RED")
230 .setTitle("Muted")
231 .setDescription(
PineaFana00db1b2023-01-02 15:32:54 +0000232 `You have been muted in ${interaction.guild.name}` +
pineafan62ce1922022-08-25 20:34:45 +0100233 (reason
234 ? ` for:\n> ${reason}`
235 : ".\n\n" +
236 `You will be unmuted at: <t:${
237 Math.round(new Date().getTime() / 1000) + muteTime
238 }:D> at <t:${Math.round(new Date().getTime() / 1000) + muteTime}:T> (<t:${
239 Math.round(new Date().getTime() / 1000) + muteTime
240 }:R>)`) +
241 (confirmation.components!["appeal"]!.response
242 ? `You can appeal this here: <#${confirmation.components!["appeal"]!.response}>`
243 : "")
244 )
245 .setStatus("Danger")
246 ],
247 components: [
TheCodedProf21c08592022-09-13 14:14:43 -0400248 new ActionRowBuilder().addComponents(
pineafan62ce1922022-08-25 20:34:45 +0100249 config.moderation.mute.text
250 ? [
TheCodedProf21c08592022-09-13 14:14:43 -0400251 new ButtonBuilder()
252 .setStyle(ButtonStyle.Link)
pineafan62ce1922022-08-25 20:34:45 +0100253 .setLabel(config.moderation.mute.text)
254 .setURL(config.moderation.mute.link)
255 ]
256 : []
257 )
258 ]
259 });
260 dmd = true;
261 }
262 } catch {
263 dmd = false;
264 }
265 const member = user;
266 let errors = 0;
267 try {
268 if (config.moderation.mute.timeout) {
PineaFan100df682023-01-02 13:26:08 +0000269 await member.timeout(muteTime * 1000, reason || "*No reason provided*");
pineafan62ce1922022-08-25 20:34:45 +0100270 if (config.moderation.mute.role !== null) {
271 await member.roles.add(config.moderation.mute.role);
272 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
PineaFana00db1b2023-01-02 15:32:54 +0000273 guild: interaction.guild.id,
pineafan62ce1922022-08-25 20:34:45 +0100274 user: user.id,
275 expires: new Date().getTime() + muteTime * 1000
276 });
277 }
278 }
279 } catch {
280 errors++;
281 }
282 try {
283 if (config.moderation.mute.role !== null) {
284 await member.roles.add(config.moderation.mute.role);
285 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
PineaFana00db1b2023-01-02 15:32:54 +0000286 guild: interaction.guild.id,
pineafan62ce1922022-08-25 20:34:45 +0100287 user: user.id,
288 role: config.moderation.mute.role
289 });
290 }
291 } catch (e) {
292 console.log(e);
293 errors++;
294 }
295 if (errors === 2) {
296 await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100297 embeds: [
298 new EmojiEmbed()
299 .setEmoji("PUNISH.MUTE.RED")
pineafan62ce1922022-08-25 20:34:45 +0100300 .setTitle("Mute")
301 .setDescription("Something went wrong and the user was not muted")
Skyler Grey75ea9172022-08-06 10:22:23 +0100302 .setStatus("Danger")
303 ],
pineafan62ce1922022-08-25 20:34:45 +0100304 components: []
305 }); // TODO: make this clearer
306 if (dmd && dm) await dm.delete();
307 return;
Skyler Greyad002172022-08-16 18:48:26 +0100308 }
PineaFana00db1b2023-01-02 15:32:54 +0000309 await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason);
pineafan62ce1922022-08-25 20:34:45 +0100310 const failed = !dmd && notify;
Skyler Grey75ea9172022-08-06 10:22:23 +0100311 await interaction.editReply({
312 embeds: [
313 new EmojiEmbed()
pineafan62ce1922022-08-25 20:34:45 +0100314 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
Skyler Grey75ea9172022-08-06 10:22:23 +0100315 .setTitle("Mute")
pineafan62ce1922022-08-25 20:34:45 +0100316 .setDescription(
317 "The member was muted" +
318 (failed ? ", but could not be notified" : "") +
319 (confirmation.components!["appeal"]!.response
320 ? ` and an appeal ticket was opened in <#${confirmation.components!["appeal"]!.response}>`
321 : "")
322 )
323 .setStatus(failed ? "Warning" : "Success")
Skyler Grey75ea9172022-08-06 10:22:23 +0100324 ],
325 components: []
pineafan62ce1922022-08-25 20:34:45 +0100326 });
327 const data = {
328 meta: {
329 type: "memberMute",
330 displayName: "Member Muted",
331 calculateType: "guildMemberPunish",
332 color: NucleusColors.yellow,
333 emoji: "PUNISH.WARN.YELLOW",
334 timestamp: new Date().getTime()
335 },
336 list: {
337 memberId: entry(member.user.id, `\`${member.user.id}\``),
338 name: entry(member.user.id, renderUser(member.user)),
339 mutedUntil: entry(
340 new Date().getTime() + muteTime * 1000,
341 renderDelta(new Date().getTime() + muteTime * 1000)
342 ),
343 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
344 mutedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user)),
345 reason: entry(reason, reason ? reason : "*No reason provided*")
346 },
347 hidden: {
PineaFana00db1b2023-01-02 15:32:54 +0000348 guild: interaction.guild.id
pineafan62ce1922022-08-25 20:34:45 +0100349 }
350 };
351 log(data);
352 } else {
353 await interaction.editReply({
354 embeds: [
355 new EmojiEmbed()
356 .setEmoji("PUNISH.BAN.GREEN")
357 .setTitle("Softban")
358 .setDescription("No changes were made")
359 .setStatus("Success")
360 ],
361 components: []
362 });
pineafan8b4b17f2022-02-27 20:42:52 +0000363 }
pineafan63fc5e22022-08-04 22:04:10 +0100364};
pineafan8b4b17f2022-02-27 20:42:52 +0000365
pineafanbd02b4a2022-08-05 22:01:38 +0100366const check = (interaction: CommandInteraction) => {
PineaFana00db1b2023-01-02 15:32:54 +0000367 if (!interaction.guild) return;
Skyler Grey75ea9172022-08-06 10:22:23 +0100368 const member = interaction.member as GuildMember;
PineaFana00db1b2023-01-02 15:32:54 +0000369 const me = interaction.guild.me!;
Skyler Grey75ea9172022-08-06 10:22:23 +0100370 const apply = interaction.options.getMember("user") as GuildMember;
pineafan62ce1922022-08-25 20:34:45 +0100371 const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
372 const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0;
373 const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100374 // Do not allow muting the owner
PineaFana00db1b2023-01-02 15:32:54 +0000375 if (member.id === interaction.guild.ownerId) throw new Error("You cannot mute the owner of the server");
pineafan8b4b17f2022-02-27 20:42:52 +0000376 // Check if Nucleus can mute the member
pineafan3a02ea32022-08-11 21:35:04 +0100377 if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000378 // Check if Nucleus has permission to mute
pineafan3a02ea32022-08-11 21:35:04 +0100379 if (!me.permissions.has("MODERATE_MEMBERS")) throw new Error("I do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000380 // Do not allow muting Nucleus
pineafan3a02ea32022-08-11 21:35:04 +0100381 if (member.id === me.id) throw new Error("I cannot mute myself");
pineafan8b4b17f2022-02-27 20:42:52 +0000382 // Allow the owner to mute anyone
PineaFana00db1b2023-01-02 15:32:54 +0000383 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000384 // Check if the user has moderate_members permission
pineafan3a02ea32022-08-11 21:35:04 +0100385 if (!member.permissions.has("MODERATE_MEMBERS"))
386 throw new Error("You do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000387 // Check if the user is below on the role list
pineafan3a02ea32022-08-11 21:35:04 +0100388 if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000389 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100390 return true;
391};
pineafan8b4b17f2022-02-27 20:42:52 +0000392
Skyler Grey75ea9172022-08-06 10:22:23 +0100393export { command, callback, check };