blob: 5f42b274cfa539d7a87fded2131214fed4fe4163 [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";
pineafan8b4b17f2022-02-27 20:42:52 +000010
11const command = (builder: SlashCommandSubcommandBuilder) =>
12 builder
13 .setName("mute")
14 .setDescription("Mutes a member using Discord's \"Timeout\" feature")
15 .addUserOption(option => option.setName("user").setDescription("The user to mute").setRequired(true))
16 .addIntegerOption(option => option.setName("days").setDescription("The number of days to mute the user for | Default 0").setMinValue(0).setMaxValue(27).setRequired(false))
17 .addIntegerOption(option => option.setName("hours").setDescription("The number of hours to mute the user for | Default 0").setMinValue(0).setMaxValue(23).setRequired(false))
18 .addIntegerOption(option => option.setName("minutes").setDescription("The number of minutes to mute the user for | Default 0").setMinValue(0).setMaxValue(59).setRequired(false))
19 .addIntegerOption(option => option.setName("seconds").setDescription("The number of seconds to mute the user for | Default 0").setMinValue(0).setMaxValue(59).setRequired(false))
20 .addStringOption(option => option.setName("reason").setDescription("The reason for the mute").setRequired(false))
pineafan377794f2022-04-18 19:01:01 +010021 .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> => {
PineappleFanb3dd83c2022-06-17 10:53:48 +010025 const { log, NucleusColors, renderUser, entry } = client.logger
pineafan8b4b17f2022-02-27 20:42:52 +000026 const user = interaction.options.getMember("user") as GuildMember
27 const reason = interaction.options.getString("reason")
28 const time = {
29 days: interaction.options.getInteger("days") || 0,
30 hours: interaction.options.getInteger("hours") || 0,
31 minutes: interaction.options.getInteger("minutes") || 0,
32 seconds: interaction.options.getInteger("seconds") || 0
33 }
pineafan4edb7762022-06-26 19:21:04 +010034 let config = await client.database.guilds.read(interaction.guild.id)
pineafane625d782022-05-09 18:04:32 +010035 let serverSettingsDescription = (config.moderation.mute.timeout ? "given a timeout" : "")
36 if (config.moderation.mute.role) serverSettingsDescription += (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`
37
pineafan8b4b17f2022-02-27 20:42:52 +000038 let muteTime = (time.days * 24 * 60 * 60) + (time.hours * 60 * 60) + (time.minutes * 60) + time.seconds
39 if (muteTime == 0) {
40 let m = await interaction.reply({embeds: [
pineafan4edb7762022-06-26 19:21:04 +010041 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000042 .setEmoji("PUNISH.MUTE.GREEN")
43 .setTitle("Mute")
44 .setDescription("How long should the user be muted")
45 .setStatus("Success")
46 ], components: [
47 new MessageActionRow().addComponents([
48 new Discord.MessageButton()
49 .setCustomId("1m")
50 .setLabel("1 Minute")
51 .setStyle("SECONDARY"),
52 new Discord.MessageButton()
53 .setCustomId("10m")
54 .setLabel("10 Minutes")
55 .setStyle("SECONDARY"),
56 new Discord.MessageButton()
57 .setCustomId("30m")
58 .setLabel("30 Minutes")
59 .setStyle("SECONDARY"),
60 new Discord.MessageButton()
61 .setCustomId("1h")
62 .setLabel("1 Hour")
63 .setStyle("SECONDARY")
64 ]),
65 new MessageActionRow().addComponents([
66 new Discord.MessageButton()
67 .setCustomId("6h")
68 .setLabel("6 Hours")
69 .setStyle("SECONDARY"),
70 new Discord.MessageButton()
71 .setCustomId("12h")
72 .setLabel("12 Hours")
73 .setStyle("SECONDARY"),
74 new Discord.MessageButton()
75 .setCustomId("1d")
76 .setLabel("1 Day")
77 .setStyle("SECONDARY"),
78 new Discord.MessageButton()
79 .setCustomId("1w")
80 .setLabel("1 Week")
81 .setStyle("SECONDARY")
82 ]),
83 new MessageActionRow().addComponents([
84 new Discord.MessageButton()
85 .setCustomId("cancel")
86 .setLabel("Cancel")
87 .setStyle("DANGER")
88 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
89 ])
90 ], ephemeral: true, fetchReply: true})
91 let component;
92 try {
pineafanc6158ab2022-06-17 16:34:07 +010093 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan8b4b17f2022-02-27 20:42:52 +000094 } catch { return }
95 component.deferUpdate();
pineafan4edb7762022-06-26 19:21:04 +010096 if (component.customId == "cancel") return interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000097 .setEmoji("PUNISH.MUTE.RED")
98 .setTitle("Mute")
99 .setDescription("Mute cancelled")
100 .setStatus("Danger")
101 ]})
102 switch (component.customId) {
103 case "1m": { muteTime = 60; break; }
104 case "10m": { muteTime = 60 * 10; break; }
105 case "30m": { muteTime = 60 * 30; break; }
106 case "1h": { muteTime = 60 * 60; break; }
107 case "6h": { muteTime = 60 * 60 * 6; break; }
108 case "12h": { muteTime = 60 * 60 * 12; break; }
109 case "1d": { muteTime = 60 * 60 * 24; break; }
110 case "1w": { muteTime = 60 * 60 * 24 * 7; break; }
111 }
112 } else {
113 await interaction.reply({embeds: [
pineafan4edb7762022-06-26 19:21:04 +0100114 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000115 .setEmoji("PUNISH.MUTE.GREEN")
116 .setTitle("Mute")
117 .setDescription("Loading...")
118 .setStatus("Success")
119 ], ephemeral: true, fetchReply: true})
120 }
pineafan5d1908e2022-02-28 21:34:47 +0000121 // TODO:[Modals] Replace this with a modal
pineafan377794f2022-04-18 19:01:01 +0100122 let confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000123 .setEmoji("PUNISH.MUTE.RED")
124 .setTitle("Mute")
125 .setDescription(keyValueList({
pineafan4edb7762022-06-26 19:21:04 +0100126 "user": renderUser(user),
pineafan8b4b17f2022-02-27 20:42:52 +0000127 "time": `${humanizeDuration(muteTime * 1000, {round: true})}`,
pineafan5d1908e2022-02-28 21:34:47 +0000128 "reason": `\n> ${reason ? reason : "*No reason provided*"}`
pineafan8b4b17f2022-02-27 20:42:52 +0000129 })
pineafane625d782022-05-09 18:04:32 +0100130 + `The user will be ` + serverSettingsDescription + "\n"
pineafan8b4b17f2022-02-27 20:42:52 +0000131 + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n\n`
132 + `Are you sure you want to mute <@!${(interaction.options.getMember("user") as GuildMember).id}>?`)
133 .setColor("Danger")
pineafan377794f2022-04-18 19:01:01 +0100134 .send(true)
135 if (confirmation.success) {
pineafan8b4b17f2022-02-27 20:42:52 +0000136 let dmd = false
pineafan5d1908e2022-02-28 21:34:47 +0000137 let dm;
pineafan4edb7762022-06-26 19:21:04 +0100138 let config = await client.database.guilds.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000139 try {
140 if (interaction.options.getString("notify") != "no") {
pineafan5d1908e2022-02-28 21:34:47 +0000141 dm = await (interaction.options.getMember("user") as GuildMember).send({
pineafan4edb7762022-06-26 19:21:04 +0100142 embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000143 .setEmoji("PUNISH.MUTE.RED")
144 .setTitle("Muted")
145 .setDescription(`You have been muted in ${interaction.guild.name}` +
pineafan1dc15722022-03-14 21:27:34 +0000146 (interaction.options.getString("reason") ? ` for:\n> ${interaction.options.getString("reason")}` : ".\n\n" +
pineafan8b4b17f2022-02-27 20:42:52 +0000147 `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>)`))
148 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100149 ],
150 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
151 .setStyle("LINK")
152 .setLabel(config.moderation.mute.text)
153 .setURL(config.moderation.mute.link)
154 ] : [])]
pineafan8b4b17f2022-02-27 20:42:52 +0000155 })
156 dmd = true
157 }
158 } catch {}
pineafan4edb7762022-06-26 19:21:04 +0100159 let member = (interaction.options.getMember("user") as GuildMember)
pineafan8b4b17f2022-02-27 20:42:52 +0000160 try {
pineafane625d782022-05-09 18:04:32 +0100161 if (config.moderation.mute.timeout) {
pineafan4edb7762022-06-26 19:21:04 +0100162 member.timeout(muteTime * 1000, interaction.options.getString("reason") || "No reason provided")
pineafane625d782022-05-09 18:04:32 +0100163 }
164 if (config.moderation.mute.role) {
pineafan4edb7762022-06-26 19:21:04 +0100165 member.roles.add(config.moderation.mute.role)
pineafane625d782022-05-09 18:04:32 +0100166 }
pineafan8b4b17f2022-02-27 20:42:52 +0000167 } catch {
pineafan4edb7762022-06-26 19:21:04 +0100168 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000169 .setEmoji("PUNISH.MUTE.RED")
170 .setTitle(`Mute`)
pineafan377794f2022-04-18 19:01:01 +0100171 .setDescription("Something went wrong and the user was not mute")
pineafan8b4b17f2022-02-27 20:42:52 +0000172 .setStatus("Danger")
173 ], components: []})
pineafan5d1908e2022-02-28 21:34:47 +0000174 if (dmd) await dm.delete()
175 return
pineafan8b4b17f2022-02-27 20:42:52 +0000176 }
pineafan4edb7762022-06-26 19:21:04 +0100177 try { await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason) } catch {}
pineafan5d1908e2022-02-28 21:34:47 +0000178 let failed = (dmd == false && interaction.options.getString("notify") != "no")
pineafan4edb7762022-06-26 19:21:04 +0100179 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000180 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
181 .setTitle(`Mute`)
182 .setDescription("The member was muted" + (failed ? ", but could not be notified" : ""))
183 .setStatus(failed ? "Warning" : "Success")
184 ], components: []})
pineafan377794f2022-04-18 19:01:01 +0100185 let data = {
186 meta:{
187 type: 'memberMute',
188 displayName: 'Member Muted',
189 calculateType: 'guildMemberPunish',
190 color: NucleusColors.yellow,
191 emoji: 'PUNISH.WARN.YELLOW',
192 timestamp: new Date().getTime()
193 },
194 list: {
pineafan4edb7762022-06-26 19:21:04 +0100195 user: entry(member.user.id, renderUser(member.user)),
pineafan377794f2022-04-18 19:01:01 +0100196 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
197 time: entry(muteTime, `${humanizeDuration(muteTime * 1000, {round: true})}`),
198 reason: (interaction.options.getString("reason") ? `\n> ${interaction.options.getString("reason")}` : "No reason provided")
199 },
200 hidden: {
201 guild: interaction.guild.id
202 }
203 }
pineafan4edb7762022-06-26 19:21:04 +0100204 log(data);
pineafan8b4b17f2022-02-27 20:42:52 +0000205 } else {
pineafan4edb7762022-06-26 19:21:04 +0100206 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000207 .setEmoji("PUNISH.MUTE.GREEN")
208 .setTitle(`Mute`)
209 .setDescription("No changes were made")
210 .setStatus("Success")
211 ], components: []})
212 }
213}
214
215const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan5d1908e2022-02-28 21:34:47 +0000216 let member = (interaction.member as GuildMember)
217 let me = (interaction.guild.me as GuildMember)
218 let apply = (interaction.options.getMember("user") as GuildMember)
219 if (member == null || me == null || apply == null) throw "That member is not in the server"
220 let memberPos = member.roles ? member.roles.highest.position : 0
221 let mePos = me.roles ? me.roles.highest.position : 0
222 let applyPos = apply.roles ? apply.roles.highest.position : 0
pineafan8b4b17f2022-02-27 20:42:52 +0000223 // Check if Nucleus can mute the member
pineafan5d1908e2022-02-28 21:34:47 +0000224 if (! (mePos > applyPos)) throw "I do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000225 // Check if Nucleus has permission to mute
pineafan4edb7762022-06-26 19:21:04 +0100226 if (! me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the Moderate members permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000227 // Do not allow the user to have admin or be the owner
PineappleFan5fe720d2022-05-19 12:01:49 +0100228 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 +0000229 // Do not allow muting Nucleus
PineappleFan5fe720d2022-05-19 12:01:49 +0100230 if (member.id == me.id) throw "I cannot mute myself"
pineafan8b4b17f2022-02-27 20:42:52 +0000231 // Allow the owner to mute anyone
pineafan663dc472022-05-10 18:13:47 +0100232 if (member.id == interaction.guild.ownerId) return true
pineafan8b4b17f2022-02-27 20:42:52 +0000233 // Check if the user has moderate_members permission
pineafan4edb7762022-06-26 19:21:04 +0100234 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the Moderate members permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000235 // Check if the user is below on the role list
pineafan5d1908e2022-02-28 21:34:47 +0000236 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000237 // Allow mute
238 return true
239}
240
241export { command, callback, check };