blob: b2cc94e1f8364ee14dd3230ffa8cc455d0892f4d [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";
pineafan377794f2022-04-18 19:01:01 +01004import generateEmojiEmbed 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";
pineafan377794f2022-04-18 19:01:01 +01009import readConfig from "../../utils/readConfig.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
24const callback = async (interaction: CommandInteraction) => {
pineafan377794f2022-04-18 19:01:01 +010025 // @ts-ignore
26 const { log, NucleusColors, renderUser, entry } = interaction.client.logger
pineafan8b4b17f2022-02-27 20:42:52 +000027 const user = interaction.options.getMember("user") as GuildMember
28 const reason = interaction.options.getString("reason")
29 const time = {
30 days: interaction.options.getInteger("days") || 0,
31 hours: interaction.options.getInteger("hours") || 0,
32 minutes: interaction.options.getInteger("minutes") || 0,
33 seconds: interaction.options.getInteger("seconds") || 0
34 }
pineafane625d782022-05-09 18:04:32 +010035 let config = await readConfig(interaction.guild.id)
36 let serverSettingsDescription = (config.moderation.mute.timeout ? "given a timeout" : "")
37 if (config.moderation.mute.role) serverSettingsDescription += (serverSettingsDescription ? " and " : "") + `given the <@&${config.moderation.mute.role}> role`
38
pineafan8b4b17f2022-02-27 20:42:52 +000039 let muteTime = (time.days * 24 * 60 * 60) + (time.hours * 60 * 60) + (time.minutes * 60) + time.seconds
40 if (muteTime == 0) {
41 let m = await interaction.reply({embeds: [
pineafan377794f2022-04-18 19:01:01 +010042 new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000043 .setEmoji("PUNISH.MUTE.GREEN")
44 .setTitle("Mute")
45 .setDescription("How long should the user be muted")
46 .setStatus("Success")
47 ], components: [
48 new MessageActionRow().addComponents([
49 new Discord.MessageButton()
50 .setCustomId("1m")
51 .setLabel("1 Minute")
52 .setStyle("SECONDARY"),
53 new Discord.MessageButton()
54 .setCustomId("10m")
55 .setLabel("10 Minutes")
56 .setStyle("SECONDARY"),
57 new Discord.MessageButton()
58 .setCustomId("30m")
59 .setLabel("30 Minutes")
60 .setStyle("SECONDARY"),
61 new Discord.MessageButton()
62 .setCustomId("1h")
63 .setLabel("1 Hour")
64 .setStyle("SECONDARY")
65 ]),
66 new MessageActionRow().addComponents([
67 new Discord.MessageButton()
68 .setCustomId("6h")
69 .setLabel("6 Hours")
70 .setStyle("SECONDARY"),
71 new Discord.MessageButton()
72 .setCustomId("12h")
73 .setLabel("12 Hours")
74 .setStyle("SECONDARY"),
75 new Discord.MessageButton()
76 .setCustomId("1d")
77 .setLabel("1 Day")
78 .setStyle("SECONDARY"),
79 new Discord.MessageButton()
80 .setCustomId("1w")
81 .setLabel("1 Week")
82 .setStyle("SECONDARY")
83 ]),
84 new MessageActionRow().addComponents([
85 new Discord.MessageButton()
86 .setCustomId("cancel")
87 .setLabel("Cancel")
88 .setStyle("DANGER")
89 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
90 ])
91 ], ephemeral: true, fetchReply: true})
92 let component;
93 try {
94 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
95 } catch { return }
96 component.deferUpdate();
pineafan377794f2022-04-18 19:01:01 +010097 if (component.customId == "cancel") return interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000098 .setEmoji("PUNISH.MUTE.RED")
99 .setTitle("Mute")
100 .setDescription("Mute cancelled")
101 .setStatus("Danger")
102 ]})
103 switch (component.customId) {
104 case "1m": { muteTime = 60; break; }
105 case "10m": { muteTime = 60 * 10; break; }
106 case "30m": { muteTime = 60 * 30; break; }
107 case "1h": { muteTime = 60 * 60; break; }
108 case "6h": { muteTime = 60 * 60 * 6; break; }
109 case "12h": { muteTime = 60 * 60 * 12; break; }
110 case "1d": { muteTime = 60 * 60 * 24; break; }
111 case "1w": { muteTime = 60 * 60 * 24 * 7; break; }
112 }
113 } else {
114 await interaction.reply({embeds: [
pineafan377794f2022-04-18 19:01:01 +0100115 new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000116 .setEmoji("PUNISH.MUTE.GREEN")
117 .setTitle("Mute")
118 .setDescription("Loading...")
119 .setStatus("Success")
120 ], ephemeral: true, fetchReply: true})
121 }
pineafan5d1908e2022-02-28 21:34:47 +0000122 // TODO:[Modals] Replace this with a modal
pineafan377794f2022-04-18 19:01:01 +0100123 let confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000124 .setEmoji("PUNISH.MUTE.RED")
125 .setTitle("Mute")
126 .setDescription(keyValueList({
127 "user": `<@!${user.id}> (${user.user.username})`,
128 "time": `${humanizeDuration(muteTime * 1000, {round: true})}`,
pineafan5d1908e2022-02-28 21:34:47 +0000129 "reason": `\n> ${reason ? reason : "*No reason provided*"}`
pineafan8b4b17f2022-02-27 20:42:52 +0000130 })
pineafane625d782022-05-09 18:04:32 +0100131 + `The user will be ` + serverSettingsDescription + "\n"
pineafan8b4b17f2022-02-27 20:42:52 +0000132 + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n\n`
133 + `Are you sure you want to mute <@!${(interaction.options.getMember("user") as GuildMember).id}>?`)
134 .setColor("Danger")
135// pluralize("day", interaction.options.getInteger("delete"))
136// const pluralize = (word: string, count: number) => { return count === 1 ? word : word + "s" }
pineafan377794f2022-04-18 19:01:01 +0100137 .send(true)
138 if (confirmation.success) {
pineafan8b4b17f2022-02-27 20:42:52 +0000139 let dmd = false
pineafan5d1908e2022-02-28 21:34:47 +0000140 let dm;
pineafan377794f2022-04-18 19:01:01 +0100141 let config = await readConfig(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000142 try {
143 if (interaction.options.getString("notify") != "no") {
pineafan5d1908e2022-02-28 21:34:47 +0000144 dm = await (interaction.options.getMember("user") as GuildMember).send({
pineafan377794f2022-04-18 19:01:01 +0100145 embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000146 .setEmoji("PUNISH.MUTE.RED")
147 .setTitle("Muted")
148 .setDescription(`You have been muted in ${interaction.guild.name}` +
pineafan1dc15722022-03-14 21:27:34 +0000149 (interaction.options.getString("reason") ? ` for:\n> ${interaction.options.getString("reason")}` : ".\n\n" +
pineafan8b4b17f2022-02-27 20:42:52 +0000150 `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>)`))
151 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100152 ],
153 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
154 .setStyle("LINK")
155 .setLabel(config.moderation.mute.text)
156 .setURL(config.moderation.mute.link)
157 ] : [])]
pineafan8b4b17f2022-02-27 20:42:52 +0000158 })
159 dmd = true
160 }
161 } catch {}
162 try {
pineafane625d782022-05-09 18:04:32 +0100163 if (config.moderation.mute.timeout) {
164 (interaction.options.getMember("user") as GuildMember).timeout(muteTime * 1000, interaction.options.getString("reason") || "No reason provided")
165 }
166 if (config.moderation.mute.role) {
167 (interaction.options.getMember("user") as GuildMember).roles.add(config.moderation.mute.role)
168 }
pineafan8b4b17f2022-02-27 20:42:52 +0000169 } catch {
pineafan377794f2022-04-18 19:01:01 +0100170 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000171 .setEmoji("PUNISH.MUTE.RED")
172 .setTitle(`Mute`)
pineafan377794f2022-04-18 19:01:01 +0100173 .setDescription("Something went wrong and the user was not mute")
pineafan8b4b17f2022-02-27 20:42:52 +0000174 .setStatus("Danger")
175 ], components: []})
pineafan5d1908e2022-02-28 21:34:47 +0000176 if (dmd) await dm.delete()
177 return
pineafan8b4b17f2022-02-27 20:42:52 +0000178 }
pineafan5d1908e2022-02-28 21:34:47 +0000179 let failed = (dmd == false && interaction.options.getString("notify") != "no")
pineafan377794f2022-04-18 19:01:01 +0100180 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000181 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
182 .setTitle(`Mute`)
183 .setDescription("The member was muted" + (failed ? ", but could not be notified" : ""))
184 .setStatus(failed ? "Warning" : "Success")
185 ], components: []})
pineafan377794f2022-04-18 19:01:01 +0100186 let data = {
187 meta:{
188 type: 'memberMute',
189 displayName: 'Member Muted',
190 calculateType: 'guildMemberPunish',
191 color: NucleusColors.yellow,
192 emoji: 'PUNISH.WARN.YELLOW',
193 timestamp: new Date().getTime()
194 },
195 list: {
196 user: entry((interaction.options.getMember("user") as GuildMember).user.id, renderUser((interaction.options.getMember("user") as GuildMember).user)),
197 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
198 time: entry(muteTime, `${humanizeDuration(muteTime * 1000, {round: true})}`),
199 reason: (interaction.options.getString("reason") ? `\n> ${interaction.options.getString("reason")}` : "No reason provided")
200 },
201 hidden: {
202 guild: interaction.guild.id
203 }
204 }
205 log(data, interaction.client);
pineafan8b4b17f2022-02-27 20:42:52 +0000206 } else {
pineafan377794f2022-04-18 19:01:01 +0100207 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000208 .setEmoji("PUNISH.MUTE.GREEN")
209 .setTitle(`Mute`)
210 .setDescription("No changes were made")
211 .setStatus("Success")
212 ], components: []})
213 }
214}
215
216const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan5d1908e2022-02-28 21:34:47 +0000217 let member = (interaction.member as GuildMember)
218 let me = (interaction.guild.me as GuildMember)
219 let apply = (interaction.options.getMember("user") as GuildMember)
220 if (member == null || me == null || apply == null) throw "That member is not in the server"
221 let memberPos = member.roles ? member.roles.highest.position : 0
222 let mePos = me.roles ? me.roles.highest.position : 0
223 let applyPos = apply.roles ? apply.roles.highest.position : 0
pineafan8b4b17f2022-02-27 20:42:52 +0000224 // Check if Nucleus can mute the member
pineafan5d1908e2022-02-28 21:34:47 +0000225 if (! (mePos > applyPos)) throw "I do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000226 // Check if Nucleus has permission to mute
PineappleFan5fe720d2022-05-19 12:01:49 +0100227 if (! me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the `moderate_members` permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000228 // Do not allow the user to have admin or be the owner
PineappleFan5fe720d2022-05-19 12:01:49 +0100229 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 +0000230 // Do not allow muting Nucleus
PineappleFan5fe720d2022-05-19 12:01:49 +0100231 if (member.id == me.id) throw "I cannot mute myself"
pineafan8b4b17f2022-02-27 20:42:52 +0000232 // Allow the owner to mute anyone
pineafan663dc472022-05-10 18:13:47 +0100233 if (member.id == interaction.guild.ownerId) return true
pineafan8b4b17f2022-02-27 20:42:52 +0000234 // Check if the user has moderate_members permission
pineafan663dc472022-05-10 18:13:47 +0100235 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the `moderate_members` permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000236 // Check if the user is below on the role list
pineafan5d1908e2022-02-28 21:34:47 +0000237 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000238 // Allow mute
239 return true
240}
241
242export { command, callback, check };