blob: 1e9a8122f1c8ce64c9c1f2e5e7981257c0bb733d [file] [log] [blame]
pineafan63fc5e22022-08-04 22:04:10 +01001import { LoadingEmbed } from "./../../utils/defaultEmbeds.js";
pineafanbd02b4a2022-08-05 22:01:38 +01002import Discord, { CommandInteraction, GuildMember, Message, MessageActionRow, MessageButton } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00003import { 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";
8import humanizeDuration from "humanize-duration";
pineafan6702cef2022-06-13 17:52:37 +01009import client from "../../utils/client.js";
pineafan73a7c4a2022-07-24 10:38:04 +010010import { areTicketsEnabled, create } from "../../actions/createModActionTicket.js";
pineafan8b4b17f2022-02-27 20:42:52 +000011
12const command = (builder: SlashCommandSubcommandBuilder) =>
13 builder
pineafan63fc5e22022-08-04 22:04:10 +010014 .setName("mute")
15 .setDescription("Mutes a member, stopping them from talking in the server")
16 .addUserOption(option => option.setName("user").setDescription("The user to mute").setRequired(true))
17 .addIntegerOption(option => option.setName("days").setDescription("The number of days to mute the user for | Default: 0").setMinValue(0).setMaxValue(27).setRequired(false))
18 .addIntegerOption(option => option.setName("hours").setDescription("The number of hours to mute the user for | Default: 0").setMinValue(0).setMaxValue(23).setRequired(false))
19 .addIntegerOption(option => option.setName("minutes").setDescription("The number of minutes to mute the user for | Default: 0").setMinValue(0).setMaxValue(59).setRequired(false))
20 .addIntegerOption(option => option.setName("seconds").setDescription("The number of seconds to mute the user for | Default: 0").setMinValue(0).setMaxValue(59).setRequired(false));
pineafan8b4b17f2022-02-27 20:42:52 +000021
pineafanbd02b4a2022-08-05 22:01:38 +010022const callback = async (interaction: CommandInteraction): Promise<void | unknown> => {
pineafan63fc5e22022-08-04 22:04:10 +010023 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger;
24 const user = interaction.options.getMember("user") as GuildMember;
pineafan8b4b17f2022-02-27 20:42:52 +000025 const time = {
26 days: interaction.options.getInteger("days") || 0,
27 hours: interaction.options.getInteger("hours") || 0,
28 minutes: interaction.options.getInteger("minutes") || 0,
29 seconds: interaction.options.getInteger("seconds") || 0
pineafan63fc5e22022-08-04 22:04:10 +010030 };
31 const config = await client.database.guilds.read(interaction.guild.id);
32 let serverSettingsDescription = (config.moderation.mute.timeout ? "given a timeout" : "");
33 if (config.moderation.mute.role) serverSettingsDescription += (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010034
pineafan63fc5e22022-08-04 22:04:10 +010035 let muteTime = (time.days * 24 * 60 * 60) + (time.hours * 60 * 60) + (time.minutes * 60) + time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +010036 if (muteTime === 0) {
pineafan63fc5e22022-08-04 22:04:10 +010037 const m = await interaction.reply({embeds: [
pineafan4edb7762022-06-26 19:21:04 +010038 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000039 .setEmoji("PUNISH.MUTE.GREEN")
40 .setTitle("Mute")
41 .setDescription("How long should the user be muted")
42 .setStatus("Success")
43 ], components: [
44 new MessageActionRow().addComponents([
45 new Discord.MessageButton()
46 .setCustomId("1m")
47 .setLabel("1 Minute")
48 .setStyle("SECONDARY"),
49 new Discord.MessageButton()
50 .setCustomId("10m")
51 .setLabel("10 Minutes")
52 .setStyle("SECONDARY"),
53 new Discord.MessageButton()
54 .setCustomId("30m")
55 .setLabel("30 Minutes")
56 .setStyle("SECONDARY"),
57 new Discord.MessageButton()
58 .setCustomId("1h")
59 .setLabel("1 Hour")
60 .setStyle("SECONDARY")
61 ]),
62 new MessageActionRow().addComponents([
63 new Discord.MessageButton()
64 .setCustomId("6h")
65 .setLabel("6 Hours")
66 .setStyle("SECONDARY"),
67 new Discord.MessageButton()
68 .setCustomId("12h")
69 .setLabel("12 Hours")
70 .setStyle("SECONDARY"),
71 new Discord.MessageButton()
72 .setCustomId("1d")
73 .setLabel("1 Day")
74 .setStyle("SECONDARY"),
75 new Discord.MessageButton()
76 .setCustomId("1w")
77 .setLabel("1 Week")
78 .setStyle("SECONDARY")
79 ]),
80 new MessageActionRow().addComponents([
81 new Discord.MessageButton()
82 .setCustomId("cancel")
83 .setLabel("Cancel")
84 .setStyle("DANGER")
85 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
86 ])
pineafanbd02b4a2022-08-05 22:01:38 +010087 ], ephemeral: true, fetchReply: true}) as Message;
pineafan8b4b17f2022-02-27 20:42:52 +000088 let component;
89 try {
pineafanbd02b4a2022-08-05 22:01:38 +010090 component = await m.awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan63fc5e22022-08-04 22:04:10 +010091 } catch { return; }
pineafan8b4b17f2022-02-27 20:42:52 +000092 component.deferUpdate();
pineafane23c4ec2022-07-27 21:56:27 +010093 if (component.customId === "cancel") return interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000094 .setEmoji("PUNISH.MUTE.RED")
95 .setTitle("Mute")
96 .setDescription("Mute cancelled")
97 .setStatus("Danger")
pineafan63fc5e22022-08-04 22:04:10 +010098 ]});
pineafan8b4b17f2022-02-27 20:42:52 +000099 switch (component.customId) {
pineafan63fc5e22022-08-04 22:04:10 +0100100 case "1m": { muteTime = 60; break; }
101 case "10m": { muteTime = 60 * 10; break; }
102 case "30m": { muteTime = 60 * 30; break; }
103 case "1h": { muteTime = 60 * 60; break; }
104 case "6h": { muteTime = 60 * 60 * 6; break; }
105 case "12h": { muteTime = 60 * 60 * 12; break; }
106 case "1d": { muteTime = 60 * 60 * 24; break; }
107 case "1w": { muteTime = 60 * 60 * 24 * 7; break; }
pineafan8b4b17f2022-02-27 20:42:52 +0000108 }
109 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100110 await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
pineafan8b4b17f2022-02-27 20:42:52 +0000111 }
pineafan5d1908e2022-02-28 21:34:47 +0000112 // TODO:[Modals] Replace this with a modal
pineafan73a7c4a2022-07-24 10:38:04 +0100113 let reason = null;
pineafan02ba0232022-07-24 22:16:15 +0100114 let notify = true;
115 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100116 let confirmation;
117 while (true) {
118 confirmation = await new confirmationMessage(interaction)
119 .setEmoji("PUNISH.MUTE.RED")
120 .setTitle("Mute")
121 .setDescription(keyValueList({
122 "user": renderUser(user.user),
123 "time": `${humanizeDuration(muteTime * 1000, {round: true})}`,
124 "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*"
125 })
pineafan63fc5e22022-08-04 22:04:10 +0100126 + "The user will be " + serverSettingsDescription + "\n"
127 + `The user **will${notify ? "" : " not"}** be notified\n\n`
pineafan73a7c4a2022-07-24 10:38:04 +0100128 + `Are you sure you want to mute <@!${user.id}>?`)
129 .setColor("Danger")
130 .addCustomBoolean(
pineafan02ba0232022-07-24 22:16:15 +0100131 "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)),
132 async () => await create(interaction.guild, interaction.options.getUser("user"), interaction.user, reason),
133 "An appeal ticket will be created when Confirm is clicked", "CONTROL.TICKET", createAppealTicket)
134 .addCustomBoolean("notify", "Notify user", false, null, null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF" ), notify)
pineafan73a7c4a2022-07-24 10:38:04 +0100135 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100136 .send(true);
137 reason = reason ?? "";
138 if (confirmation.cancelled) return;
139 if (confirmation.success) break;
140 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100141 if (confirmation.components) {
pineafan63fc5e22022-08-04 22:04:10 +0100142 notify = confirmation.components.notify.active;
143 createAppealTicket = confirmation.components.appeal.active;
pineafan02ba0232022-07-24 22:16:15 +0100144 }
pineafan73a7c4a2022-07-24 10:38:04 +0100145 }
pineafan377794f2022-04-18 19:01:01 +0100146 if (confirmation.success) {
pineafan63fc5e22022-08-04 22:04:10 +0100147 let dmd = false;
pineafan5d1908e2022-02-28 21:34:47 +0000148 let dm;
pineafan63fc5e22022-08-04 22:04:10 +0100149 const config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000150 try {
pineafan02ba0232022-07-24 22:16:15 +0100151 if (notify) {
pineafan73a7c4a2022-07-24 10:38:04 +0100152 dm = await user.send({
pineafan4edb7762022-06-26 19:21:04 +0100153 embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000154 .setEmoji("PUNISH.MUTE.RED")
155 .setTitle("Muted")
156 .setDescription(`You have been muted in ${interaction.guild.name}` +
pineafan73a7c4a2022-07-24 10:38:04 +0100157 (reason ? ` for:\n> ${reason}` : ".\n\n" +
pineafan02ba0232022-07-24 22:16:15 +0100158 `You will be unmuted at: <t:${Math.round((new Date).getTime() / 1000) + muteTime}:D> at <t:${Math.round((new Date).getTime() / 1000) + muteTime}:T> (<t:${Math.round((new Date).getTime() / 1000) + muteTime}:R>)`) +
pineafan63fc5e22022-08-04 22:04:10 +0100159 (confirmation.components.appeal.response ? `You can appeal this here: <#${confirmation.components.appeal.response}>` : ""))
pineafan8b4b17f2022-02-27 20:42:52 +0000160 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100161 ],
162 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
163 .setStyle("LINK")
164 .setLabel(config.moderation.mute.text)
165 .setURL(config.moderation.mute.link)
166 ] : [])]
pineafan63fc5e22022-08-04 22:04:10 +0100167 });
168 dmd = true;
pineafan8b4b17f2022-02-27 20:42:52 +0000169 }
pineafan63fc5e22022-08-04 22:04:10 +0100170 } catch { dmd = false; }
171 const member = user;
172 let errors = 0;
pineafan8b4b17f2022-02-27 20:42:52 +0000173 try {
pineafane625d782022-05-09 18:04:32 +0100174 if (config.moderation.mute.timeout) {
pineafan63fc5e22022-08-04 22:04:10 +0100175 await member.timeout(muteTime * 1000, reason || "No reason provided");
pineafan73a7c4a2022-07-24 10:38:04 +0100176 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100177 await member.roles.add(config.moderation.mute.role);
pineafan73a7c4a2022-07-24 10:38:04 +0100178 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
179 guild: interaction.guild.id,
180 user: user.id,
181 expires: new Date().getTime() + muteTime * 1000
pineafan63fc5e22022-08-04 22:04:10 +0100182 });
pineafan73a7c4a2022-07-24 10:38:04 +0100183 }
pineafane625d782022-05-09 18:04:32 +0100184 }
pineafan63fc5e22022-08-04 22:04:10 +0100185 } catch { errors++; }
pineafan73a7c4a2022-07-24 10:38:04 +0100186 try {
187 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100188 await member.roles.add(config.moderation.mute.role);
pineafan73a7c4a2022-07-24 10:38:04 +0100189 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
190 guild: interaction.guild.id,
191 user: user.id,
192 role: config.moderation.mute.role
pineafan63fc5e22022-08-04 22:04:10 +0100193 });
pineafan73a7c4a2022-07-24 10:38:04 +0100194 }
pineafan63fc5e22022-08-04 22:04:10 +0100195 } catch (e){ console.log(e); errors++; }
pineafane23c4ec2022-07-27 21:56:27 +0100196 if (errors === 2) {
pineafan4edb7762022-06-26 19:21:04 +0100197 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000198 .setEmoji("PUNISH.MUTE.RED")
pineafan63fc5e22022-08-04 22:04:10 +0100199 .setTitle("Mute")
pineafan73a7c4a2022-07-24 10:38:04 +0100200 .setDescription("Something went wrong and the user was not muted")
pineafan8b4b17f2022-02-27 20:42:52 +0000201 .setStatus("Danger")
pineafan63fc5e22022-08-04 22:04:10 +0100202 ], components: []}); // TODO: make this clearer
203 if (dmd) await dm.delete();
204 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000205 }
pineafan63fc5e22022-08-04 22:04:10 +0100206 await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason);
207 const failed = (dmd === false && notify);
pineafan4edb7762022-06-26 19:21:04 +0100208 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000209 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
pineafan63fc5e22022-08-04 22:04:10 +0100210 .setTitle("Mute")
211 .setDescription("The member was muted" + (failed ? ", but could not be notified" : "") + (confirmation.components.appeal.response ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>` : ""))
pineafan5d1908e2022-02-28 21:34:47 +0000212 .setStatus(failed ? "Warning" : "Success")
pineafan63fc5e22022-08-04 22:04:10 +0100213 ], components: []});
214 const data = {
pineafan377794f2022-04-18 19:01:01 +0100215 meta:{
pineafan63fc5e22022-08-04 22:04:10 +0100216 type: "memberMute",
217 displayName: "Member Muted",
218 calculateType: "guildMemberPunish",
pineafan377794f2022-04-18 19:01:01 +0100219 color: NucleusColors.yellow,
pineafan63fc5e22022-08-04 22:04:10 +0100220 emoji: "PUNISH.WARN.YELLOW",
pineafan377794f2022-04-18 19:01:01 +0100221 timestamp: new Date().getTime()
222 },
223 list: {
pineafan73a7c4a2022-07-24 10:38:04 +0100224 memberId: entry(member.user.id, `\`${member.user.id}\``),
225 name: entry(member.user.id, renderUser(member.user)),
226 mutedUntil: entry(new Date().getTime() + muteTime * 1000, renderDelta(new Date().getTime() + muteTime * 1000)),
227 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
pineafan377794f2022-04-18 19:01:01 +0100228 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
pineafan63fc5e22022-08-04 22:04:10 +0100229 reason: entry(reason, reason ? reason : "*No reason provided*")
pineafan377794f2022-04-18 19:01:01 +0100230 },
231 hidden: {
232 guild: interaction.guild.id
233 }
pineafan63fc5e22022-08-04 22:04:10 +0100234 };
pineafan4edb7762022-06-26 19:21:04 +0100235 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000236 } else {
pineafan4edb7762022-06-26 19:21:04 +0100237 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000238 .setEmoji("PUNISH.MUTE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100239 .setTitle("Mute")
pineafan8b4b17f2022-02-27 20:42:52 +0000240 .setDescription("No changes were made")
241 .setStatus("Success")
pineafan63fc5e22022-08-04 22:04:10 +0100242 ], components: []});
pineafan8b4b17f2022-02-27 20:42:52 +0000243 }
pineafan63fc5e22022-08-04 22:04:10 +0100244};
pineafan8b4b17f2022-02-27 20:42:52 +0000245
pineafanbd02b4a2022-08-05 22:01:38 +0100246const check = (interaction: CommandInteraction) => {
pineafan63fc5e22022-08-04 22:04:10 +0100247 const member = (interaction.member as GuildMember);
248 const me = (interaction.guild.me as GuildMember);
249 const apply = (interaction.options.getMember("user") as GuildMember);
250 if (member === null || me === null || apply === null) throw "That member is not in the server";
251 const memberPos = member.roles ? member.roles.highest.position : 0;
252 const mePos = me.roles ? me.roles.highest.position : 0;
253 const applyPos = apply.roles ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100254 // Do not allow muting the owner
pineafan63fc5e22022-08-04 22:04:10 +0100255 if (member.id === interaction.guild.ownerId) throw "You cannot mute the owner of the server";
pineafan8b4b17f2022-02-27 20:42:52 +0000256 // Check if Nucleus can mute the member
pineafan63fc5e22022-08-04 22:04:10 +0100257 if (! (mePos > applyPos)) throw "I do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000258 // Check if Nucleus has permission to mute
pineafane23c4ec2022-07-27 21:56:27 +0100259 if (! me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000260 // Do not allow muting Nucleus
pineafan63fc5e22022-08-04 22:04:10 +0100261 if (member.id === me.id) throw "I cannot mute myself";
pineafan8b4b17f2022-02-27 20:42:52 +0000262 // Allow the owner to mute anyone
pineafan63fc5e22022-08-04 22:04:10 +0100263 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000264 // Check if the user has moderate_members permission
pineafane23c4ec2022-07-27 21:56:27 +0100265 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000266 // Check if the user is below on the role list
pineafan63fc5e22022-08-04 22:04:10 +0100267 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000268 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100269 return true;
270};
pineafan8b4b17f2022-02-27 20:42:52 +0000271
272export { command, callback, check };