blob: 854f38f1b41ad0e7130361d3866fd19f68de6ca5 [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";
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> => {
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 }
pineafan6702cef2022-06-13 17:52:37 +010035 let config = await client.database.read(interaction.guild.id)
pineafane625d782022-05-09 18:04:32 +010036 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")
pineafan377794f2022-04-18 19:01:01 +0100135 .send(true)
136 if (confirmation.success) {
pineafan8b4b17f2022-02-27 20:42:52 +0000137 let dmd = false
pineafan5d1908e2022-02-28 21:34:47 +0000138 let dm;
pineafan6702cef2022-06-13 17:52:37 +0100139 let config = await client.database.read(interaction.guild.id);
pineafan8b4b17f2022-02-27 20:42:52 +0000140 try {
141 if (interaction.options.getString("notify") != "no") {
pineafan5d1908e2022-02-28 21:34:47 +0000142 dm = await (interaction.options.getMember("user") as GuildMember).send({
pineafan377794f2022-04-18 19:01:01 +0100143 embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000144 .setEmoji("PUNISH.MUTE.RED")
145 .setTitle("Muted")
146 .setDescription(`You have been muted in ${interaction.guild.name}` +
pineafan1dc15722022-03-14 21:27:34 +0000147 (interaction.options.getString("reason") ? ` for:\n> ${interaction.options.getString("reason")}` : ".\n\n" +
pineafan8b4b17f2022-02-27 20:42:52 +0000148 `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>)`))
149 .setStatus("Danger")
pineafan377794f2022-04-18 19:01:01 +0100150 ],
151 components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton()
152 .setStyle("LINK")
153 .setLabel(config.moderation.mute.text)
154 .setURL(config.moderation.mute.link)
155 ] : [])]
pineafan8b4b17f2022-02-27 20:42:52 +0000156 })
157 dmd = true
158 }
159 } catch {}
160 try {
pineafane625d782022-05-09 18:04:32 +0100161 if (config.moderation.mute.timeout) {
162 (interaction.options.getMember("user") as GuildMember).timeout(muteTime * 1000, interaction.options.getString("reason") || "No reason provided")
163 }
164 if (config.moderation.mute.role) {
165 (interaction.options.getMember("user") as GuildMember).roles.add(config.moderation.mute.role)
166 }
pineafan8b4b17f2022-02-27 20:42:52 +0000167 } catch {
pineafan377794f2022-04-18 19:01:01 +0100168 await interaction.editReply({embeds: [new generateEmojiEmbed()
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 }
pineafan5d1908e2022-02-28 21:34:47 +0000177 let failed = (dmd == false && interaction.options.getString("notify") != "no")
pineafan377794f2022-04-18 19:01:01 +0100178 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000179 .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`)
180 .setTitle(`Mute`)
181 .setDescription("The member was muted" + (failed ? ", but could not be notified" : ""))
182 .setStatus(failed ? "Warning" : "Success")
183 ], components: []})
pineafan377794f2022-04-18 19:01:01 +0100184 let data = {
185 meta:{
186 type: 'memberMute',
187 displayName: 'Member Muted',
188 calculateType: 'guildMemberPunish',
189 color: NucleusColors.yellow,
190 emoji: 'PUNISH.WARN.YELLOW',
191 timestamp: new Date().getTime()
192 },
193 list: {
194 user: entry((interaction.options.getMember("user") as GuildMember).user.id, renderUser((interaction.options.getMember("user") as GuildMember).user)),
195 mutedBy: entry(interaction.member.user.id, renderUser(interaction.member.user)),
196 time: entry(muteTime, `${humanizeDuration(muteTime * 1000, {round: true})}`),
197 reason: (interaction.options.getString("reason") ? `\n> ${interaction.options.getString("reason")}` : "No reason provided")
198 },
199 hidden: {
200 guild: interaction.guild.id
201 }
202 }
203 log(data, interaction.client);
pineafan8b4b17f2022-02-27 20:42:52 +0000204 } else {
pineafan377794f2022-04-18 19:01:01 +0100205 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000206 .setEmoji("PUNISH.MUTE.GREEN")
207 .setTitle(`Mute`)
208 .setDescription("No changes were made")
209 .setStatus("Success")
210 ], components: []})
211 }
212}
213
214const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan5d1908e2022-02-28 21:34:47 +0000215 let member = (interaction.member as GuildMember)
216 let me = (interaction.guild.me as GuildMember)
217 let apply = (interaction.options.getMember("user") as GuildMember)
218 if (member == null || me == null || apply == null) throw "That member is not in the server"
219 let memberPos = member.roles ? member.roles.highest.position : 0
220 let mePos = me.roles ? me.roles.highest.position : 0
221 let applyPos = apply.roles ? apply.roles.highest.position : 0
pineafan8b4b17f2022-02-27 20:42:52 +0000222 // Check if Nucleus can mute the member
pineafan5d1908e2022-02-28 21:34:47 +0000223 if (! (mePos > applyPos)) throw "I do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000224 // Check if Nucleus has permission to mute
PineappleFan5fe720d2022-05-19 12:01:49 +0100225 if (! me.permissions.has("MODERATE_MEMBERS")) throw "I do not have the `moderate_members` permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000226 // Do not allow the user to have admin or be the owner
PineappleFan5fe720d2022-05-19 12:01:49 +0100227 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 +0000228 // Do not allow muting Nucleus
PineappleFan5fe720d2022-05-19 12:01:49 +0100229 if (member.id == me.id) throw "I cannot mute myself"
pineafan8b4b17f2022-02-27 20:42:52 +0000230 // Allow the owner to mute anyone
pineafan663dc472022-05-10 18:13:47 +0100231 if (member.id == interaction.guild.ownerId) return true
pineafan8b4b17f2022-02-27 20:42:52 +0000232 // Check if the user has moderate_members permission
pineafan663dc472022-05-10 18:13:47 +0100233 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the `moderate_members` permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000234 // Check if the user is below on the role list
pineafan5d1908e2022-02-28 21:34:47 +0000235 if (! (memberPos > applyPos)) throw "You do not have a role higher than that member"
pineafan8b4b17f2022-02-27 20:42:52 +0000236 // Allow mute
237 return true
238}
239
240export { command, callback, check };