blob: f98bd6a50f584c2306e88746293ebb93b2c11712 [file] [log] [blame]
pineafan377794f2022-04-18 19:01:01 +01001import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00002import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
3import { WrappedCheck } from "jshaiku";
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
14 .setName("mute")
pineafan73a7c4a2022-07-24 10:38:04 +010015 .setDescription("Mutes a member, stopping them from talking in the server")
pineafan8b4b17f2022-02-27 20:42:52 +000016 .addUserOption(option => option.setName("user").setDescription("The user to mute").setRequired(true))
pineafan73a7c4a2022-07-24 10:38:04 +010017 .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
pineafan6702cef2022-06-13 17:52:37 +010022const callback = async (interaction: CommandInteraction): Promise<any> => {
pineafan73a7c4a2022-07-24 10:38:04 +010023 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger
pineafan8b4b17f2022-02-27 20:42:52 +000024 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
30 }
pineafan4edb7762022-06-26 19:21:04 +010031 let config = await client.database.guilds.read(interaction.guild.id)
pineafane625d782022-05-09 18:04:32 +010032 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`
34
pineafan8b4b17f2022-02-27 20:42:52 +000035 let muteTime = (time.days * 24 * 60 * 60) + (time.hours * 60 * 60) + (time.minutes * 60) + time.seconds
36 if (muteTime == 0) {
37 let 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 ])
87 ], ephemeral: true, fetchReply: true})
88 let component;
89 try {
pineafanc6158ab2022-06-17 16:34:07 +010090 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan8b4b17f2022-02-27 20:42:52 +000091 } catch { return }
92 component.deferUpdate();
pineafan4edb7762022-06-26 19:21:04 +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")
98 ]})
99 switch (component.customId) {
100 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; }
108 }
109 } else {
110 await interaction.reply({embeds: [
pineafan4edb7762022-06-26 19:21:04 +0100111 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000112 .setEmoji("PUNISH.MUTE.GREEN")
113 .setTitle("Mute")
114 .setDescription("Loading...")
115 .setStatus("Success")
116 ], ephemeral: true, fetchReply: true})
117 }
pineafan5d1908e2022-02-28 21:34:47 +0000118 // TODO:[Modals] Replace this with a modal
pineafan73a7c4a2022-07-24 10:38:04 +0100119 let reason = null;
pineafan02ba0232022-07-24 22:16:15 +0100120 let notify = true;
121 let createAppealTicket = false;
pineafan73a7c4a2022-07-24 10:38:04 +0100122 let confirmation;
123 while (true) {
124 confirmation = await new confirmationMessage(interaction)
125 .setEmoji("PUNISH.MUTE.RED")
126 .setTitle("Mute")
127 .setDescription(keyValueList({
128 "user": renderUser(user.user),
129 "time": `${humanizeDuration(muteTime * 1000, {round: true})}`,
130 "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*"
131 })
132 + `The user will be ` + serverSettingsDescription + "\n"
pineafan02ba0232022-07-24 22:16:15 +0100133 + `The user **will${notify ? '' : ' not'}** be notified\n\n`
pineafan73a7c4a2022-07-24 10:38:04 +0100134 + `Are you sure you want to mute <@!${user.id}>?`)
135 .setColor("Danger")
136 .addCustomBoolean(
pineafan02ba0232022-07-24 22:16:15 +0100137 "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)),
138 async () => await create(interaction.guild, interaction.options.getUser("user"), interaction.user, reason),
139 "An appeal ticket will be created when Confirm is clicked", "CONTROL.TICKET", createAppealTicket)
140 .addCustomBoolean("notify", "Notify user", false, null, null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF" ), notify)
pineafan73a7c4a2022-07-24 10:38:04 +0100141 .addReasonButton(reason ?? "")
142 .send(true)
143 reason = reason ?? ""
pineafan02ba0232022-07-24 22:16:15 +0100144 if (confirmation.cancelled) return
145 if (confirmation.success) break
146 if (confirmation.newReason) reason = confirmation.newReason
147 if (confirmation.components) {
148 notify = confirmation.components.notify.active
149 createAppealTicket = confirmation.components.appeal.active
150 }
pineafan73a7c4a2022-07-24 10:38:04 +0100151 }
pineafan377794f2022-04-18 19:01:01 +0100152 if (confirmation.success) {
pineafan8b4b17f2022-02-27 20:42:52 +0000153 let dmd = false
pineafan5d1908e2022-02-28 21:34:47 +0000154 let dm;
pineafan4edb7762022-06-26 19:21:04 +0100155 let config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000156 try {
pineafan02ba0232022-07-24 22:16:15 +0100157 if (notify) {
pineafan73a7c4a2022-07-24 10:38:04 +0100158 dm = await user.send({
pineafan4edb7762022-06-26 19:21:04 +0100159 embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000160 .setEmoji("PUNISH.MUTE.RED")
161 .setTitle("Muted")
162 .setDescription(`You have been muted in ${interaction.guild.name}` +
pineafan73a7c4a2022-07-24 10:38:04 +0100163 (reason ? ` for:\n> ${reason}` : ".\n\n" +
pineafan02ba0232022-07-24 22:16:15 +0100164 `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>)`) +
165 (confirmation.components.appeal.response ? `You can appeal this here: <#${confirmation.components.appeal.response}>` : ``))
pineafan8b4b17f2022-02-27 20:42:52 +0000166 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100167 ],
168 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
169 .setStyle("LINK")
170 .setLabel(config.moderation.mute.text)
171 .setURL(config.moderation.mute.link)
172 ] : [])]
pineafan8b4b17f2022-02-27 20:42:52 +0000173 })
174 dmd = true
175 }
176 } catch {}
pineafan73a7c4a2022-07-24 10:38:04 +0100177 let member = user
178 let errors = 0
pineafan8b4b17f2022-02-27 20:42:52 +0000179 try {
pineafane625d782022-05-09 18:04:32 +0100180 if (config.moderation.mute.timeout) {
pineafan73a7c4a2022-07-24 10:38:04 +0100181 await member.timeout(muteTime * 1000, reason || "No reason provided")
182 if (config.moderation.mute.role !== null) {
183 await member.roles.add(config.moderation.mute.role)
184 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
185 guild: interaction.guild.id,
186 user: user.id,
187 expires: new Date().getTime() + muteTime * 1000
188 })
189 }
pineafane625d782022-05-09 18:04:32 +0100190 }
pineafan73a7c4a2022-07-24 10:38:04 +0100191 } catch { errors++ }
192 try {
193 if (config.moderation.mute.role !== null) {
194 await member.roles.add(config.moderation.mute.role)
195 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
196 guild: interaction.guild.id,
197 user: user.id,
198 role: config.moderation.mute.role
199 })
200 }
201 } catch (e){ console.log(e); errors++ }
202 if (errors == 2) {
pineafan4edb7762022-06-26 19:21:04 +0100203 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000204 .setEmoji("PUNISH.MUTE.RED")
205 .setTitle(`Mute`)
pineafan73a7c4a2022-07-24 10:38:04 +0100206 .setDescription("Something went wrong and the user was not muted")
pineafan8b4b17f2022-02-27 20:42:52 +0000207 .setStatus("Danger")
pineafan02ba0232022-07-24 22:16:15 +0100208 ], components: []}) // TODO: make this clearer
pineafan5d1908e2022-02-28 21:34:47 +0000209 if (dmd) await dm.delete()
210 return
pineafan8b4b17f2022-02-27 20:42:52 +0000211 }
pineafan4edb7762022-06-26 19:21:04 +0100212 try { await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason) } catch {}
pineafan02ba0232022-07-24 22:16:15 +0100213 let failed = (dmd == false && notify)
pineafan4edb7762022-06-26 19:21:04 +0100214 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000215 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
216 .setTitle(`Mute`)
pineafan02ba0232022-07-24 22:16:15 +0100217 .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 +0000218 .setStatus(failed ? "Warning" : "Success")
219 ], components: []})
pineafan377794f2022-04-18 19:01:01 +0100220 let data = {
221 meta:{
222 type: 'memberMute',
223 displayName: 'Member Muted',
224 calculateType: 'guildMemberPunish',
225 color: NucleusColors.yellow,
226 emoji: 'PUNISH.WARN.YELLOW',
227 timestamp: new Date().getTime()
228 },
229 list: {
pineafan73a7c4a2022-07-24 10:38:04 +0100230 memberId: entry(member.user.id, `\`${member.user.id}\``),
231 name: entry(member.user.id, renderUser(member.user)),
232 mutedUntil: entry(new Date().getTime() + muteTime * 1000, renderDelta(new Date().getTime() + muteTime * 1000)),
233 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
pineafan377794f2022-04-18 19:01:01 +0100234 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
pineafan73a7c4a2022-07-24 10:38:04 +0100235 reason: entry(reason, reason ? reason : '*No reason provided*')
pineafan377794f2022-04-18 19:01:01 +0100236 },
237 hidden: {
238 guild: interaction.guild.id
239 }
240 }
pineafan4edb7762022-06-26 19:21:04 +0100241 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000242 } else {
pineafan4edb7762022-06-26 19:21:04 +0100243 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000244 .setEmoji("PUNISH.MUTE.GREEN")
245 .setTitle(`Mute`)
246 .setDescription("No changes were made")
247 .setStatus("Success")
248 ], components: []})
249 }
250}
251
252const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan5d1908e2022-02-28 21:34:47 +0000253 let member = (interaction.member as GuildMember)
254 let me = (interaction.guild.me as GuildMember)
255 let apply = (interaction.options.getMember("user") as GuildMember)
256 if (member == null || me == null || apply == null) throw "That member is not in the server"
257 let memberPos = member.roles ? member.roles.highest.position : 0
258 let mePos = me.roles ? me.roles.highest.position : 0
259 let applyPos = apply.roles ? apply.roles.highest.position : 0
pineafan8b4b17f2022-02-27 20:42:52 +0000260 // Check if Nucleus can mute the member
pineafan5d1908e2022-02-28 21:34:47 +0000261 if (! (mePos > applyPos)) throw "I do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000262 // Check if Nucleus has permission to mute
pineafan4edb7762022-06-26 19:21:04 +0100263 if (! me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the Moderate members permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000264 // Do not allow the user to have admin or be the owner
PineappleFan5fe720d2022-05-19 12:01:49 +0100265 if (apply.permissions.has("ADMINISTRATOR") || (interaction.options.getMember("user") as GuildMember).id == interaction.guild.ownerId) throw "You cannot mute an admin or the owner"
pineafan8b4b17f2022-02-27 20:42:52 +0000266 // Do not allow muting Nucleus
PineappleFan5fe720d2022-05-19 12:01:49 +0100267 if (member.id == me.id) throw "I cannot mute myself"
pineafan8b4b17f2022-02-27 20:42:52 +0000268 // Allow the owner to mute anyone
pineafan663dc472022-05-10 18:13:47 +0100269 if (member.id == interaction.guild.ownerId) return true
pineafan8b4b17f2022-02-27 20:42:52 +0000270 // Check if the user has moderate_members permission
pineafan4edb7762022-06-26 19:21:04 +0100271 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the Moderate members permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000272 // Check if the user is below on the role list
pineafan5d1908e2022-02-28 21:34:47 +0000273 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000274 // Allow mute
275 return true
276}
277
278export { command, callback, check };