blob: 183c487a79510767b18184688232094396aa2baf [file] [log] [blame]
pineafan63fc5e22022-08-04 22:04:10 +01001import { LoadingEmbed } from "./../../utils/defaultEmbeds.js";
pineafan377794f2022-04-18 19:01:01 +01002import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00003import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
4import { WrappedCheck } from "jshaiku";
pineafan4edb7762022-06-26 19:21:04 +01005import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +00006import getEmojiByName from "../../utils/getEmojiByName.js";
7import confirmationMessage from "../../utils/confirmationMessage.js";
8import keyValueList from "../../utils/generateKeyValueList.js";
9import humanizeDuration from "humanize-duration";
pineafan6702cef2022-06-13 17:52:37 +010010import client from "../../utils/client.js";
pineafan73a7c4a2022-07-24 10:38:04 +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")
16 .setDescription("Mutes a member, stopping them from talking in the server")
17 .addUserOption(option => option.setName("user").setDescription("The user to mute").setRequired(true))
18 .addIntegerOption(option => option.setName("days").setDescription("The number of days to mute the user for | Default: 0").setMinValue(0).setMaxValue(27).setRequired(false))
19 .addIntegerOption(option => option.setName("hours").setDescription("The number of hours to mute the user for | Default: 0").setMinValue(0).setMaxValue(23).setRequired(false))
20 .addIntegerOption(option => option.setName("minutes").setDescription("The number of minutes to mute the user for | Default: 0").setMinValue(0).setMaxValue(59).setRequired(false))
21 .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 +000022
pineafan6702cef2022-06-13 17:52:37 +010023const callback = async (interaction: CommandInteraction): Promise<any> => {
pineafan63fc5e22022-08-04 22:04:10 +010024 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger;
25 const user = interaction.options.getMember("user") as GuildMember;
pineafan8b4b17f2022-02-27 20:42:52 +000026 const time = {
27 days: interaction.options.getInteger("days") || 0,
28 hours: interaction.options.getInteger("hours") || 0,
29 minutes: interaction.options.getInteger("minutes") || 0,
30 seconds: interaction.options.getInteger("seconds") || 0
pineafan63fc5e22022-08-04 22:04:10 +010031 };
32 const config = await client.database.guilds.read(interaction.guild.id);
33 let serverSettingsDescription = (config.moderation.mute.timeout ? "given a timeout" : "");
34 if (config.moderation.mute.role) serverSettingsDescription += (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`;
pineafane625d782022-05-09 18:04:32 +010035
pineafan63fc5e22022-08-04 22:04:10 +010036 let muteTime = (time.days * 24 * 60 * 60) + (time.hours * 60 * 60) + (time.minutes * 60) + time.seconds;
pineafane23c4ec2022-07-27 21:56:27 +010037 if (muteTime === 0) {
pineafan63fc5e22022-08-04 22:04:10 +010038 const m = await interaction.reply({embeds: [
pineafan4edb7762022-06-26 19:21:04 +010039 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000040 .setEmoji("PUNISH.MUTE.GREEN")
41 .setTitle("Mute")
42 .setDescription("How long should the user be muted")
43 .setStatus("Success")
44 ], components: [
45 new MessageActionRow().addComponents([
46 new Discord.MessageButton()
47 .setCustomId("1m")
48 .setLabel("1 Minute")
49 .setStyle("SECONDARY"),
50 new Discord.MessageButton()
51 .setCustomId("10m")
52 .setLabel("10 Minutes")
53 .setStyle("SECONDARY"),
54 new Discord.MessageButton()
55 .setCustomId("30m")
56 .setLabel("30 Minutes")
57 .setStyle("SECONDARY"),
58 new Discord.MessageButton()
59 .setCustomId("1h")
60 .setLabel("1 Hour")
61 .setStyle("SECONDARY")
62 ]),
63 new MessageActionRow().addComponents([
64 new Discord.MessageButton()
65 .setCustomId("6h")
66 .setLabel("6 Hours")
67 .setStyle("SECONDARY"),
68 new Discord.MessageButton()
69 .setCustomId("12h")
70 .setLabel("12 Hours")
71 .setStyle("SECONDARY"),
72 new Discord.MessageButton()
73 .setCustomId("1d")
74 .setLabel("1 Day")
75 .setStyle("SECONDARY"),
76 new Discord.MessageButton()
77 .setCustomId("1w")
78 .setLabel("1 Week")
79 .setStyle("SECONDARY")
80 ]),
81 new MessageActionRow().addComponents([
82 new Discord.MessageButton()
83 .setCustomId("cancel")
84 .setLabel("Cancel")
85 .setStyle("DANGER")
86 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
87 ])
pineafan63fc5e22022-08-04 22:04:10 +010088 ], ephemeral: true, fetchReply: true});
pineafan8b4b17f2022-02-27 20:42:52 +000089 let component;
90 try {
pineafanc6158ab2022-06-17 16:34:07 +010091 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan63fc5e22022-08-04 22:04:10 +010092 } catch { return; }
pineafan8b4b17f2022-02-27 20:42:52 +000093 component.deferUpdate();
pineafane23c4ec2022-07-27 21:56:27 +010094 if (component.customId === "cancel") return interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000095 .setEmoji("PUNISH.MUTE.RED")
96 .setTitle("Mute")
97 .setDescription("Mute cancelled")
98 .setStatus("Danger")
pineafan63fc5e22022-08-04 22:04:10 +010099 ]});
pineafan8b4b17f2022-02-27 20:42:52 +0000100 switch (component.customId) {
pineafan63fc5e22022-08-04 22:04:10 +0100101 case "1m": { muteTime = 60; break; }
102 case "10m": { muteTime = 60 * 10; break; }
103 case "30m": { muteTime = 60 * 30; break; }
104 case "1h": { muteTime = 60 * 60; break; }
105 case "6h": { muteTime = 60 * 60 * 6; break; }
106 case "12h": { muteTime = 60 * 60 * 12; break; }
107 case "1d": { muteTime = 60 * 60 * 24; break; }
108 case "1w": { muteTime = 60 * 60 * 24 * 7; break; }
pineafan8b4b17f2022-02-27 20:42:52 +0000109 }
110 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100111 await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
pineafan8b4b17f2022-02-27 20:42:52 +0000112 }
pineafan5d1908e2022-02-28 21:34:47 +0000113 // TODO:[Modals] Replace this with a modal
pineafan73a7c4a2022-07-24 10:38:04 +0100114 let reason = null;
pineafan02ba0232022-07-24 22:16:15 +0100115 let notify = true;
116 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100117 let confirmation;
118 while (true) {
119 confirmation = await new confirmationMessage(interaction)
120 .setEmoji("PUNISH.MUTE.RED")
121 .setTitle("Mute")
122 .setDescription(keyValueList({
123 "user": renderUser(user.user),
124 "time": `${humanizeDuration(muteTime * 1000, {round: true})}`,
125 "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*"
126 })
pineafan63fc5e22022-08-04 22:04:10 +0100127 + "The user will be " + serverSettingsDescription + "\n"
128 + `The user **will${notify ? "" : " not"}** be notified\n\n`
pineafan73a7c4a2022-07-24 10:38:04 +0100129 + `Are you sure you want to mute <@!${user.id}>?`)
130 .setColor("Danger")
131 .addCustomBoolean(
pineafan02ba0232022-07-24 22:16:15 +0100132 "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)),
133 async () => await create(interaction.guild, interaction.options.getUser("user"), interaction.user, reason),
134 "An appeal ticket will be created when Confirm is clicked", "CONTROL.TICKET", createAppealTicket)
135 .addCustomBoolean("notify", "Notify user", false, null, null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF" ), notify)
pineafan73a7c4a2022-07-24 10:38:04 +0100136 .addReasonButton(reason ?? "")
pineafan63fc5e22022-08-04 22:04:10 +0100137 .send(true);
138 reason = reason ?? "";
139 if (confirmation.cancelled) return;
140 if (confirmation.success) break;
141 if (confirmation.newReason) reason = confirmation.newReason;
pineafan02ba0232022-07-24 22:16:15 +0100142 if (confirmation.components) {
pineafan63fc5e22022-08-04 22:04:10 +0100143 notify = confirmation.components.notify.active;
144 createAppealTicket = confirmation.components.appeal.active;
pineafan02ba0232022-07-24 22:16:15 +0100145 }
pineafan73a7c4a2022-07-24 10:38:04 +0100146 }
pineafan377794f2022-04-18 19:01:01 +0100147 if (confirmation.success) {
pineafan63fc5e22022-08-04 22:04:10 +0100148 let dmd = false;
pineafan5d1908e2022-02-28 21:34:47 +0000149 let dm;
pineafan63fc5e22022-08-04 22:04:10 +0100150 const config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000151 try {
pineafan02ba0232022-07-24 22:16:15 +0100152 if (notify) {
pineafan73a7c4a2022-07-24 10:38:04 +0100153 dm = await user.send({
pineafan4edb7762022-06-26 19:21:04 +0100154 embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000155 .setEmoji("PUNISH.MUTE.RED")
156 .setTitle("Muted")
157 .setDescription(`You have been muted in ${interaction.guild.name}` +
pineafan73a7c4a2022-07-24 10:38:04 +0100158 (reason ? ` for:\n> ${reason}` : ".\n\n" +
pineafan02ba0232022-07-24 22:16:15 +0100159 `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 +0100160 (confirmation.components.appeal.response ? `You can appeal this here: <#${confirmation.components.appeal.response}>` : ""))
pineafan8b4b17f2022-02-27 20:42:52 +0000161 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100162 ],
163 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
164 .setStyle("LINK")
165 .setLabel(config.moderation.mute.text)
166 .setURL(config.moderation.mute.link)
167 ] : [])]
pineafan63fc5e22022-08-04 22:04:10 +0100168 });
169 dmd = true;
pineafan8b4b17f2022-02-27 20:42:52 +0000170 }
pineafan63fc5e22022-08-04 22:04:10 +0100171 } catch { dmd = false; }
172 const member = user;
173 let errors = 0;
pineafan8b4b17f2022-02-27 20:42:52 +0000174 try {
pineafane625d782022-05-09 18:04:32 +0100175 if (config.moderation.mute.timeout) {
pineafan63fc5e22022-08-04 22:04:10 +0100176 await member.timeout(muteTime * 1000, reason || "No reason provided");
pineafan73a7c4a2022-07-24 10:38:04 +0100177 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100178 await member.roles.add(config.moderation.mute.role);
pineafan73a7c4a2022-07-24 10:38:04 +0100179 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
180 guild: interaction.guild.id,
181 user: user.id,
182 expires: new Date().getTime() + muteTime * 1000
pineafan63fc5e22022-08-04 22:04:10 +0100183 });
pineafan73a7c4a2022-07-24 10:38:04 +0100184 }
pineafane625d782022-05-09 18:04:32 +0100185 }
pineafan63fc5e22022-08-04 22:04:10 +0100186 } catch { errors++; }
pineafan73a7c4a2022-07-24 10:38:04 +0100187 try {
188 if (config.moderation.mute.role !== null) {
pineafan63fc5e22022-08-04 22:04:10 +0100189 await member.roles.add(config.moderation.mute.role);
pineafan73a7c4a2022-07-24 10:38:04 +0100190 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
191 guild: interaction.guild.id,
192 user: user.id,
193 role: config.moderation.mute.role
pineafan63fc5e22022-08-04 22:04:10 +0100194 });
pineafan73a7c4a2022-07-24 10:38:04 +0100195 }
pineafan63fc5e22022-08-04 22:04:10 +0100196 } catch (e){ console.log(e); errors++; }
pineafane23c4ec2022-07-27 21:56:27 +0100197 if (errors === 2) {
pineafan4edb7762022-06-26 19:21:04 +0100198 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000199 .setEmoji("PUNISH.MUTE.RED")
pineafan63fc5e22022-08-04 22:04:10 +0100200 .setTitle("Mute")
pineafan73a7c4a2022-07-24 10:38:04 +0100201 .setDescription("Something went wrong and the user was not muted")
pineafan8b4b17f2022-02-27 20:42:52 +0000202 .setStatus("Danger")
pineafan63fc5e22022-08-04 22:04:10 +0100203 ], components: []}); // TODO: make this clearer
204 if (dmd) await dm.delete();
205 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000206 }
pineafan63fc5e22022-08-04 22:04:10 +0100207 await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason);
208 const failed = (dmd === false && notify);
pineafan4edb7762022-06-26 19:21:04 +0100209 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000210 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
pineafan63fc5e22022-08-04 22:04:10 +0100211 .setTitle("Mute")
212 .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 +0000213 .setStatus(failed ? "Warning" : "Success")
pineafan63fc5e22022-08-04 22:04:10 +0100214 ], components: []});
215 const data = {
pineafan377794f2022-04-18 19:01:01 +0100216 meta:{
pineafan63fc5e22022-08-04 22:04:10 +0100217 type: "memberMute",
218 displayName: "Member Muted",
219 calculateType: "guildMemberPunish",
pineafan377794f2022-04-18 19:01:01 +0100220 color: NucleusColors.yellow,
pineafan63fc5e22022-08-04 22:04:10 +0100221 emoji: "PUNISH.WARN.YELLOW",
pineafan377794f2022-04-18 19:01:01 +0100222 timestamp: new Date().getTime()
223 },
224 list: {
pineafan73a7c4a2022-07-24 10:38:04 +0100225 memberId: entry(member.user.id, `\`${member.user.id}\``),
226 name: entry(member.user.id, renderUser(member.user)),
227 mutedUntil: entry(new Date().getTime() + muteTime * 1000, renderDelta(new Date().getTime() + muteTime * 1000)),
228 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
pineafan377794f2022-04-18 19:01:01 +0100229 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
pineafan63fc5e22022-08-04 22:04:10 +0100230 reason: entry(reason, reason ? reason : "*No reason provided*")
pineafan377794f2022-04-18 19:01:01 +0100231 },
232 hidden: {
233 guild: interaction.guild.id
234 }
pineafan63fc5e22022-08-04 22:04:10 +0100235 };
pineafan4edb7762022-06-26 19:21:04 +0100236 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000237 } else {
pineafan4edb7762022-06-26 19:21:04 +0100238 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000239 .setEmoji("PUNISH.MUTE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100240 .setTitle("Mute")
pineafan8b4b17f2022-02-27 20:42:52 +0000241 .setDescription("No changes were made")
242 .setStatus("Success")
pineafan63fc5e22022-08-04 22:04:10 +0100243 ], components: []});
pineafan8b4b17f2022-02-27 20:42:52 +0000244 }
pineafan63fc5e22022-08-04 22:04:10 +0100245};
pineafan8b4b17f2022-02-27 20:42:52 +0000246
247const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan63fc5e22022-08-04 22:04:10 +0100248 const member = (interaction.member as GuildMember);
249 const me = (interaction.guild.me as GuildMember);
250 const apply = (interaction.options.getMember("user") as GuildMember);
251 if (member === null || me === null || apply === null) throw "That member is not in the server";
252 const memberPos = member.roles ? member.roles.highest.position : 0;
253 const mePos = me.roles ? me.roles.highest.position : 0;
254 const applyPos = apply.roles ? apply.roles.highest.position : 0;
pineafanc1c18792022-08-03 21:41:36 +0100255 // Do not allow muting the owner
pineafan63fc5e22022-08-04 22:04:10 +0100256 if (member.id === interaction.guild.ownerId) throw "You cannot mute the owner of the server";
pineafan8b4b17f2022-02-27 20:42:52 +0000257 // Check if Nucleus can mute the member
pineafan63fc5e22022-08-04 22:04:10 +0100258 if (! (mePos > applyPos)) throw "I do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000259 // Check if Nucleus has permission to mute
pineafane23c4ec2022-07-27 21:56:27 +0100260 if (! me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000261 // Do not allow muting Nucleus
pineafan63fc5e22022-08-04 22:04:10 +0100262 if (member.id === me.id) throw "I cannot mute myself";
pineafan8b4b17f2022-02-27 20:42:52 +0000263 // Allow the owner to mute anyone
pineafan63fc5e22022-08-04 22:04:10 +0100264 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000265 // Check if the user has moderate_members permission
pineafane23c4ec2022-07-27 21:56:27 +0100266 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the *Moderate Members* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000267 // Check if the user is below on the role list
pineafan63fc5e22022-08-04 22:04:10 +0100268 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member";
pineafan8b4b17f2022-02-27 20:42:52 +0000269 // Allow mute
pineafan63fc5e22022-08-04 22:04:10 +0100270 return true;
271};
pineafan8b4b17f2022-02-27 20:42:52 +0000272
273export { command, callback, check };