feat(bots/discord/commands): add mute and unmute commands

This commit is contained in:
PalmDevs
2024-06-24 01:15:12 +07:00
parent 467acff57a
commit c0fa2fe1c3
4 changed files with 144 additions and 1 deletions

View File

@@ -0,0 +1,65 @@
import { SlashCommandBuilder } from 'discord.js'
import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { applyRolePreset } from '$/utils/discord/rolePresets'
import type { Command } from '..'
import { applyReferenceToModerationActionEmbed, createModerationActionEmbed } from '$/utils/discord/embeds'
import { parse } from 'simple-duration'
export default {
data: new SlashCommandBuilder()
.setName('mute')
.setDescription('Mute a member')
.addUserOption(option => option.setName('member').setRequired(true).setDescription('The member to mute'))
.addStringOption(option => option.setName('reason').setDescription('The reason for muting the member'))
.addStringOption(option => option.setName('duration').setDescription('The duration of the mute'))
.toJSON(),
memberRequirements: {
permissions: 8n,
},
global: false,
async execute({ config, logger }, interaction) {
const user = interaction.options.getUser('member', true)
const reason = interaction.options.getString('reason')
const duration = interaction.options.getString('duration')
const durationMs = duration ? parse(duration) : null
if (Number.isInteger(durationMs) && durationMs! < 1)
throw new CommandError(
CommandErrorType.InvalidDuration,
'The duration must be at least 1 millisecond long.',
)
const member = await interaction.guild!.members.fetch(user.id)
if (!member)
throw new CommandError(
CommandErrorType.InvalidUser,
'The provided member is not in the server or does not exist.',
)
await applyRolePreset(member, 'mute', durationMs ? Date.now() + durationMs : null)
const embed = createModerationActionEmbed(
'Muted',
user,
interaction.user,
reason ?? 'No reason provided',
durationMs,
)
const reply = await interaction.reply({ embeds: [embed] }).then(it => it.fetch())
const logConfig = config.moderation?.log
if (logConfig) {
const channel = await interaction.guild!.channels.fetch(logConfig.thread ?? logConfig.channel)
if (!channel || !channel.isTextBased())
return void logger.warn('The moderation log channel does not exist, skipping logging')
await channel.send({ embeds: [applyReferenceToModerationActionEmbed(embed, reply.url)] })
}
},
} satisfies Command

View File

@@ -0,0 +1,44 @@
import { SlashCommandBuilder } from 'discord.js'
import CommandError, { CommandErrorType } from '$/classes/CommandError'
import { applyReferenceToModerationActionEmbed, createModerationActionEmbed } from '$/utils/discord/embeds'
import { removeRolePreset } from '$/utils/discord/rolePresets'
import type { Command } from '..'
export default {
data: new SlashCommandBuilder()
.setName('unmute')
.setDescription('Unmute a member')
.addUserOption(option => option.setName('member').setRequired(true).setDescription('The member to mute'))
.toJSON(),
memberRequirements: {
permissions: 8n,
},
global: false,
async execute({ config, logger }, interaction) {
const user = interaction.options.getUser('member', true)
const member = await interaction.guild!.members.fetch(user.id)
if (!member)
throw new CommandError(
CommandErrorType.InvalidUser,
'The provided member is not in the server or does not exist.',
)
await removeRolePreset(member, 'mute')
const embed = createModerationActionEmbed('Muted', user, interaction.user)
const reply = await interaction.reply({ embeds: [embed] }).then(it => it.fetch())
const logConfig = config.moderation?.log
if (logConfig) {
const channel = await interaction.guild!.channels.fetch(logConfig.thread ?? logConfig.channel)
if (!channel || !channel.isTextBased())
return void logger.warn('The moderation log channel does not exist, skipping logging')
await channel.send({ embeds: [applyReferenceToModerationActionEmbed(embed, reply.url)] })
}
},
} satisfies Command

View File

@@ -3,3 +3,8 @@ type IfTrue<Condition, True, False> = IfExtends<Condition, true, True, False>
type EmptyObject<K = PropertyKey> = Record<K, never>
type ValuesOf<T> = T[keyof T]
type MaybeArray<T> = T | T[]
declare module 'simple-duration' {
export function parse(duration: string): number
export function stringify(duration: number): string
}

View File

@@ -1,5 +1,5 @@
import { DefaultEmbedColor, MessageScanHumanizedMode, ReVancedLogoURL } from '$/constants'
import { EmbedBuilder } from 'discord.js'
import { EmbedBuilder, type EmbedField, type User } from 'discord.js'
import type { ConfigMessageScanResponseMessage } from '../../../config.schema'
export const createErrorEmbed = (title: string, description?: string) =>
@@ -40,6 +40,35 @@ export const createMessageScanResponseEmbed = (
return applyCommonEmbedStyles(embed, true, true, true)
}
export const createModerationActionEmbed = (
action: string,
user: User,
moderator: User,
reason?: string,
expires?: number | null,
) => {
const fields: EmbedField[] = []
if (reason) fields.push({ name: 'Reason', value: reason, inline: true })
if (Number.isInteger(expires) || expires === null)
fields.push({
name: 'Expires',
value: Number.isInteger(expires) ? new Date(expires! * 1000).toLocaleString() : 'Never',
inline: true,
})
const embed = new EmbedBuilder()
.setTitle(`${action} ${user.tag}`)
.setDescription(`${user.toString()} was ${action.toLowerCase()} by ${moderator.toString()}`)
.addFields(fields)
return applyCommonEmbedStyles(embed, true, true, true)
}
export const applyReferenceToModerationActionEmbed = (embed: EmbedBuilder, reference: string) => {
embed.addFields({ name: 'Reference', value: `[Jump to message](${reference})`, inline: true })
return embed
}
export const applyCommonEmbedStyles = (
embed: EmbedBuilder,
setThumbnail = false,