From 95f76716ed254a741d9ee47d11a93bcf11a66ede Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 24 Sep 2021 15:32:36 +0100 Subject: [PATCH] feat: #206 convert to interactions (buttons and application commands) (#238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: make command handler slash command-ready Only `help` and `settings` commands work so far * feat(commands): finish new settings command * fix(settings): convert `roles` and `ping` to an array * fix(commands): make `add` a slash command * fix(commands): make `blacklist` a slash command * fix(commands): remove URLs from `help` command * Add weblate badge and overview image * Update sponsors * sqlite things * imrpovements * update eslint rules * (⚠ untested) close command * fix: default locale for getting command option names * Update README.md * Update README.md * Update README.md * update new command to slash commands and fix close command * fixes and improvements * fix: closing a ticket when the creator has left * Revert "fix: closing a ticket when the creator has left" This reverts commit afc40ae17077782e344fd8cee03a089966c2347e. * fix: closing a ticket when the creator has left * fix: localisation issues in settings command * fix: delete category channel * New button and select panels + updated message panels Includes new options for panel embed message image and thumbnail Co-Authored-By: Puneet Gopinath Co-Authored-By: thevisuales <6569806+thevisuales@users.noreply.github.com> * Finish converting to buttons, added close button Co-Authored-By: Puneet Gopinath Co-Authored-By: thevisuales <6569806+thevisuales@users.noreply.github.com> * fully convert to slash commands * re-add "... has created a ticket" message * locales * fix add and remove commands * fix remove command * fix stats command * eslint Co-authored-by: Puneet Gopinath Co-authored-by: thevisuales <6569806+thevisuales@users.noreply.github.com> --- .eslintrc.js | 3 +- README.md | 14 +- package.json | 18 +- pnpm-lock.yaml | 69 +- src/commands/add.js | 106 +-- src/commands/blacklist.js | 215 +++-- src/commands/close.js | 307 ++++--- src/commands/extra/settings.schema.json | 113 --- src/commands/help.js | 75 +- src/commands/new.js | 234 +++-- src/commands/panel.js | 290 +++--- src/commands/remove.js | 108 ++- src/commands/settings.js | 497 +++++++---- src/commands/stats.js | 73 +- src/commands/survey.js | 57 +- src/commands/tag.js | 130 +-- src/commands/topic.js | 62 +- src/database/index.js | 5 +- src/database/models/category.model.js | 5 +- src/database/models/guild.model.js | 13 +- src/database/models/panel.model.js | 18 +- src/listeners/debug.js | 2 +- src/listeners/guildCreate.js | 1 + src/listeners/guildDelete.js | 1 - src/listeners/interactionCreate.js | 323 +++++++ src/listeners/messageCreate.js | 98 +- src/listeners/messageDelete.js | 2 +- src/listeners/messageReactionAdd.js | 164 ---- src/listeners/messageReactionRemove.js | 72 -- src/listeners/messageUpdate.js | 2 +- src/listeners/ready.js | 3 +- src/locales/en-GB.json | 1086 +++++++++++++---------- src/logger.js | 2 +- src/modules/commands/command.js | 108 +-- src/modules/commands/manager.js | 222 ++--- src/modules/tickets/manager.js | 225 ++--- src/utils/discord.js | 20 +- user/example.config.js | 6 +- 38 files changed, 2481 insertions(+), 2268 deletions(-) delete mode 100644 src/commands/extra/settings.schema.json create mode 100644 src/listeners/interactionCreate.js delete mode 100644 src/listeners/messageReactionAdd.js delete mode 100644 src/listeners/messageReactionRemove.js diff --git a/.eslintrc.js b/.eslintrc.js index 6e8a582..9a258ff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -100,7 +100,8 @@ module.exports = { } ], 'max-lines': [ - 'warn' + 'warn', + 500 ], 'max-statements-per-line': [ 'error' diff --git a/README.md b/README.md index 1dfa983..8e04037 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ [![GitHub stars](https://img.shields.io/github/stars/discord-tickets/bot?style=flat-square)](https://github.com/discord-tickets/bot/stargazers) [![GitHub forks](https://img.shields.io/github/forks/discord-tickets/bot?style=flat-square)](https://github.com/discord-tickets/bot/stargazers) [![License](https://img.shields.io/github/license/discord-tickets/bot?style=flat-square)](https://github.com/discord-tickets/bot/blob/main/LICENSE) +![](https://img.shields.io/badge/dynamic/json?color=5865F2&label=bots&query=clients&url=https%3A%2F%2Fstats.discordtickets.app&logo=discord&logoColor=white&style=flat-square) +![](https://img.shields.io/badge/dynamic/json?color=5865F2&label=tickets&query=tickets&url=https%3A%2F%2Fstats.discordtickets.app&logo=discord&logoColor=white&style=flat-square) [![Codacy](https://img.shields.io/codacy/grade/b974eb5f984c40868e07d82c968bd02d?logo=codacy&style=flat-square)](https://www.codacy.com/gh/discord-tickets/bot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=discord-tickets/bot&utm_campaign=Badge_Grade) [![Discord](https://img.shields.io/discord/451745464480432129?label=discord&color=7289DA&style=flat-square)](https://go.eartharoid.me/discord) -[![Crowdin](https://badges.crowdin.net/discord-tickets/localized.svg)](https://i18n.discordtickets.app/project/discord-tickets) +[![Weblate](https://i18n.capestar.net/widgets/discord-tickets/en_GB/bot/svg-badge.svg)](https://i18n.capestar.net/engage/discord-tickets/en_GB/)
@@ -37,6 +39,8 @@ You can also configure the functionality of the bot to your liking and add comma If the bot hasn't already been translated to your (community's) language, you can [translate](https://github.com/discord-tickets/.github/blob/main//CONTRIBUTING.md#translating) it yourself. Plugin authors are encouraged to support multiple languages as well. +[![Weblate](https://i18n.capestar.net/widgets/discord-tickets/en_GB/bot/multi-auto.svg)](https://i18n.capestar.net/engage/discord-tickets/en_GB/) + 3. **Multiple ticket categories** Each ticket category has its own settings for messages and the support team roles. There's also multiple methods of creating a ticket. @@ -88,7 +92,13 @@ Thank you to everyone to has contributed to Discord Tickets, including everyone ## Sponsors -Does your community or company use Discord Tickets? Sponsor the project to get your logo shown here. +*Does your community or company use Discord Tickets? [Sponsor the project](https://github.com/discord-tickets/bot/?sponsor=1) to get your logo shown here.* + +**These awesome people and communities sponsor Discord Tickets:** + +- [reSkybounds](https://reskybounds.com/) ([Discord](https://discord.reskybounds.com/)) +- [Cal#0004](https://discord.com/users/239036926152146944) + ### Donate diff --git a/package.json b/package.json index 8d1277d..c3d8f90 100644 --- a/package.json +++ b/package.json @@ -33,22 +33,18 @@ "dependencies": { "@eartharoid/i18n": "^1.0.0", "boxen": "^5.0.1", - "command-line-args": "^5.2.0", "cryptr": "^6.0.2", "discord.js": "^13.1.0", "dotenv": "^8.6.0", - "jsonschema": "^1.4.0", "keyv": "^4.0.3", "leeks.js": "^0.2.2", "leekslazylogger": "^3.0.2", + "ms": "^2.1.3", "mustache": "^4.2.0", - "node-emoji": "^1.11.0", "node-fetch": "^2.6.1", "semver": "^7.3.5", "sequelize": "^6.6.5", - "string-argv": "^0.3.1", - "terminal-link": "^2.1.1", - "to-time-monthsfork": "^1.1.4" + "terminal-link": "^2.1.1" }, "devDependencies": { "all-contributors-cli": "^6.20.0", @@ -64,10 +60,10 @@ "sqlite3": "^5.0.2" }, "peerDependencies": { - "mariadb": "^2.5.2", - "mysql2": "^2.2.5", - "pg": "^8.5.1", - "pg-hstore": "^2.3.3", - "tedious": "^11.0.3" + "mariadb": "^2.5.4", + "mysql2": "^2.3.0", + "pg": "^8.7.1", + "pg-hstore": "^2.3.4", + "tedious": "^11.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc6940e..3c7097c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,19 +4,17 @@ specifiers: '@eartharoid/i18n': ^1.0.0 all-contributors-cli: ^6.20.0 boxen: ^5.0.1 - command-line-args: ^5.2.0 cryptr: ^6.0.2 discord.js: ^13.1.0 dotenv: ^8.6.0 eslint: ^7.32.0 - jsonschema: ^1.4.0 keyv: ^4.0.3 leeks.js: ^0.2.2 leekslazylogger: ^3.0.2 mariadb: ^2.5.4 + ms: ^2.1.3 mustache: ^4.2.0 mysql2: ^2.3.0 - node-emoji: ^1.11.0 node-fetch: ^2.6.1 nodemon: ^2.0.12 pg: ^8.7.1 @@ -24,30 +22,24 @@ specifiers: semver: ^7.3.5 sequelize: ^6.6.5 sqlite3: ^5.0.2 - string-argv: ^0.3.1 tedious: ^11.4.0 terminal-link: ^2.1.1 - to-time-monthsfork: ^1.1.4 dependencies: '@eartharoid/i18n': 1.0.0 boxen: 5.0.1 - command-line-args: 5.2.0 cryptr: 6.0.2 discord.js: 13.1.0 dotenv: 8.6.0 - jsonschema: 1.4.0 keyv: 4.0.3 leeks.js: 0.2.2 leekslazylogger: 3.0.2 + ms: 2.1.3 mustache: 4.2.0 - node-emoji: 1.11.0 node-fetch: 2.6.1 semver: 7.3.5 sequelize: 6.6.5_aa1b3c7f5b5df187fb1a5c6073dca637 - string-argv: 0.3.1 terminal-link: 2.1.1 - to-time-monthsfork: 1.1.4 optionalDependencies: sqlite3: 5.0.2 @@ -591,11 +583,6 @@ packages: sprintf-js: 1.0.3 dev: true - /array-back/3.1.0: - resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} - engines: {node: '>=6'} - dev: false - /asn1/0.2.4: resolution: {integrity: sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==} dependencies: @@ -659,10 +646,6 @@ packages: dev: false optional: true - /bignumber.js/2.4.0: - resolution: {integrity: sha1-g4qZLan51zfg9LLbC+YrsJ3Qxeg=} - dev: false - /binary-extensions/2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -908,16 +891,6 @@ packages: dependencies: delayed-stream: 1.0.0 - /command-line-args/5.2.0: - resolution: {integrity: sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==} - engines: {node: '>=4.0.0'} - dependencies: - array-back: 3.1.0 - find-replace: 3.0.0 - lodash.camelcase: 4.3.0 - typical: 4.0.0 - dev: false - /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} @@ -1343,13 +1316,6 @@ packages: to-regex-range: 5.0.1 dev: true - /find-replace/3.0.0: - resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} - engines: {node: '>=4.0.0'} - dependencies: - array-back: 3.1.0 - dev: false - /find-up/4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1903,10 +1869,6 @@ packages: dev: false optional: true - /jsonschema/1.4.0: - resolution: {integrity: sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw==} - dev: false - /jsonwebtoken/8.5.1: resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} engines: {node: '>=4', npm: '>=1.4.28'} @@ -2019,10 +1981,6 @@ packages: p-locate: 4.1.0 dev: true - /lodash.camelcase/4.3.0: - resolution: {integrity: sha1-soqmKIorn8ZRA1x3EfZathkDMaY=} - dev: false - /lodash.clonedeep/4.5.0: resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} dev: true @@ -2273,12 +2231,6 @@ packages: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} optional: true - /node-emoji/1.11.0: - resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - dependencies: - lodash: 4.17.21 - dev: false - /node-fetch/2.6.1: resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==} engines: {node: 4.x || >=6.0.0} @@ -3102,11 +3054,6 @@ packages: engines: {node: '>=4', npm: '>=6'} dev: true - /string-argv/0.3.1: - resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} - engines: {node: '>=0.6.19'} - dev: false - /string-width/1.0.2: resolution: {integrity: sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=} engines: {node: '>=0.10.0'} @@ -3314,13 +3261,6 @@ packages: is-number: 7.0.0 dev: true - /to-time-monthsfork/1.1.4: - resolution: {integrity: sha512-3bWuIwm9QeOAq/UClxFp86QMSJ4GVHmAT8X+pkM0mIMVrpJPLfSieY5qvSsfLJugLNWTVpYJ2ayKWXH3jcAdow==} - engines: {node: '>=4.6'} - dependencies: - bignumber.js: 2.4.0 - dev: false - /toposort-class/1.0.1: resolution: {integrity: sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=} dev: false @@ -3417,11 +3357,6 @@ packages: is-typedarray: 1.0.0 dev: true - /typical/4.0.0: - resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} - engines: {node: '>=8'} - dev: false - /undefsafe/2.0.3: resolution: {integrity: sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==} dependencies: diff --git a/src/commands/add.js b/src/commands/add.js index 54ea608..b6afbd0 100644 --- a/src/commands/add.js +++ b/src/commands/add.js @@ -1,6 +1,6 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars + Interaction, // eslint-disable-line no-unused-vars MessageEmbed } = require('discord.js'); @@ -8,111 +8,111 @@ module.exports = class AddCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [], - args: [ - { - description: i18n('commands.add.args.member.description'), - example: i18n('commands.add.args.member.example'), - name: i18n('commands.add.args.member.name'), - required: true - }, - { - description: i18n('commands.add.args.ticket.description'), - example: i18n('commands.add.args.ticket.example'), - name: i18n('commands.add.args.ticket.name'), - required: false - } - ], description: i18n('commands.add.description'), internal: true, name: i18n('commands.add.name'), - process_args: false + options: [ + { + description: i18n('commands.add.options.member.description'), + name: i18n('commands.add.options.member.name'), + required: true, + type: Command.option_types.USER + }, + { + description: i18n('commands.add.options.ticket.description'), + name: i18n('commands.add.options.ticket.name'), + required: false, + type: Command.option_types.CHANNEL + } + ] }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); - const ticket = message.mentions.channels.first() ?? message.channel; - const t_row = await this.client.tickets.resolve(ticket.id, message.guild.id); + const channel = interaction.options.getChannel(default_i18n('commands.add.options.ticket.name')) ?? interaction.channel; + const t_row = await this.client.tickets.resolve(channel.id, interaction.guild.id); if (!t_row) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.add.response.not_a_ticket.title')) .setDescription(i18n('commands.add.response.not_a_ticket.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - const member = message.mentions.members.first() ?? message.guild.members.cache.get(args); + const member = interaction.options.getMember(default_i18n('commands.add.options.member.name')); if (!member) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.add.response.no_member.title')) .setDescription(i18n('commands.add.response.no_member.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - if (t_row.creator !== message.author.id && !await this.client.utils.isStaff(message.member)) { - return await message.channel.send({ + if (t_row.creator !== interaction.member.id && !await this.client.utils.isStaff(interaction.member)) { + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.add.response.no_permission.title')) .setDescription(i18n('commands.add.response.no_permission.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - if (message.channel.id !== ticket.id) { - await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('commands.add.response.added.title')) - .setDescription(i18n('commands.add.response.added.description', member.toString(), ticket.toString())) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } + await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setAuthor(member.user.username, member.user.displayAvatarURL()) + .setTitle(i18n('commands.add.response.added.title')) + .setDescription(i18n('commands.add.response.added.description', member.toString(), channel.toString())) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); - await ticket.send({ + await channel.send({ embeds: [ new MessageEmbed() .setColor(settings.colour) .setAuthor(member.user.username, member.user.displayAvatarURL()) .setTitle(i18n('ticket.member_added.title')) - .setDescription(i18n('ticket.member_added.description', member.toString(), message.author.toString())) - .setFooter(settings.footer, message.guild.iconURL()) + .setDescription(i18n('ticket.member_added.description', member.toString(), interaction.user.toString())) + .setFooter(settings.footer, interaction.guild.iconURL()) ] }); - await ticket.permissionOverwrites.edit(member, { + await channel.permissionOverwrites.edit(member, { ATTACH_FILES: true, READ_MESSAGE_HISTORY: true, SEND_MESSAGES: true, VIEW_CHANNEL: true - }, `${message.author.tag} added ${member.user.tag} to the ticket`); + }, `${interaction.user.tag} added ${member.user.tag} to the ticket`); - await this.client.tickets.archives.updateMember(ticket.id, member); + await this.client.tickets.archives.updateMember(channel.id, member); - this.client.log.info(`${message.author.tag} added ${member.user.tag} to ${ticket.id}`); + this.client.log.info(`${interaction.user.tag} added ${member.user.tag} to ${channel.id}`); } }; diff --git a/src/commands/blacklist.js b/src/commands/blacklist.js index ef7b90c..29c7997 100644 --- a/src/commands/blacklist.js +++ b/src/commands/blacklist.js @@ -1,125 +1,156 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars - MessageEmbed + Interaction, // eslint-disable-line no-unused-vars + MessageEmbed, + Role } = require('discord.js'); module.exports = class BlacklistCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [ - i18n('commands.blacklist.aliases.unblacklist') - ], - args: [ - { - description: i18n('commands.blacklist.args.member_or_role.description'), - example: i18n('commands.blacklist.args.member_or_role.example'), - name: i18n('commands.blacklist.args.member_or_role.name'), - required: false - } - ], description: i18n('commands.blacklist.description'), internal: true, name: i18n('commands.blacklist.name'), - permissions: ['MANAGE_GUILD'], - process_args: false + options: [ + { + description: i18n('commands.blacklist.options.add.description'), + name: i18n('commands.blacklist.options.add.name'), + options: [ + { + description: i18n('commands.blacklist.options.add.options.member_or_role.description'), + name: i18n('commands.blacklist.options.add.options.member_or_role.name'), + required: true, + type: Command.option_types.MENTIONABLE + } + ], + type: Command.option_types.SUB_COMMAND + }, + { + description: i18n('commands.blacklist.options.remove.description'), + name: i18n('commands.blacklist.options.remove.name'), + options: [ + { + description: i18n('commands.blacklist.options.remove.options.member_or_role.description'), + name: i18n('commands.blacklist.options.remove.options.member_or_role.name'), + required: true, + type: Command.option_types.MENTIONABLE + } + ], + type: Command.option_types.SUB_COMMAND + }, + { + description: i18n('commands.blacklist.options.show.description'), + name: i18n('commands.blacklist.options.show.name'), + type: Command.option_types.SUB_COMMAND + } + ], + staff_only: true }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); + const blacklist = JSON.parse(JSON.stringify(settings.blacklist)); // not the same as `const blacklist = { ...settings.blacklist };` ..? - const member = message.mentions.members.first(); + switch (interaction.options.getSubcommand()) { + case default_i18n('commands.blacklist.options.add.name'): { + const member_or_role = interaction.options.getMentionable(default_i18n('commands.blacklist.options.add.options.member_or_role.name')); + const type = member_or_role instanceof Role ? 'role' : 'member'; - if (member && (await this.client.utils.isStaff(member) || member.permissions.has(this.permissions))) { - return await message.channel.send({ + if (type === 'member' && await this.client.utils.isStaff(member_or_role)) { + return await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.blacklist.response.illegal_action.title')) + .setDescription(i18n('commands.blacklist.response.illegal_action.description', member_or_role.toString())) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } + + blacklist[type + 's'].push(member_or_role.id); + await interaction.reply({ embeds: [ new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.blacklist.response.illegal_action.title')) - .setDescription(i18n('commands.blacklist.response.illegal_action.description', `<@${member.id}>`)) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setColor(settings.success_colour) + .setTitle(i18n(`commands.blacklist.response.${type}_added.title`)) + .setDescription(i18n(`commands.blacklist.response.${type}_added.description`, member_or_role.id)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); + await settings.update({ blacklist }); + break; } + case default_i18n('commands.blacklist.options.remove.name'): { + const member_or_role = interaction.options.getMentionable(default_i18n('commands.blacklist.options.remove.options.member_or_role.name')); + const type = member_or_role instanceof Role ? 'role' : 'member'; + const index = blacklist[type + 's'].findIndex(element => element === member_or_role.id); + if (index === -1) { + return await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.blacklist.response.invalid.title')) + .setDescription(i18n('commands.blacklist.response.invalid.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } - const role = message.mentions.roles.first(); - let id; - const input = args.trim().split(/\s/g)[0]; - - if (member) { - id = member.id; - } else if (role) { - id = role.id; - } else if (/\d{17,19}/.test(input)) { - id = input; - } else if (settings.blacklist.length === 0) { - return await message.channel.send({ + blacklist[type + 's'].splice(index, 1); + await interaction.reply({ embeds: [ new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.blacklist.response.empty_list.title')) - .setDescription(i18n('commands.blacklist.response.empty_list.description', settings.command_prefix)) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } else { - // list blacklisted members - const blacklist = settings.blacklist.map(element => { - const is_role = message.guild.roles.cache.has(element); - if (is_role) return `» <@&${element}> (\`${element}\`)`; - else return `» <@${element}> (\`${element}\`)`; - }); - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.blacklist.response.list.title')) - .setDescription(blacklist.join('\n')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setColor(settings.success_colour) + .setTitle(i18n(`commands.blacklist.response.${type}_removed.title`)) + .setDescription(i18n(`commands.blacklist.response.${type}_removed.description`, member_or_role.id)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); + await settings.update({ blacklist }); + break; + } + case default_i18n('commands.blacklist.options.show.name'): { + if (blacklist.members.length === 0 && blacklist.roles.length === 0) { + return await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setTitle(i18n('commands.blacklist.response.empty_list.title')) + .setDescription(i18n('commands.blacklist.response.empty_list.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } else { + const members = blacklist.members.map(id => `**·** <@${id}>`); + const roles = blacklist.roles.map(id => `**·** <@&${id}>`); + return await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setTitle(i18n('commands.blacklist.response.list.title')) + .addField(i18n('commands.blacklist.response.list.fields.members'), members.join('\n') || 'none') + .addField(i18n('commands.blacklist.response.list.fields.roles'), roles.join('\n') || 'none') + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } } - - const is_role = role !== undefined || message.guild.roles.cache.has(id); - const member_or_role = is_role ? 'role' : 'member'; - const index = settings.blacklist.findIndex(element => element === id); - - const new_blacklist = [...settings.blacklist]; - - if (index === -1) { - new_blacklist.push(id); - await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n(`commands.blacklist.response.${member_or_role}_added.title`)) - .setDescription(i18n(`commands.blacklist.response.${member_or_role}_added.description`, id)) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } else { - new_blacklist.splice(index, 1); - await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n(`commands.blacklist.response.${member_or_role}_removed.title`)) - .setDescription(i18n(`commands.blacklist.response.${member_or_role}_removed.description`, id)) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); } - - settings.blacklist = new_blacklist; - await settings.save(); } }; diff --git a/src/commands/close.js b/src/commands/close.js index a63f958..9060ab1 100644 --- a/src/commands/close.js +++ b/src/commands/close.js @@ -1,260 +1,297 @@ const Command = require('../modules/commands/command'); -// eslint-disable-next-line no-unused-vars const { - Message, // eslint-disable-line no-unused-vars - MessageEmbed, - MessageMentions + Interaction, // eslint-disable-line no-unused-vars + MessageActionRow, + MessageButton, + MessageEmbed } = require('discord.js'); const { Op } = require('sequelize'); -const toTime = require('to-time-monthsfork'); +const ms = require('ms'); module.exports = class CloseCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [ - i18n('commands.close.aliases.delete'), - i18n('commands.close.aliases.lock') - ], - args: [ - { - alias: i18n('commands.close.args.ticket.alias'), - description: i18n('commands.close.args.ticket.description'), - example: i18n('commands.close.args.ticket.example'), - name: i18n('commands.close.args.ticket.name'), - required: false, - type: String - }, - { - alias: i18n('commands.close.args.reason.alias'), - description: i18n('commands.close.args.reason.description'), - example: i18n('commands.close.args.reason.example'), - name: i18n('commands.close.args.reason.name'), - required: false, - type: String - }, - { - alias: i18n('commands.close.args.time.alias'), - description: i18n('commands.close.args.time.description'), - example: i18n('commands.close.args.time.example'), - name: i18n('commands.close.args.time.name'), - required: false, - type: String - } - ], description: i18n('commands.close.description'), internal: true, name: i18n('commands.close.name'), - process_args: true + options: [ + { + description: i18n('commands.close.options.reason.description'), + name: i18n('commands.close.options.reason.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.close.options.ticket.description'), + name: i18n('commands.close.options.ticket.name'), + required: false, + type: Command.option_types.INTEGER + }, + { + description: i18n('commands.close.options.time.description'), + name: i18n('commands.close.options.time.name'), + required: false, + type: Command.option_types.STRING + } + ] }); } /** - * @param {Message} message - * @param {*} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const arg_ticket = this.args[0].name; - const arg_reason = this.args[1].name; - const arg_time = this.args[2].name; - - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); - if (args[arg_time]) { - let period; + const reason = interaction.options.getString(default_i18n('commands.close.options.reason.name')); + const ticket = interaction.options.getInteger(default_i18n('commands.close.options.ticket.name')); + const time = interaction.options.getString(default_i18n('commands.close.options.time.name')); + if (time) { + let period; try { - period = toTime(args[arg_time]).ms(); + period = ms(time); } catch { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.close.response.invalid_time.title')) .setDescription(i18n('commands.close.response.invalid_time.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { - guild: message.guild.id, - last_message: { [Op.lte]: new Date(Date.now() - period) } + guild: interaction.guild.id, + last_message: { [Op.lte]: new Date(Date.now() - period) }, + open: true } }); if (tickets.count === 0) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.close.response.no_tickets.title')) .setDescription(i18n('commands.close.response.no_tickets.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } else { - const collector_message = await message.channel.send({ + await interaction.reply({ + components: [ + new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId(`confirm_close_multiple:${interaction.id}`) + .setLabel(i18n('commands.close.response.confirm_multiple.buttons.confirm', tickets.count, tickets.count)) + .setEmoji('✅') + .setStyle('SUCCESS') + ) + .addComponents( + new MessageButton() + .setCustomId(`cancel_close_multiple:${interaction.id}`) + .setLabel(i18n('commands.close.response.confirm_multiple.buttons.cancel')) + .setEmoji('❌') + .setStyle('SECONDARY') + ) + ], embeds: [ new MessageEmbed() .setColor(settings.colour) .setTitle(i18n('commands.close.response.confirm_multiple.title')) .setDescription(i18n('commands.close.response.confirm_multiple.description', tickets.count, tickets.count)) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) + ], + ephemeral: true }); - await collector_message.react('✅'); - const filter = (reaction, user) => user.id === message.author.id && reaction.emoji.name === '✅'; - const collector = collector_message.createReactionCollector({ + const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); + const collector = interaction.channel.createMessageComponentCollector({ filter, time: 30000 }); - collector.on('collect', async () => { - await collector_message.reactions.removeAll(); + collector.on('collect', async i => { + await i.deferUpdate(); - await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('commands.close.response.closed_multiple.title', tickets.count, tickets.count)) - .setDescription(i18n('commands.close.response.closed_multiple.description', tickets.count, tickets.count)) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); + if (i.customId === `confirm_close_multiple:${interaction.id}`) { + for (const ticket of tickets.rows) { + await this.client.tickets.close(ticket.id, interaction.user.id, interaction.guild.id, reason); + } - for (const ticket of tickets.rows) { - await this.client.tickets.close(ticket.id, message.author.id, message.guild.id, args[arg_reason]); + await i.editReply({ + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setTitle(i18n('commands.close.response.closed_multiple.title', tickets.count, tickets.count)) + .setDescription(i18n('commands.close.response.closed_multiple.description', tickets.count, tickets.count)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } else { + await i.editReply({ + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.close.response.canceled.title')) + .setDescription(i18n('commands.close.response.canceled.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); } + collector.stop(); }); collector.on('end', async collected => { if (collected.size === 0) { - await collector_message.reactions.removeAll(); - await collector_message.edit({ + await interaction.editReply({ + components: [], embeds: [ new MessageEmbed() .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) .setTitle(i18n('commands.close.response.confirmation_timeout.title')) .setDescription(i18n('commands.close.response.confirmation_timeout.description')) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); - setTimeout(async () => { - await collector_message - .delete() - .catch(() => this.client.log.warn('Failed to delete response (collector) message')); - await message - .delete() - .catch(() => this.client.log.warn('Failed to delete original message')); - }, 15000); } }); } - } else { - let t_row; - if (args[arg_ticket]) { - args[arg_ticket] = args[arg_ticket].replace(MessageMentions.CHANNELS_PATTERN, '$1'); - t_row = await this.client.tickets.resolve(args[arg_ticket], message.guild.id); + let t_row; + if (ticket) { + t_row = await this.client.tickets.resolve(ticket, interaction.guild.id); if (!t_row) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.close.response.unresolvable.title')) - .setDescription(i18n('commands.close.response.unresolvable.description', args[arg_ticket])) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setDescription(i18n('commands.close.response.unresolvable.description', ticket)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } } else { - t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } }); - + t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); if (!t_row) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.close.response.not_a_ticket.title')) - .setDescription(i18n('commands.close.response.not_a_ticket.description', settings.command_prefix)) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setDescription(i18n('commands.close.response.not_a_ticket.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } } - const collector_message = await message.channel.send({ + await interaction.reply({ + components: [ + new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId(`confirm_close:${interaction.id}`) + .setLabel(i18n('commands.close.response.confirm.buttons.confirm')) + .setEmoji('✅') + .setStyle('SUCCESS') + ) + .addComponents( + new MessageButton() + .setCustomId(`cancel_close:${interaction.id}`) + .setLabel(i18n('commands.close.response.confirm.buttons.cancel')) + .setEmoji('❌') + .setStyle('SECONDARY') + ) + ], embeds: [ new MessageEmbed() .setColor(settings.colour) .setTitle(i18n('commands.close.response.confirm.title')) - .setDescription(i18n('commands.close.response.confirm.description', t_row.number)) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setDescription(settings.log_messages ? i18n('commands.close.response.confirm.description_with_archive') : i18n('commands.close.response.confirm.description')) + .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) + ], + ephemeral: true }); - await collector_message.react('✅'); - const filter = (reaction, user) => user.id === message.author.id && reaction.emoji.name === '✅'; - const collector = collector_message.createReactionCollector({ + const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); + const collector = interaction.channel.createMessageComponentCollector({ filter, time: 30000 }); - collector.on('collect', async () => { - collector.stop(); + collector.on('collect', async i => { + await i.deferUpdate(); - if (message.channel.id === t_row.id) { - await collector_message.delete(); - } else { - await collector_message.reactions.removeAll(); - await collector_message.edit({ + if (i.customId === `confirm_close:${interaction.id}`) { + await this.client.tickets.close(t_row.id, interaction.user.id, interaction.guild.id, reason); + await i.editReply({ + components: [], embeds: [ new MessageEmbed() .setColor(settings.success_colour) - .setTitle(i18n('commands.close.response.closed.title')) + .setTitle(i18n('commands.close.response.closed.title', t_row.number)) .setDescription(i18n('commands.close.response.closed.description', t_row.number)) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } else { + await i.editReply({ + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.close.response.canceled.title')) + .setDescription(i18n('commands.close.response.canceled.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - await this.client.tickets.close(t_row.id, message.author.id, message.guild.id, args[arg_reason]); + collector.stop(); }); collector.on('end', async collected => { if (collected.size === 0) { - await collector_message.reactions.removeAll(); - await collector_message.edit({ + await interaction.editReply({ + components: [], embeds: [ new MessageEmbed() .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) .setTitle(i18n('commands.close.response.confirmation_timeout.title')) .setDescription(i18n('commands.close.response.confirmation_timeout.description')) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); - setTimeout(async () => { - await collector_message - .delete() - .catch(() => this.client.log.warn('Failed to delete response (collector) message')); - await message - .delete() - .catch(() => this.client.log.warn('Failed to delete original message')); - }, 15000); } }); - } } }; diff --git a/src/commands/extra/settings.schema.json b/src/commands/extra/settings.schema.json deleted file mode 100644 index 20853ad..0000000 --- a/src/commands/extra/settings.schema.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "type": "object", - "properties": { - "categories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "claiming": { - "type": "boolean" - }, - "image": { - "type": [ - "string", - "null" - ] - }, - "max_per_member": { - "type": "number" - }, - "name": { - "type": "string" - }, - "name_format": { - "type": "string" - }, - "opening_message": { - "type": "string" - }, - "opening_questions": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "ping": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "require_topic": { - "type": "boolean" - }, - "roles": { - "type": "array", - "items": { - "type": "string" - } - }, - "survey": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "name", - "roles" - ] - } - }, - "colour": { - "type": "string" - }, - "command_prefix": { - "type": "string" - }, - "error_colour": { - "type": "string" - }, - "footer": { - "type": "string" - }, - "locale": { - "type": "string" - }, - "log_messages": { - "type": "boolean" - }, - "success_colour": { - "type": "string" - }, - "surveys": { - "type": "object" - }, - "tags": { - "type": "object" - } - }, - "required": [ - "categories", - "colour", - "command_prefix", - "error_colour", - "footer", - "locale", - "log_messages", - "success_colour", - "surveys", - "tags" - ] -} diff --git a/src/commands/help.js b/src/commands/help.js index ed500cc..eed7c8a 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -1,6 +1,6 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars + Interaction, // eslint-disable-line no-unused-vars MessageEmbed } = require('discord.js'); @@ -8,61 +8,42 @@ module.exports = class HelpCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [ - i18n('commands.help.aliases.command'), - i18n('commands.help.aliases.commands') - ], - args: [ - { - description: i18n('commands.help.args.command.description'), - example: i18n('commands.help.args.command.example'), - name: i18n('commands.help.args.command.name'), - required: false - } - ], description: i18n('commands.help.description'), internal: true, - name: i18n('commands.help.name'), - process_args: false + name: i18n('commands.help.name') }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); const i18n = this.client.i18n.getLocale(settings.locale); - const cmd = this.manager.commands.find(command => command.aliases.includes(args.toLowerCase())); - - if (cmd) { - return await cmd.sendUsage(message.channel, args); - } else { - const is_staff = await this.client.utils.isStaff(message.member); - const commands = this.manager.commands.filter(command => { - if (command.permissions.length >= 1) return message.member.permissions.has(command.permissions); - else if (command.staff_only) return is_staff; - else return true; - }); - const list = commands.map(command => { - const description = command.description.length > 50 - ? command.description.substring(0, 50) + '...' - : command.description; - return `**\`${settings.command_prefix}${command.name}\` ·** ${description}`; - }); - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.help.response.list.title')) - .setDescription(i18n('commands.help.response.list.description', { prefix: settings.command_prefix })) - .addField(i18n('commands.help.response.list.fields.commands'), list.join('\n')) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } + const is_staff = await this.client.utils.isStaff(interaction.member); + const commands = this.manager.commands.filter(command => { + if (command.permissions.length >= 1) return interaction.member.permissions.has(command.permissions); + else if (command.staff_only) return is_staff; + else return true; + }); + const list = commands.map(command => { + const description = command.description.length > 50 + ? command.description.substring(0, 50) + '...' + : command.description; + return `**\`/${command.name}\` ·** ${description}`; + }); + return await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setTitle(i18n('commands.help.response.list.title')) + .setDescription(i18n('commands.help.response.list.description')) + .addField(i18n('commands.help.response.list.fields.commands'), list.join('\n')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); } }; diff --git a/src/commands/new.js b/src/commands/new.js index cc91203..8bec13e 100644 --- a/src/commands/new.js +++ b/src/commands/new.js @@ -1,72 +1,64 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars - MessageEmbed + Interaction, // eslint-disable-line no-unused-vars, + MessageActionRow, + MessageEmbed, + MessageSelectMenu } = require('discord.js'); -const { letters } = require('../utils/emoji'); -const { wait } = require('../utils'); module.exports = class NewCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [ - i18n('commands.new.aliases.create'), - i18n('commands.new.aliases.open'), - i18n('commands.new.aliases.ticket') - ], - args: [ - { - description: i18n('commands.new.args.topic.description'), - example: i18n('commands.new.args.topic.example'), - name: i18n('commands.new.args.topic.name'), - required: false - } - ], description: i18n('commands.new.description'), internal: true, name: i18n('commands.new.name'), - process_args: false + options: [ + { + description: i18n('commands.new.options.topic.description'), + name: i18n('commands.new.options.topic.name'), + required: false, + type: Command.option_types.STRING + } + ] }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); - const editOrSend = async (msg, content) => { - if (msg) return await msg.edit(content); - else return await message.channel.send(content); - }; + const topic = interaction.options.getString(default_i18n('commands.new.options.topic.name')); - const create = async (cat_row, response) => { + const create = async (cat_row, i) => { const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { category: cat_row.id, - creator: message.author.id, + creator: interaction.user.id, open: true } }); if (tickets.count >= cat_row.max_per_member) { if (cat_row.max_per_member === 1) { - response = await editOrSend(response, - { - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) - .setTitle(i18n('commands.new.response.has_a_ticket.title')) - .setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id)) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) - ] - } - ); + const response = { + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.has_a_ticket.title')) + .setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }; + await i ? i.editReply(response) : interaction.reply(response); } else { const list = tickets.rows.map(row => { if (row.topic) { @@ -77,136 +69,120 @@ module.exports = class NewCommand extends Command { return `<#${row.id}>`; } }); - response = await editOrSend(response, - { - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) - .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) - .setDescription(i18n('commands.new.response.max_tickets.description', settings.command_prefix, list.join('\n'))) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) - ] - } - ); + const response = { + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) + .setDescription(i18n('commands.new.response.max_tickets.description', list.join('\n'))) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }; + await i ? i.editReply(response) : interaction.reply(response); } } else { try { - const t_row = await this.client.tickets.create(message.guild.id, message.author.id, cat_row.id, args); - response = await editOrSend(response, - { - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) - .setTitle(i18n('commands.new.response.created.title')) - .setDescription(i18n('commands.new.response.created.description', `<#${t_row.id}>`)) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) - ] - } - ); + const t_row = await this.client.tickets.create(interaction.guild.id, interaction.user.id, cat_row.id, topic); + const response = { + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.created.title')) + .setDescription(i18n('commands.new.response.created.description', `<#${t_row.id}>`)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }; + await i ? i.editReply(response) : interaction.reply(response); } catch (error) { - response = await editOrSend(response, - { - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) - .setTitle(i18n('commands.new.response.error.title')) - .setDescription(error.message) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) - ] - } - ); + const response = { + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.error.title')) + .setDescription(error.message) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }; + await i ? i.editReply(response) : interaction.reply(response); } } - - setTimeout(async () => { - await response - .delete() - .catch(() => this.client.log.warn('Failed to delete response message')); - await message - .delete() - .catch(() => this.client.log.warn('Failed to delete original message')); - }, 15000); }; - const categories = await this.client.db.models.Category.findAndCountAll({ where: { guild: message.guild.id } }); + const categories = await this.client.db.models.Category.findAndCountAll({ where: { guild: interaction.guild.id } }); if (categories.count === 0) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) .setTitle(i18n('commands.new.response.no_categories.title')) .setDescription(i18n('commands.new.response.no_categories.description')) - .setFooter(settings.footer, message.guild.iconURL()) + .setFooter(settings.footer, interaction.guild.iconURL()) ] }); } else if (categories.count === 1) { create(categories.rows[0]); // skip the category selection } else { - const letters_array = Object.values(letters); // convert the A-Z emoji object to an array - const category_list = categories.rows.map((category, i) => `${letters_array[i]} » ${category.name}`); // list category names with an A-Z emoji - const collector_message = await message.channel.send({ + await interaction.reply({ + components: [ + new MessageActionRow() + .addComponents( + new MessageSelectMenu() + .setCustomId(`select_category:${interaction.id}`) + .setPlaceholder('Select a category') + .addOptions(categories.rows.map(row => ({ + label: row.name, + value: row.id + }))) + ) + ], embeds: [ new MessageEmbed() .setColor(settings.colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) .setTitle(i18n('commands.new.response.select_category.title')) - .setDescription(i18n('commands.new.response.select_category.description', category_list.join('\n'))) - .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), message.guild.iconURL()) - ] + .setDescription(i18n('commands.new.response.select_category.description')) + .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) + ], + ephemeral: true }); - for (const i in categories.rows) { - collector_message.react(letters_array[i]); // add the correct number of letter reactions - await wait(1000); // 1 reaction per second rate-limit - } - - const filter = (reaction, user) => { - const allowed = letters_array.slice(0, categories.count); // get the first x letters of the emoji array - return user.id === message.author.id && allowed.includes(reaction.emoji.name); - }; - const collector = collector_message.createReactionCollector({ + const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); + const collector = interaction.channel.createMessageComponentCollector({ filter, time: 30000 }); - collector.on('collect', async reaction => { + collector.on('collect', async i => { + await i.deferUpdate(); + create(categories.rows.find(row => row.id === i.values[0]), i); collector.stop(); - const index = letters_array.findIndex(value => value === reaction.emoji.name); // find where the letter is in the alphabet - if (index === -1) { - return setTimeout(async () => { - await collector_message.delete(); - }, 15000); - } - await collector_message.reactions.removeAll(); - create(categories.rows[index], collector_message); // create the ticket, passing the existing response message to be edited instead of creating a new one }); collector.on('end', async collected => { if (collected.size === 0) { - await collector_message.reactions.removeAll(); - await collector_message.edit({ + await interaction.editReply({ + components: [], embeds: [ new MessageEmbed() .setColor(settings.error_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) .setTitle(i18n('commands.new.response.select_category_timeout.title')) .setDescription(i18n('commands.new.response.select_category_timeout.description')) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); - setTimeout(async () => { - await collector_message - .delete() - .catch(() => this.client.log.warn('Failed to delete response (collector) message')); - await message - .delete() - .catch(() => this.client.log.warn('Failed to delete original message')); - }, 15000); } }); } diff --git a/src/commands/panel.js b/src/commands/panel.js index d7399d4..a791374 100644 --- a/src/commands/panel.js +++ b/src/commands/panel.js @@ -1,85 +1,98 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars - MessageEmbed + Interaction, // eslint-disable-line no-unused-vars + MessageActionRow, + MessageButton, + MessageEmbed, + MessageSelectMenu } = require('discord.js'); -const { - some, wait -} = require('../utils'); -const { emojify } = require('node-emoji'); +const { some } = require('../utils'); module.exports = class PanelCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [], - args: [ - { - alias: i18n('commands.panel.args.title.alias'), - description: i18n('commands.panel.args.title.description'), - example: i18n('commands.panel.args.title.example'), - name: i18n('commands.panel.args.title.name'), - required: false, - type: String - }, - { - alias: i18n('commands.panel.args.description.alias'), - description: i18n('commands.panel.args.description.description'), - example: i18n('commands.panel.args.description.example'), - name: i18n('commands.panel.args.description.name'), - required: true, - type: String - }, - { - alias: i18n('commands.panel.args.emoji.alias'), - description: i18n('commands.panel.args.emoji.description'), - example: i18n('commands.panel.args.emoji.example'), - multiple: true, - name: i18n('commands.panel.args.emoji.name'), - required: false, - type: String - }, - { - alias: i18n('commands.panel.args.categories.alias'), - description: i18n('commands.panel.args.categories.description'), - example: i18n('commands.panel.args.categories.example'), - multiple: true, - name: i18n('commands.panel.args.categories.name'), - required: true, - type: String - } - ], description: i18n('commands.panel.description'), internal: true, name: i18n('commands.panel.name'), - permissions: ['MANAGE_GUILD'], - process_args: true + options: [ + { + description: i18n('commands.panel.options.categories.description'), + multiple: true, + name: i18n('commands.panel.options.categories.name'), + required: true, + type: Command.option_types.STRING + }, + { + description: i18n('commands.panel.options.description.description'), + name: i18n('commands.panel.options.description.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.panel.options.image.description'), + name: i18n('commands.panel.options.image.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.panel.options.just_type.description') + ' (false)', + name: i18n('commands.panel.options.just_type.name'), + required: false, + type: Command.option_types.BOOLEAN + }, + { + description: i18n('commands.panel.options.title.description'), + name: i18n('commands.panel.options.title.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.panel.options.thumbnail.description'), + name: i18n('commands.panel.options.thumbnail.name'), + required: false, + type: Command.option_types.STRING + } + ], + staff_only: true }); } /** - * @param {Message} message - * @param {*} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - // localised command and arg names are a pain - const arg_title = this.args[0].name; - const arg_description = this.args[1].name; - const arg_emoji = this.args[2].name; - const arg_categories = this.args[3].name; - - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); - if (!args[arg_emoji]) args[arg_emoji] = []; + const categories = interaction.options.getString(default_i18n('commands.panel.options.categories.name')) + .replace(/\s/g, '') + .split(','); + const description = interaction.options.getString(default_i18n('commands.panel.options.description.name')); + const image = interaction.options.getString(default_i18n('commands.panel.options.image.name')); + const just_type = interaction.options.getBoolean(default_i18n('commands.panel.options.just_type.name')); + const title = interaction.options.getString(default_i18n('commands.panel.options.title.name')); + const thumbnail = interaction.options.getString(default_i18n('commands.panel.options.thumbnail.name')); - args[arg_emoji] = args[arg_emoji].map(emoji => emojify(emoji.replace(/\\/g, ''))); + if (just_type && categories.length > 1) { + return await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.panel.response.too_many_categories.title')) + .setDescription(i18n('commands.panel.response.too_many_categories.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } - const invalid_category = await some(args[arg_categories], async id => { + const invalid_category = await some(categories, async id => { const cat_row = await this.client.db.models.Category.findOne({ where: { - guild: message.guild.id, + guild: interaction.guild.id, id } }); @@ -87,36 +100,36 @@ module.exports = class PanelCommand extends Command { }); if (invalid_category) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.panel.response.invalid_category.title')) .setDescription(i18n('commands.panel.response.invalid_category.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - let panel_channel, - panel_message; - - let categories_map = args[arg_categories][0]; + let panel_channel; const embed = new MessageEmbed() .setColor(settings.colour) - .setFooter(settings.footer, message.guild.iconURL()); + .setFooter(settings.footer, interaction.guild.iconURL()); - if (args[arg_title]) embed.setTitle(args[arg_title]); + if (description) embed.setDescription(description); + if (image) embed.setImage(image); + if (title) embed.setTitle(title); + if (thumbnail) embed.setThumbnail(thumbnail); - if (args[arg_emoji].length === 0) { - // reaction-less panel - panel_channel = await message.guild.channels.create('create-a-ticket', { + if (just_type) { + panel_channel = await interaction.guild.channels.create('create-a-ticket', { permissionOverwrites: [ { allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY'], deny: ['ATTACH_FILES', 'EMBED_LINKS', 'ADD_REACTIONS'], - id: message.guild.roles.everyone + id: interaction.guild.roles.everyone }, { allow: ['SEND_MESSAGES', 'EMBED_LINKS', 'ADD_REACTIONS'], @@ -125,92 +138,75 @@ module.exports = class PanelCommand extends Command { ], position: 1, rateLimitPerUser: 30, - reason: `${message.author.tag} created a new reaction-less panel`, + reason: `${interaction.user.tag} created a new message panel`, + type: 'GUILD_TEXT' + }); + await panel_channel.send({ embeds: [embed] }); + this.client.log.info(`${interaction.user.tag} has created a new message panel`); + } else { + panel_channel = await interaction.guild.channels.create('create-a-ticket', { + permissionOverwrites: [ + { + allow: ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'], + deny: ['SEND_MESSAGES', 'ADD_REACTIONS'], + id: interaction.guild.roles.everyone + }, + { + allow: ['SEND_MESSAGES', 'EMBED_LINKS', 'ADD_REACTIONS'], + id: this.client.user.id + } + ], + position: 1, + reason: `${interaction.user.tag} created a new panel`, type: 'GUILD_TEXT' }); - embed.setDescription(args[arg_description]); - panel_message = await panel_channel.send({ embeds: [embed] }); - - this.client.log.info(`${message.author.tag} has created a new reaction-less panel`); - } else { - if (args[arg_categories].length !== args[arg_emoji].length) { - // send error - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.panel.response.mismatch.title')) - .setDescription(i18n('commands.panel.response.mismatch.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } else { - panel_channel = await message.guild.channels.create('create-a-ticket', { - permissionOverwrites: [ - { - allow: ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'], - deny: ['SEND_MESSAGES', 'ADD_REACTIONS'], - id: message.guild.roles.everyone - }, - { - allow: ['SEND_MESSAGES', 'EMBED_LINKS', 'ADD_REACTIONS'], - id: this.client.user.id - } + if (categories.length === 1) { + // single category + await panel_channel.send({ + components: [ + new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId(`panel.single:${categories[0]}`) + .setLabel(i18n('panel.create_ticket')) + .setStyle('PRIMARY') + ) ], - position: 1, - reason: `${message.author.tag} created a new panel`, - type: 'GUILD_TEXT' + embeds: [embed] }); - - if (args[arg_emoji].length === 1) { - // single category - categories_map = {}; - categories_map[args[arg_emoji][0]] = args[arg_categories][0]; - embed.setDescription(args[arg_description]); - panel_message = await panel_channel.send({ embeds: [embed] }); - await panel_message.react(args[arg_emoji][0]); - } else { - // multi category - let description = ''; - categories_map = {}; - - for (const i in args[arg_emoji]) { - categories_map[args[arg_emoji][i]] = args[arg_categories][i]; - const cat_row = await this.client.db.models.Category.findOne({ - where: { - guild: message.guild.id, - id: args[arg_categories][i] - } - }); - description += `\n> ${args[arg_emoji][i]} | ${cat_row.name}`; - } - - embed.setDescription(args[arg_description] + '\n' + description); - panel_message = await panel_channel.send({ - embeds: [ - embed - ] - }); - - for (const emoji of args[arg_emoji]) { - await panel_message.react(emoji); - await wait(1000); // 1 reaction per second rate-limit - } - - } - - this.client.log.info(`${message.author.tag} has created a new panel`); + this.client.log.info(`${interaction.user.tag} has created a new button panel`); + } else { + // multi category + const rows = await this.client.db.models.Category.findAll({ where: { guild: interaction.guild.id } }); + await panel_channel.send({ + components: [ + new MessageActionRow() + .addComponents( + new MessageSelectMenu() + .setCustomId(`panel.multiple:${panel_channel.id}`) + .setPlaceholder('Select a category') + .addOptions(rows.map(row => ({ + label: row.name, + value: row.id + }))) + ) + ], + embeds: [embed] + }); + this.client.log.info(`${interaction.user.tag} has created a new select panel`); } } - message.channel.send({ content: `✅ ${panel_channel}` }); + interaction.reply({ + content: `✅ ${panel_channel}`, + ephemeral: true + }); await this.client.db.models.Panel.create({ - categories: categories_map, + category: categories.length === 1 ? categories[0] : null, channel: panel_channel.id, - guild: message.guild.id, - message: panel_message.id + guild: interaction.guild.id }); } }; diff --git a/src/commands/remove.js b/src/commands/remove.js index 0b148c8..85de1da 100644 --- a/src/commands/remove.js +++ b/src/commands/remove.js @@ -1,6 +1,6 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars + Interaction, // eslint-disable-line no-unused-vars MessageEmbed } = require('discord.js'); @@ -8,106 +8,104 @@ module.exports = class RemoveCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [], - args: [ - { - description: i18n('commands.remove.args.member.description'), - example: i18n('commands.remove.args.member.example'), - name: i18n('commands.remove.args.member.name'), - required: true - }, - { - description: i18n('commands.remove.args.ticket.description'), - example: i18n('commands.remove.args.ticket.example'), - name: i18n('commands.remove.args.ticket.name'), - required: false - } - ], description: i18n('commands.remove.description'), internal: true, name: i18n('commands.remove.name'), - process_args: false + options: [ + { + description: i18n('commands.remove.options.member.description'), + name: i18n('commands.remove.options.member.name'), + required: true, + type: Command.option_types.USER + }, + { + description: i18n('commands.remove.options.ticket.description'), + name: i18n('commands.remove.options.ticket.name'), + required: false, + type: Command.option_types.CHANNEL + } + ] }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); - const ticket = message.mentions.channels.first() ?? message.channel; - const t_row = await this.client.tickets.resolve(ticket.id, message.guild.id); + const channel = interaction.options.getChannel(default_i18n('commands.remove.options.channel.name')) ?? interaction.channel; + const t_row = await this.client.tickets.resolve(channel.id, interaction.guild.id); if (!t_row) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) - .setTitle(i18n('commands.remove.response.not_a_ticket.title')) - .setDescription(i18n('commands.remove.response.not_a_ticket.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setTitle(i18n('commands.remove.response.not_a_channel.title')) + .setDescription(i18n('commands.remove.response.not_a_channel.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - const member = message.mentions.members.first() ?? message.guild.members.cache.get(args); + const member = interaction.options.getMember(default_i18n('commands.remove.options.member.name')); if (!member) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.remove.response.no_member.title')) .setDescription(i18n('commands.remove.response.no_member.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - if (t_row.creator !== message.author.id && !await this.client.utils.isStaff(message.member)) { - return await message.channel.send({ + if (t_row.creator !== interaction.user.id && !await this.client.utils.isStaff(interaction.member)) { + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.remove.response.no_permission.title')) .setDescription(i18n('commands.remove.response.no_permission.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - if (message.channel.id !== ticket.id) { - await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('commands.remove.response.removed.title')) - .setDescription(i18n('commands.remove.response.removed.description', member.toString(), ticket.toString())) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } + await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setAuthor(member.user.username, member.user.displayAvatarURL()) + .setTitle(i18n('commands.remove.response.removed.title')) + .setDescription(i18n('commands.remove.response.removed.description', member.toString(), channel.toString())) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); - await ticket.send({ + await channel.send({ embeds: [ new MessageEmbed() .setColor(settings.colour) .setAuthor(member.user.username, member.user.displayAvatarURL()) .setTitle(i18n('ticket.member_removed.title')) - .setDescription(i18n('ticket.member_removed.description', member.toString(), message.author.toString())) - .setFooter(settings.footer, message.guild.iconURL()) + .setDescription(i18n('ticket.member_removed.description', member.toString(), interaction.user.toString())) + .setFooter(settings.footer, interaction.guild.iconURL()) ] }); - await ticket.permissionOverwrites - .get(member.user.id) - ?.delete(`${message.author.tag} removed ${member.user.tag} from the ticket`); + await channel.permissionOverwrites.delete(member.user.id, `${interaction.user.tag} removed ${member.user.tag} from the ticket`); - this.client.log.info(`${message.author.tag} removed ${member.user.tag} from ${ticket.id}`); + this.client.log.info(`${interaction.user.tag} removed ${member.user.tag} from ${channel.id}`); } }; diff --git a/src/commands/settings.js b/src/commands/settings.js index 1b9cdea..17d3170 100644 --- a/src/commands/settings.js +++ b/src/commands/settings.js @@ -1,201 +1,350 @@ +/* eslint-disable max-lines */ const Command = require('../modules/commands/command'); -const fetch = require('node-fetch'); const { - Message, // eslint-disable-line no-unused-vars - MessageAttachment + Interaction, // eslint-disable-line no-unused-vars + MessageEmbed } = require('discord.js'); -const { Validator } = require('jsonschema'); module.exports = class SettingsCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [ - i18n('commands.settings.aliases.config') - ], - args: [], description: i18n('commands.settings.description'), internal: true, name: i18n('commands.settings.name'), - permissions: ['MANAGE_GUILD'], - process_args: false + options: [ + { + description: i18n('commands.settings.options.categories.description'), + name: i18n('commands.settings.options.categories.name'), + options: [ + { + description: i18n('commands.settings.options.categories.options.create.description'), + name: i18n('commands.settings.options.categories.options.create.name'), + options: [ + { + description: i18n('commands.settings.options.categories.options.create.options.name.description'), + name: i18n('commands.settings.options.categories.options.create.options.name.name'), + required: true, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.create.options.roles.description'), + name: i18n('commands.settings.options.categories.options.create.options.roles.name'), + required: true, + type: Command.option_types.STRING + } + ], + type: Command.option_types.SUB_COMMAND + }, + { + description: i18n('commands.settings.options.categories.options.delete.description'), + name: i18n('commands.settings.options.categories.options.delete.name'), + options: [ + { + description: i18n('commands.settings.options.categories.options.delete.options.id.description'), + name: i18n('commands.settings.options.categories.options.delete.options.id.name'), + required: true, + type: Command.option_types.STRING + } + ], + type: Command.option_types.SUB_COMMAND + }, + { + description: i18n('commands.settings.options.categories.options.edit.description'), + name: i18n('commands.settings.options.categories.options.edit.name'), + options: [ + { + description: i18n('commands.settings.options.categories.options.edit.options.id.description'), + name: i18n('commands.settings.options.categories.options.edit.options.id.name'), + required: true, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.claiming.description'), + name: i18n('commands.settings.options.categories.options.edit.options.claiming.name'), + required: false, + type: Command.option_types.BOOLEAN + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.image.description'), + name: i18n('commands.settings.options.categories.options.edit.options.image.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.max_per_member.description'), + name: i18n('commands.settings.options.categories.options.edit.options.max_per_member.name'), + required: false, + type: Command.option_types.INTEGER + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.name.description'), + name: i18n('commands.settings.options.categories.options.edit.options.name.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.name_format.description'), + name: i18n('commands.settings.options.categories.options.edit.options.name_format.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.opening_message.description'), + name: i18n('commands.settings.options.categories.options.edit.options.opening_message.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.ping.description'), + name: i18n('commands.settings.options.categories.options.edit.options.ping.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.require_topic.description'), + name: i18n('commands.settings.options.categories.options.edit.options.require_topic.name'), + required: false, + type: Command.option_types.BOOLEAN + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.roles.description'), + name: i18n('commands.settings.options.categories.options.edit.options.roles.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.categories.options.edit.options.survey.description'), + name: i18n('commands.settings.options.categories.options.edit.options.survey.name'), + required: false, + type: Command.option_types.STRING + } + ], + type: Command.option_types.SUB_COMMAND + }, + { + description: i18n('commands.settings.options.categories.options.list.description'), + name: i18n('commands.settings.options.categories.options.list.name'), + type: Command.option_types.SUB_COMMAND + } + ], + type: Command.option_types.SUB_COMMAND_GROUP + }, + { + description: i18n('commands.settings.options.set.description'), + name: i18n('commands.settings.options.set.name'), + options: [ + { + description: i18n('commands.settings.options.set.options.close_button.description'), + name: i18n('commands.settings.options.set.options.close_button.name'), + required: false, + type: Command.option_types.BOOLEAN + }, + { + description: i18n('commands.settings.options.set.options.colour.description'), + name: i18n('commands.settings.options.set.options.colour.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.set.options.error_colour.description'), + name: i18n('commands.settings.options.set.options.error_colour.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.set.options.footer.description'), + name: i18n('commands.settings.options.set.options.footer.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.set.options.locale.description'), + name: i18n('commands.settings.options.set.options.locale.name'), + required: false, + type: Command.option_types.STRING + }, + { + description: i18n('commands.settings.options.set.options.log_messages.description'), + name: i18n('commands.settings.options.set.options.log_messages.name'), + required: false, + type: Command.option_types.BOOLEAN + }, + { + description: i18n('commands.settings.options.set.options.success_colour.description'), + name: i18n('commands.settings.options.set.options.success_colour.name'), + required: false, + type: Command.option_types.STRING + } + ], + type: Command.option_types.SUB_COMMAND + } + ], + permissions: ['MANAGE_GUILD'] }); - this.schema = require('./extra/settings.schema.json'); - this.v = new Validator(); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); - const attachments = [...message.attachments.values()]; - - if (attachments.length >= 1) { - - // load settings from json - this.client.log.info(`Downloading settings for "${message.guild.name}"`); - const data = await (await fetch(attachments[0].url)).json(); - - const { - valid, errors - } = this.v.validate(data, this.schema); - - if (!valid) { - this.client.log.warn('Settings validation error'); - return await message.channel.send({ content: i18n('commands.settings.response.invalid', errors.map(error => `\`${error.stack}\``).join(',\n')) }); - } - - settings.colour = data.colour; - settings.command_prefix = data.command_prefix; - settings.error_colour = data.error_colour; - settings.footer = data.footer; - settings.locale = data.locale; - settings.log_messages = data.log_messages; - settings.success_colour = data.success_colour; - settings.tags = data.tags; - await settings.save(); - - for (const c of data.categories) { - if (c.id) { - // existing category - const cat_row = await this.client.db.models.Category.findOne({ where: { id: c.id } }); - cat_row.claiming = c.claiming; - cat_row.image = c.image; - cat_row.max_per_member = c.max_per_member; - cat_row.name = c.name; - cat_row.name_format = c.name_format; - cat_row.opening_message = c.opening_message; - cat_row.opening_questions = c.opening_questions; - cat_row.ping = c.ping; - cat_row.require_topic = c.require_topic; - cat_row.roles = c.roles; - cat_row.survey = c.survey; - cat_row.save(); - - const cat_channel = await this.client.channels.fetch(c.id); - - if (cat_channel) { - if (cat_channel.name !== c.name) await cat_channel.setName(c.name, `Tickets category updated by ${message.author.tag}`); - - for (const r of c.roles) { - await cat_channel.permissionOverwrites.edit(r, { - ATTACH_FILES: true, - READ_MESSAGE_HISTORY: true, - SEND_MESSAGES: true, - VIEW_CHANNEL: true - }, `Tickets category updated by ${message.author.tag}`); + switch (interaction.options.getSubcommand()) { + case default_i18n('commands.settings.options.categories.options.create.name'): { + const name = interaction.options.getString(default_i18n('commands.settings.options.categories.options.create.options.name.name')); + const roles = interaction.options.getString(default_i18n('commands.settings.options.categories.options.create.options.roles.name'))?.replace(/\s/g, '').split(','); + const allowed_permissions = ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES']; + const cat_channel = await interaction.guild.channels.create(name, { + permissionOverwrites: [ + ...[ + { + deny: ['VIEW_CHANNEL'], + id: interaction.guild.roles.everyone + }, + { + allow: allowed_permissions, + id: this.client.user.id } - } - } else { - // create a new category - const allowed_permissions = ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES']; - const cat_channel = await message.guild.channels.create(c.name, { - permissionOverwrites: [ - ...[ - { - deny: ['VIEW_CHANNEL'], - id: message.guild.roles.everyone - }, - { - allow: allowed_permissions, - id: this.client.user.id - } - ], - ...c.roles.map(r => ({ - allow: allowed_permissions, - id: r - })) - ], - position: 1, - reason: `Tickets category created by ${message.author.tag}`, - type: 'GUILD_CATEGORY' - }); - - await this.client.db.models.Category.create({ - claiming: c.claiming, - guild: message.guild.id, - id: cat_channel.id, - image: c.image, - max_per_member: c.max_per_member, - name: c.name, - name_format: c.name_format, - opening_message: c.opening_message, - opening_questions: c.opening_questions, - ping: c.ping, - require_topic: c.require_topic, - roles: c.roles, - survey: c.survey - }); - } - } - - for (const survey in data.surveys) { - const survey_data = { - guild: message.guild.id, - name: survey - }; - const [s_row] = await this.client.db.models.Survey.findOrCreate({ - defaults: survey_data, - where: survey_data + ], + ...roles.map(r => ({ + allow: allowed_permissions, + id: r + })) + ], + position: 1, + reason: `Tickets category created by ${interaction.user.tag}`, + type: 'GUILD_CATEGORY' + }); + await this.client.db.models.Category.create({ + guild: interaction.guild.id, + id: cat_channel.id, + name, + roles + }); + await this.client.commands.updatePermissions(interaction.guild); + interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setTitle(i18n('commands.settings.response.category_created', name)) + ], + ephemeral: true + }); + break; + } + case default_i18n('commands.settings.options.categories.options.delete.name'): { + const category = await this.client.db.models.Category.findOne({ where: { id: interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name')) } }); + if (category) { + const channel = this.client.channels.cache.get(interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name'))); + if (channel) channel.delete(); + await category.destroy(); + interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setTitle(i18n('commands.settings.response.category_deleted', category.name)) + ], + ephemeral: true + }); + } else { + interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.settings.response.category_does_not_exist')) + ], + ephemeral: true }); - s_row.questions = data.surveys[survey]; - await s_row.save(); } - - this.client.log.success(`Updated guild settings for "${message.guild.name}"`); - return await message.channel.send({ content: i18n('commands.settings.response.updated') }); - } else { - // upload settings as json to be edited - - const categories = await this.client.db.models.Category.findAll({ where: { guild: message.guild.id } }); - - const surveys = await this.client.db.models.Survey.findAll({ where: { guild: message.guild.id } }); - - const data = { - categories: categories.map(c => ({ - claiming: c.claiming, - id: c.id, - image: c.image, - max_per_member: c.max_per_member, - name: c.name, - name_format: c.name_format, - opening_message: c.opening_message, - opening_questions: c.opening_questions, - ping: c.ping, - require_topic: c.require_topic, - roles: c.roles, - survey: c.survey - })), - colour: settings.colour, - command_prefix: settings.command_prefix, - error_colour: settings.error_colour, - footer: settings.footer, - locale: settings.locale, - log_messages: settings.log_messages, - success_colour: settings.success_colour, - surveys: {}, - tags: settings.tags - }; - - for (const survey in surveys) { - const { - name, questions - } = surveys[survey]; - data.surveys[name] = questions; + break; + } + case default_i18n('commands.settings.options.categories.options.edit.name'): { + const category = await this.client.db.models.Category.findOne({ where: { id: interaction.options.getString(default_i18n('commands.settings.options.categories.options.delete.options.id.name')) } }); + if (!category) { + return interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.settings.response.category_does_not_exist')) + ], + ephemeral: true + }); } - - const attachment = new MessageAttachment( - Buffer.from(JSON.stringify(data, null, 2)), - `Settings for ${message.guild.name}.json` - ); - - return await message.channel.send({ files: [attachment] }); + const claiming = interaction.options.getBoolean(default_i18n('commands.settings.options.categories.options.edit.options.claiming.name')); + const image = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.image.name')); + const max_per_member = interaction.options.getInteger(default_i18n('commands.settings.options.categories.options.edit.options.max_per_member.name')); + const name = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.name.name')); + const name_format = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.name_format.name')); + const opening_message = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.opening_message.name')); + const ping = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.ping.name')); + const require_topic = interaction.options.getBoolean(default_i18n('commands.settings.options.categories.options.edit.options.require_topic.name')); + const roles = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.roles.name')); + const survey = interaction.options.getString(default_i18n('commands.settings.options.categories.options.edit.options.survey.name')); + if (claiming !== null) category.set('claiming', claiming); + if (max_per_member !== null) category.set('max_per_member', max_per_member); + if (image !== null) category.set('image', image); + if (name !== null) category.set('name', name); + if (name_format !== null) category.set('name_format', name_format); + if (opening_message !== null) category.set('opening_message', opening_message); + if (ping !== null) category.set('ping', ping.replace(/\s/g, '').split(',')); + if (require_topic !== null) category.set('require_topic', require_topic); + if (roles !== null) category.set('roles', roles.replace(/\s/g, '').split(',')); + if (survey !== null) category.set('survey', survey); + await category.save(); + interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setTitle(i18n('commands.settings.response.category_updated', category.name)) + ], + ephemeral: true + }); + break; + } + case default_i18n('commands.settings.options.categories.options.list.name'): { + const categories = await this.client.db.models.Category.findAll({ where: { guild: interaction.guild.id } }); + await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setTitle(i18n('commands.settings.response.category_list')) + .setDescription(categories.map(c => `- ${c.name} (\`${c.id}\`)`).join('\n')) + ], + ephemeral: true + }); + break; + } + case default_i18n('commands.settings.options.set.name'): { + const close_button = interaction.options.getBoolean(default_i18n('commands.settings.options.set.options.close_button.name')); + const colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.colour.name')); + const error_colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.error_colour.name')); + const footer = interaction.options.getString(default_i18n('commands.settings.options.set.options.footer.name')); + const locale = interaction.options.getString(default_i18n('commands.settings.options.set.options.locale.name')); + const log_messages = interaction.options.getBoolean(default_i18n('commands.settings.options.set.options.log_messages.name')); + const success_colour = interaction.options.getString(default_i18n('commands.settings.options.set.options.success_colour.name')); + if (close_button !== null) settings.set('close_button', close_button); + if (colour !== null) settings.set('colour', colour.toUpperCase()); + if (error_colour !== null) settings.set('error_colour', error_colour.toUpperCase()); + if (footer !== null) settings.set('footer', footer); + if (locale !== null) settings.set('locale', locale); + if (log_messages !== null) settings.set('log_messages', log_messages); + if (success_colour !== null) settings.set('success_colour', success_colour.toUpperCase()); + await settings.save(); + interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setTitle(i18n('commands.settings.response.settings_updated')) + ], + ephemeral: true + }); + break; + } } } }; \ No newline at end of file diff --git a/src/commands/stats.js b/src/commands/stats.js index 91ad106..79a832d 100644 --- a/src/commands/stats.js +++ b/src/commands/stats.js @@ -1,7 +1,7 @@ const Command = require('../modules/commands/command'); const Keyv = require('keyv'); const { - Message, // eslint-disable-line no-unused-vars + Interaction, // eslint-disable-line no-unused-vars MessageEmbed } = require('discord.js'); @@ -9,12 +9,9 @@ module.exports = class StatsCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [], - args: [], description: i18n('commands.stats.description'), internal: true, name: i18n('commands.stats.name'), - process_args: false, staff_only: true }); @@ -22,25 +19,24 @@ module.exports = class StatsCommand extends Command { } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); const i18n = this.client.i18n.getLocale(settings.locale); const messages = await this.client.db.models.Message.findAndCountAll(); - let stats = await this.cache.get(message.guild.id); + let stats = await this.cache.get(interaction.guild.id); if (!stats) { - const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { guild: message.guild.id } }); + const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { guild: interaction.guild.id } }); stats = { // maths messages: settings.log_messages ? await messages.rows .reduce(async (acc, row) => (await this.client.db.models.Ticket.findOne({ where: { id: row.ticket } })) - .guild === message.guild.id + .guild === interaction.guild.id ? await acc + 1 : await acc, 0) : null, @@ -49,38 +45,51 @@ module.exports = class StatsCommand extends Command { : acc, 0) / tickets.count), tickets: tickets.count }; - await this.cache.set(message.guild.id, stats, 60 * 60 * 1000); // cache for an hour + await this.cache.set(interaction.guild.id, stats, 60 * 60 * 1000); // cache for an hour } const guild_embed = new MessageEmbed() .setColor(settings.colour) .setTitle(i18n('commands.stats.response.guild.title')) .setDescription(i18n('commands.stats.response.guild.description')) - .addField(i18n('commands.stats.fields.tickets'), stats.tickets, true) + .addField(i18n('commands.stats.fields.tickets'), String(stats.tickets), true) .addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', stats.response_time), true) - .setFooter(settings.footer, message.guild.iconURL()); + .setFooter(settings.footer, interaction.guild.iconURL()); - if (stats.messages) guild_embed.addField(i18n('commands.stats.fields.messages'), stats.messages, true); + if (stats.messages) guild_embed.addField(i18n('commands.stats.fields.messages'), String(stats.messages), true); - await message.channel.send({ - embeds: [ - guild_embed - ] - }); + const embeds = [guild_embed]; if (this.client.guilds.cache.size > 1) { - await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('commands.stats.response.global.title')) - .setDescription(i18n('commands.stats.response.global.description')) - .addField(i18n('commands.stats.fields.tickets'), stats.tickets, true) - .addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', stats.response_time), true) - .addField(i18n('commands.stats.fields.messages'), stats.messages, true) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); + let global = await this.cache.get('global'); + + if (!global) { + const tickets = await this.client.db.models.Ticket.findAndCountAll(); + global = { // maths + messages: settings.log_messages + ? await messages.count + : null, + response_time: Math.floor(tickets.rows.reduce((acc, row) => row.first_response + ? acc + ((Math.abs(new Date(row.createdAt) - new Date(row.first_response)) / 1000) / 60) + : acc, 0) / tickets.count), + tickets: tickets.count + }; + await this.cache.set('global', global, 60 * 60 * 1000); // cache for an hour + } + + const global_embed = new MessageEmbed() + .setColor(settings.colour) + .setTitle(i18n('commands.stats.response.global.title')) + .setDescription(i18n('commands.stats.response.global.description')) + .addField(i18n('commands.stats.fields.tickets'), String(global.tickets), true) + .addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', global.response_time), true) + .setFooter(settings.footer, interaction.guild.iconURL()); + + if (stats.messages) global_embed.addField(i18n('commands.stats.fields.messages'), String(global.messages), true); + + embeds.push(global_embed); } + + await interaction.reply({ embeds }); } }; diff --git a/src/commands/survey.js b/src/commands/survey.js index 313d532..86be0a7 100644 --- a/src/commands/survey.js +++ b/src/commands/survey.js @@ -13,38 +13,44 @@ module.exports = class SurveyCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [ - i18n('commands.survey.aliases.surveys') - ], - args: [ - { - description: i18n('commands.survey.args.survey.description'), - example: i18n('commands.survey.args.survey.example'), - name: i18n('commands.survey.args.survey.name'), - required: false - } - ], description: i18n('commands.survey.description'), internal: true, name: i18n('commands.survey.name'), - process_args: false, + options: async guild => { + const surveys = await this.client.db.models.Survey.findAll({ where: { guild: guild.id } }); + return [ + { + choices: surveys.map(survey => ({ + name: survey.name, + value: survey.name + })), + description: i18n('commands.survey.options.survey.description'), + name: i18n('commands.survey.options.survey.name'), + required: true, + type: Command.option_types.STRING + } + ]; + }, staff_only: true + }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); + const name = interaction.options.getString(default_i18n('commands.survey.options.survey.name')); + const survey = await this.client.db.models.Survey.findOne({ where: { - guild: message.guild.id, - name: args + guild: interaction.guild.id, + name } }); @@ -55,7 +61,6 @@ module.exports = class SurveyCommand extends Command { const users = new Set(); - for (const i in responses) { const ticket = await this.client.db.models.Ticket.findOne({ where: { id: responses[i].ticket } }); users.add(ticket.creator); @@ -85,19 +90,23 @@ module.exports = class SurveyCommand extends Command { `${survey.name}.html` ); - return await message.channel.send({ files: [attachment] }); + return await interaction.reply({ + ephemeral: true, + files: [attachment] + }); } else { - const surveys = await this.client.db.models.Survey.findAll({ where: { guild: message.guild.id } }); + const surveys = await this.client.db.models.Survey.findAll({ where: { guild: interaction.guild.id } }); const list = surveys.map(s => `❯ **\`${s.name}\`**`); - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.colour) .setTitle(i18n('commands.survey.response.list.title')) .setDescription(list.join('\n')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } } diff --git a/src/commands/tag.js b/src/commands/tag.js index 805375b..e356e22 100644 --- a/src/commands/tag.js +++ b/src/commands/tag.js @@ -1,132 +1,72 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars + Interaction, // eslint-disable-line no-unused-vars MessageEmbed } = require('discord.js'); -const { parseArgsStringToArgv: argv } = require('string-argv'); -const parseArgs = require('command-line-args'); module.exports = class TagCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [ - i18n('commands.tag.aliases.faq'), - i18n('commands.tag.aliases.t'), - i18n('commands.tag.aliases.tags') - ], - args: [ - { - description: i18n('commands.tag.args.command.description'), - example: i18n('commands.tag.args.tag.example'), - name: i18n('commands.tag.args.tag.name'), - required: false - } - ], description: i18n('commands.tag.description'), internal: true, name: i18n('commands.tag.name'), - process_args: false, + options: async guild => { + const settings = await client.utils.getSettings(guild.id); + return Object.keys(settings.tags).map(tag => ({ + description: settings.tags[tag].substring(0, 100), + name: tag, + options: [...settings.tags[tag].matchAll(/(? ({ + description: match[1], + name: match[1], + required: true, + type: Command.option_types.STRING + })), + required: false, + type: Command.option_types.SUB_COMMAND + })); + }, staff_only: true }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); const i18n = this.client.i18n.getLocale(settings.locale); - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } }); + const tag_name = interaction.options.getSubcommand(); + const tag = settings.tags[tag_name]; + const args = interaction.options.data[0]?.options; - args = args.split(/\s/g); // convert to an array - const tag_name = args.shift(); // shift the first element - args = args.join(' '); // convert back to a string with the first word removed - - if (tag_name && settings.tags[tag_name]) { - const tag = settings.tags[tag_name]; - const placeholders = [...tag.matchAll(/(? p[1]); - const requires_ticket = placeholders.some(p => p.startsWith('ticket.')); - - if (requires_ticket && !t_row) { - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.tag.response.not_a_ticket.title')) - .setDescription(i18n('commands.tag.response.not_a_ticket.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } - - const expected = placeholders - .filter(p => p.startsWith(':')) - .map(p => ({ - name: p.substr(1, p.length), - type: String - })); - - if (expected.length >= 1) { - try { - args = parseArgs(expected, { argv: argv(args) }); - } catch (error) { - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.tag.response.error')) - .setDescription(`\`\`\`${error.message}\`\`\``) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } - } else { - args = {}; - } - - for (const p of expected) { - if (!args[p.name]) { - const list = expected.map(p => `\`${p.name}\``); - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('commands.tag.response.error')) - .setDescription(i18n('commands.tag.response.missing', list.join(', '))) - .setFooter(settings.footer, message.guild.iconURL()) - ] - }); - } - } - - if (requires_ticket) { - args.ticket = t_row.toJSON(); - args.ticket.topic = t_row.topic ? this.client.cryptr.decrypt(t_row.topic) : null; - } - - // note that this regex is slightly different to the other - const text = tag.replace(/(? this.client.i18n.resolve(args, $1)); - return await message.channel.send({ + if (tag) { + const text = tag.replace(/(? { + const arg = args.find(arg => arg.name === $1); + return arg ? arg.value : $; + }); + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.colour) .setDescription(text) - ] + ], + ephemeral: false }); } else { const list = Object.keys(settings.tags).map(t => `❯ **\`${t}\`**`); - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.colour) .setTitle(i18n('commands.tag.response.list.title')) .setDescription(list.join('\n')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } diff --git a/src/commands/topic.js b/src/commands/topic.js index 9da4d42..7fa6139 100644 --- a/src/commands/topic.js +++ b/src/commands/topic.js @@ -1,6 +1,6 @@ const Command = require('../modules/commands/command'); const { - Message, // eslint-disable-line no-unused-vars + Interaction, // eslint-disable-line no-unused-vars MessageEmbed } = require('discord.js'); @@ -8,55 +8,56 @@ module.exports = class TopicCommand extends Command { constructor(client) { const i18n = client.i18n.getLocale(client.config.locale); super(client, { - aliases: [], - args: [ - { - description: i18n('commands.topic.args.new_topic.description'), - example: i18n('commands.topic.args.new_topic.example'), - name: i18n('commands.topic.args.new_topic.name'), - required: true - } - ], description: i18n('commands.topic.description'), internal: true, name: i18n('commands.topic.name'), - process_args: false + options: [ + { + description: i18n('commands.topic.options.new_topic.description'), + name: i18n('commands.topic.options.new_topic.name'), + required: true, + type: Command.option_types.STRING + } + ] }); } /** - * @param {Message} message - * @param {string} args + * @param {Interaction} interaction * @returns {Promise} */ - async execute(message, args) { - const settings = await this.client.utils.getSettings(message.guild); + async execute(interaction) { + const settings = await this.client.utils.getSettings(interaction.guild.id); + const default_i18n = this.client.i18n.getLocale(this.client.config.defaults.locale); // command properties could be in a different locale const i18n = this.client.i18n.getLocale(settings.locale); - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } }); + const topic = interaction.options.getString(default_i18n('commands.topic.options.new_topic.name')); + + const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); if (!t_row) { - return await message.channel.send({ + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('commands.topic.response.not_a_ticket.title')) .setDescription(i18n('commands.topic.response.not_a_ticket.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true }); } - await t_row.update({ topic: this.client.cryptr.encrypt(args) }); + await t_row.update({ topic: this.client.cryptr.encrypt(topic) }); - const member = await message.guild.members.fetch(t_row.creator); - /* await */message.channel.setTopic(`${member} | ${args}`, { reason: 'User updated ticket topic' }); + const member = await interaction.guild.members.fetch(t_row.creator); + interaction.channel.setTopic(`${member} | ${topic}`, { reason: 'User updated ticket topic' }); const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); const description = cat_row.opening_message .replace(/{+\s?(user)?name\s?}+/gi, member.displayName) .replace(/{+\s?(tag|ping|mention)?\s?}+/gi, member.user.toString()); - const opening_message = await message.channel.messages.fetch(t_row.opening_message); + const opening_message = await interaction.channel.messages.fetch(t_row.opening_message); await opening_message.edit({ embeds: [ @@ -64,22 +65,23 @@ module.exports = class TopicCommand extends Command { .setColor(settings.colour) .setAuthor(member.user.username, member.user.displayAvatarURL()) .setDescription(description) - .addField(i18n('ticket.opening_message.fields.topic'), args) - .setFooter(settings.footer, message.guild.iconURL()) + .addField(i18n('ticket.opening_message.fields.topic'), topic) + .setFooter(settings.footer, interaction.guild.iconURL()) ] }); - await message.channel.send({ + await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.success_colour) - .setAuthor(message.author.username, message.author.displayAvatarURL()) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) .setTitle(i18n('commands.topic.response.changed.title')) .setDescription(i18n('commands.topic.response.changed.description')) - .setFooter(settings.footer, message.guild.iconURL()) - ] + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: false }); - this.client.log.info(`${message.author.tag} changed the topic of ${message.channel.id}`); + this.client.log.info(`${interaction.user.tag} changed the topic of #${interaction.channel.name}`); } }; diff --git a/src/database/index.js b/src/database/index.js index 5784cf2..9df4cbd 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -39,7 +39,8 @@ module.exports = async client => { logging: text => client.log.debug(text), storage: path('./user/database.sqlite') }); - client.log.warn('SQLite is not sufficient for a production environment if you want to use ticket archives. You should disable "log_messages" in your servers\' settings or use a different database.'); + client.config.defaults.log_messages = false; + client.log.warn('Message logging is disabled due to insufficient database'); } else { client.log.info(`Connecting to ${types[type].name} database...`); sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, { @@ -66,7 +67,7 @@ module.exports = async client => { require(`./models/${model}`)(client, sequelize); } - await sequelize.sync({ alter: { drop: false } }); + await sequelize.sync({ alter: true }); return sequelize; }; \ No newline at end of file diff --git a/src/database/models/category.model.js b/src/database/models/category.model.js index c346133..d9887e8 100644 --- a/src/database/models/category.model.js +++ b/src/database/models/category.model.js @@ -86,5 +86,8 @@ module.exports = ({ config }, sequelize) => { allowNull: true, type: DataTypes.STRING } - }, { tableName: DB_TABLE_PREFIX + 'categories' }); + }, { + paranoid: true, + tableName: DB_TABLE_PREFIX + 'categories' + }); }; \ No newline at end of file diff --git a/src/database/models/guild.model.js b/src/database/models/guild.model.js index 5e95f1d..bf8bfbf 100644 --- a/src/database/models/guild.model.js +++ b/src/database/models/guild.model.js @@ -3,7 +3,10 @@ module.exports = ({ config }, sequelize) => { const { DB_TABLE_PREFIX } = process.env; sequelize.define('Guild', { blacklist: { - defaultValue: [], + defaultValue: { + members: [], + roles: [] + }, get() { const raw_value = this.getDataValue('blacklist'); return raw_value @@ -14,14 +17,14 @@ module.exports = ({ config }, sequelize) => { }, type: DataTypes.JSON }, + close_button: { + defaultValue: false, + type: DataTypes.BOOLEAN + }, colour: { defaultValue: config.defaults.colour, type: DataTypes.STRING }, - command_prefix: { - defaultValue: config.defaults.command_prefix, - type: DataTypes.STRING - }, error_colour: { defaultValue: 'RED', type: DataTypes.STRING diff --git a/src/database/models/panel.model.js b/src/database/models/panel.model.js index aba5b3c..ea36aef 100644 --- a/src/database/models/panel.model.js +++ b/src/database/models/panel.model.js @@ -2,17 +2,9 @@ const { DataTypes } = require('sequelize'); module.exports = (client, sequelize) => { const { DB_TABLE_PREFIX } = process.env; sequelize.define('Panel', { - categories: { - allowNull: false, - get() { - const raw_value = this.getDataValue('categories'); - return raw_value - ? typeof raw_value === 'string' - ? JSON.parse(raw_value) - : raw_value - : null; - }, - type: DataTypes.JSON + category: { + allowNull: true, + type: DataTypes.CHAR(19) }, channel: { allowNull: false, @@ -25,10 +17,6 @@ module.exports = (client, sequelize) => { model: DB_TABLE_PREFIX + 'guilds' }, type: DataTypes.CHAR(19) - }, - message: { - allowNull: false, - type: DataTypes.CHAR(19) } }, { tableName: DB_TABLE_PREFIX + 'panels' }); }; \ No newline at end of file diff --git a/src/listeners/debug.js b/src/listeners/debug.js index cf54593..ad62c68 100644 --- a/src/listeners/debug.js +++ b/src/listeners/debug.js @@ -6,7 +6,7 @@ module.exports = class DebugEventListener extends EventListener { } async execute(data) { - if (this.client.config.debug) { + if (this.client.config.developer.debug) { this.client.log.debug(data); } } diff --git a/src/listeners/guildCreate.js b/src/listeners/guildCreate.js index 355ec00..0d01ae5 100644 --- a/src/listeners/guildCreate.js +++ b/src/listeners/guildCreate.js @@ -7,5 +7,6 @@ module.exports = class GuildCreateEventListener extends EventListener { async execute(guild) { this.client.log.info(`Added to "${guild.name}"`); + this.client.commands.publish(guild); } }; \ No newline at end of file diff --git a/src/listeners/guildDelete.js b/src/listeners/guildDelete.js index 6d06581..a4b55cd 100644 --- a/src/listeners/guildDelete.js +++ b/src/listeners/guildDelete.js @@ -7,6 +7,5 @@ module.exports = class GuildDeleteEventListener extends EventListener { async execute(guild) { this.client.log.info(`Removed from "${guild.name}"`); - await guild.deleteSettings(); } }; \ No newline at end of file diff --git a/src/listeners/interactionCreate.js b/src/listeners/interactionCreate.js new file mode 100644 index 0000000..c1449c3 --- /dev/null +++ b/src/listeners/interactionCreate.js @@ -0,0 +1,323 @@ +const EventListener = require('../modules/listeners/listener'); +const { + Interaction, // eslint-disable-line no-unused-vars + MessageActionRow, + MessageButton, + MessageEmbed +} = require('discord.js'); + +module.exports = class InteractionCreateEventListener extends EventListener { + constructor(client) { + super(client, { event: 'interactionCreate' }); + } + + /** + * @param {Interaction} interaction + */ + async execute(interaction) { + this.client.log.debug(interaction); + + const settings = await this.client.utils.getSettings(interaction.guild.id); + const i18n = this.client.i18n.getLocale(settings.locale); + + const blacklisted = settings.blacklist.members.includes[interaction.user.id] || + interaction.member?.roles.cache?.some(role => settings.blacklist.roles.includes(role)); + if (blacklisted) { + return interaction.reply({ + content: i18n('blacklisted'), + ephemeral: true + }); + } + + const handlePanel = async id => { + const cat_row = await this.client.db.models.Category.findOne({ where: { id } }); + + if (!cat_row) { + this.client.log.warn('Could not find a category with the ID given by a panel interaction'); + return interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('command_execution_error.title')) + .setDescription(i18n('command_execution_error.description')) + ], + ephemeral: true + }); + } + + const tickets = await this.client.db.models.Ticket.findAndCountAll({ + where: { + category: cat_row.id, + creator: interaction.user.id, + open: true + } + }); + + if (tickets.count >= cat_row.max_per_member) { + if (cat_row.max_per_member === 1) { + return interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.has_a_ticket.title')) + .setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } else { + const list = tickets.rows.map(row => { + if (row.topic) { + const description = row.topic.substring(0, 30); + const ellipses = row.topic.length > 30 ? '...' : ''; + return `<#${row.id}>: \`${description}${ellipses}\``; + } else { + return `<#${row.id}>`; + } + }); + return interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) + .setDescription(i18n('commands.new.response.max_tickets.description', list.join('\n'))) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } + } else { + try { + const t_row = await this.client.tickets.create(interaction.guild.id, interaction.user.id, id); + return interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.created.title')) + .setDescription(i18n('commands.new.response.created.description', `<#${t_row.id}>`)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } catch (error) { + this.client.log.error(error); + return interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.new.response.error.title')) + .setDescription(error.message) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } + } + }; + + if (interaction.isCommand()) { + // handle slash commands + this.client.commands.handle(interaction); + } else if (interaction.isButton()) { + if (interaction.customId.startsWith('panel.single')) { + // handle single-category panels + handlePanel(interaction.customId.split(':')[1]); + } else if (interaction.customId.startsWith('ticket.claim')) { + // handle ticket claiming + if (!(await this.client.utils.isStaff(interaction.member))) return; + const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); + await t_row.update({ claimed_by: interaction.user.id }); + await interaction.channel.permissionOverwrites.edit(interaction.user.id, { VIEW_CHANNEL: true }, `Ticket claimed by ${interaction.user.tag}`); + + const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); + + for (const role of cat_row.roles) { + await interaction.channel.permissionOverwrites.edit(role, { VIEW_CHANNEL: false }, `Ticket claimed by ${interaction.user.tag}`); + } + + this.client.log.info(`${interaction.user.tag} has claimed "${interaction.channel.name}" in "${interaction.guild.name}"`); + + await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('ticket.claimed.title')) + .setDescription(i18n('ticket.claimed.description', interaction.member.toString())) + .setFooter(settings.footer, interaction.guild.iconURL()) + ] + }); + + const components = new MessageActionRow(); + + if (cat_row.claiming) { + components.addComponents( + new MessageButton() + .setCustomId('ticket.unclaim') + .setLabel(i18n('ticket.unclaim')) + .setEmoji('♻️') + .setStyle('SECONDARY') + ); + } + + if (settings.close_button) { + components.addComponents( + new MessageButton() + .setCustomId('ticket.close') + .setLabel(i18n('ticket.close')) + .setEmoji('✖️') + .setStyle('DANGER') + ); + } + + await interaction.message.edit({ components: [components] }); + } else if (interaction.customId.startsWith('ticket.unclaim')) { + // handle ticket unclaiming + if (!(await this.client.utils.isStaff(interaction.member))) return; + const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); + await t_row.update({ claimed_by: null }); + + await interaction.channel.permissionOverwrites.delete(interaction.user.id, `Ticket released by ${interaction.user.tag}`); + + const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); + + for (const role of cat_row.roles) { + await interaction.channel.permissionOverwrites.edit(role, { VIEW_CHANNEL: true }, `Ticket released by ${interaction.user.tag}`); + } + + this.client.log.info(`${interaction.user.tag} has released "${interaction.channel.name}" in "${interaction.guild.name}"`); + + await interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('ticket.released.title')) + .setDescription(i18n('ticket.released.description', interaction.member.toString())) + .setFooter(settings.footer, interaction.guild.iconURL()) + ] + }); + + const components = new MessageActionRow(); + + if (cat_row.claiming) { + components.addComponents( + new MessageButton() + .setCustomId('ticket.claim') + .setLabel(i18n('ticket.claim')) + .setEmoji('🙌') + .setStyle('SECONDARY') + ); + } + + if (settings.close_button) { + components.addComponents( + new MessageButton() + .setCustomId('ticket.close') + .setLabel(i18n('ticket.close')) + .setEmoji('✖️') + .setStyle('DANGER') + ); + } + + await interaction.message.edit({ components: [components] }); + } else if (interaction.customId.startsWith('ticket.close')) { + // handle ticket close button + const t_row = await this.client.db.models.Ticket.findOne({ where: { id: interaction.channel.id } }); + await interaction.reply({ + components: [ + new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId(`confirm_close:${interaction.id}`) + .setLabel(i18n('commands.close.response.confirm.buttons.confirm')) + .setEmoji('✅') + .setStyle('SUCCESS') + ) + .addComponents( + new MessageButton() + .setCustomId(`cancel_close:${interaction.id}`) + .setLabel(i18n('commands.close.response.confirm.buttons.cancel')) + .setEmoji('❌') + .setStyle('SECONDARY') + ) + ], + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setTitle(i18n('commands.close.response.confirm.title')) + .setDescription(settings.log_messages ? i18n('commands.close.response.confirm.description_with_archive') : i18n('commands.close.response.confirm.description')) + .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 30)), interaction.guild.iconURL()) + ], + ephemeral: true + }); + + + const filter = i => i.user.id === interaction.user.id && i.customId.includes(interaction.id); + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 30000 + }); + + collector.on('collect', async i => { + await i.deferUpdate(); + + if (i.customId === `confirm_close:${interaction.id}`) { + await this.client.tickets.close(t_row.id, interaction.user.id, interaction.guild.id); + await i.editReply({ + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setTitle(i18n('commands.close.response.closed.title', t_row.number)) + .setDescription(i18n('commands.close.response.closed.description', t_row.number)) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } else { + await i.editReply({ + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setTitle(i18n('commands.close.response.canceled.title')) + .setDescription(i18n('commands.close.response.canceled.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } + + collector.stop(); + }); + + collector.on('end', async collected => { + if (collected.size === 0) { + await interaction.editReply({ + components: [], + embeds: [ + new MessageEmbed() + .setColor(settings.error_colour) + .setAuthor(interaction.user.username, interaction.user.displayAvatarURL()) + .setTitle(i18n('commands.close.response.confirmation_timeout.title')) + .setDescription(i18n('commands.close.response.confirmation_timeout.description')) + .setFooter(settings.footer, interaction.guild.iconURL()) + ], + ephemeral: true + }); + } + }); + } + } else if (interaction.isSelectMenu()) { + if (interaction.customId.startsWith('panel.multiple')) { + // handle multi-category panels and new command + handlePanel(interaction.values[0]); + } + } + } +}; \ No newline at end of file diff --git a/src/listeners/messageCreate.js b/src/listeners/messageCreate.js index f1de5d6..fbab7a4 100644 --- a/src/listeners/messageCreate.js +++ b/src/listeners/messageCreate.js @@ -1,6 +1,9 @@ const EventListener = require('../modules/listeners/listener'); - -const { MessageEmbed } = require('discord.js'); +const fetch = require('node-fetch'); +const { + MessageAttachment, + MessageEmbed +} = require('discord.js'); module.exports = class MessageCreateEventListener extends EventListener { constructor(client) { @@ -10,30 +13,107 @@ module.exports = class MessageCreateEventListener extends EventListener { async execute(message) { if (!message.guild) return; - const settings = await this.client.utils.getSettings(message.guild); + const settings = await this.client.utils.getSettings(message.guild.id); const i18n = this.client.i18n.getLocale(settings.locale); const t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } }); if (t_row) { - if (settings.log_messages && !message.system) this.client.tickets.archives.addMessage(message); // add the message to the archives (if it is in a ticket channel) + const should_log_message = process.env.DB_TYPE.toLowerCase() !== 'sqlite' && settings.log_messages && !message.system; + if (should_log_message) this.client.tickets.archives.addMessage(message); // add the message to the archives (if it is in a ticket channel) const ignore = [this.client.user.id, t_row.creator]; if (!t_row.first_response && !ignore.includes(message.author.id)) t_row.first_response = new Date(); t_row.last_message = new Date(); await t_row.save(); + } else if (message.content.startsWith('tickets/')) { + if (!message.member.permissions.has('MANAGE_GUILD')) return; + + const match = message.content.toLowerCase().match(/tickets\/(\w+)/i); + + if (!match) return; + + switch (match[1]) { + case 'surveys': { + const attachments = [...message.attachments.values()]; + if (attachments.length >= 1) { + this.client.log.info(`Downloading surveys for "${message.guild.name}"`); + const data = await (await fetch(attachments[0].url)).json(); + for (const survey in data) { + const survey_data = { + guild: message.guild.id, + name: survey + }; + const [s_row] = await this.client.db.models.Survey.findOrCreate({ + defaults: survey_data, + where: survey_data + }); + s_row.questions = data[survey]; + await s_row.save(); + } + this.client.log.success(`Updated surveys for "${message.guild.name}"`); + message.channel.send({ content: i18n('commands.settings.response.settings_updated') }); + } else { + const surveys = await this.client.db.models.Survey.findAll({ where: { guild: message.guild.id } }); + const data = {}; + + for (const survey in surveys) { + const { + name, questions + } = surveys[survey]; + data[name] = questions; + } + + const attachment = new MessageAttachment( + Buffer.from(JSON.stringify(data, null, 2)), + 'surveys.json' + ); + message.channel.send({ files: [attachment] }); + } + break; + } + case 'tags': { + const attachments = [...message.attachments.values()]; + if (attachments.length >= 1) { + this.client.log.info(`Downloading tags for "${message.guild.name}"`); + const data = await (await fetch(attachments[0].url)).json(); + settings.tags = data; + await settings.save(); + this.client.log.success(`Updated tags for "${message.guild.name}"`); + this.client.commands.publish(message.guild); + message.channel.send({ content: i18n('commands.settings.response.settings_updated') }); + } else { + const list = Object.keys(settings.tags).map(t => `❯ **\`${t}\`**`); + const attachment = new MessageAttachment( + Buffer.from(JSON.stringify(settings.tags, null, 2)), + 'tags.json' + ); + return await message.channel.send({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setTitle(i18n('commands.tag.response.list.title')) + .setDescription(list.join('\n')) + .setFooter(settings.footer, message.guild.iconURL()) + ], + files: [attachment] + }); + } + break; + } + } } else { if (message.author.bot) return; const p_row = await this.client.db.models.Panel.findOne({ where: { channel: message.channel.id } }); - if (p_row && typeof p_row.categories === 'string') { - // handle reaction-less panel + if (p_row) { + // handle message panels await message.delete(); - const cat_row = await this.client.db.models.Category.findOne({ where: { id: p_row.categories } }); + const cat_row = await this.client.db.models.Category.findOne({ where: { id: p_row.category } }); const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { @@ -72,7 +152,7 @@ module.exports = class MessageCreateEventListener extends EventListener { .setColor(settings.error_colour) .setAuthor(message.author.username, message.author.displayAvatarURL()) .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) - .setDescription(i18n('commands.new.response.max_tickets.description', settings.command_prefix, list.join('\n'))) + .setDescription(i18n('commands.new.response.max_tickets.description', list.join('\n'))) .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.author.iconURL()); try { response = await message.author.send({ embeds: [embed] }); @@ -105,8 +185,6 @@ module.exports = class MessageCreateEventListener extends EventListener { } } } - - this.client.commands.handle(message); // pass the message to the command handler } }; diff --git a/src/listeners/messageDelete.js b/src/listeners/messageDelete.js index d817a59..540a2e9 100644 --- a/src/listeners/messageDelete.js +++ b/src/listeners/messageDelete.js @@ -8,7 +8,7 @@ module.exports = class MessageDeleteEventListener extends EventListener { async execute(message) { if (!message.guild) return; - const settings = await this.client.utils.getSettings(message.guild); + const settings = await this.client.utils.getSettings(message.guild.id); if (settings.log_messages && !message.system) this.client.tickets.archives.deleteMessage(message); // mark the message as deleted in the database (if it exists) } diff --git a/src/listeners/messageReactionAdd.js b/src/listeners/messageReactionAdd.js deleted file mode 100644 index 0ae9fda..0000000 --- a/src/listeners/messageReactionAdd.js +++ /dev/null @@ -1,164 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -const { MessageEmbed } = require('discord.js'); - -module.exports = class MessageReactionAddEventListener extends EventListener { - constructor(client) { - super(client, { event: 'messageReactionAdd' }); - } - - async execute(reaction, user) { - - if (reaction.partial) { - try { - await reaction.fetch(); - } catch (error) { - return this.client.log.error(error); - } - } - - if (user.partial) { - try { - await user.fetch(); - } catch (error) { - return this.client.log.error(error); - } - } - - if (user.id === this.client.user.id) return; - - const guild = reaction.message.guild; - if (!guild) return; - - const settings = await this.client.utils.getSettings(guild); - const i18n = this.client.i18n.getLocale(settings.locale); - - const channel = reaction.message.channel; - const member = await guild.members.fetch(user.id); - - if (settings.blacklist.includes(user.id)) { - return this.client.log.info(`Ignoring blacklisted member ${user.tag}`); - } else { - settings.blacklist.forEach(element => { - if (guild.roles.cache.has(element) && member.roles.cache.has(element)) { - return this.client.log.info(`Ignoring member ${user.tag} with blacklisted role`); - } - }); - } - - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: channel.id } }); - - if (t_row && t_row.opening_message === reaction.message.id) { - if (reaction.emoji.name === '🙌' && await this.client.utils.isStaff(member)) { - // ticket claiming - - await t_row.update({ claimed_by: member.user.id }); - - await channel.permissionOverwrites.edit(member.user.id, { VIEW_CHANNEL: true }, `Ticket claimed by ${member.user.tag}`); - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); - - for (const role of cat_row.roles) { - await channel.permissionOverwrites.edit(role, { VIEW_CHANNEL: false }, `Ticket claimed by ${member.user.tag}`); - } - - this.client.log.info(`${member.user.tag} has claimed "${channel.name}" in "${guild.name}"`); - - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('ticket.claimed.title')) - .setDescription(i18n('ticket.claimed.description', member.toString())) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - } else { - await reaction.users.remove(user.id); - } - } else { - const p_row = await this.client.db.models.Panel.findOne({ where: { message: reaction.message.id } }); - - if (p_row && typeof p_row.categories !== 'string') { - // panels - await reaction.users.remove(user.id); - - const category_id = p_row.categories[reaction.emoji.name]; - if (!category_id) return; - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: category_id } }); - - const tickets = await this.client.db.models.Ticket.findAndCountAll({ - where: { - category: cat_row.id, - creator: user.id, - open: true - } - }); - - let response; - - if (tickets.count >= cat_row.max_per_member) { - if (cat_row.max_per_member === 1) { - const embed = new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(user.username, user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.has_a_ticket.title')) - .setDescription(i18n('commands.new.response.has_a_ticket.description', tickets.rows[0].id)) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), guild.iconURL()); - try { - response = await user.send({ embeds: [embed] }); - } catch { - response = await channel.send({ embeds: [embed] }); - } - } else { - const list = tickets.rows.map(row => { - if (row.topic) { - const description = row.topic.substring(0, 30); - const ellipses = row.topic.length > 30 ? '...' : ''; - return `<#${row.id}>: \`${description}${ellipses}\``; - } else { - return `<#${row.id}>`; - } - }); - const embed = new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(user.username, user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.max_tickets.title', tickets.count)) - .setDescription(i18n('commands.new.response.max_tickets.description', settings.command_prefix, list.join('\n'))) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), user.iconURL()); - try { - response = await user.send({ embeds: [embed] }); - } catch { - response = await channel.send({ embeds: [embed] }); - } - } - } else { - try { - await this.client.tickets.create(guild.id, user.id, cat_row.id); - } catch (error) { - const embed = new MessageEmbed() - .setColor(settings.error_colour) - .setAuthor(user.username, user.displayAvatarURL()) - .setTitle(i18n('commands.new.response.error.title')) - .setDescription(error.message) - .setFooter(this.client.utils.footer(settings.footer, i18n('message_will_be_deleted_in', 15)), guild.iconURL()); - try { - response = await user.send({ embeds: [embed] }); - } catch { - response = await channel.send({ embeds: [embed] }); - } - } - } - - if (response) { - setTimeout(async () => { - await response.delete(); - }, 15000); - } - } - } - - } -}; diff --git a/src/listeners/messageReactionRemove.js b/src/listeners/messageReactionRemove.js deleted file mode 100644 index 6d72dbe..0000000 --- a/src/listeners/messageReactionRemove.js +++ /dev/null @@ -1,72 +0,0 @@ -const EventListener = require('../modules/listeners/listener'); - -const { MessageEmbed } = require('discord.js'); - -module.exports = class MessageReactionRemoveEventListener extends EventListener { - constructor(client) { - super(client, { event: 'messageReactionRemove' }); - } - - async execute(reaction, user) { - // release (unclaim) ticket - if (reaction.partial) { - try { - await reaction.fetch(); - } catch (error) { - return this.client.log.error(error); - } - } - - if (user.partial) { - try { - await user.fetch(); - } catch (error) { - return this.client.log.error(error); - } - } - - if (user.id === this.client.user.id) return; - - const guild = reaction.message.guild; - if (!guild) return; - - const settings = await this.client.utils.getSettings(guild); - const i18n = this.client.i18n.getLocale(settings.locale); - - const channel = reaction.message.channel; - const member = await guild.members.fetch(user.id); - - const t_row = await this.client.db.models.Ticket.findOne({ where: { id: channel.id } }); - - if (t_row && t_row.opening_message === reaction.message.id) { - if (reaction.emoji.name === '🙌' && await this.client.utils.isStaff(member)) { - // ticket claiming - - await t_row.update({ claimed_by: null }); - - await channel.permissionOverwrites - .get(member.user.id) - ?.delete(`Ticket released by ${member.user.tag}`); - - const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); - - for (const role of cat_row.roles) { - await channel.permissionOverwrites.edit(role, { VIEW_CHANNEL: true }, `Ticket released by ${member.user.tag}`); - } - - this.client.log.info(`${member.user.tag} has released "${channel.name}" in "${guild.name}"`); - - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setAuthor(member.user.username, member.user.displayAvatarURL()) - .setTitle(i18n('ticket.released.title')) - .setDescription(i18n('ticket.released.description', member.toString())) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - } - } - } -}; diff --git a/src/listeners/messageUpdate.js b/src/listeners/messageUpdate.js index 43481a1..9f06053 100644 --- a/src/listeners/messageUpdate.js +++ b/src/listeners/messageUpdate.js @@ -16,7 +16,7 @@ module.exports = class MessageUpdateEventListener extends EventListener { if (!newm.guild) return; - const settings = await this.client.utils.getSettings(newm.guild); + const settings = await this.client.utils.getSettings(newm.guild.id); if (settings.log_messages && !newm.system) this.client.tickets.archives.updateMessage(newm); // update the message in the database } diff --git a/src/listeners/ready.js b/src/listeners/ready.js index 3230b54..dc01439 100644 --- a/src/listeners/ready.js +++ b/src/listeners/ready.js @@ -11,9 +11,10 @@ module.exports = class ReadyEventListener extends EventListener { async execute() { this.client.log.success(`Connected to Discord as "${this.client.user.tag}"`); + this.client.log.info('Loading commands'); this.client.commands.load(); // load internal commands - this.client.plugins.plugins.forEach(p => p.load()); // call load function for each plugin + this.client.commands.publish(); // send commands to discord if (this.client.config.presence.presences.length > 1) { const { selectPresence } = require('../utils/discord'); diff --git a/src/locales/en-GB.json b/src/locales/en-GB.json index 59b4a7e..3fdbf12 100644 --- a/src/locales/en-GB.json +++ b/src/locales/en-GB.json @@ -1,488 +1,600 @@ { - "bot": { - "missing_permissions": { - "description": "Discord Tickets requires the following permissions:\n%s", - "title": "⚠️" - }, - "version": "[Discord Tickets](%s) v%s by [eartharoid](%s)" - }, - "cmd_usage": { - "args": { - "description": "**Description:** %s", - "example": "**Example:** `%s`" - }, - "description": "**Usage:**\n`%s`\n\n**Example:**\n`%s`\n\nRequired arguments are prefixed with `❗`.", - "invalid_named_args": { - "description": "There is an error in your command syntax: `%s`.\nType `%s` for an example.\nPlease ask a member of staff if you are unsure.", - "title": "❌ Invalid syntax" - }, - "named_args": "This command uses named arguments.\n\n", - "title": "`%s` command usage" - }, - "collector_expires_in": "Expires in %d seconds", - "commands": { - "add": { - "args": { - "member": { - "description": "The member to add to the ticket", - "example": "@someone", - "name": "member" - }, - "ticket": { - "description": "The ticket to add the member to", - "example": "217", - "name": "ticket" - } - }, - "description": "Add a member to a ticket", - "name": "add", - "response": { - "added": { - "description": "%s has been added to %s.", - "title": "✅ Member added" - }, - "no_member": { - "description": "Please mention the member you want to add.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "blacklist": { - "aliases": { - "unblacklist": "unblacklist" - }, - "args": { - "member_or_role": { - "description": "The member or role to add/remove", - "example": "@NaughtyMember", - "name": "memberOrRole" - } - }, - "description": "Blacklist/unblacklist a member from interacting with the bot", - "name": "blacklist", - "response": { - "empty_list": { - "description": "There are no members or roles blacklisted. Type `%sblacklist ` to add a member or role to the blacklist.", - "title": "📃 Blacklisted members and roles" - }, - "illegal_action": { - "description": "%s is a staff member and cannot be blacklisted.", - "title": "❌ You can't blacklist this member" - }, - "list": { - "title": "📃 Blacklisted members and roles" - }, - "member_added": { - "description": "<@%s> has been added to the blacklist. They will no longer be able to interact with the bot.", - "title": "✅ Added member to blacklist" - }, - "member_removed": { - "description": "<@%s> has been removed from the blacklist. They can now use the bot again.", - "title": "✅ Removed member from blacklist" - }, - "role_added": { - "description": "<@&%s> has been added to the blacklist. Members with this role will no longer be able to interact with the bot.", - "title": "✅ Added role to blacklist" - }, - "role_removed": { - "description": "<@&%s> has been removed from the blacklist. Members with this role can now use the bot again.", - "title": "✅ Removed role from blacklist" - } - } - }, - "close": { - "aliases": { - "delete": "delete", - "lock": "lock" - }, - "args": { - "reason": { - "alias": "r", - "description": "The reason for closing the ticket(s)", - "example": "Resolved", - "name": "reason" - }, - "ticket": { - "alias": "t", - "description": "The ticket to close, either the number or the channel mention/ID", - "example": "217", - "name": "ticket" - }, - "time": { - "alias": "T", - "description": "Close all tickets that have been inactive for the specified time", - "example": "1w", - "name": "time" - } - }, - "description": "Close a ticket channel", - "name": "close", - "response": { - "closed": { - "description": "Ticket #%s has been closed.", - "title": "✅ Ticket closed" - }, - "closed_multiple": { - "description": [ - "%d ticket has been closed.", - "%d tickets have been closed." - ], - "title": [ - "✅ Ticket closed", - "✅ Tickets closed" - ] - }, - "confirm": { - "description": "React with ✅ to close this ticket.", - "description_with_archive": "You will be able to view an archived version of it after.\nReact with ✅ to close this ticket.", - "title": "❔ Are you sure?" - }, - "confirmation_timeout": { - "description": "You took too long to confirm.", - "title": "❌ Reaction time expired" - }, - "confirm_multiple": { - "description": [ - "React with ✅ to close %d ticket.", - "React with ✅ to close %d tickets." - ], - "title": "❔ Are you sure?" - }, - "invalid_time": { - "description": "The time period provided could not be parsed.", - "title": "❌ Invalid input" - }, - "not_a_ticket": { - "description": "Please use this command in a ticket channel or use the ticket flag.\nType `%shelp close` for more information.", - "title": "❌ This isn't a ticket channel" - }, - "no_tickets": { - "description": "There are no tickets which have been inactive for this time period.", - "title": "❌ No tickets to close" - }, - "unresolvable": { - "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", - "title": "❌ Error" - } - } - }, - "help": { - "aliases": { - "command": "command", - "commands": "commands" - }, - "args": { - "command": { - "description": "The command to display information about", - "example": "new", - "name": "command" - } - }, - "description": "List commands you have access to, or find out more about a command", - "name": "help", - "response": { - "list": { - "description": "The commands you have access to are listed below. For more information about a command, type `{prefix}help [command]`. To create a ticket, type `{prefix}new [topic]`.", - "fields": { - "commands": "Commands" - }, - "title": "❔ Help" - } - } - }, - "new": { - "aliases": { - "create": "create", - "open": "open", - "ticket": "ticket" - }, - "args": { - "topic": { - "description": "The topic of the ticket", - "example": "Problem with billing", - "name": "topic" - } - }, - "description": "Create a new ticket", - "name": "new", - "response": { - "created": { - "description": "Your ticket has been created: %s.", - "title": "✅ Ticket created" - }, - "error": { - "title": "❌ Error" - }, - "has_a_ticket": { - "description": "Please use your existing ticket (<#%s>) or close it before creating another.", - "title": "❌ You already have an open ticket" - }, - "max_tickets": { - "description": "Please use `%sclose` to close any unneeded tickets.\n\n%s", - "title": "❌ You already have %d open tickets" - }, - "no_categories": { - "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", - "title": "❌ Can't create ticket" - }, - "select_category": { - "description": "Select the category most relevant to your ticket's topic:\n\n%s", - "title": "🔤 Please select the ticket category" - }, - "select_category_timeout": { - "description": "You took too long to select the ticket category.", - "title": "❌ Reaction time expired" - } - }, - "request_topic": { - "description": "Please briefly state what this ticket is about in a a few words.", - "title": "Ticket topic" - } - }, - "panel": { - "args": { - "categories": { - "alias": "c", - "description": "A category ID", - "example": "451745464954650634", - "name": "categories" - }, - "description": { - "alias": "d", - "description": "The description for the panel message", - "example": "\"React to this message to open a ticket.\"", - "name": "description" - }, - "emoji": { - "alias": "e", - "description": "An emoji", - "example": "🎫", - "name": "emoji" - }, - "title": { - "alias": "t", - "description": "The title for the panel message", - "example": "\"Support tickets\"", - "name": "title" - } - }, - "description": "Create a new ticket panel", - "name": "panel", - "response": { - "invalid_category": { - "description": "One or more of the specified category IDs is invalid.", - "title": "❌ Invalid category" - }, - "mismatch": { - "description": "Please provide the name number of emojis and category IDs.", - "title": "❌ Invalid input" - } - } - }, - "remove": { - "args": { - "member": { - "description": "The member to remove from the ticket", - "example": "@someone", - "name": "member" - }, - "ticket": { - "description": "The ticket to remove the member from", - "example": "217", - "name": "ticket" - } - }, - "description": "Remove a member from a ticket", - "name": "remove", - "response": { - "removed": { - "description": "%s has been removed from %s.", - "title": "✅ Member removed" - }, - "no_member": { - "description": "Please mention the member you want to remove.", - "title": "❌ Unknown member" - }, - "no_permission": { - "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", - "title": "❌ Insufficient permission" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel, or mention the channel.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "settings": { - "aliases": { - "config": "config" - }, - "description": "Configure Discord Tickets", - "name": "settings", - "response": { - "invalid": "❌ Settings data is invalid; please refer to the documentation.\n%s", - "updated": "✅ Settings have been updated." - } - }, - "stats": { - "description": "Display ticket statistics", - "fields": { - "messages": "Messages", - "response_time": { - "minutes": "%s minutes", - "title": "Avg. response time" - }, - "tickets": "Tickets" - }, - "name": "stats", - "response": { - "global": { - "description": "Statistics about tickets across all guilds where this Discord TIckets instance is used.", - "title": "📊 Global stats" - }, - "guild": { - "description": "Statistics about tickets within this guild. This data is cached for an hour.", - "title": "📊 This server's stats" - } - } - }, - "survey": { - "aliases": { - "surveys": "surveys" - }, - "args": { - "survey": { - "description": "The name of the survey to view responses of", - "example": "support", - "name": "survey" - } - }, - "description": "View survey responses", - "name": "survey", - "response": { - "list": { - "title": "📃 Surveys" - } - } - }, - "tag": { - "aliases": { - "faq": "faq", - "t": "t", - "tags": "tags" - }, - "args": { - "tag": { - "description": "The name of the tag to use", - "example": "website", - "name": "tag" - } - }, - "description": "Use a tag response", - "name": "tag", - "response": { - "error": "❌ Error", - "list": { - "title": "📃 Tag list" - }, - "missing": "This tag requires the following arguments:\n%s", - "not_a_ticket": { - "description": "This tag can only be used within a ticket channel as it uses ticket references.", - "title": "❌ This isn't a ticket channel" - } - } - }, - "topic": { - "args": { - "new_topic": { - "description": "The new topic of the ticket", - "example": "billing issue", - "name": "new_topic" - } - }, - "description": "Change the topic of the ticket", - "name": "topic", - "response": { - "changed": { - "description": "This ticket's topic has been changed.", - "title": "✅ Topic changed" - }, - "not_a_ticket": { - "description": "Please use this command in the ticket channel you want to change the topic of.", - "title": "❌ This isn't a ticket channel" - } - } - } - }, - "command_execution_error": { - "description": "An unexpected error occurred during command execution.\nPlease ask an administrator to check the console output / logs for details.", - "title": "⚠️" - }, - "message_will_be_deleted_in": "This message will be deleted in %d seconds", - "missing_permissions": { - "description": "You do not have the permissions required to use this command:\n%s", - "title": "❌" - }, - "staff_only": { - "description": "You must be a member of staff to use this command.", - "title": "❌" - }, - "ticket": { - "claimed": { - "description": "%s has claimed this ticket.", - "title": "✅ Ticket claimed" - }, - "closed": { - "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member": { - "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_by_member_with_reason": { - "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "closed_with_reason": { - "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", - "title": "✅ Ticket closed" - }, - "member_added": { - "description": "%s has been added by %s", - "title": "Member added" - }, - "member_removed": { - "description": "%s has been removed by %s", - "title": "Member removed" - }, - "opening_message": { - "fields": { - "topic": "Topic" - } - }, - "questions": "Please answer the following questions:\n\n%s", - "released": { - "description": "%s has released this ticket.", - "title": "✅ Ticket released" - }, - "survey": { - "complete": { - "description": "Thank you for your feedback.", - "title": "✅ Thank you" - }, - "start": { - "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", - "title": "❔ Feedback" - } - } - } -} + "blacklisted": "❌ You are blacklisted", + "bot": { + "missing_permissions": { + "description": "Discord Tickets requires the following permissions:\n%s", + "title": "⚠️" + }, + "version": "[Discord Tickets](%s) v%s by [eartharoid](%s)" + }, + "collector_expires_in": "Expires in %d seconds", + "command_execution_error": { + "description": "An unexpected error occurred during command execution.\nPlease ask an administrator to check the console output / logs for details.", + "title": "⚠️" + }, + "commands": { + "add": { + "description": "Add a member to a ticket", + "name": "add", + "options": { + "member": { + "description": "The member to add to the ticket", + "name": "member" + }, + "ticket": { + "description": "The ticket to add the member to", + "name": "ticket" + } + }, + "response": { + "added": { + "description": "%s has been added to %s.", + "title": "✅ Member added" + }, + "no_member": { + "description": "Please mention the member you want to add.", + "title": "❌ Unknown member" + }, + "no_permission": { + "description": "You are not the creator of this ticket and you are not a staff member; you can't add members to this ticket.", + "title": "❌ Insufficient permission" + }, + "not_a_ticket": { + "description": "Please use this command in the ticket channel, or mention the channel.", + "title": "❌ This isn't a ticket channel" + } + } + }, + "blacklist": { + "description": "View or modify the blacklist", + "name": "blacklist", + "options": { + "add": { + "description": "Add a member or role to the blacklist", + "name": "add", + "options": { + "member_or_role": { + "description": "The member or role to add to the blacklist", + "name": "member_or_role" + } + } + }, + "remove": { + "description": "Remove a member or role from the blacklist", + "name": "remove", + "options": { + "member_or_role": { + "description": "The member or role to remove from the blacklist", + "name": "member_or_role" + } + } + }, + "show": { + "description": "Show the members and roles in the blacklist", + "name": "show" + } + }, + "response": { + "empty_list": { + "description": "There are no members or roles blacklisted. Type `/blacklist add` to add a member or role to the blacklist.", + "title": "📃 Blacklisted members and roles" + }, + "illegal_action": { + "description": "%s is a staff member and cannot be blacklisted.", + "title": "❌ You can't blacklist this member" + }, + "invalid": { + "description": "This member or role can not be removed from the blacklist as they are not blacklisted.", + "title": "❌ Error" + }, + "list": { + "fields": { + "members": "Members", + "roles": "Roles" + }, + "title": "📃 Blacklisted members and roles" + }, + "member_added": { + "description": "<@%s> has been added to the blacklist. They will no longer be able to interact with the bot.", + "title": "✅ Added member to blacklist" + }, + "member_removed": { + "description": "<@%s> has been removed from the blacklist. They can now use the bot again.", + "title": "✅ Removed member from blacklist" + }, + "role_added": { + "description": "<@&%s> has been added to the blacklist. Members with this role will no longer be able to interact with the bot.", + "title": "✅ Added role to blacklist" + }, + "role_removed": { + "description": "<@&%s> has been removed from the blacklist. Members with this role can now use the bot again.", + "title": "✅ Removed role from blacklist" + } + } + }, + "close": { + "description": "Close a ticket channel", + "name": "close", + "options": { + "reason": { + "description": "The reason for closing the ticket(s)", + "name": "reason" + }, + "ticket": { + "description": "The ticket to close, either the number or the channel ID", + "name": "ticket" + }, + "time": { + "description": "Close all tickets that have been inactive for the specified time", + "name": "time" + } + }, + "response": { + "canceled": { + "description": "You canceled the operation.", + "title": "🚫 Canceled" + }, + "closed": { + "description": "Ticket #%s has been closed.", + "title": "✅ Ticket closed" + }, + "closed_multiple": { + "description": [ + "%d ticket has been closed.", + "%d tickets have been closed." + ], + "title": [ + "✅ Ticket closed", + "✅ Tickets closed" + ] + }, + "confirm": { + "buttons": { + "cancel": "Cancel", + "confirm": "Close" + }, + "description": "Please confirm your decision.", + "description_with_archive": "The ticket will be archived for future reference.", + "title": "❔ Are you sure?" + }, + "confirm_multiple": { + "buttons": { + "cancel": "Cancel", + "confirm": [ + "Close %d ticket", + "Close %d tickets" + ] + }, + "description": [ + "You are about to close %d ticket.", + "You are about to close %d tickets." + ], + "title": "❔ Are you sure?" + }, + "confirmation_timeout": { + "description": "You took too long to confirm.", + "title": "❌ Interaction time expired" + }, + "invalid_time": { + "description": "The time period provided could not be parsed.", + "title": "❌ Invalid input" + }, + "no_tickets": { + "description": "There are no tickets which have been inactive for this time period.", + "title": "❌ No tickets to close" + }, + "not_a_ticket": { + "description": "Please use this command in a ticket channel or use the ticket flag.\nType `/help close` for more information.", + "title": "❌ This isn't a ticket channel" + }, + "unresolvable": { + "description": "`%s` could not be resolved to a ticket. Please provide the ticket ID/mention or number.", + "title": "❌ Error" + } + } + }, + "help": { + "description": "List the commands you have access to", + "name": "help", + "options": {}, + "response": { + "list": { + "description": "The commands you have access to are listed below. To create a ticket, type **`/new`**.", + "fields": { + "commands": "Commands" + }, + "title": "❔ Help" + } + } + }, + "new": { + "description": "Create a new ticket", + "name": "new", + "options": { + "topic": { + "description": "The topic of the ticket", + "name": "topic" + } + }, + "request_topic": { + "description": "Please briefly state what this ticket is about in a a few words.", + "title": "⚠️ Ticket topic" + }, + "response": { + "created": { + "description": "Your ticket has been created: %s.", + "title": "✅ Ticket created" + }, + "error": { + "title": "❌ Error" + }, + "has_a_ticket": { + "description": "Please use your existing ticket (<#%s>) or close it before creating another.", + "title": "❌ You already have an open ticket" + }, + "max_tickets": { + "description": "Please use `/close` to close any unneeded tickets.\n\n%s", + "title": "❌ You already have %d open tickets" + }, + "no_categories": { + "description": "A server administrator must create at least one ticket category before a new ticket can be opened.", + "title": "❌ Can't create ticket" + }, + "select_category": { + "description": "Select the category most relevant to your ticket's topic.", + "title": "🔤 Please select the ticket category" + }, + "select_category_timeout": { + "description": "You took too long to select the ticket category.", + "title": "❌ Interaction time expired" + } + } + }, + "panel": { + "description": "Create a new ticket panel", + "name": "panel", + "options": { + "categories": { + "description": "A comma-separated list of category IDs", + "name": "categories" + }, + "description": { + "description": "The description for the panel message", + "name": "description" + }, + "image": { + "description": "An image URL for the panel message", + "name": "image" + }, + "just_type": { + "description": "Create a \"just type\" panel?", + "name": "just_type" + }, + "title": { + "description": "The title for the panel message", + "name": "title" + }, + "thumbnail": { + "description": "A thumbnail image URL for the panel message", + "name": "thumbnail" + } + }, + "response": { + "invalid_category": { + "description": "One or more of the specified category IDs is invalid.", + "title": "❌ Invalid category" + }, + "too_many_categories": { + "description": "The \"just type\" panel can only be used with a single category.", + "title": "❌ Too many categories" + } + } + }, + "remove": { + "description": "Remove a member from a ticket", + "name": "remove", + "options": { + "member": { + "description": "The member to remove from the ticket", + "name": "member" + }, + "ticket": { + "description": "The ticket to remove the member from", + "name": "ticket" + } + }, + "response": { + "no_member": { + "description": "Please mention the member you want to remove.", + "title": "❌ Unknown member" + }, + "no_permission": { + "description": "You are not the creator of this ticket and you are not a staff member; you can't remove members from this ticket.", + "title": "❌ Insufficient permission" + }, + "not_a_ticket": { + "description": "Please use this command in the ticket channel, or mention the channel.", + "title": "❌ This isn't a ticket channel" + }, + "removed": { + "description": "%s has been removed from %s.", + "title": "✅ Member removed" + } + } + }, + "settings": { + "description": "Configure Discord Tickets", + "name": "settings", + "options": { + "categories": { + "description": "Manage your ticket categories", + "name": "categories", + "options": { + "create": { + "description": "Create a new category", + "name": "create", + "options": { + "name": { + "description": "The name of the category", + "name": "name" + }, + "roles": { + "description": "A comma-separated list of staff role IDs for this category", + "name": "roles" + } + } + }, + "delete": { + "description": "Delete a category", + "name": "delete", + "options": { + "id": { + "description": "The ID of the category to delete", + "name": "id" + } + } + }, + "edit": { + "description": "Make changes to a category's configuration", + "name": "edit", + "options": { + "claiming": { + "description": "Enable ticket claiming?", + "name": "claiming" + }, + "id": { + "description": "The ID of the category to edit", + "name": "id" + }, + "image": { + "description": "An image URL", + "name": "image" + }, + "max_per_member": { + "description": "The maximum number of tickets a member can have in this category", + "name": "max_per_member" + }, + "name": { + "description": "The category name", + "name": "name" + }, + "name_format": { + "description": "The ticket name format", + "name": "name_format" + }, + "opening_message": { + "description": "The text to send when a ticket is opened", + "name": "opening_message" + }, + "ping": { + "description": "A comma-separated list of role IDs to ping", + "name": "ping" + }, + "require_topic": { + "description": "Require the user to give the topic of the ticket?", + "name": "require_topic" + }, + "roles": { + "description": "A comma-separated list of staff role IDs", + "name": "roles" + }, + "survey": { + "description": "The survey to use", + "name": "survey" + } + } + }, + "list": { + "description": "List categories", + "name": "list" + } + } + }, + "set": { + "description": "Set options", + "name": "set", + "options": { + "close_button": { + "description": "Enable closing with a button?", + "name": "close_button" + }, + "colour": { + "description": "The standard colour", + "name": "colour" + }, + "error_colour": { + "description": "The error colour", + "name": "error_colour" + }, + "footer": { + "description": "The embed footer text", + "name": "footer" + }, + "locale": { + "description": "The locale (language)", + "name": "locale" + }, + "log_messages": { + "description": "Store messages from tickets?", + "name": "log_messages" + }, + "success_colour": { + "description": "The success colour", + "name": "success_colour" + } + } + } + }, + "response": { + "category_created": "✅ The `%s` ticket category has been created", + "category_deleted": "✅ The `%s` ticket category has been deleted", + "category_does_not_exist": "❌ No category exists with the provided ID", + "category_updated": "✅ The `%s` ticket category has been updated", + "category_list": "Ticket categories", + "settings_updated": "✅ Settings have been updated" + } + }, + "stats": { + "description": "Display ticket statistics", + "fields": { + "messages": "Messages", + "response_time": { + "minutes": "%s minutes", + "title": "Avg. response time" + }, + "tickets": "Tickets" + }, + "name": "stats", + "options": {}, + "response": { + "global": { + "description": "Statistics about tickets across all guilds where this Discord TIckets instance is used.", + "title": "📊 Global stats" + }, + "guild": { + "description": "Statistics about tickets within this guild. This data is cached for an hour.", + "title": "📊 This server's stats" + } + } + }, + "survey": { + "description": "View survey responses", + "name": "survey", + "options": { + "survey": { + "description": "The name of the survey to view responses of", + "name": "survey" + } + }, + "response": { + "list": { + "title": "📃 Surveys" + } + } + }, + "tag": { + "description": "Use a tag response", + "name": "tag", + "options": { + "tag": { + "description": "The name of the tag to use", + "name": "tag" + } + }, + "response": { + "error": "❌ Error", + "list": { + "title": "📃 Tag list" + }, + "missing": "This tag requires the following arguments:\n%s", + "not_a_ticket": { + "description": "This tag can only be used within a ticket channel as it uses ticket references.", + "title": "❌ This isn't a ticket channel" + } + } + }, + "topic": { + "description": "Change the topic of the ticket", + "name": "topic", + "options": { + "new_topic": { + "description": "The new topic of the ticket", + "name": "new_topic" + } + }, + "response": { + "changed": { + "description": "This ticket's topic has been changed.", + "title": "✅ Topic changed" + }, + "not_a_ticket": { + "description": "Please use this command in the ticket channel you want to change the topic of.", + "title": "❌ This isn't a ticket channel" + } + } + } + }, + "message_will_be_deleted_in": "This message will be deleted in %d seconds", + "missing_permissions": { + "description": "You do not have the permissions required to use this command:\n%s", + "title": "❌ Error" + }, + "panel": { + "create_ticket": "Create a ticket" + }, + "ticket": { + "claim": "Claim", + "claimed": { + "description": "%s has claimed this ticket.", + "title": "✅ Ticket claimed" + }, + "close": "Close", + "closed": { + "description": "This ticket has been closed.\nThe channel will be deleted in 5 seconds.", + "title": "✅ Ticket closed" + }, + "closed_by_member": { + "description": "This ticket has been closed by %s.\nThe channel will be deleted in 5 seconds.", + "title": "✅ Ticket closed" + }, + "closed_by_member_with_reason": { + "description": "This ticket has been closed by %s: `%s`\nThe channel will be deleted in 5 seconds.", + "title": "✅ Ticket closed" + }, + "closed_with_reason": { + "description": "This ticket has been closed: `%s`\nThe channel will be deleted in 5 seconds.", + "title": "✅ Ticket closed" + }, + "member_added": { + "description": "%s has been added by %s", + "title": "Member added" + }, + "member_removed": { + "description": "%s has been removed by %s", + "title": "Member removed" + }, + "opening_message": { + "content": "%s\n%s has created a new ticket", + "fields": { + "topic": "Topic" + } + }, + "questions": "Please answer the following questions:\n\n%s", + "released": { + "description": "%s has released this ticket.", + "title": "✅ Ticket released" + }, + "survey": { + "complete": { + "description": "Thank you for your feedback.", + "title": "✅ Thank you" + }, + "start": { + "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.", + "title": "❔ Feedback" + } + }, + "unclaim": "Release" + }, + "updated_permissions": "✅ Slash command permissions updated" +} \ No newline at end of file diff --git a/src/logger.js b/src/logger.js index 7768cd6..f6e8dfc 100644 --- a/src/logger.js +++ b/src/logger.js @@ -2,7 +2,7 @@ const { path } = require('./utils/fs'); const config = require('../user/config'); const Logger = require('leekslazylogger'); module.exports = new Logger({ - debug: config.debug, + debug: config.developer.debug, directory: path('./logs/'), keepFor: config.logs.keep_for, levels: { diff --git a/src/modules/commands/command.js b/src/modules/commands/command.js index f4a0237..8a193a9 100644 --- a/src/modules/commands/command.js +++ b/src/modules/commands/command.js @@ -1,6 +1,6 @@ const { Message, // eslint-disable-line no-unused-vars - MessageEmbed + Interaction // eslint-disable-line no-unused-vars } = require('discord.js'); /** @@ -9,11 +9,13 @@ const { module.exports = class Command { /** * - * @typedef CommandArgument - * @property {string} name - The argument's name - * @property {string} description - The argument's description - * @property {string} example - An example value - * @property {boolean?} required - Is this arg required? Defaults to `false` + * @typedef CommandOption + * @property {string} name - The option's name + * @property {number} type - The option's type (use `Command.option_types`) + * @property {string} description - The option's description + * @property {CommandOption[]} [options] - The option's options + * @property {(string|number)[]} [choices] - The option's choices + * @property {boolean} [required] - Is this arg required? Defaults to `false` */ /** * Create a new Command @@ -23,8 +25,7 @@ module.exports = class Command { * @param {string} data.description - The description of the command (1-100) * @param {boolean} [data.staff_only] - Only allow staff to use this command? * @param {string[]} [data.permissions] - Array of permissions needed for a user to use this command - * @param {boolean} [data.process_args] - Should the command handler process named arguments? - * @param {CommandArgument[]} [data.args] - The command's arguments (see [docs](https://github.com/75lb/command-line-args/blob/master/doc/option-definition.md) if using processed args) + * @param {CommandOption[]} [data.options] - The command's options */ constructor(client, data) { @@ -44,14 +45,6 @@ module.exports = class Command { */ this.name = data.name; - /** - * The command's aliases - * @type {string[]} - */ - this.aliases = data.aliases ?? []; - - if (!this.aliases.includes(this.name)) this.aliases.unshift(this.name); - /** * The command description * @type {string} @@ -71,18 +64,11 @@ module.exports = class Command { */ this.permissions = data.permissions ?? []; - /** - * Should the command handler process named arguments? - * @type {boolean} - * @default false - */ - this.process_args = data.process_args === true; - /** * The command options - * @type {CommandArgument[]} + * @type {CommandOption[]} */ - this.args = data.args ?? []; + this.options = data.options ?? []; /** * True if command is internal, false if it is from a plugin @@ -100,64 +86,40 @@ module.exports = class Command { try { this.manager.register(this); // register the command - } catch (e) { - return this.client.log.error(e); + } catch (error) { + return this.client.log.error(error); } - - } /** * The code to be executed when a command is invoked * @abstract - * @param {Message} message - The message that invoked this command - * @param {(object|string)} [args] - Named command arguments, or the message content with the prefix and command removed + * @param {Interaction} interaction - The message that invoked this command */ - async execute(message, args) { } // eslint-disable-line no-unused-vars + async execute(interaction) { } // eslint-disable-line no-unused-vars - /** - * Send a message with the command usage - * @param {TextChannel} channel - The channel to send the message to - * @param {string} [alias] - The command alias - * @returns {Promise} - */ - async sendUsage(channel, alias) { - const settings = await this.client.utils.getSettings(channel.guild); - if (!alias) alias = this.name; - - const prefix = settings.command_prefix; - const i18n = this.client.i18n.getLocale(settings.locale); - - const addArgs = (embed, arg) => { - const required = arg.required ? '`❗` ' : ''; - let description = `» ${i18n('cmd_usage.args.description', arg.description)}`; - if (arg.example) description += `\n» ${i18n('cmd_usage.args.example', arg.example)}`; - embed.addField(required + arg.name, description); + async build(guild) { + return { + defaultPermission: !this.staff_only, + description: this.description, + name: this.name, + options: typeof this.options === 'function' ? await this.options(guild) : this.options }; + } - let usage, - example, - embed; - - if (this.process_args) { - usage = `${prefix + alias} ${this.args.map(arg => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(' ')}`; - example = `${prefix + alias} \n${this.args.map(arg => `--${arg.name} ${arg.example || ''}`).join('\n')}`; - embed = new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('cmd_usage.title', alias)) - .setDescription(i18n('cmd_usage.named_args') + i18n('cmd_usage.description', usage, example)); - } else { - usage = `${prefix + alias} ${this.args.map(arg => arg.required ? `<${arg.name}>` : `[${arg.name}]`).join(' ')}`; - example = `${prefix + alias} ${this.args.map(arg => `${arg.example || ''}`).join(' ')}`; - embed = new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('cmd_usage.title', alias)) - .setDescription(i18n('cmd_usage.description', usage, example)); - } - - this.args.forEach(arg => addArgs(embed, arg)); - return await channel.send({ embeds: [embed] }); - + static get option_types() { + return { + SUB_COMMAND: 1, + SUB_COMMAND_GROUP: 2, + STRING: 3, // eslint-disable-line sort-keys + INTEGER: 4, // eslint-disable-line sort-keys + BOOLEAN: 5, // eslint-disable-line sort-keys + USER: 6, + CHANNEL: 7, // eslint-disable-line sort-keys + ROLE: 8, + MENTIONABLE: 9, // eslint-disable-line sort-keys + NUMBER: 10 + }; } }; \ No newline at end of file diff --git a/src/modules/commands/manager.js b/src/modules/commands/manager.js index ff99568..9bb637f 100644 --- a/src/modules/commands/manager.js +++ b/src/modules/commands/manager.js @@ -2,16 +2,13 @@ const { Client, // eslint-disable-line no-unused-vars Collection, - Message, // eslint-disable-line no-unused-vars + Interaction, // eslint-disable-line no-unused-vars MessageEmbed } = require('discord.js'); const fs = require('fs'); const { path } = require('../../utils/fs'); -const { parseArgsStringToArgv: argv } = require('string-argv'); -const parseArgs = require('command-line-args'); - /** * Manages the loading and execution of commands */ @@ -48,81 +45,127 @@ module.exports = class CommandManager { } /** Register a command */ - register(cmd) { - const exists = this.commands.has(cmd.name); - const is_internal = (exists && cmd.internal) || (exists && this.commands.get(cmd.name).internal); + register(command) { + const exists = this.commands.has(command.name); + const is_internal = (exists && command.internal) || (exists && this.commands.get(command.name).internal); if (is_internal) { - const plugin = this.client.plugins.plugins.find(p => p.commands.includes(cmd.name)); - if (plugin) this.client.log.commands(`The "${plugin.name}" plugin has overridden the internal "${cmd.name}" command`); - else this.client.log.commands(`An unknown plugin has overridden the internal "${cmd.name}" command`); - if(cmd.internal) return; + const plugin = this.client.plugins.plugins.find(p => p.commands.includes(command.name)); + if (plugin) this.client.log.commands(`The "${plugin.name}" plugin has overridden the internal "${command.name}" command`); + else this.client.log.commands(`An unknown plugin has overridden the internal "${command.name}" command`); + if(command.internal) return; } else if (exists) { - throw new Error(`A non-internal command with the name "${cmd.name}" already exists`); + throw new Error(`A non-internal command with the name "${command.name}" already exists`); } - this.commands.set(cmd.name, cmd); - this.client.log.commands(`Loaded "${cmd.name}" command`); + this.commands.set(command.name, command); + this.client.log.commands(`Loaded "${command.name}" command`); + } + + async publish(guild) { + if (!guild) { + return this.client.guilds.cache.forEach(guild => { + this.publish(guild); + }); + } + + try { + const commands = await Promise.all(this.client.commands.commands.map(async command => await command.build(guild))); + await this.client.application.commands.set(commands, guild.id); + await this.updatePermissions(guild); + this.client.log.success(`Published ${this.client.commands.commands.size} commands to "${guild.name}"`); + } catch (error) { + this.client.log.warn('An error occurred whilst publishing the commands'); + this.client.log.error(error); + } + } + + async updatePermissions(guild) { + guild.commands.fetch().then(async commands => { + const permissions = []; + const settings = await this.client.utils.getSettings(guild.id); + const blacklist = []; + settings.blacklist.users?.forEach(userId => { + blacklist.push({ + id: userId, + permission: false, + type: 'USER' + }); + }); + settings.blacklist.roles?.forEach(roleId => { + blacklist.push({ + id: roleId, + permission: false, + type: 'ROLE' + }); + }); + + const categories = await this.client.db.models.Category.findAll({ where: { guild: guild.id } }); + const staff_roles = new Set(categories.map(category => category.roles).flat()); + + commands.forEach(async g_cmd => { + const cmd_permissions = [...blacklist]; + const command = this.client.commands.commands.get(g_cmd.name); + + if (command.staff_only) { + cmd_permissions.push({ + id: guild.roles.everyone.id, + permission: false, + type: 'ROLE' + }); + staff_roles.forEach(roleId => { + cmd_permissions.push({ + id: roleId, + permission: true, + type: 'ROLE' + }); + }); + } + + permissions.push({ + id: g_cmd.id, + permissions: cmd_permissions + }); + }); + + this.client.log.debug(`Command permissions for "${guild.name}"`, require('util').inspect(permissions, { + colors: true, + depth: 10 + })); + + try { + await guild.commands.permissions.set({ fullPermissions: permissions }); + } catch (error) { + this.client.log.warn('An error occurred whilst updating command permissions'); + this.client.log.error(error); + } + }); } /** * Execute a command - * @param {Message} message - Command message + * @param {Interaction} interaction - Command message */ - async handle(message) { - if (message.author.bot) return; // ignore self and other bots - - const settings = await this.client.utils.getSettings(message.guild); + async handle(interaction) { + if (!interaction.guild) return this.client.log.debug('Ignoring non-guild command interaction'); + const settings = await this.client.utils.getSettings(interaction.guild.id); const i18n = this.client.i18n.getLocale(settings.locale); - const prefix = settings.command_prefix; - const escaped_prefix = prefix.toLowerCase().replace(/(?=\W)/g, '\\'); // (lazy) escape every character so it can be used in a RexExp - const client_mention = `<@!?${this.client.user.id}>`; - let cmd_name = message.content.match(new RegExp(`^(${escaped_prefix}|${client_mention}\\s?)(\\S+)`, 'mi')); // capture prefix and command - if (!cmd_name) return; // stop here if the message is not a command + const command = this.commands.get(interaction.commandName); + if (!command) return; - const raw_args = message.content.replace(cmd_name[0], '').trim(); // remove the prefix and command - cmd_name = cmd_name[2].toLowerCase(); // set cmd_name to the actual command alias, effectively removing the prefix - - const cmd = this.commands.find(cmd => cmd.aliases.includes(cmd_name)); - if (!cmd) return; - - let is_blacklisted = false; - if (settings.blacklist.includes(message.author.id)) { - is_blacklisted = true; - this.client.log.info(`Ignoring blacklisted member ${message.author.tag}`); - } else { - settings.blacklist.forEach(element => { - if (message.guild.roles.cache.has(element) && message.member.roles.cache.has(element)) { - is_blacklisted = true; - this.client.log.info(`Ignoring member ${message.author.tag} with blacklisted role`); - } - }); - } - - if (is_blacklisted) { - try { - return message.react('❌'); - } catch (error) { - return this.client.log.warn('Failed to react to a message'); - } - } - - const bot_permissions = message.guild.me.permissionsIn(message.channel); + const bot_permissions = interaction.guild.me.permissionsIn(interaction.channel); const required_bot_permissions = [ - 'ADD_REACTIONS', 'ATTACH_FILES', 'EMBED_LINKS', 'MANAGE_CHANNELS', - 'MANAGE_MESSAGES', - 'READ_MESSAGE_HISTORY', - 'SEND_MESSAGES' + 'MANAGE_MESSAGES' ]; if (!bot_permissions.has(required_bot_permissions)) { const perms = required_bot_permissions.map(p => `\`${p}\``).join(', '); - if (bot_permissions.has(['EMBED_LINKS', 'SEND_MESSAGES'])) { - await message.channel.send({ + if (bot_permissions.has('EMBED_LINKS')) { + await interaction.reply({ embeds: [ new MessageEmbed() .setColor('ORANGE') @@ -130,76 +173,33 @@ module.exports = class CommandManager { .setDescription(i18n('bot.missing_permissions.description', perms)) ] }); - } else if (bot_permissions.has('SEND_MESSAGES')) { - await message.channel.send({ content: '⚠️ ' + i18n('bot.missing_permissions.description', perms) }); - } else if (bot_permissions.has('ADD_REACTIONS')) { - await message.react('⚠️'); } else { - this.client.log.warn('Unable to respond to command due to missing permissions'); + await interaction.reply({ content: i18n('bot.missing_permissions.description', perms) }); } return; } - const missing_permissions = cmd.permissions instanceof Array && !message.member.permissions.has(cmd.permissions); + const missing_permissions = command.permissions instanceof Array && !interaction.member.permissions.has(command.permissions); if (missing_permissions) { - const perms = cmd.permissions.map(p => `\`${p}\``).join(', '); - return await message.channel.send({ + const perms = command.permissions.map(p => `\`${p}\``).join(', '); + return await interaction.reply({ embeds: [ new MessageEmbed() .setColor(settings.error_colour) .setTitle(i18n('missing_permissions.title')) .setDescription(i18n('missing_permissions.description', perms)) - ] + ], + ephemeral: true }); } - if (cmd.staff_only && await this.client.utils.isStaff(message.member) === false) { - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('staff_only.title')) - .setDescription(i18n('staff_only.description')) - ] - }); - } - - let args = raw_args; - - if (cmd.process_args) { - try { - args = parseArgs(cmd.args, { argv: argv(raw_args) }); - } catch (error) { - const help_cmd = `${settings.command_prefix}${i18n('commands.help.name')} ${cmd_name}`; - return await message.channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.error_colour) - .setTitle(i18n('cmd_usage.invalid_named_args.title')) - .setDescription(i18n('cmd_usage.invalid_named_args.description', error.message, help_cmd)) - ] - }); - } - for (const arg of cmd.args) { - if (arg.required && args[arg.name] === undefined) { - return await cmd.sendUsage(message.channel, cmd_name); // send usage if any required arg is missing - } - } - } else { - const args_num = raw_args.split(/\s/g).filter(arg => arg.length !== 0).length; // count the number of single-word args were given - const required_args = cmd.args.reduce((acc, arg) => arg.required ? acc + 1 : acc, 0); // count how many of the args are required - if (args_num < required_args) { - return await cmd.sendUsage(message.channel, cmd_name); - } - } - try { - this.client.log.commands(`Executing "${cmd.name}" command (invoked by ${message.author.tag})`); - await cmd.execute(message, args); // execute the command + this.client.log.commands(`Executing "${command.name}" command (invoked by ${interaction.user.tag})`); + await command.execute(interaction); // execute the command } catch (e) { - this.client.log.warn(`An error occurred whilst executing the ${cmd.name} command`); + this.client.log.warn(`An error occurred whilst executing the ${command.name} command`); this.client.log.error(e); - await message.channel.send({ + await interaction.reply({ embeds: [ new MessageEmbed() .setColor('ORANGE') diff --git a/src/modules/tickets/manager.js b/src/modules/tickets/manager.js index da9d228..e97befa 100644 --- a/src/modules/tickets/manager.js +++ b/src/modules/tickets/manager.js @@ -1,7 +1,11 @@ /* eslint-disable max-lines */ const EventEmitter = require('events'); const TicketArchives = require('./archives'); -const { MessageEmbed } = require('discord.js'); +const { + MessageActionRow, + MessageButton, + MessageEmbed +} = require('discord.js'); /** Manages tickets */ module.exports = class TicketManager extends EventEmitter { @@ -70,23 +74,13 @@ module.exports = class TicketManager extends EventEmitter { }); (async () => { - const settings = await this.client.utils.getSettings(guild); + const settings = await this.client.utils.getSettings(guild.id); const i18n = this.client.i18n.getLocale(settings.locale); topic = t_row.topic ? this.client.cryptr.decrypt(t_row.topic) : ''; - if (cat_row.ping instanceof Array && cat_row.ping.length > 0) { - const mentions = cat_row.ping.map(id => id === 'everyone' - ? '@everyone' - : id === 'here' - ? '@here' - : `<@&${id}>`); - - await t_channel.send({ content: mentions.join(', ') }); - } - if (cat_row.image) { await t_channel.send({ content: cat_row.image }); } @@ -102,8 +96,39 @@ module.exports = class TicketManager extends EventEmitter { if (topic) embed.addField(i18n('ticket.opening_message.fields.topic'), topic); + const components = new MessageActionRow(); + + if (cat_row.claiming) { + components.addComponents( + new MessageButton() + .setCustomId('ticket.claim') + .setLabel(i18n('ticket.claim')) + .setEmoji('🙌') + .setStyle('SECONDARY') + ); + } + + if (settings.close_button) { + components.addComponents( + new MessageButton() + .setCustomId('ticket.close') + .setLabel(i18n('ticket.close')) + .setEmoji('✖️') + .setStyle('DANGER') + ); + } + + const mentions = cat_row.ping instanceof Array && cat_row.ping.length > 0 + ? cat_row.ping.map(id => id === 'everyone' + ? '@everyone' + : id === 'here' + ? '@here' + : `<@&${id}>`) + .join(', ') + : ''; const sent = await t_channel.send({ - content: creator.user.toString(), + components: [components], + content: i18n('ticket.opening_message.content', mentions, creator.user.toString()), embeds: [embed] }); await sent.pin({ reason: 'Ticket opening message' }); @@ -118,10 +143,6 @@ module.exports = class TicketManager extends EventEmitter { .catch(() => this.client.log.warn('Failed to delete system pin message')); } - if (cat_row.claiming) { - await sent.react('🙌'); - } - let questions; if (cat_row.opening_questions) { questions = cat_row.opening_questions @@ -134,7 +155,7 @@ module.exports = class TicketManager extends EventEmitter { embeds: [ new MessageEmbed() .setColor(settings.colour) - .setTitle('⚠️ ' + i18n('commands.new.request_topic.title')) + .setTitle(i18n('commands.new.request_topic.title')) .setDescription(i18n('commands.new.request_topic.description')) .setFooter(this.client.utils.footer(settings.footer, i18n('collector_expires_in', 120)), guild.iconURL()) ] @@ -213,7 +234,7 @@ module.exports = class TicketManager extends EventEmitter { this.emit('beforeClose', ticket_id); const guild = this.client.guilds.cache.get(t_row.guild); - const settings = await this.client.utils.getSettings(guild); + const settings = await this.client.utils.getSettings(guild.id); const i18n = this.client.i18n.getLocale(settings.locale); const channel = await this.client.channels.fetch(t_row.id); @@ -273,98 +294,102 @@ module.exports = class TicketManager extends EventEmitter { }; if (channel) { - const creator = await guild.members.fetch(t_row.creator); + guild.members.fetch(t_row.creator) + .then(async creator => { + const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); + if (creator && cat_row.survey) { + const survey = await this.client.db.models.Survey.findOne({ + where: { + guild: t_row.guild, + name: cat_row.survey + } + }); - const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } }); - - if (creator && cat_row.survey) { - const survey = await this.client.db.models.Survey.findOne({ - where: { - guild: t_row.guild, - name: cat_row.survey - } - }); - - if (survey) { - const r_collector_message = await channel.send({ - content: creator.toString(), - embeds: [ - new MessageEmbed() - .setColor(settings.colour) - .setTitle(i18n('ticket.survey.start.title')) - .setDescription(i18n('ticket.survey.start.description', creator.toString(), survey.questions.length)) - .setFooter(i18n('collector_expires_in', 60)) - ] - }); - - await r_collector_message.react('✅'); - - const filter = (reaction, user) => user.id === creator.user.id && reaction.emoji.name === '✅'; - - const r_collector = r_collector_message.createReactionCollector({ - filter, - time: 60000 - }); - - r_collector.on('collect', async () => { - r_collector.stop(); - const filter = message => message.author.id === creator.id; - let answers = []; - let number = 1; - for (const question of survey.questions) { - await channel.send({ + if (survey) { + const r_collector_message = await channel.send({ + content: creator.toString(), embeds: [ new MessageEmbed() .setColor(settings.colour) - .setTitle(`${number++}/${survey.questions.length}`) - .setDescription(question) + .setTitle(i18n('ticket.survey.start.title')) + .setDescription(i18n('ticket.survey.start.description', creator.toString(), survey.questions.length)) .setFooter(i18n('collector_expires_in', 60)) ] }); - try { - const collected = await channel.awaitMessages({ - errors: ['time'], - filter, - max: 1, - time: 60000 + await r_collector_message.react('✅'); + + const filter = (reaction, user) => user.id === creator.user.id && reaction.emoji.name === '✅'; + + const r_collector = r_collector_message.createReactionCollector({ + filter, + time: 60000 + }); + + r_collector.on('collect', async () => { + r_collector.stop(); + const filter = message => message.author.id === creator.id; + let answers = []; + let number = 1; + for (const question of survey.questions) { + await channel.send({ + embeds: [ + new MessageEmbed() + .setColor(settings.colour) + .setTitle(`${number++}/${survey.questions.length}`) + .setDescription(question) + .setFooter(i18n('collector_expires_in', 60)) + ] + }); + + try { + const collected = await channel.awaitMessages({ + errors: ['time'], + filter, + max: 1, + time: 60000 + }); + answers.push(collected.first().content); + } catch (collected) { + return await close(); + } + } + + await channel.send({ + embeds: [ + new MessageEmbed() + .setColor(settings.success_colour) + .setTitle(i18n('ticket.survey.complete.title')) + .setDescription(i18n('ticket.survey.complete.description')) + .setFooter(settings.footer, guild.iconURL()) + ] }); - answers.push(collected.first().content); - } catch (collected) { - return await close(); - } + + answers = answers.map(a => this.client.cryptr.encrypt(a)); + await this.client.db.models.SurveyResponse.create({ + answers, + survey: survey.id, + ticket: t_row.id + }); + + await close(); + + }); + + r_collector.on('end', async collected => { + if (collected.size === 0) { + await close(); + } + }); } - - await channel.send({ - embeds: [ - new MessageEmbed() - .setColor(settings.success_colour) - .setTitle(i18n('ticket.survey.complete.title')) - .setDescription(i18n('ticket.survey.complete.description')) - .setFooter(settings.footer, guild.iconURL()) - ] - }); - - answers = answers.map(a => this.client.cryptr.encrypt(a)); - await this.client.db.models.SurveyResponse.create({ - answers, - survey: survey.id, - ticket: t_row.id - }); - + } else { await close(); - - }); - - r_collector.on('end', async collected => { - if (collected.size === 0) { - await close(); - } - }); - } - } else { - await close(); - } + } + }) + .catch(async () => { + this.client.log.debug('Skipping survey as member has left'); + await close(); + }); } this.emit('close', ticket_id); diff --git a/src/utils/discord.js b/src/utils/discord.js index 6b97178..77d4800 100644 --- a/src/utils/discord.js +++ b/src/utils/discord.js @@ -1,8 +1,6 @@ -const { - Guild, // eslint-disable-line no-unused-vars - GuildMember // eslint-disable-line no-unused-vars -} = require('discord.js'); +const { GuildMember } = require('discord.js'); // eslint-disable-line no-unused-vars +const { Model } = require('sequelize'); // eslint-disable-line no-unused-vars const config = require('../../user/config'); let current_presence = -1; @@ -33,12 +31,12 @@ module.exports = class DiscordUtils { } /** - * get a guild's settings - * @param {Guild} guild - The Guild + * Fet a guild's settings + * @param {string} id - The guild's ID * @returns {Promise} */ - async getSettings(guild) { - const data = { id: guild.id }; + async getSettings(id) { + const data = { id }; const [settings] = await this.client.db.models.Guild.findOrCreate({ defaults: data, where: data @@ -48,11 +46,11 @@ module.exports = class DiscordUtils { /** * Delete a guild's settings - * @param {Guild} guild - The Guild + * @param {string} id - The guild ID * @returns {Promise} */ - async deleteSettings(guild) { - const row = await this.getSettings(guild); + async deleteSettings(id) { + const row = await this.getSettings(id); return await row.destroy(); } diff --git a/user/example.config.js b/user/example.config.js index 3ea17fe..406af9e 100644 --- a/user/example.config.js +++ b/user/example.config.js @@ -16,17 +16,15 @@ * ############################################################################################### */ -const prefix = '-'; module.exports = { - debug: false, defaults: { colour: '#009999', - command_prefix: prefix, log_messages: true, name_format: 'ticket-{number}', opening_message: 'Hello {name}, thank you for creating a ticket. A member of staff will soon be available to assist you.\n\n__All messages in this channel are stored for future reference.__' }, + developer: { debug: false }, locale: 'en-GB', logs: { enabled: true, @@ -39,7 +37,7 @@ module.exports = { duration: 60, presences: [ { - activity: `${prefix}new`, + activity: '/new', type: 'PLAYING' }, {