diff options
21 files changed, 417 insertions, 471 deletions
@@ -1,140 +1,19 @@ - -# Created by https://www.toptal.com/developers/gitignore/api/node,yarn,vscode,webstorm -# Edit at https://www.toptal.com/developers/gitignore?templates=node,yarn,vscode,webstorm - -### Node ### # Logs -logs *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release # Dependency directories node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test -.env*.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt dist -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - ### vscode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +!.vscode/*.code-snippets *.code-workspace -### WebStorm ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - # Generated files .idea/**/contentModel.xml @@ -147,90 +26,13 @@ dist .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### WebStorm Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -# https://plugins.jetbrains.com/plugin/7973-sonarlint -.idea/**/sonarlint/ - -# SonarQube Plugin -# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator-enh.xml -.idea/**/markdown-navigator/ - # Cache file creation bug -# See https://youtrack.jetbrains.com/issue/JBR-2257 -.idea/$CACHE_FILE$ .yarn/cache/* .yarn/sdks/eslint/* .yarn/sdks/prettier/* .yarn/install-state.gz .yarn/build-state.url .yarn/releases.gz -# CodeStream plugin -# https://plugins.jetbrains.com/plugin/12206-codestream -.idea/codestream.xml ### yarn ### # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored @@ -240,16 +42,8 @@ fabric.properties !.yarn/plugins !.yarn/sdks !.yarn/versions - -# if you are NOT using Zero-installs, then: -# comment the following lines !.yarn/cache -# and uncomment the following lines -# .pnp.* - -# End of https://www.toptal.com/developers/gitignore/api/node,yarn,vscode,webstorm - # Options and credentials for the bot src/config/options.ts diff --git a/.vscode/typescript.code-snippets b/.vscode/typescript.code-snippets new file mode 100644 index 0000000..0984c3c --- /dev/null +++ b/.vscode/typescript.code-snippets @@ -0,0 +1,73 @@ +/* prettier-ignore */ +{ + /** + * Place your snippets for typescript here. Each snippet is defined under a snippet name and has a prefix, body and + * description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + * $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the + * same ids are connected. + */ + + "Setup Slash Command": { + "prefix": "slash", + "body": [ + "slash: true,", + "slashOptions: [", + "\t$0", + "]" + ] + }, + + "Slash Command User Argument": { + "prefix": "user", + "body": [ + "{", + "\tname: 'user',", + "\tdescription: 'The user you would like to$1',", + "\ttype: 'USER',", + "\trequired: $2", + "},$0" + ] + }, + + "Slash Command String Argument": { + "prefix": "string", + "body": [ + "{", + "\tname: '$1',", + "\tdescription: '$2',", + "\ttype: 'STRING',", + "\trequired: $3", + "},$0" + ] + }, + + "Slash Command Choice Argument": { + "prefix": "choice", + "body": [ + "{", + "\tname: '$1',", + "\tdescription: '$2',", + "\ttype: 'STRING',", + "\tchoices: [", + "\t\t{", + "\t\t\tname: '$3',", + "\t\t\tvalue: '$4'", + "\t\t},", + "\t],", + "\trequired: $5", + "},$0" + ] + }, + + "Slash Boolean Argument": { + "prefix": "boolean", + "body": [ + "{", + "\tname: '$1',", + "\tdescription: '$2',", + "\ttype: 'BOOLEAN',", + "\trequired: $3", + "},$0" + ] + } +} diff --git a/src/commands/dev/superUser.ts b/src/commands/dev/superUser.ts index 981c0da..4d5ce2d 100644 --- a/src/commands/dev/superUser.ts +++ b/src/commands/dev/superUser.ts @@ -1,6 +1,6 @@ import { Constants } from 'discord-akairo'; import { User } from 'discord.js'; -import { BushCommand, BushMessage, Global } from '../../lib'; +import { BushCommand, BushMessage, BushSlashMessage, Global } from '../../lib'; export default class SuperUserCommand extends BushCommand { public constructor() { @@ -39,7 +39,7 @@ export default class SuperUserCommand extends BushCommand { }; return { action, user }; } - public async exec(message: BushMessage, args: { action: 'add' | 'remove'; user: User }): Promise<unknown> { + public async exec(message: BushMessage | BushSlashMessage, args: { action: 'add' | 'remove'; user: User }): Promise<unknown> { if (!message.author.isOwner()) return await message.util.reply(`${this.client.util.emojis.error} Only my developers can run this command.`); diff --git a/src/commands/info/botInfo.ts b/src/commands/info/botInfo.ts index 4a94318..80ca29d 100644 --- a/src/commands/info/botInfo.ts +++ b/src/commands/info/botInfo.ts @@ -1,5 +1,5 @@ -import { Message, MessageEmbed } from 'discord.js'; -import { BushCommand } from '../../lib'; +import { MessageEmbed } from 'discord.js'; +import { BushCommand, BushMessage, BushSlashMessage } from '../../lib'; export default class BotInfoCommand extends BushCommand { public constructor() { @@ -17,7 +17,7 @@ export default class BotInfoCommand extends BushCommand { }); } - public async exec(message: Message): Promise<void> { + 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 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', ''); diff --git a/src/commands/info/ping.ts b/src/commands/info/ping.ts index c1be3fb..804ede2 100644 --- a/src/commands/info/ping.ts +++ b/src/commands/info/ping.ts @@ -1,5 +1,5 @@ import { Message, MessageEmbed } from 'discord.js'; -import { BushCommand, BushSlashMessage } from '../../lib'; +import { BushCommand, BushMessage, BushSlashMessage } from '../../lib'; export default class PingCommand extends BushCommand { public constructor() { @@ -17,7 +17,7 @@ export default class PingCommand extends BushCommand { }); } - public async exec(message: Message): Promise<void> { + 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 \`\`\``; diff --git a/src/commands/moderation/block.ts b/src/commands/moderation/_block.ts index e69de29..e69de29 100644 --- a/src/commands/moderation/block.ts +++ b/src/commands/moderation/_block.ts diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/_unban.ts index e69de29..e69de29 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/_unban.ts diff --git a/src/commands/moderation/unblock.ts b/src/commands/moderation/_unblock.ts index e69de29..e69de29 100644 --- a/src/commands/moderation/unblock.ts +++ b/src/commands/moderation/_unblock.ts diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/_unmute.ts index e69de29..e69de29 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/_unmute.ts diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 0c68497..244014b 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,5 +1,5 @@ -import { Message, User } from 'discord.js'; -import { BushCommand } from '../../lib'; +import { User } from 'discord.js'; +import { BushCommand, BushMessage, BushSlashMessage } from '../../lib'; export default class BanCommand extends BushCommand { public constructor() { @@ -140,7 +140,7 @@ export default class BanCommand extends BushCommand { // } // } async exec( - message: Message, + message: BushMessage | BushSlashMessage, { user, reason, time }: { user: User; reason?: string; time?: number | string } ): Promise<unknown> { return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 8375198..919c14b 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,15 +1,19 @@ -import { GuildMember, Message } from 'discord.js'; -import { BushCommand } from '../../lib'; +import { BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '../../lib'; export default class KickCommand extends BushCommand { public constructor() { super('kick', { aliases: ['kick'], category: 'moderation', + description: { + content: 'Kick a user.', + usage: 'kick <member> <reason>', + examples: ['kick @user bad'] + }, args: [ { id: 'user', - type: 'member', + type: 'user', prompt: { start: 'What user would you like to kick?', retry: '{error} Choose a valid user to kick.' @@ -20,19 +24,13 @@ export default class KickCommand extends BushCommand { type: 'string', match: 'restContent', prompt: { - start: 'Why would you like to kick this user?', - retry: '{error} Choose a valid user to kick.', + start: 'Why should this user be kicked?', + retry: '{error} Choose a valid kick reason.', optional: true } } ], - clientPermissions: ['KICK_MEMBERS'], - userPermissions: ['KICK_MEMBERS'], - description: { - content: 'Kick a member from the server.', - usage: 'kick <member> <reason>', - examples: ['kick @user bad'] - }, + slash: true, slashOptions: [ { type: 'USER', @@ -43,67 +41,30 @@ export default class KickCommand extends BushCommand { { type: 'STRING', name: 'reason', - description: 'Why would you like to kick this user?', + description: 'Why should this user be kicked?', required: false } ], - slash: true + clientPermissions: ['SEND_MESSAGES', 'KICK_MEMBERS'], + userPermissions: ['KICK_MEMBERS'] }); } - // private async *genResponses( - // message: Message | CommandInteraction, - // user: GuildMember, - // reason?: string - // ): AsyncIterable<string> { - // let modlogEnry: ModLog; - // // 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 { - // modlogEnry = ModLog.build({ - // user: user.id, - // guild: message.guild.id, - // moderator: message instanceof Message ? message.author.id : message.user.id, - // type: ModLogType.KICK, - // reason - // }); - // await modlogEnry.save(); - // } catch (e) { - // this.client.console.error(`KickCommand`, `Error saving to database. ${e?.stack || e}`); - // yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; - // return; - // } - // try { - // await user.send(`You were kicked in ${message.guild.name} with reason \`${reason || 'No reason given'}\``); - // } catch { - // yield `${this.client.util.emojis.warn} Unable to dm user`; - // } - // try { - // await user.kick( - // `Kicked by ${message instanceof Message ? message.author.tag : message.user.tag} with ${ - // reason ? `reason ${reason}` : 'no reason' - // }` - // ); - // } catch { - // yield `${this.client.util.emojis.error} Error kicking :/`; - // await modlogEnry.destroy(); - // return; - // } - // yield `${this.client.util.emojis.success} Kicked <@!${user.id}> with reason \`${reason || 'No reason given'}\``; - // } + async exec(message: BushMessage | BushSlashMessage, { user, reason }: { user: BushUser; reason?: string }): Promise<unknown> { + const member = message.guild.members.cache.get(user.id) as BushGuildMember; + const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'kick'); + // const victimBoldTag = `**${member.user.tag}**`; + + if (typeof canModerateResponse !== 'boolean') { + return message.util.reply(canModerateResponse); + } - async exec(message: Message, { user, reason }: { user: GuildMember; reason?: string }): Promise<unknown> { - return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); - // for await (const response of this.genResponses(message, user, reason)) { - // await message.util.send(response); - // } + const response = await member.bushKick({ + reason, + moderator: message.author + }); + + } } diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts index 5be50a4..36f72fc 100644 --- a/src/commands/moderation/modlog.ts +++ b/src/commands/moderation/modlog.ts @@ -1,7 +1,7 @@ import { Argument } from 'discord-akairo'; import { MessageEmbed } from 'discord.js'; import moment from 'moment'; -import { BushCommand, BushMessage, ModLog } from '../../lib'; +import { BushCommand, BushMessage, BushSlashMessage, ModLog } from '../../lib'; export default class ModlogCommand extends BushCommand { public constructor() { @@ -48,7 +48,7 @@ export default class ModlogCommand extends BushCommand { return modLog.join(`\n`); } - async exec(message: BushMessage, { search }: { search: string }): Promise<unknown> { + async exec(message: BushMessage | BushSlashMessage, { search }: { search: string }): Promise<unknown> { const foundUser = await this.client.util.resolveUserAsync(search); if (foundUser) { const logs = await ModLog.findAll({ diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 33c0e32..bc3abf2 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,5 +1,5 @@ import { Argument } from 'discord-akairo'; -import { BushCommand, BushGuildMember, BushMessage, BushUser } from '../../lib'; +import { BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '../../lib'; export default class MuteCommand extends BushCommand { public constructor() { @@ -8,8 +8,8 @@ export default class MuteCommand extends BushCommand { category: 'moderation', description: { content: 'Mute a user.', - usage: 'mute <member> <reason> [--time]', - examples: ['mute @user bad boi --time 1h'] + usage: 'mute <member> [reason] [duration]', + examples: ['mute 322862723090219008 1 day commands in #general'] }, args: [ { @@ -31,8 +31,7 @@ export default class MuteCommand extends BushCommand { } } ], - clientPermissions: ['MANAGE_ROLES'], - userPermissions: ['MANAGE_MESSAGES'], + slash: true, slashOptions: [ { type: 'USER', @@ -47,37 +46,29 @@ export default class MuteCommand extends BushCommand { required: false } ], - slash: true + channel: 'guild', + clientPermissions: ['SEND_MESSAGES', 'MANAGE_ROLES'], + userPermissions: ['MANAGE_MESSAGES'] }); } async exec( - message: BushMessage, + message: BushMessage | BushSlashMessage, { user, reason }: { user: BushUser; reason?: { duration: number; contentWithoutTime: 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); + const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'mute'); const victimBoldTag = `**${member.user.tag}**`; - switch (canModerateResponse) { - case 'moderator': - return message.util.reply(`${error} You cannot mute ${victimBoldTag} because they are a moderator.`); - case 'user hierarchy': - return message.util.reply( - `${error} You cannot mute ${victimBoldTag} because they have higher or equal role hierarchy as you do.` - ); - case 'client hierarchy': - return message.util.reply( - `${error} You cannot mute ${victimBoldTag} because they have higher or equal role hierarchy as I do.` - ); - case 'self': - return message.util.reply(`${error} You cannot mute yourself.`); + + if (typeof canModerateResponse !== 'boolean') { + return message.util.reply(canModerateResponse); } let time: number; if (reason) { time = typeof reason === 'string' - ? await Argument.cast('duration', this.client.commandHandler.resolver, message, reason) + ? await Argument.cast('duration', this.client.commandHandler.resolver, message as BushMessage, reason) : reason.duration; } const parsedReason = reason.contentWithoutTime; diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 83e85e0..6bac9e8 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -1,28 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { GuildMember, Message, Role } from 'discord.js'; -import { AllowedMentions, BushCommand } from '../../lib'; +import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushRole, BushSlashMessage } from '../../lib'; export default class RoleCommand extends BushCommand { - private roleWhitelist: Record<string, string[]> = { - 'Partner': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Suggester': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper', 'Trial Helper', 'Contributor'], - 'Level Locked': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Files': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Reactions': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Links': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Bots': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No VC': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'No Giveaways': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper'], - 'No Support': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway Donor': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (200m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (100m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (50m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (25m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (10m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (5m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'], - 'Giveaway (1m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'] - }; public constructor() { super('role', { aliases: ['role', 'addrole', 'removerole'], @@ -32,59 +11,103 @@ export default class RoleCommand extends BushCommand { usage: 'role <add|remove> <user> <role>', examples: ['role add tyman adminperms'] }, - clientPermissions: ['MANAGE_ROLES', 'EMBED_LINKS', 'SEND_MESSAGES'], - channel: 'guild', - typing: true, - args: [ + slash: true, + slashOptions: [ { - id: 'user', - type: 'member', - prompt: { - start: `What user do you want to add/remove the role on?`, - retry: `{error} Choose a valid user to add/remove the role on.` - } + name: 'action', + description: 'Would you like to add or remove a role?', + type: 'STRING', + choices: [ + { + name: 'add', + value: 'add' + }, + { + name: 'remove', + value: 'remove' + } + ], + required: true }, { - id: 'role', - type: 'role', - match: 'restContent', - prompt: { - start: `What role do you want to add/remove?`, - retry: `{error} Choose a valid role to add/remove.` - } - } - ], - slashOptions: [ - { - type: 'USER', name: 'user', - description: 'The user to add/remove the role on', + description: 'The user you would like to add/remove the role from.', + type: 'USER', required: true }, { - type: 'ROLE', name: 'role', - description: 'The role to add/remove', + description: 'The role you would like to add/remove from the user.', + type: 'ROLE', required: true } - ] + ], + channel: 'guild', + typing: true, + clientPermissions: ['MANAGE_ROLES', 'EMBED_LINKS', 'SEND_MESSAGES'], + userPermissions: ['SEND_MESSAGES'] }); } - public async exec(message: Message, { user, role }: { user: GuildMember; role: Role }): Promise<unknown> { + *args(): unknown { + const action: 'add' | 'remove' = yield { + id: 'action', + type: [['add'], ['remove']], + prompt: { + start: 'Would you like to `add` or `remove` a role?', + retry: '{error} Choose whether you would you like to `add` or `remove` a role.' + } + }; + let action2: 'to' | 'from'; + if (action === 'add') action2 = 'to'; + else if (action === 'remove') action2 = 'from'; + else return; + const user = yield { + id: 'user', + type: 'member', + prompt: { + 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', + type: 'role', + match: 'restContent', + prompt: { + start: `What role do you want to ${action}?`, + retry: `{error} Choose a valid role to ${action}.` + } + }; + return { action, user, role }; + } + + public async exec( + 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)) { - const mappedRole = this.client.consts.moulberryBushRoleMap.find((m) => m.id === role.id); - if (!mappedRole || !this.roleWhitelist[mappedRole.name]) { - return message.util.reply({ + const mappings = this.client.consts.mappings; + let mappedRole: { name: string; id: string }; + for (let i = 0; i < mappings.roleMap.length; i++) { + const a = mappings.roleMap[i]; + if (a.id == role.id) mappedRole = a; + } + if (!mappedRole || !mappings.roleWhitelist[mappedRole.name]) { + return await message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is not whitelisted, and you do not have manage roles permission.`, allowedMentions: AllowedMentions.none() }); } - const allowedRoles = this.roleWhitelist[mappedRole.name].map((r) => { - return this.client.consts.moulberryBushRoleMap.find((m) => m.name === r).id; + const allowedRoles = mappings.roleWhitelist[mappedRole.name].map((r) => { + for (let i = 0; i < mappings.roleMap.length; i++) { + if (mappings.roleMap[i].name == r) return mappings.roleMap[i].id; + } + return; }); if (!message.member.roles.cache.some((role) => allowedRoles.includes(role.id))) { - return message.util.reply({ + return await message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is whitelisted, but you do not have any of the roles required to manage it.`, allowedMentions: AllowedMentions.none() }); @@ -92,51 +115,51 @@ export default class RoleCommand extends BushCommand { } if (!this.client.ownerID.includes(message.author.id)) { if (role.comparePositionTo(message.member.roles.highest) >= 0) { - return message.util.reply({ + return await message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is higher or equal to your highest role.`, allowedMentions: AllowedMentions.none() }); } if (role.comparePositionTo(message.guild.me.roles.highest) >= 0) { - return message.util.reply({ + return await message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is higher or equal to my highest role.`, allowedMentions: AllowedMentions.none() }); } if (role.managed) { - await message.util.reply({ + await await message.util.reply({ content: `${this.client.util.emojis.error} <@&${role.id}> is managed by an integration and cannot be managed.`, allowedMentions: AllowedMentions.none() }); } } - // No checks if the user has MANAGE_ROLES - if (user.roles.cache.has(role.id)) { - try { - await user.roles.remove(role.id); - } catch { - return message.util.reply({ + // no checks if the user has MANAGE_ROLES + if (action == 'remove') { + 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}>!`, + allowedMentions: AllowedMentions.none() + }); + } else { + return await message.util.reply({ content: `${this.client.util.emojis.error} Could not remove <@&${role.id}> from <@${user.id}>.`, allowedMentions: AllowedMentions.none() }); } - return message.util.reply({ - content: `${this.client.util.emojis.success} Successfully removed <@&${role.id}> from <@${user.id}>!`, - allowedMentions: AllowedMentions.none() - }); - } else { - try { - await user.roles.add(role.id); - } catch { - return message.util.reply({ + } else if (action == 'add') { + const success = await user.roles.add(role.id).catch(() => {}); + if (success) { + return await message.util.reply({ + content: `${this.client.util.emojis.success} Successfully added <@&${role.id}> to <@${user.id}>!`, + allowedMentions: AllowedMentions.none() + }); + } else { + return await message.util.reply({ content: `${this.client.util.emojis.error} Could not add <@&${role.id}> to <@${user.id}>.`, allowedMentions: AllowedMentions.none() }); } - return message.util.reply({ - content: `${this.client.util.emojis.success} Successfully added <@&${role.id}> to <@${user.id}>!`, - allowedMentions: AllowedMentions.none() - }); } } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index f2b9142..d1c17d4 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -1,61 +1,89 @@ -import { GuildMember, Message } from 'discord.js'; -import { BushCommand, Guild, ModLog, ModLogType } from '../../lib'; +import { BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '../../lib'; export default class WarnCommand extends BushCommand { public constructor() { super('warn', { aliases: ['warn'], category: 'moderation', - userPermissions: ['MANAGE_MESSAGES'], + description: { + content: 'Warn a user.', + usage: 'warn <member> [reason]', + examples: ['warn @Tyman being cool'] + }, args: [ { - id: 'member', - type: 'member' + id: 'user', + type: 'user', + prompt: { + start: 'What user would you like to warn?', + retry: '{error} Choose a valid user to warn.' + } }, { id: 'reason', - type: 'contentWithDuration', - match: 'rest' + type: 'content', + match: 'rest', + prompt: { + start: 'Why should this user be warned?', + retry: '{error} Choose a valid warn reason.', + optional: true + } } ], - description: { - content: 'Warn a member and log it in modlogs', - usage: 'warn <member> <reason>', - examples: ['warn @Tyman being cool'] - } + slash: true, + slashOptions: [ + { + type: 'USER', + name: 'user', + description: 'What user would you like to warn?', + required: true + }, + { + type: 'STRING', + name: 'reason', + description: 'Why should this user be warned?', + required: false + } + ], + channel: 'guild', + clientPermissions: ['SEND_MESSAGES'], + userPermissions: ['MANAGE_MESSAGES'] }); } - public async exec(message: Message, { member, reason }: { member: GuildMember; reason: string }): Promise<unknown> { - return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); + public async exec( + message: BushMessage | BushSlashMessage, + { user, reason }: { user: BushUser; reason: string } + ): Promise<unknown> { + const member = message.guild.members.cache.get(user.id) as BushGuildMember; + const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member, 'warn'); + const victimBoldTag = `**${member.user.tag}**`; - // 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 { - const entry = ModLog.build({ - user: member.id, - guild: message.guild.id, - moderator: message.author.id, - type: ModLogType.WARN, - reason - }); - await entry.save(); - } catch { - await message.util.send('Error saving to database, please contact the developers'); - return; + if (typeof canModerateResponse !== 'boolean') { + return message.util.reply(canModerateResponse); } - try { - await member.send(`You were warned in ${message.guild.name} for reason "${reason}".`); - } catch { - await message.util.send('Error messaging user, warning still saved.'); - return; + + const { result: response, caseNum } = await member.warn({ + reason, + moderator: message.author + }); + + switch (response) { + case 'error creating modlog entry': + return message.util.reply( + `${this.client.util.emojis.error} While warning ${victimBoldTag}, 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} **${member.user.tag}** has been warned for the ${this.client.util.ordinal( + caseNum + )} time, however I could not send them a dm.` + ); + case 'success': + return message.util.reply( + `${this.client.util.emojis.success} Successfully warned **${member.user.tag}** for the ${this.client.util.ordinal( + caseNum + )} time.` + ); } - await message.util.send(`${member.user.tag} was warned for reason "${reason}".`); } } diff --git a/src/commands/moulberry-bush/capePerms.ts b/src/commands/moulberry-bush/capePerms.ts index 539ddf6..a564fc3 100644 --- a/src/commands/moulberry-bush/capePerms.ts +++ b/src/commands/moulberry-bush/capePerms.ts @@ -1,7 +1,7 @@ import { Constants } from 'discord-akairo'; import { MessageEmbed } from 'discord.js'; import got from 'got'; -import { BushCommand, BushMessage } from '../../lib'; +import { BushCommand, BushMessage, BushSlashMessage } from '../../lib'; export default class CapePermissionsCommand extends BushCommand { private nameMap = { @@ -67,7 +67,7 @@ export default class CapePermissionsCommand extends BushCommand { }); } - public async exec(message: BushMessage, args: { ign: string }): Promise<unknown> { + public async exec(message: BushMessage | BushSlashMessage, args: { ign: string }): Promise<unknown> { interface Capeperms { success: boolean; perms: User[]; diff --git a/src/commands/moulberry-bush/level.ts b/src/commands/moulberry-bush/level.ts index 90a4b7f..5dcafe5 100644 --- a/src/commands/moulberry-bush/level.ts +++ b/src/commands/moulberry-bush/level.ts @@ -1,5 +1,4 @@ -import { Message, User } from 'discord.js'; -import { BushCommand, BushSlashMessage, Level } from '../../lib'; +import { BushCommand, BushMessage, BushSlashMessage, BushUser, Level } from '../../lib'; /* import canvas from 'canvas'; import { MessageAttachment } from 'discord.js'; @@ -128,7 +127,7 @@ export default class LevelCommand extends BushCommand { return image.toBuffer(); } */ - private async getResponse(user: User): Promise<string> { + private async getResponse(user: BushUser): Promise<string> { const userLevelRow = await Level.findByPk(user.id); if (userLevelRow) { return `${user ? `${user.tag}'s` : 'Your'} level is ${userLevelRow.level} (${userLevelRow.xp} XP)`; @@ -137,7 +136,7 @@ export default class LevelCommand extends BushCommand { } } - async exec(message: Message | BushSlashMessage, { user }: { user?: User }): Promise<void> { + async exec(message: BushMessage | BushSlashMessage, { user }: { user?: BushUser }): Promise<void> { // await message.reply( // new MessageAttachment( // await this.getImage(user || message.author), diff --git a/src/commands/moulberry-bush/rule.ts b/src/commands/moulberry-bush/rule.ts index 516aadf..f8d312f 100644 --- a/src/commands/moulberry-bush/rule.ts +++ b/src/commands/moulberry-bush/rule.ts @@ -1,6 +1,6 @@ import { Argument, Constants } from 'discord-akairo'; import { MessageEmbed, User } from 'discord.js'; -import { AllowedMentions, BushCommand, BushMessage } from '../../lib'; +import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage } from '../../lib'; const rules = [ { @@ -106,7 +106,10 @@ export default class RuleCommand extends BushCommand { }); } - public async exec(message: BushMessage, { rule, user }: { rule: undefined | number; user: User }): Promise<unknown> { + public async exec( + message: BushMessage | BushSlashMessage, + { rule, user }: { rule: undefined | number; user: User } + ): Promise<unknown> { const rulesEmbed = new MessageEmbed() .setColor('#ef3929') .setFooter(`Triggered by ${message.author.tag}`, message.author.avatarURL({ dynamic: true })) @@ -130,21 +133,19 @@ export default class RuleCommand extends BushCommand { return; async function respond(): Promise<unknown> { if (!user) { - return ( - // If the original message was a reply -> imitate it - message.reference?.messageID && !message.util.isSlash - ? await message.channel.messages.fetch(message.reference.messageID).then(async (message) => { - await message.util.reply({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }); - }) - : await message.util.send({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }) - ); + // If the original message was a reply -> imitate it + (message as BushMessage).reference?.messageID && !message.util.isSlash + ? await message.channel.messages.fetch((message as BushMessage).reference.messageID).then(async (message) => { + await message.util.reply({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }); + }) + : await message.util.send({ embeds: [rulesEmbed], allowedMentions: AllowedMentions.users() }); } else { - return message.reference?.messageID && !message.util.isSlash + return (message as BushMessage).reference?.messageID && !message.util.isSlash ? await message.util.send({ content: `<@!${user.id}>`, embeds: [rulesEmbed], allowedMentions: AllowedMentions.users(), - reply: { messageReference: message.reference.messageID } + reply: { messageReference: (message as BushMessage).reference.messageID } }) : await message.util.send({ content: `<@!${user.id}>`, diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 20ce365..4a38b3e 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -35,6 +35,7 @@ import { BushGuildMemberResolvable, BushGuildResolvable, BushMessage, + BushSlashMessage, Global, Guild, ModLog, @@ -287,7 +288,7 @@ export class BushClientUtil extends ClientUtil { /** Paginates an array of embeds using buttons. */ public async buttonPaginate( - message: BushMessage, + message: BushMessage | BushSlashMessage, embeds: MessageEmbed[], text: string | null = null, deleteOnExit?: boolean @@ -397,7 +398,7 @@ export class BushClientUtil extends ClientUtil { } /** Sends a message with a button for the user to delete it. */ - public async sendWithDeleteButton(message: BushMessage, options: MessageOptions): Promise<void> { + public async sendWithDeleteButton(message: BushMessage | BushSlashMessage, options: MessageOptions): Promise<void> { updateOptions(); const msg = await message.util.reply(options as MessageOptions & { split?: false }); const filter = (interaction: ButtonInteraction) => interaction.customID == 'paginate__stop' && interaction.message == msg; @@ -565,30 +566,45 @@ export class BushClientUtil extends ClientUtil { * Checks if a moderator can perform a moderation action on another user. * @param moderator - The person trying to perform the action. * @param victim - The person getting punished. + * @param type - The type of punishment - used to format the response. * @param checkModerator - Whether or not to check if the victim is a moderator. */ public moderationPermissionCheck( moderator: BushGuildMember, victim: BushGuildMember, + type: 'mute' | 'unmute' | 'warn' | 'kick' | 'ban' | 'unban' | 'add a punishment role to' | 'remove a punishment role from', checkModerator = true - ): true | 'user hierarchy' | 'client hierarchy' | 'moderator' | 'self' { - if (moderator.guild.id !== victim.guild.id) throw 'wtf'; + ): true | string { + if (moderator.guild.id !== victim.guild.id) { + throw 'moderator and victim not in same guild'; + } const isOwner = moderator.guild.ownerID === moderator.id; - if (moderator.id === victim.id) return 'self'; - if (moderator.roles.highest.position <= victim.roles.highest.position && !isOwner) return 'user hierarchy'; - if (victim.roles.highest.position >= victim.guild.me.roles.highest.position) return 'client hierarchy'; - if (checkModerator && victim.permissions.has('MANAGE_MESSAGES')) return 'moderator'; + if (moderator.id === victim.id) { + return `${this.client.util.emojis.error} You cannot ${type} yourself.`; + } + if (moderator.roles.highest.position <= victim.roles.highest.position && !isOwner) { + return `${this.client.util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; + } + if (victim.roles.highest.position >= victim.guild.me.roles.highest.position) { + return `${this.client.util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; + } + if (checkModerator && victim.permissions.has('MANAGE_MESSAGES')) { + return `${this.client.util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; + } return true; } - public async createModLogEntry(options: { - type: ModLogType; - user: BushGuildMemberResolvable; - moderator: BushGuildMemberResolvable; - reason: string; - duration: number; - guild: BushGuildResolvable; - }): Promise<ModLog> { + public async createModLogEntry( + options: { + type: ModLogType; + user: BushGuildMemberResolvable; + moderator: BushGuildMemberResolvable; + reason: string; + duration?: number; + guild: BushGuildResolvable; + }, + getCaseNumber = false + ): Promise<{ log: ModLog; caseNum: number }> { const user = this.client.users.resolveID(options.user); const moderator = this.client.users.resolveID(options.moderator); const guild = this.client.guilds.resolveID(options.guild); @@ -612,10 +628,15 @@ export class BushClientUtil extends ClientUtil { duration: duration, guild }); - return modLogEntry.save().catch((e) => { + const saveResult: ModLog = await modLogEntry.save().catch((e) => { this.client.console.error('createModLogEntry', e?.stack || e); return null; }); + + if (!getCaseNumber) return { log: saveResult, caseNum: null }; + + const caseNum = (await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild } }))?.length; + return { log: saveResult, caseNum }; } public async createPunishmentEntry(options: { diff --git a/src/lib/extensions/discord-akairo/BushSlashMessage.ts b/src/lib/extensions/discord-akairo/BushSlashMessage.ts index cf2f391..63358b0 100644 --- a/src/lib/extensions/discord-akairo/BushSlashMessage.ts +++ b/src/lib/extensions/discord-akairo/BushSlashMessage.ts @@ -1,12 +1,13 @@ import { AkairoMessage } from 'discord-akairo'; import { CommandInteraction } from 'discord.js'; -import { BushClient, BushCommandUtil, BushGuild, BushUser } from '..'; +import { BushClient, BushCommandUtil, BushGuild, BushGuildMember, BushUser } from '..'; export class BushSlashMessage extends AkairoMessage { public declare client: BushClient; public declare util: BushCommandUtil; public declare guild: BushGuild; public declare author: BushUser; + public declare member: BushGuildMember; public constructor( client: BushClient, interaction: CommandInteraction, diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 54b26f0..e7f1ddf 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -15,7 +15,7 @@ interface BushPunishmentRoleOptions extends BushTimedPunishmentOptions { role: RoleResolvable; } -type PunishmentResponse = 'success'; +type PunishmentResponse = 'success' | 'error creating modlog entry' | 'failed to dm'; type WarnResponse = PunishmentResponse; @@ -28,13 +28,11 @@ type MuteResponse = | 'invalid mute role' | 'mute role not manageable' | 'error giving mute role' - | 'error creating modlog entry' - | 'error creating mute entry' - | 'failed to dm'; + | 'error creating mute entry'; type UnmuteResponse = PunishmentResponse; -type KickResponse = PunishmentResponse; +type KickResponse = PunishmentResponse | 'missing permissions' | 'error kicking'; interface BushBanOptions extends BushTimedPunishmentOptions { deleteDays?: number; @@ -51,8 +49,33 @@ export class BushGuildMember extends GuildMember { super(client, data, guild); } - public async warn(options: BushPunishmentOptions): Promise<WarnResponse> { - throw 'not implemented'; + public async warn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number }> { + //add modlog entry + const { log, caseNum } = await this.client.util + .createModLogEntry( + { + type: ModLogType.WARN, + user: this, + moderator: options.moderator, + reason: options.reason, + guild: this.guild + }, + true + ) + .catch(() => null); + if (!log) return { result: 'error creating modlog entry', caseNum: null }; + + //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'}**.${ + ending ? `\n\n${ending}` : '' + }` + }).catch(() => null); + + if (!dmSuccess) return { result: 'failed to dm', caseNum }; + + return { result: 'success', caseNum }; } public punishRole(options: BushPunishmentRoleOptions): Promise<PunishmentRoleResponse> { @@ -68,9 +91,13 @@ 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)); + //add role - const success = await this.roles.add(muteRole).catch(() => null); - if (!success) return 'error giving mute 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 const modlog = await this.client.util @@ -115,7 +142,34 @@ export class BushGuildMember extends GuildMember { } public async bushKick(options: BushPunishmentOptions): Promise<KickResponse> { - throw 'not implemented'; + //checks + if (!this.guild.me.permissions.has('KICK_MEMBERS') || !this.kickable) return 'missing permissions'; + + //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'}**.${ + ending ? `\n\n${ending}` : '' + }` + }).catch(() => null); + + //Kick + const kickSuccess = await this.kick().catch(() => null); + if (!kickSuccess) return 'error kicking'; + + //add modlog entry + const modlog = await this.client.util + .createModLogEntry({ + type: ModLogType.KICK, + user: this, + moderator: options.moderator, + reason: options.reason, + guild: this.guild + }) + .catch(() => null); + if (!modlog) return 'error creating modlog entry'; + if (!dmSuccess) return 'failed to dm'; + return 'success'; } public async bushBan(options?: BushBanOptions): Promise<BanResponse> { |