diff options
Diffstat (limited to 'src')
23 files changed, 587 insertions, 206 deletions
diff --git a/src/commands/config/muteRole.ts b/src/commands/config/muteRole.ts index 6fda0b8..9a172be 100644 --- a/src/commands/config/muteRole.ts +++ b/src/commands/config/muteRole.ts @@ -27,9 +27,9 @@ export default class MuteRoleCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'ROLE', name: 'role', description: "What would you like to set the server's mute role to?", + type: 'ROLE', required: true } ] diff --git a/src/commands/config/prefix.ts b/src/commands/config/prefix.ts index c7a1a9f..dc51671 100644 --- a/src/commands/config/prefix.ts +++ b/src/commands/config/prefix.ts @@ -26,9 +26,9 @@ export default class PrefixCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'STRING', name: 'prefix', description: 'What would you like the new prefix to be?', + type: 'STRING', required: false } ] diff --git a/src/commands/config/welcomeChannel.ts b/src/commands/config/welcomeChannel.ts index f15e07d..71d59f8 100644 --- a/src/commands/config/welcomeChannel.ts +++ b/src/commands/config/welcomeChannel.ts @@ -27,9 +27,9 @@ export default class WelcomeChannelCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'CHANNEL', name: 'channel', description: 'What channel would you like me to send welcome messages in?', + type: 'CHANNEL', required: false } ] diff --git a/src/commands/dev/reload.ts b/src/commands/dev/reload.ts index c6a2140..928632c 100644 --- a/src/commands/dev/reload.ts +++ b/src/commands/dev/reload.ts @@ -21,9 +21,9 @@ export default class ReloadCommand extends BushCommand { typing: true, slashOptions: [ { - type: 'BOOLEAN', name: 'fast', description: 'Whether to use esbuild for fast compiling or not', + type: 'BOOLEAN', required: false } ], diff --git a/src/commands/dev/setLevel.ts b/src/commands/dev/setLevel.ts index 9c7daeb..db0cfab 100644 --- a/src/commands/dev/setLevel.ts +++ b/src/commands/dev/setLevel.ts @@ -32,15 +32,15 @@ export default class SetLevelCommand extends BushCommand { ownerOnly: true, slashOptions: [ { - type: 'USER', name: 'user', description: 'The user to change the level of', + type: 'USER', required: true }, { - type: 'INTEGER', name: 'level', description: 'The level to set the user to', + type: 'INTEGER', required: true } ], diff --git a/src/commands/dev/testDuration.ts b/src/commands/dev/testDuration.ts index 719292e..2d636d2 100644 --- a/src/commands/dev/testDuration.ts +++ b/src/commands/dev/testDuration.ts @@ -27,9 +27,9 @@ export default class TestDurationCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'STRING', name: 'reason', description: 'Enter text and a duration here.', + type: 'STRING', required: false } ], diff --git a/src/commands/info/botInfo.ts b/src/commands/info/botInfo.ts index d9961af..d5941b2 100644 --- a/src/commands/info/botInfo.ts +++ b/src/commands/info/botInfo.ts @@ -1,5 +1,5 @@ import { BushCommand, BushMessage, BushSlashMessage } from '@lib'; -import { MessageEmbed } from 'discord.js'; +import { MessageEmbed, version as discordJSVersion } from 'discord.js'; export default class BotInfoCommand extends BushCommand { public constructor() { @@ -18,32 +18,24 @@ export default class BotInfoCommand extends BushCommand { } public async exec(message: BushMessage | BushSlashMessage): Promise<void> { - const owners = (await this.client.util.mapIDs(this.client.ownerID)).map((u) => u.tag).join('\n'); + const developers = (await this.client.util.mapIDs(this.client.config.owners)).map((u) => u?.tag).join('\n'); const currentCommit = (await this.client.util.shell('git rev-parse HEAD')).stdout.replace('\n', ''); const repoUrl = (await this.client.util.shell('git remote get-url origin')).stdout.replace('\n', ''); const embed = new MessageEmbed() .setTitle('Bot Info:') - .addFields([ - { - name: 'Owners', - value: owners, - inline: true - }, - { - name: 'Uptime', - value: this.client.util.capitalize(this.client.util.humanizeDuration(this.client.uptime)) - }, - { - name: 'User count', - value: this.client.users.cache.size.toLocaleString(), - inline: true - }, - { - name: 'Current commit', - value: `[${currentCommit.substring(0, 7)}](${repoUrl}/commit/${currentCommit})` - } - ]) - .setTimestamp(); + .addField('**Uptime**', this.client.util.humanizeDuration(this.client.uptime), true) + .addField('**Servers**', this.client.guilds.cache.size.toLocaleString(), true) + .addField('**Users**', this.client.users.cache.size.toLocaleString(), true) + .addField('**Discord.js Version**', discordJSVersion, true) + .addField('**Node.js Version**', process.version.slice(1), true) + .addField('**Commands**', this.client.commandHandler.modules.size.toLocaleString(), true) + .addField('**Listeners**', this.client.listenerHandler.modules.size.toLocaleString(), true) + .addField('**Inhibitors**', this.client.inhibitorHandler.modules.size.toLocaleString(), true) + .addField('**Tasks**', this.client.taskHandler.modules.size.toLocaleString(), true) + .addField('**Current Commit**', `[${currentCommit.substring(0, 7)}](${repoUrl}/commit/${currentCommit})`, true) + .addField('**Developers**', developers, true) + .setTimestamp() + .setColor(this.client.util.colors.default); await message.util.reply({ embeds: [embed] }); } } diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 439f0ef..e061453 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -33,9 +33,9 @@ export default class HelpCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'STRING', name: 'command', description: 'The command you would like to find information about.', + type: 'STRING', required: false } ] diff --git a/src/commands/info/ping.ts b/src/commands/info/ping.ts index 4638aa9..59f475f 100644 --- a/src/commands/info/ping.ts +++ b/src/commands/info/ping.ts @@ -20,8 +20,8 @@ export default class PingCommand extends BushCommand { public async exec(message: BushMessage): Promise<void> { const sentMessage = (await message.util.send('Pong!')) as Message; const timestamp: number = message.editedTimestamp ? message.editedTimestamp : message.createdTimestamp; - const botLatency = `\`\`\`\n ${Math.floor(sentMessage.createdTimestamp - timestamp)}ms \`\`\``; - const apiLatency = `\`\`\`\n ${Math.round(message.client.ws.ping)}ms \`\`\``; + const botLatency = `${'```'}\n ${Math.round(sentMessage.createdTimestamp - timestamp)}ms ${'```'}`; + const apiLatency = `${'```'}\n ${Math.round(message.client.ws.ping)}ms ${'```'}`; const embed = new MessageEmbed() .setTitle('Pong! 🏓') .addField('Bot Latency', botLatency, true) @@ -39,8 +39,8 @@ export default class PingCommand extends BushCommand { const timestamp1 = message.interaction.createdTimestamp; await message.interaction.reply('Pong!'); const timestamp2 = await message.interaction.fetchReply().then((m) => (m as Message).createdTimestamp); - const botLatency = `\`\`\`\n ${Math.floor(timestamp2 - timestamp1)}ms \`\`\``; - const apiLatency = `\`\`\`\n ${Math.round(this.client.ws.ping)}ms \`\`\``; + const botLatency = `${'```'}\n ${Math.round(timestamp2 - timestamp1)}ms ${'```'}`; + const apiLatency = `${'```'}\n ${Math.round(this.client.ws.ping)}ms ${'```'}`; const embed = new MessageEmbed() .setTitle('Pong! 🏓') .addField('Bot Latency', botLatency, true) diff --git a/src/commands/info/pronouns.ts b/src/commands/info/pronouns.ts index 60701d1..0a5c0bc 100644 --- a/src/commands/info/pronouns.ts +++ b/src/commands/info/pronouns.ts @@ -51,9 +51,9 @@ export default class PronounsCommand extends BushCommand { clientPermissions: ['SEND_MESSAGES'], slashOptions: [ { - type: 'USER', name: 'user', description: 'The user to get pronouns for', + type: 'USER', required: false } ], diff --git a/src/commands/moderation/_unmute.ts b/src/commands/moderation/_unmute.ts deleted file mode 100644 index e69de29..0000000 --- a/src/commands/moderation/_unmute.ts +++ /dev/null diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index be7a51f..d713a15 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,4 +1,5 @@ -import { BushCommand, BushMessage, BushSlashMessage } from '@lib'; +import { BushCommand, BushGuildMember, BushMessage, BushSlashMessage } from '@lib'; +import { Argument } from 'discord-akairo'; import { User } from 'discord.js'; export default class BanCommand extends BushCommand { @@ -6,6 +7,11 @@ export default class BanCommand extends BushCommand { super('ban', { aliases: ['ban'], category: 'moderation', + description: { + content: 'Ban a member from the server.', + usage: 'ban <member> <reason> [--delete ]', + examples: ['ban 322862723090219008 1 day commands in #general --delete 7'] + }, args: [ { id: 'user', @@ -20,137 +26,109 @@ export default class BanCommand extends BushCommand { type: 'contentWithDuration', match: 'restContent', prompt: { - start: 'Why would you like to ban this user?', - retry: '{error} Choose a ban reason.', + start: 'Why should this user be banned and for how long?', + retry: '{error} Choose a valid ban reason and duration.', optional: true } + }, + { + id: 'days', + flag: '--days', + match: 'option', + type: Argument.range('integer', 0, 7, true), + default: 0 } ], - clientPermissions: ['BAN_MEMBERS'], - userPermissions: ['BAN_MEMBERS'], - description: { - content: 'Ban a member from the server.', - usage: 'ban <member> <reason> [--time]', - examples: ['ban @user bad --time 69d'] - }, + slash: true, slashOptions: [ { - type: 'USER', name: 'user', - description: 'Who would you like to ban?', + description: 'What user would you like to ban?', + type: 'USER', required: true }, { - type: 'STRING', name: 'reason', - description: 'Why are they getting banned?', + description: 'Why should this user be banned and for how long?', + type: 'STRING', required: false + }, + { + name: 'days', + description: "How many days of the user's messages would you like to delete?", + type: 'INTEGER', + required: false, + choices: [ + { name: '0', value: 0 }, + { name: '1', value: 1 }, + { name: '2', value: 2 }, + { name: '3', value: 3 }, + { name: '4', value: 4 }, + { name: '5', value: 5 }, + { name: '6', value: 6 }, + { name: '7', value: 7 } + ] } ], - slash: true + channel: 'guild', + clientPermissions: ['BAN_MEMBERS'], + userPermissions: ['BAN_MEMBERS'] }); } - // async *genResponses( - // message: Message | CommandInteraction, - // user: User, - // reason?: string, - // time?: number - // ): AsyncIterable<string> { - // const duration = moment.duration(); - // let modLogEntry: ModLog; - // let banEntry: Ban; - // // const translatedTime: string[] = []; - // // Create guild entry so postgres doesn't get mad when I try and add a modlog entry - // await Guild.findOrCreate({ - // where: { - // id: message.guild.id - // }, - // defaults: { - // id: message.guild.id - // } - // }); - // try { - // if (time) { - // duration.add(time); - // /* const parsed = [...time.matchAll(durationRegex)]; - // if (parsed.length < 1) { - // yield `${this.client.util.emojis.error} Invalid time.`; - // return; - // } - // for (const part of parsed) { - // const translated = Object.keys(durationAliases).find((k) => durationAliases[k].includes(part[2])); - // translatedTime.push(part[1] + ' ' + translated); - // duration.add(Number(part[1]), translated as 'weeks' | 'days' | 'hours' | 'months' | 'minutes'); - // } */ - // modLogEntry = ModLog.build({ - // user: user.id, - // guild: message.guild.id, - // reason, - // type: ModLogType.TEMP_BAN, - // duration: duration.asMilliseconds(), - // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - // }); - // banEntry = Ban.build({ - // user: user.id, - // guild: message.guild.id, - // reason, - // expires: new Date(new Date().getTime() + duration.asMilliseconds()), - // modlog: modLogEntry.id - // }); - // } else { - // modLogEntry = ModLog.build({ - // user: user.id, - // guild: message.guild.id, - // reason, - // type: ModLogType.BAN, - // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - // }); - // banEntry = Ban.build({ - // user: user.id, - // guild: message.guild.id, - // reason, - // modlog: modLogEntry.id - // }); - // } - // await modLogEntry.save(); - // await banEntry.save(); - - // try { - // await user.send( - // `You were banned in ${message.guild.name} ${duration ? duration.humanize() : 'permanently'} with reason \`${ - // reason || 'No reason given' - // }\`` - // ); - // } catch { - // yield `${this.client.util.emojis.warn} Unable to dm user`; - // } - // await message.guild.members.ban(user, { - // reason: `Banned by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${ - // reason ? `reason ${reason}` : 'no reason' - // }` - // }); - // yield `${this.client.util.emojis.success} Banned <@!${user.id}> ${ - // duration ? duration.humanize() : 'permanently' - // } with reason \`${reason || 'No reason given'}\``; - // } catch { - // yield `${this.client.util.emojis.error} Error banning :/`; - // await banEntry.destroy(); - // await modLogEntry.destroy(); - // return; - // } - // } async exec( message: BushMessage | BushSlashMessage, - { user, reason, time }: { user: User; reason?: string; time?: number | string } + { user, reason, days }: { user: User; reason?: { duration: number; contentWithoutTime: string }; days?: number } ): Promise<unknown> { - return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); + const member = message.guild.members.cache.get(user.id) as BushGuildMember; + const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'ban'); + + if (canModerateResponse !== true) { + return message.util.reply(canModerateResponse); + } + + if (!Number.isInteger(days) || days < 0 || days > 7) { + return message.util.reply(`${this.client.util.emojis.error} The delete days must be an integer between 0 and 7.`); + } + + let time: number; + if (reason) { + time = + typeof reason === 'string' + ? await Argument.cast('duration', this.client.commandHandler.resolver, message as BushMessage, reason) + : reason.duration; + } + const parsedReason = reason.contentWithoutTime; + + const response = await member.bushBan({ + reason: parsedReason, + moderator: message.author, + duration: time, + deleteDays: days ?? 0 + }); - // if (typeof time === 'string') { - // time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number; - // //// time = this.client.commandHandler.resolver.type('duration') - // } - // for await (const response of this.genResponses(message, user, reason, time)) { - // await message.util.send(response); - // } + switch (response) { + case 'missing permissions': + return message.util.reply( + `${this.client.util.emojis.error} Could not ban **${member.user.tag}** because I do not have permissions` + ); + case 'error banning': + return message.util.reply( + `${this.client.util.emojis.error} An error occurred while trying to ban **${member.user.tag}**.` + ); + case 'error creating ban entry': + return message.util.reply( + `${this.client.util.emojis.error} While banning **${member.user.tag}**, there was an error creating a ban entry, please report this to my developers.` + ); + case 'error creating modlog entry': + return message.util.reply( + `${this.client.util.emojis.error} While banning **${member.user.tag}**, there was an error creating a modlog entry, please report this to my developers.` + ); + case 'failed to dm': + return message.util.reply( + `${this.client.util.emojis.warn} Banned **${member.user.tag}** however I could not send them a dm.` + ); + case 'success': + return message.util.reply(`${this.client.util.emojis.success} Successfully banned **${member.user.tag}**.`); + } } } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index f960488..d70bbfb 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -22,7 +22,7 @@ export default class KickCommand extends BushCommand { { id: 'reason', type: 'string', - match: 'restContent', + match: 'rest', prompt: { start: 'Why should this user be kicked?', retry: '{error} Choose a valid kick reason.', @@ -33,15 +33,15 @@ export default class KickCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'USER', name: 'user', description: 'What user would you like to kick?', + type: 'USER', required: true }, { - type: 'STRING', name: 'reason', description: 'Why should this user be kicked?', + type: 'STRING', required: false } ], @@ -55,7 +55,7 @@ export default class KickCommand extends BushCommand { const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'kick'); // const victimBoldTag = `**${member.user.tag}**`; - if (typeof canModerateResponse !== 'boolean') { + if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); } @@ -63,5 +63,26 @@ export default class KickCommand extends BushCommand { reason, moderator: message.author }); + + switch (response) { + case 'missing permissions': + return message.util.reply( + `${this.client.util.emojis.error} Could not kick **${member.user.tag}** because I am missing the \`Kick Members\` permission.` + ); + case 'error kicking': + return message.util.reply( + `${this.client.util.emojis.error} An error occurred while trying to kick **${member.user.tag}**.` + ); + case 'error creating modlog entry': + return message.util.reply( + `${this.client.util.emojis.error} While muting **${member.user.tag}**, there was an error creating a modlog entry, please report this to my developers.` + ); + case 'failed to dm': + return message.util.reply( + `${this.client.util.emojis.warn} Kicked **${member.user.tag}** however I could not send them a dm.` + ); + case 'success': + return message.util.reply(`${this.client.util.emojis.success} Successfully kicked **${member.user.tag}**.`); + } } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 26ccb21..0eae547 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -34,15 +34,15 @@ export default class MuteCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'USER', name: 'user', description: 'What user would you like to mute?', + type: 'USER', required: true }, { - type: 'STRING', name: 'reason', description: 'Why should this user be muted and for how long?', + type: 'STRING', required: false } ], @@ -60,7 +60,7 @@ export default class MuteCommand extends BushCommand { const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'mute'); const victimBoldTag = `**${member.user.tag}**`; - if (typeof canModerateResponse !== 'boolean') { + if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); } diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 33f474e..29913d5 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -4,7 +4,7 @@ import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushRole, B export default class RoleCommand extends BushCommand { public constructor() { super('role', { - aliases: ['role', 'addrole', 'removerole'], + aliases: ['role'], category: 'moderation', description: { content: "Manages users' roles.", @@ -69,7 +69,6 @@ export default class RoleCommand extends BushCommand { start: `What user do you want to ${action} the role ${action2}?`, retry: `{error} Choose a valid user to ${action} the role ${action2}.` } - //unordered: true }; const role = yield { id: 'role', @@ -87,7 +86,7 @@ export default class RoleCommand extends BushCommand { message: BushMessage | BushSlashMessage, { action, user, role }: { action: 'add' | 'remove'; user: BushGuildMember; role: BushRole } ): Promise<unknown> { - if (!message.member.permissions.has('MANAGE_ROLES') && !this.client.ownerID.includes(message.author.id)) { + if (!message.member.permissions.has('MANAGE_ROLES') && !message.author.isOwner()) { const mappings = this.client.consts.mappings; let mappedRole: { name: string; id: string }; for (let i = 0; i < mappings.roleMap.length; i++) { @@ -112,8 +111,7 @@ export default class RoleCommand extends BushCommand { allowedMentions: AllowedMentions.none() }); } - } - if (!this.client.ownerID.includes(message.author.id)) { + } else if (!message.author.isOwner()) { if (role.comparePositionTo(message.member.roles.highest) >= 0) { return await message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is higher or equal to your highest role.`, @@ -127,7 +125,7 @@ export default class RoleCommand extends BushCommand { }); } if (role.managed) { - await await message.util.reply({ + return await message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is managed by an integration and cannot be managed.`, allowedMentions: AllowedMentions.none() }); @@ -138,7 +136,7 @@ export default class RoleCommand extends BushCommand { const success = await user.roles.remove(role.id).catch(() => {}); if (success) { return await message.util.reply({ - content: `${this.client.util.emojis.success}Successfully removed <@&${role.id}> from <@${user.id}>!`, + content: `${this.client.util.emojis.success} Successfully removed <@&${role.id}> from <@${user.id}>!`, allowedMentions: AllowedMentions.none() }); } else { diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts new file mode 100644 index 0000000..4f52666 --- /dev/null +++ b/src/commands/moderation/unban.ts @@ -0,0 +1,85 @@ +import { BushCommand, BushMessage, BushSlashMessage } from '@lib'; +import { User } from 'discord.js'; + +export default class UnbanCommand extends BushCommand { + public constructor() { + super('unban', { + aliases: ['unban'], + category: 'moderation', + description: { + content: 'Unban a member from the server.', + usage: 'unban <member> <reason> [--delete ]', + examples: ['unban 322862723090219008 I changed my mind, commands are allowed in #general'] + }, + args: [ + { + id: 'user', + type: 'user', + prompt: { + start: 'What user would you like to unban?', + retry: '{error} Choose a valid user to unban.' + } + }, + { + id: 'reason', + type: 'string', + match: 'restContent', + prompt: { + start: 'Why should this user be unbanned?', + retry: '{error} Choose a valid unban reason.', + optional: true + } + } + ], + slash: true, + slashOptions: [ + { + name: 'user', + description: 'What user would you like to unban?', + type: 'USER', + required: true + }, + { + name: 'reason', + description: 'Why should this user be unbanned?', + type: 'STRING', + required: false + } + ], + channel: 'guild', + clientPermissions: ['BAN_MEMBERS'], + userPermissions: ['BAN_MEMBERS'] + }); + } + async exec(message: BushMessage | BushSlashMessage, { user, reason }: { user: User; reason?: string }): Promise<unknown> { + if (!(user instanceof User)) { + user = this.client.util.resolveUser(user, this.client.users.cache); + } + const response = await message.guild.unban({ + user, + moderator: message.author, + reason + }); + + switch (response) { + case 'missing permissions': + return message.util.reply( + `${this.client.util.emojis.error} Could not unban **${user.tag}** because I do not have permissions` + ); + case 'error unbanning': + return message.util.reply(`${this.client.util.emojis.error} An error occurred while trying to unban **${user.tag}**.`); + case 'error removing ban entry': + return message.util.reply( + `${this.client.util.emojis.error} While unbanning **${user.tag}**, there was an error removing their ban entry, please report this to my developers.` + ); + case 'error creating modlog entry': + return message.util.reply( + `${this.client.util.emojis.error} While unbanning **${user.tag}**, there was an error creating a modlog entry, please report this to my developers.` + ); + case 'user not banned': + return message.util.reply(`${this.client.util.emojis.warn} **${user.tag}** but I tried to unban them anyways.`); + case 'success': + return message.util.reply(`${this.client.util.emojis.success} Successfully unbanned **${user.tag}**.`); + } + } +} diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts new file mode 100644 index 0000000..4030fb7 --- /dev/null +++ b/src/commands/moderation/unmute.ts @@ -0,0 +1,109 @@ +import { BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '@lib'; + +export default class UnmuteCommand extends BushCommand { + public constructor() { + super('unmute', { + aliases: ['unmute'], + category: 'moderation', + description: { + content: 'unmute a user.', + usage: 'unmute <member> [reason] [duration]', + examples: ['unmute 322862723090219008 1 day commands in #general'] + }, + args: [ + { + id: 'user', + type: 'user', + prompt: { + start: 'What user would you like to unmute?', + retry: '{error} Choose a valid user to unmute.' + } + }, + { + id: 'reason', + type: 'string', + match: 'rest', + prompt: { + start: 'Why should this user be unmuted?', + retry: '{error} Choose a valid unmute reason.', + optional: true + } + } + ], + slash: true, + slashOptions: [ + { + name: 'user', + description: 'What user would you like to unmute?', + type: 'USER', + required: true + }, + { + name: 'reason', + description: 'Why should this user be unmuted?', + type: 'STRING', + required: false + } + ], + channel: 'guild', + clientPermissions: ['SEND_MESSAGES', 'MANAGE_ROLES'], + userPermissions: ['MANAGE_MESSAGES'] + }); + } + async exec(message: BushMessage | BushSlashMessage, { user, reason }: { user: BushUser; reason?: string }): Promise<unknown> { + const error = this.client.util.emojis.error; + const member = message.guild.members.cache.get(user.id) as BushGuildMember; + const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'unmute'); + const victimBoldTag = `**${member.user.tag}**`; + + if (canModerateResponse !== true) { + return message.util.reply(canModerateResponse); + } + + const response = await member.unmute({ + reason, + moderator: message.author + }); + + switch (response) { + case 'missing permissions': + return message.util.reply( + `${error} Could not unmute ${victimBoldTag} because I am missing the \`Manage Roles\` permission.` + ); + case 'no mute role': + return message.util.reply( + `${error} Could not unmute ${victimBoldTag}, you must set a mute role with \`${message.guild.getSetting( + 'prefix' + )}muterole\`.` + ); + case 'invalid mute role': + return message.util.reply( + `${error} Could not unmute ${victimBoldTag} because the current mute role no longer exists. Please set a new mute role with \`${message.guild.getSetting( + 'prefix' + )}muterole\`.` + ); + case 'mute role not manageable': + return message.util.reply( + `${error} Could not unmute ${victimBoldTag} because I cannot assign the current mute role, either change the role's position or set a new mute role with \`${message.guild.getSetting( + 'prefix' + )}muterole\`.` + ); + case 'error removing mute role': + return message.util.reply(`${error} Could not unmute ${victimBoldTag}, there was an error removing their mute role.`); + case 'error creating modlog entry': + return message.util.reply( + `${error} While muting ${victimBoldTag}, there was an error creating a modlog entry, please report this to my developers.` + ); + case 'error removing mute entry': + return message.util.reply( + `${error} While muting ${victimBoldTag}, there was an error removing their mute entry, please report this to my developers.` + ); + case 'failed to dm': + return message.util.reply( + `${this.client.util.emojis.warn} unmuted **${member.user.tag}** however I could not send them a dm.` + ); + case 'success': + return message.util.reply(`${this.client.util.emojis.success} Successfully unmuted **${member.user.tag}**.`); + } + } +} diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 5d679ab..3d353ca 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -33,15 +33,15 @@ export default class WarnCommand extends BushCommand { slash: true, slashOptions: [ { - type: 'USER', name: 'user', description: 'What user would you like to warn?', + type: 'USER', required: true }, { - type: 'STRING', name: 'reason', description: 'Why should this user be warned?', + type: 'STRING', required: false } ], @@ -58,7 +58,7 @@ export default class WarnCommand extends BushCommand { const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'warn'); const victimBoldTag = `**${member.user.tag}**`; - if (typeof canModerateResponse !== 'boolean') { + if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); } diff --git a/src/commands/moulberry-bush/level.ts b/src/commands/moulberry-bush/level.ts index 7f3509f..fc1e93e 100644 --- a/src/commands/moulberry-bush/level.ts +++ b/src/commands/moulberry-bush/level.ts @@ -30,9 +30,9 @@ export default class LevelCommand extends BushCommand { ], slashOptions: [ { - type: 'USER', name: 'user', description: 'The user to get the level of', + type: 'USER', required: false } ], diff --git a/src/commands/moderation/_unban.ts b/src/commands/utilities/whoHasRole.ts index e69de29..e69de29 100644 --- a/src/commands/moderation/_unban.ts +++ b/src/commands/utilities/whoHasRole.ts diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 6c6d49a..a981d30 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -643,21 +643,7 @@ export class BushClientUtil extends ClientUtil { guild: BushGuildResolvable; modlog: string; }): Promise<Mute | Ban | PunishmentRole> { - let dbModel: typeof Mute | typeof Ban | typeof PunishmentRole; - switch (options.type) { - case 'mute': - dbModel = Mute; - break; - case 'ban': - dbModel = Ban; - break; - case 'role': - dbModel = PunishmentRole; - break; - default: - throw 'choose a valid punishment entry type'; - } - + const dbModel = this.findPunishmentModel(options.type); const expires = options.duration ? new Date(new Date().getTime() + options.duration) : null; const user = this.client.users.resolveId(options.user); const guild = this.client.guilds.resolveId(options.guild); @@ -669,6 +655,53 @@ export class BushClientUtil extends ClientUtil { }); } + public async removePunishmentEntry(options: { + type: 'mute' | 'ban' | 'role'; + user: BushGuildMemberResolvable; + guild: BushGuildResolvable; + }): Promise<boolean> { + const dbModel = this.findPunishmentModel(options.type); + const user = this.client.users.resolveId(options.user); + const guild = this.client.guilds.resolveId(options.guild); + + let success = true; + + const entries = await dbModel + .findAll({ + // finding all cases of a certain type incase there were duplicates or something + where: { + user, + guild + } + }) + .catch((e) => { + this.client.console.error('removePunishmentEntry', e?.stack || e); + success = false; + }); + if (entries) { + entries.forEach(async (entry) => { + await entry.destroy().catch((e) => { + this.client.console.error('removePunishmentEntry', e?.stack || e); + }); + success = false; + }); + } + return success; + } + + private findPunishmentModel(type: 'mute' | 'ban' | 'role'): typeof Mute | typeof Ban | typeof PunishmentRole { + switch (type) { + case 'mute': + return Mute; + case 'ban': + return Ban; + case 'role': + return PunishmentRole; + default: + throw 'choose a valid punishment entry type'; + } + } + public humanizeDuration(duration: number): string { return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2 }); } diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index ea34aec..6eca44d 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -1,6 +1,7 @@ -import { Guild } from 'discord.js'; +import { Guild, User } from 'discord.js'; +import { ModLogType } from '../..'; import { Guild as GuildDB, GuildModel } from '../../models/Guild'; -import { BushClient } from '../discord-akairo/BushClient'; +import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; export class BushGuild extends Guild { public declare readonly client: BushClient; @@ -19,4 +20,63 @@ export class BushGuild extends Guild { row[setting] = value; return await row.save(); } + + public async unban(options: { + user: BushUserResolvable | User; + reason?: string; + moderator?: BushUserResolvable; + }): Promise< + | 'success' + | 'missing permissions' + | 'user not banned' + | 'error unbanning' + | 'error creating modlog entry' + | 'error removing ban entry' + > { + const user = this.client.users.resolveId(options.user); + const moderator = this.client.users.cache.get(this.client.users.resolveId(options.moderator)); + + const bans = await this.bans.fetch(); + + let notBanned = false; + if (!bans.has(user)) notBanned = true; + + const unbanSuccess = this.bans.remove(user, `${moderator.tag} | ${options.reason || 'No reason provided.'}`).catch((e) => { + if (e?.code === 'UNKNOWN_BAN') { + notBanned = true; + return true; + } else return false; + }); + + if (!unbanSuccess) return 'error unbanning'; + + // add modlog entry + const modlog = await this.client.util + .createModLogEntry({ + type: ModLogType.UNBAN, + user, + moderator: moderator.id, + reason: options.reason, + guild: this + }) + .catch(() => null); + if (!modlog) return 'error creating modlog entry'; + + // remove punishment entry + const removePunishmentEntrySuccess = await this.client.util + .removePunishmentEntry({ + type: 'ban', + user, + guild: this + }) + .catch(() => null); + if (!removePunishmentEntrySuccess) return 'error removing ban entry'; + + const userObject = this.client.users.cache.get(user); + + userObject?.send(`You have been unbanned from **${this}** for **${options.reason || 'No reason provided'}**.`); + + if (notBanned) return 'user not banned'; + return 'success'; + } } diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 9e9266e..adcae69 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -7,7 +7,7 @@ import { BushUser } from './BushUser'; interface BushPunishmentOptions { reason?: string; - moderator: BushUserResolvable; + moderator?: BushUserResolvable; } interface BushTimedPunishmentOptions extends BushPunishmentOptions { @@ -24,6 +24,8 @@ type WarnResponse = PunishmentResponse; type PunishmentRoleResponse = PunishmentResponse; +type RemovePunishmentRoleResponse = PunishmentResponse; + type MuteResponse = | PunishmentResponse | 'missing permissions' @@ -33,16 +35,22 @@ type MuteResponse = | 'error giving mute role' | 'error creating mute entry'; -type UnmuteResponse = PunishmentResponse; +type UnmuteResponse = + | PunishmentResponse + | 'missing permissions' + | 'no mute role' + | 'invalid mute role' + | 'mute role not manageable' + | 'error removing mute role' + | 'error removing mute entry'; type KickResponse = PunishmentResponse | 'missing permissions' | 'error kicking'; interface BushBanOptions extends BushTimedPunishmentOptions { deleteDays?: number; - duration?: number; } -type BanResponse = PunishmentResponse; +type BanResponse = PunishmentResponse | 'missing permissions' | 'error creating ban entry' | 'error banning'; export class BushGuildMember extends GuildMember { public declare readonly client: BushClient; @@ -53,7 +61,7 @@ export class BushGuildMember extends GuildMember { } public async warn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number }> { - //add modlog entry + // add modlog entry const { log, caseNum } = await this.client.util .createModLogEntry( { @@ -68,7 +76,7 @@ export class BushGuildMember extends GuildMember { .catch(() => null); if (!log) return { result: 'error creating modlog entry', caseNum: null }; - //dm user + // dm user const ending = this.guild.getSetting('punishmentEnding'); const dmSuccess = await this.send({ content: `You have been warned in **${this.guild}** for **${options.reason || 'No reason provided'}**.${ @@ -85,8 +93,12 @@ export class BushGuildMember extends GuildMember { throw 'not implemented'; } + public removePunishRole(options: BushPunishmentRoleOptions): Promise<RemovePunishmentRoleResponse> { + throw 'not implemented'; + } + public async mute(options: BushTimedPunishmentOptions): Promise<MuteResponse> { - //checks + // checks if (!this.guild.me.permissions.has('MANAGE_ROLES')) return 'missing permissions'; const muteRoleID = await this.guild.getSetting('muteRole'); if (!muteRoleID) return 'no mute role'; @@ -94,20 +106,20 @@ export class BushGuildMember extends GuildMember { if (!muteRole) return 'invalid mute role'; if (muteRole.position >= this.guild.me.roles.highest.position || muteRole.managed) return 'mute role not manageable'; - const moderator = this.client.users.cache.get(this.client.users.resolveId(options.moderator)); + const moderator = this.client.users.cache.get(this.client.users.resolveId(options.moderator || this.client.user)); - //add role + // add role const muteSuccess = await this.roles .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason || 'No reason provided.'}`) .catch(() => null); if (!muteSuccess) return 'error giving mute role'; - //add modlog entry + // add modlog entry const modlog = await this.client.util .createModLogEntry({ type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, user: this, - moderator: options.moderator, + moderator: moderator.id, reason: options.reason, duration: options.duration, guild: this.guild @@ -116,7 +128,7 @@ export class BushGuildMember extends GuildMember { if (!modlog) return 'error creating modlog entry'; // add punishment entry so they can be unmuted later - const mute = await this.client.util + const punishmentEntrySuccess = await this.client.util .createPunishmentEntry({ type: 'mute', user: this, @@ -125,9 +137,9 @@ export class BushGuildMember extends GuildMember { modlog: modlog.id }) .catch(() => null); - if (!mute) return 'error creating mute entry'; + if (!punishmentEntrySuccess) return 'error creating mute entry'; - //dm user + // dm user const ending = this.guild.getSetting('punishmentEnding'); const dmSuccess = await this.send({ content: `You have been muted ${ @@ -141,14 +153,61 @@ export class BushGuildMember extends GuildMember { } public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> { - throw 'not implemented'; + //checks + if (!this.guild.me.permissions.has('MANAGE_ROLES')) return 'missing permissions'; + const muteRoleID = await this.guild.getSetting('muteRole'); + if (!muteRoleID) return 'no mute role'; + const muteRole = this.guild.roles.cache.get(muteRoleID); + if (!muteRole) return 'invalid mute role'; + if (muteRole.position >= this.guild.me.roles.highest.position || muteRole.managed) return 'mute role not manageable'; + + const moderator = this.client.users.cache.get(this.client.users.resolveId(options.moderator || this.client.user)); + + //remove role + const muteSuccess = await this.roles + .remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason || 'No reason provided.'}`) + .catch(() => null); + if (!muteSuccess) return 'error removing mute role'; + + //remove modlog entry + const modlog = await this.client.util + .createModLogEntry({ + type: ModLogType.UNMUTE, + user: this, + moderator: moderator.id, + reason: options.reason, + guild: this.guild + }) + .catch(() => null); + if (!modlog) return 'error creating modlog entry'; + + // remove mute entry + const removePunishmentEntrySuccess = await this.client.util + .removePunishmentEntry({ + type: 'mute', + user: this, + guild: this.guild + }) + .catch(() => null); + if (!removePunishmentEntrySuccess) return 'error removing mute entry'; + + //dm user + const dmSuccess = await this.send({ + content: `You have been unmuted in **${this.guild}** because **${options.reason || 'No reason provided'}**.` + }).catch(() => null); + + if (!dmSuccess) return 'failed to dm'; + + return 'success'; } public async bushKick(options: BushPunishmentOptions): Promise<KickResponse> { - //checks + // checks if (!this.guild.me.permissions.has('KICK_MEMBERS') || !this.kickable) return 'missing permissions'; - //dm user + const moderator = this.client.users.cache.get(this.client.users.resolveId(options.moderator || this.client.user)); + + // dm user const ending = this.guild.getSetting('punishmentEnding'); const dmSuccess = await this.send({ content: `You have been kicked from **${this.guild}** for **${options.reason || 'No reason provided'}**.${ @@ -156,16 +215,16 @@ export class BushGuildMember extends GuildMember { }` }).catch(() => null); - //Kick - const kickSuccess = await this.kick().catch(() => null); + // kick + const kickSuccess = await this.kick(`${moderator.tag} | ${options.reason || 'No reason provided.'}`).catch(() => null); if (!kickSuccess) return 'error kicking'; - //add modlog entry + // add modlog entry const modlog = await this.client.util .createModLogEntry({ type: ModLogType.KICK, user: this, - moderator: options.moderator, + moderator: moderator.id, reason: options.reason, guild: this.guild }) @@ -176,7 +235,53 @@ export class BushGuildMember extends GuildMember { } public async bushBan(options?: BushBanOptions): Promise<BanResponse> { - throw 'not implemented'; + // checks + if (!this.guild.me.permissions.has('BAN_MEMBERS') || !this.bannable) return 'missing permissions'; + + const moderator = this.client.users.cache.get(this.client.users.resolveId(options.moderator || this.client.user)); + + // dm user + const ending = this.guild.getSetting('punishmentEnding'); + const dmSuccess = await this.send({ + content: `You have been banned ${ + options.duration ? 'for ' + this.client.util.humanizeDuration(options.duration) : 'permanently' + } from **${this.guild}** for **${options.reason || 'No reason provided'}**.${ending ? `\n\n${ending}` : ''}` + }).catch(() => null); + + // ban + const banSuccess = await this.ban({ + reason: `${moderator.tag} | ${options.reason || 'No reason provided.'}`, + days: options.deleteDays + }); + if (!banSuccess) return 'error banning'; + + // add modlog entry + const modlog = await this.client.util + .createModLogEntry({ + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: this, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this.guild + }) + .catch(() => null); + if (!modlog) return 'error creating modlog entry'; + + // add punishment entry so they can be unbanned later + const punishmentEntrySuccess = await this.client.util + .createPunishmentEntry({ + type: 'ban', + user: this, + guild: this.guild, + duration: options.duration, + modlog: modlog.id + }) + .catch(() => null); + if (!punishmentEntrySuccess) return 'error creating ban entry'; + + if (!dmSuccess) return 'failed to dm'; + return 'success'; } public isOwner(): boolean { |