blob: 808d22dbe802155b1c96d210043c37541cdc0dec [file] [log] [blame]
pineafane23c4ec2022-07-27 21:56:27 +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
15 .setName("mute")
pineafan73a7c4a2022-07-24 10:38:04 +010016 .setDescription("Mutes a member, stopping them from talking in the server")
pineafan8b4b17f2022-02-27 20:42:52 +000017 .addUserOption(option => option.setName("user").setDescription("The user to mute").setRequired(true))
pineafan73a7c4a2022-07-24 10:38:04 +010018 .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> => {
pineafan73a7c4a2022-07-24 10:38:04 +010024 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger
pineafan8b4b17f2022-02-27 20:42:52 +000025 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
31 }
pineafan4edb7762022-06-26 19:21:04 +010032 let config = await client.database.guilds.read(interaction.guild.id)
pineafane625d782022-05-09 18:04:32 +010033 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`
35
pineafan8b4b17f2022-02-27 20:42:52 +000036 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) {
pineafan8b4b17f2022-02-27 20:42:52 +000038 let 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 ])
88 ], ephemeral: true, fetchReply: true})
89 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});
pineafan8b4b17f2022-02-27 20:42:52 +000092 } catch { return }
93 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")
99 ]})
100 switch (component.customId) {
101 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; }
109 }
110 } else {
pineafane23c4ec2022-07-27 21:56:27 +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 })
127 + `The user will be ` + serverSettingsDescription + "\n"
pineafan02ba0232022-07-24 22:16:15 +0100128 + `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 ?? "")
137 .send(true)
138 reason = reason ?? ""
pineafan02ba0232022-07-24 22:16:15 +0100139 if (confirmation.cancelled) return
140 if (confirmation.success) break
141 if (confirmation.newReason) reason = confirmation.newReason
142 if (confirmation.components) {
143 notify = confirmation.components.notify.active
144 createAppealTicket = confirmation.components.appeal.active
145 }
pineafan73a7c4a2022-07-24 10:38:04 +0100146 }
pineafan377794f2022-04-18 19:01:01 +0100147 if (confirmation.success) {
pineafan8b4b17f2022-02-27 20:42:52 +0000148 let dmd = false
pineafan5d1908e2022-02-28 21:34:47 +0000149 let dm;
pineafan4edb7762022-06-26 19:21:04 +0100150 let 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>)`) +
160 (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 ] : [])]
pineafan8b4b17f2022-02-27 20:42:52 +0000168 })
169 dmd = true
170 }
171 } catch {}
pineafan73a7c4a2022-07-24 10:38:04 +0100172 let 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) {
pineafan73a7c4a2022-07-24 10:38:04 +0100176 await member.timeout(muteTime * 1000, reason || "No reason provided")
177 if (config.moderation.mute.role !== null) {
178 await member.roles.add(config.moderation.mute.role)
179 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
183 })
184 }
pineafane625d782022-05-09 18:04:32 +0100185 }
pineafan73a7c4a2022-07-24 10:38:04 +0100186 } catch { errors++ }
187 try {
188 if (config.moderation.mute.role !== null) {
189 await member.roles.add(config.moderation.mute.role)
190 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
194 })
195 }
196 } 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")
200 .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")
pineafan02ba0232022-07-24 22:16:15 +0100203 ], components: []}) // TODO: make this clearer
pineafan5d1908e2022-02-28 21:34:47 +0000204 if (dmd) await dm.delete()
205 return
pineafan8b4b17f2022-02-27 20:42:52 +0000206 }
pineafan4edb7762022-06-26 19:21:04 +0100207 try { await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason) } catch {}
pineafane23c4ec2022-07-27 21:56:27 +0100208 let 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"}`)
211 .setTitle(`Mute`)
pineafan02ba0232022-07-24 22:16:15 +0100212 .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")
214 ], components: []})
pineafan377794f2022-04-18 19:01:01 +0100215 let data = {
216 meta:{
217 type: 'memberMute',
218 displayName: 'Member Muted',
219 calculateType: 'guildMemberPunish',
220 color: NucleusColors.yellow,
221 emoji: 'PUNISH.WARN.YELLOW',
222 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)),
pineafan73a7c4a2022-07-24 10:38:04 +0100230 reason: entry(reason, reason ? reason : '*No reason provided*')
pineafan377794f2022-04-18 19:01:01 +0100231 },
232 hidden: {
233 guild: interaction.guild.id
234 }
235 }
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")
240 .setTitle(`Mute`)
241 .setDescription("No changes were made")
242 .setStatus("Success")
243 ], components: []})
244 }
245}
246
247const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan5d1908e2022-02-28 21:34:47 +0000248 let member = (interaction.member as GuildMember)
249 let me = (interaction.guild.me as GuildMember)
250 let apply = (interaction.options.getMember("user") as GuildMember)
pineafane23c4ec2022-07-27 21:56:27 +0100251 if (member === null || me === null || apply === null) throw "That member is not in the server"
pineafan5d1908e2022-02-28 21:34:47 +0000252 let memberPos = member.roles ? member.roles.highest.position : 0
253 let mePos = me.roles ? me.roles.highest.position : 0
254 let applyPos = apply.roles ? apply.roles.highest.position : 0
pineafanc1c18792022-08-03 21:41:36 +0100255 // Do not allow muting the owner
256 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
pineafan5d1908e2022-02-28 21:34:47 +0000258 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
pineafane23c4ec2022-07-27 21:56:27 +0100262 if (member.id === me.id) throw "I cannot mute myself"
pineafan8b4b17f2022-02-27 20:42:52 +0000263 // Allow the owner to mute anyone
pineafane23c4ec2022-07-27 21:56:27 +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
pineafan5d1908e2022-02-28 21:34:47 +0000268 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000269 // Allow mute
270 return true
271}
272
273export { command, callback, check };