blob: 5e1a18b86c85aceab688f6518d27efc341a388de [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))
21 .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are muted | Default: yes").setRequired(false)
pineafan5d1908e2022-02-28 21:34:47 +000022 .addChoices([["Yes", "yes"], ["No", "no"]]))
pineafan8b4b17f2022-02-27 20:42:52 +000023
pineafan6702cef2022-06-13 17:52:37 +010024const callback = async (interaction: CommandInteraction): Promise<any> => {
pineafan73a7c4a2022-07-24 10:38:04 +010025 const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger
pineafan8b4b17f2022-02-27 20:42:52 +000026 const user = interaction.options.getMember("user") as GuildMember
pineafan8b4b17f2022-02-27 20:42:52 +000027 const time = {
28 days: interaction.options.getInteger("days") || 0,
29 hours: interaction.options.getInteger("hours") || 0,
30 minutes: interaction.options.getInteger("minutes") || 0,
31 seconds: interaction.options.getInteger("seconds") || 0
32 }
pineafan4edb7762022-06-26 19:21:04 +010033 let config = await client.database.guilds.read(interaction.guild.id)
pineafane625d782022-05-09 18:04:32 +010034 let serverSettingsDescription = (config.moderation.mute.timeout ? "given a timeout" : "")
35 if (config.moderation.mute.role) serverSettingsDescription += (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`
36
pineafan8b4b17f2022-02-27 20:42:52 +000037 let muteTime = (time.days * 24 * 60 * 60) + (time.hours * 60 * 60) + (time.minutes * 60) + time.seconds
38 if (muteTime == 0) {
39 let m = await interaction.reply({embeds: [
pineafan4edb7762022-06-26 19:21:04 +010040 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000041 .setEmoji("PUNISH.MUTE.GREEN")
42 .setTitle("Mute")
43 .setDescription("How long should the user be muted")
44 .setStatus("Success")
45 ], components: [
46 new MessageActionRow().addComponents([
47 new Discord.MessageButton()
48 .setCustomId("1m")
49 .setLabel("1 Minute")
50 .setStyle("SECONDARY"),
51 new Discord.MessageButton()
52 .setCustomId("10m")
53 .setLabel("10 Minutes")
54 .setStyle("SECONDARY"),
55 new Discord.MessageButton()
56 .setCustomId("30m")
57 .setLabel("30 Minutes")
58 .setStyle("SECONDARY"),
59 new Discord.MessageButton()
60 .setCustomId("1h")
61 .setLabel("1 Hour")
62 .setStyle("SECONDARY")
63 ]),
64 new MessageActionRow().addComponents([
65 new Discord.MessageButton()
66 .setCustomId("6h")
67 .setLabel("6 Hours")
68 .setStyle("SECONDARY"),
69 new Discord.MessageButton()
70 .setCustomId("12h")
71 .setLabel("12 Hours")
72 .setStyle("SECONDARY"),
73 new Discord.MessageButton()
74 .setCustomId("1d")
75 .setLabel("1 Day")
76 .setStyle("SECONDARY"),
77 new Discord.MessageButton()
78 .setCustomId("1w")
79 .setLabel("1 Week")
80 .setStyle("SECONDARY")
81 ]),
82 new MessageActionRow().addComponents([
83 new Discord.MessageButton()
84 .setCustomId("cancel")
85 .setLabel("Cancel")
86 .setStyle("DANGER")
87 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
88 ])
89 ], ephemeral: true, fetchReply: true})
90 let component;
91 try {
pineafanc6158ab2022-06-17 16:34:07 +010092 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan8b4b17f2022-02-27 20:42:52 +000093 } catch { return }
94 component.deferUpdate();
pineafan4edb7762022-06-26 19:21:04 +010095 if (component.customId == "cancel") return interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000096 .setEmoji("PUNISH.MUTE.RED")
97 .setTitle("Mute")
98 .setDescription("Mute cancelled")
99 .setStatus("Danger")
100 ]})
101 switch (component.customId) {
102 case "1m": { muteTime = 60; break; }
103 case "10m": { muteTime = 60 * 10; break; }
104 case "30m": { muteTime = 60 * 30; break; }
105 case "1h": { muteTime = 60 * 60; break; }
106 case "6h": { muteTime = 60 * 60 * 6; break; }
107 case "12h": { muteTime = 60 * 60 * 12; break; }
108 case "1d": { muteTime = 60 * 60 * 24; break; }
109 case "1w": { muteTime = 60 * 60 * 24 * 7; break; }
110 }
111 } else {
112 await interaction.reply({embeds: [
pineafan4edb7762022-06-26 19:21:04 +0100113 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000114 .setEmoji("PUNISH.MUTE.GREEN")
115 .setTitle("Mute")
116 .setDescription("Loading...")
117 .setStatus("Success")
118 ], ephemeral: true, fetchReply: true})
119 }
pineafan5d1908e2022-02-28 21:34:47 +0000120 // TODO:[Modals] Replace this with a modal
pineafan73a7c4a2022-07-24 10:38:04 +0100121 let reason = null;
122 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"
133 + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n\n`
134 + `Are you sure you want to mute <@!${user.id}>?`)
135 .setColor("Danger")
136 .addCustomBoolean(
137 "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)),
138 async () => await create(interaction.guild, user.user, interaction.user, reason),
139 "An appeal ticket will be created when Confirm is clicked")
140 .addReasonButton(reason ?? "")
141 .send(true)
142 reason = reason ?? ""
143 if (confirmation.newReason === undefined) break
144 reason = confirmation.newReason
145 }
pineafan377794f2022-04-18 19:01:01 +0100146 if (confirmation.success) {
pineafan8b4b17f2022-02-27 20:42:52 +0000147 let dmd = false
pineafan5d1908e2022-02-28 21:34:47 +0000148 let dm;
pineafan4edb7762022-06-26 19:21:04 +0100149 let config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000150 try {
151 if (interaction.options.getString("notify") != "no") {
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" +
pineafan8b4b17f2022-02-27 20:42:52 +0000158 `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>)`))
159 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100160 ],
161 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
162 .setStyle("LINK")
163 .setLabel(config.moderation.mute.text)
164 .setURL(config.moderation.mute.link)
165 ] : [])]
pineafan8b4b17f2022-02-27 20:42:52 +0000166 })
167 dmd = true
168 }
169 } catch {}
pineafan73a7c4a2022-07-24 10:38:04 +0100170 let member = user
171 let errors = 0
pineafan8b4b17f2022-02-27 20:42:52 +0000172 try {
pineafane625d782022-05-09 18:04:32 +0100173 if (config.moderation.mute.timeout) {
pineafan73a7c4a2022-07-24 10:38:04 +0100174 await member.timeout(muteTime * 1000, reason || "No reason provided")
175 if (config.moderation.mute.role !== null) {
176 await member.roles.add(config.moderation.mute.role)
177 await client.database.eventScheduler.schedule("naturalUnmute", new Date().getTime() + muteTime * 1000, {
178 guild: interaction.guild.id,
179 user: user.id,
180 expires: new Date().getTime() + muteTime * 1000
181 })
182 }
pineafane625d782022-05-09 18:04:32 +0100183 }
pineafan73a7c4a2022-07-24 10:38:04 +0100184 } catch { errors++ }
185 try {
186 if (config.moderation.mute.role !== null) {
187 await member.roles.add(config.moderation.mute.role)
188 await client.database.eventScheduler.schedule("unmuteRole", new Date().getTime() + muteTime * 1000, {
189 guild: interaction.guild.id,
190 user: user.id,
191 role: config.moderation.mute.role
192 })
193 }
194 } catch (e){ console.log(e); errors++ }
195 if (errors == 2) {
pineafan4edb7762022-06-26 19:21:04 +0100196 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000197 .setEmoji("PUNISH.MUTE.RED")
198 .setTitle(`Mute`)
pineafan73a7c4a2022-07-24 10:38:04 +0100199 .setDescription("Something went wrong and the user was not muted")
pineafan8b4b17f2022-02-27 20:42:52 +0000200 .setStatus("Danger")
201 ], components: []})
pineafan5d1908e2022-02-28 21:34:47 +0000202 if (dmd) await dm.delete()
203 return
pineafan8b4b17f2022-02-27 20:42:52 +0000204 }
pineafan4edb7762022-06-26 19:21:04 +0100205 try { await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason) } catch {}
pineafan5d1908e2022-02-28 21:34:47 +0000206 let failed = (dmd == false && interaction.options.getString("notify") != "no")
pineafan4edb7762022-06-26 19:21:04 +0100207 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000208 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
209 .setTitle(`Mute`)
210 .setDescription("The member was muted" + (failed ? ", but could not be notified" : ""))
211 .setStatus(failed ? "Warning" : "Success")
212 ], components: []})
pineafan377794f2022-04-18 19:01:01 +0100213 let data = {
214 meta:{
215 type: 'memberMute',
216 displayName: 'Member Muted',
217 calculateType: 'guildMemberPunish',
218 color: NucleusColors.yellow,
219 emoji: 'PUNISH.WARN.YELLOW',
220 timestamp: new Date().getTime()
221 },
222 list: {
pineafan73a7c4a2022-07-24 10:38:04 +0100223 memberId: entry(member.user.id, `\`${member.user.id}\``),
224 name: entry(member.user.id, renderUser(member.user)),
225 mutedUntil: entry(new Date().getTime() + muteTime * 1000, renderDelta(new Date().getTime() + muteTime * 1000)),
226 muted: entry(new Date().getTime(), renderDelta(new Date().getTime() - 1000)),
pineafan377794f2022-04-18 19:01:01 +0100227 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
pineafan73a7c4a2022-07-24 10:38:04 +0100228 reason: entry(reason, reason ? reason : '*No reason provided*')
pineafan377794f2022-04-18 19:01:01 +0100229 },
230 hidden: {
231 guild: interaction.guild.id
232 }
233 }
pineafan4edb7762022-06-26 19:21:04 +0100234 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000235 } else {
pineafan4edb7762022-06-26 19:21:04 +0100236 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000237 .setEmoji("PUNISH.MUTE.GREEN")
238 .setTitle(`Mute`)
239 .setDescription("No changes were made")
240 .setStatus("Success")
241 ], components: []})
242 }
243}
244
245const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan5d1908e2022-02-28 21:34:47 +0000246 let member = (interaction.member as GuildMember)
247 let me = (interaction.guild.me as GuildMember)
248 let apply = (interaction.options.getMember("user") as GuildMember)
249 if (member == null || me == null || apply == null) throw "That member is not in the server"
250 let memberPos = member.roles ? member.roles.highest.position : 0
251 let mePos = me.roles ? me.roles.highest.position : 0
252 let applyPos = apply.roles ? apply.roles.highest.position : 0
pineafan8b4b17f2022-02-27 20:42:52 +0000253 // Check if Nucleus can mute the member
pineafan5d1908e2022-02-28 21:34:47 +0000254 if (! (mePos > applyPos)) throw "I do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000255 // Check if Nucleus has permission to mute
pineafan4edb7762022-06-26 19:21:04 +0100256 if (! me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the Moderate members permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000257 // Do not allow the user to have admin or be the owner
PineappleFan5fe720d2022-05-19 12:01:49 +0100258 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 +0000259 // Do not allow muting Nucleus
PineappleFan5fe720d2022-05-19 12:01:49 +0100260 if (member.id == me.id) throw "I cannot mute myself"
pineafan8b4b17f2022-02-27 20:42:52 +0000261 // Allow the owner to mute anyone
pineafan663dc472022-05-10 18:13:47 +0100262 if (member.id == interaction.guild.ownerId) return true
pineafan8b4b17f2022-02-27 20:42:52 +0000263 // Check if the user has moderate_members permission
pineafan4edb7762022-06-26 19:21:04 +0100264 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the Moderate members permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000265 // Check if the user is below on the role list
pineafan5d1908e2022-02-28 21:34:47 +0000266 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000267 // Allow mute
268 return true
269}
270
271export { command, callback, check };