blob: 0e76cbaa99bd480dce624bdfe864eb8a2b537229 [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",
195 "CONTROL.TICKET",
196 createAppealTicket
197 )
198 .addCustomBoolean(
199 "notify",
200 "Notify user",
201 false,
pineafan62ce1922022-08-25 20:34:45 +0100202 undefined,
Skyler Grey75ea9172022-08-06 10:22:23 +0100203 null,
204 "ICONS.NOTIFY." + (notify ? "ON" : "OFF"),
205 notify
206 )
pineafan73a7c4a2022-07-24 10:38:04 +0100207 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100208 .send(true);
209 reason = reason ?? "";
Skyler Greyad002172022-08-16 18:48:26 +0100210 if (confirmation.cancelled) timedOut = true;
211 if (confirmation.success) success = true;
pineafan63fc5e22022-08-04 22:04:10 +0100212 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100213 if (confirmation.components) {
pineafan62ce1922022-08-25 20:34:45 +0100214 notify = confirmation.components["notify"]!.active;
215 createAppealTicket = confirmation.components["appeal"]!.active;
pineafan02ba0232022-07-24 22:16:15 +0100216 }
pineafan62ce1922022-08-25 20:34:45 +0100217 } while (!timedOut && !success)
Skyler Greyad002172022-08-16 18:48:26 +0100218 if (timedOut) return;
219 let dmd = false;
220 let dm;
pineafan62ce1922022-08-25 20:34:45 +0100221 if (confirmation.success) {
222 try {
223 if (notify) {
224 dm = await user.send({
225 embeds: [
226 new EmojiEmbed()
227 .setEmoji("PUNISH.MUTE.RED")
228 .setTitle("Muted")
229 .setDescription(
PineaFana00db1b2023-01-02 15:32:54 +0000230 `You have been muted in ${interaction.guild.name}` +
pineafan62ce1922022-08-25 20:34:45 +0100231 (reason
232 ? ` for:\n> ${reason}`
233 : ".\n\n" +
234 `You will be unmuted at: <t:${
235 Math.round(new Date().getTime() / 1000) + muteTime
236 }:D> at <t:${Math.round(new Date().getTime() / 1000) + muteTime}:T> (<t:${
237 Math.round(new Date().getTime() / 1000) + muteTime
238 }:R>)`) +
239 (confirmation.components!["appeal"]!.response
240 ? `You can appeal this here: <#${confirmation.components!["appeal"]!.response}>`
241 : "")
242 )
243 .setStatus("Danger")
244 ],
245 components: [
TheCodedProf21c08592022-09-13 14:14:43 -0400246 new ActionRowBuilder().addComponents(
pineafan62ce1922022-08-25 20:34:45 +0100247 config.moderation.mute.text
248 ? [
TheCodedProf21c08592022-09-13 14:14:43 -0400249 new ButtonBuilder()
250 .setStyle(ButtonStyle.Link)
pineafan62ce1922022-08-25 20:34:45 +0100251 .setLabel(config.moderation.mute.text)
252 .setURL(config.moderation.mute.link)
253 ]
254 : []
255 )
256 ]
257 });
258 dmd = true;
259 }
260 } catch {
261 dmd = false;
262 }
263 const member = user;
264 let errors = 0;
265 try {
266 if (config.moderation.mute.timeout) {
PineaFan100df682023-01-02 13:26:08 +0000267 await member.timeout(muteTime * 1000, reason || "*No reason provided*");
pineafan62ce1922022-08-25 20:34:45 +0100268 if (config.moderation.mute.role !== null) {
269 await member.roles.add(config.moderation.mute.role);
270 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
PineaFana00db1b2023-01-02 15:32:54 +0000271 guild: interaction.guild.id,
pineafan62ce1922022-08-25 20:34:45 +0100272 user: user.id,
273 expires: new Date().getTime() + muteTime * 1000
274 });
275 }
276 }
277 } catch {
278 errors++;
279 }
280 try {
281 if (config.moderation.mute.role !== null) {
282 await member.roles.add(config.moderation.mute.role);
283 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
PineaFana00db1b2023-01-02 15:32:54 +0000284 guild: interaction.guild.id,
pineafan62ce1922022-08-25 20:34:45 +0100285 user: user.id,
286 role: config.moderation.mute.role
287 });
288 }
289 } catch (e) {
290 console.log(e);
291 errors++;
292 }
293 if (errors === 2) {
294 await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100295 embeds: [
296 new EmojiEmbed()
297 .setEmoji("PUNISH.MUTE.RED")
pineafan62ce1922022-08-25 20:34:45 +0100298 .setTitle("Mute")
299 .setDescription("Something went wrong and the user was not muted")
Skyler Grey75ea9172022-08-06 10:22:23 +0100300 .setStatus("Danger")
301 ],
pineafan62ce1922022-08-25 20:34:45 +0100302 components: []
303 }); // TODO: make this clearer
304 if (dmd && dm) await dm.delete();
305 return;
Skyler Greyad002172022-08-16 18:48:26 +0100306 }
PineaFana00db1b2023-01-02 15:32:54 +0000307 await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason);
pineafan62ce1922022-08-25 20:34:45 +0100308 const failed = !dmd && notify;
Skyler Grey75ea9172022-08-06 10:22:23 +0100309 await interaction.editReply({
310 embeds: [
311 new EmojiEmbed()
pineafan62ce1922022-08-25 20:34:45 +0100312 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
Skyler Grey75ea9172022-08-06 10:22:23 +0100313 .setTitle("Mute")
pineafan62ce1922022-08-25 20:34:45 +0100314 .setDescription(
315 "The member was muted" +
316 (failed ? ", but could not be notified" : "") +
317 (confirmation.components!["appeal"]!.response
318 ? ` and an appeal ticket was opened in <#${confirmation.components!["appeal"]!.response}>`
319 : "")
320 )
321 .setStatus(failed ? "Warning" : "Success")
Skyler Grey75ea9172022-08-06 10:22:23 +0100322 ],
323 components: []
pineafan62ce1922022-08-25 20:34:45 +0100324 });
325 const data = {
326 meta: {
327 type: "memberMute",
328 displayName: "Member Muted",
329 calculateType: "guildMemberPunish",
330 color: NucleusColors.yellow,
331 emoji: "PUNISH.WARN.YELLOW",
332 timestamp: new Date().getTime()
333 },
334 list: {
335 memberId: entry(member.user.id, `\`${member.user.id}\``),
336 name: entry(member.user.id, renderUser(member.user)),
337 mutedUntil: entry(
338 new Date().getTime() + muteTime * 1000,
339 renderDelta(new Date().getTime() + muteTime * 1000)
340 ),
341 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
342 mutedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user)),
343 reason: entry(reason, reason ? reason : "*No reason provided*")
344 },
345 hidden: {
PineaFana00db1b2023-01-02 15:32:54 +0000346 guild: interaction.guild.id
pineafan62ce1922022-08-25 20:34:45 +0100347 }
348 };
349 log(data);
350 } else {
351 await interaction.editReply({
352 embeds: [
353 new EmojiEmbed()
354 .setEmoji("PUNISH.BAN.GREEN")
355 .setTitle("Softban")
356 .setDescription("No changes were made")
357 .setStatus("Success")
358 ],
359 components: []
360 });
pineafan8b4b17f2022-02-27 20:42:52 +0000361 }
pineafan63fc5e22022-08-04 22:04:10 +0100362};
pineafan8b4b17f2022-02-27 20:42:52 +0000363
pineafanbd02b4a2022-08-05 22:01:38 +0100364const check = (interaction: CommandInteraction) => {
PineaFana00db1b2023-01-02 15:32:54 +0000365 if (!interaction.guild) return;
Skyler Grey75ea9172022-08-06 10:22:23 +0100366 const member = interaction.member as GuildMember;
PineaFana00db1b2023-01-02 15:32:54 +0000367 const me = interaction.guild.me!;
Skyler Grey75ea9172022-08-06 10:22:23 +0100368 const apply = interaction.options.getMember("user") as GuildMember;
pineafan62ce1922022-08-25 20:34:45 +0100369 const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
370 const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0;
371 const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100372 // Do not allow muting the owner
PineaFana00db1b2023-01-02 15:32:54 +0000373 if (member.id === interaction.guild.ownerId) throw new Error("You cannot mute the owner of the server");
pineafan8b4b17f2022-02-27 20:42:52 +0000374 // Check if Nucleus can mute the member
pineafan3a02ea32022-08-11 21:35:04 +0100375 if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000376 // Check if Nucleus has permission to mute
pineafan3a02ea32022-08-11 21:35:04 +0100377 if (!me.permissions.has("MODERATE_MEMBERS")) throw new Error("I do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000378 // Do not allow muting Nucleus
pineafan3a02ea32022-08-11 21:35:04 +0100379 if (member.id === me.id) throw new Error("I cannot mute myself");
pineafan8b4b17f2022-02-27 20:42:52 +0000380 // Allow the owner to mute anyone
PineaFana00db1b2023-01-02 15:32:54 +0000381 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000382 // Check if the user has moderate_members permission
pineafan3a02ea32022-08-11 21:35:04 +0100383 if (!member.permissions.has("MODERATE_MEMBERS"))
384 throw new Error("You do not have the *Moderate Members* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000385 // Check if the user is below on the role list
pineafan3a02ea32022-08-11 21:35:04 +0100386 if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
pineafan8b4b17f2022-02-27 20:42:52 +0000387 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100388 return true;
389};
pineafan8b4b17f2022-02-27 20:42:52 +0000390
Skyler Grey75ea9172022-08-06 10:22:23 +0100391export { command, callback, check };