diff --git a/src/commands/new.js b/src/commands/new.js index e303edf..dc0f9bd 100644 --- a/src/commands/new.js +++ b/src/commands/new.js @@ -1,5 +1,4 @@ const { MessageEmbed } = require('discord.js'); -const { OptionTypes } = require('../modules/commands/helpers'); const Command = require('../modules/commands/command'); module.exports = class NewCommand extends Command { @@ -9,37 +8,41 @@ module.exports = class NewCommand extends Command { internal: true, name: i18n('commands.new.name'), description: i18n('commands.new.description'), - // slash: false, - options: [ + aliases: [ + i18n('commands.new.aliases.open'), + i18n('commands.new.aliases.create'), + ], + args: [ { - name: i18n('commands.new.options.category.name'), - type: OptionTypes.STRING, - description: i18n('commands.new.options.topic.description'), + name: i18n('commands.new.args.category.name'), + description: i18n('commands.new.args.topic.description'), required: true, }, { - name: i18n('commands.new.options.topic.name'), - type: OptionTypes.STRING, - description: i18n('commands.new.options.topic.description'), + name: i18n('commands.new.args.topic.name'), + description: i18n('commands.new.args.topic.description'), required: false, } ] }); } - async execute({ guild, member, channel, args }, interaction) { + async execute(message, args, raw_args) { - let settings = await guild.settings; + let settings = await message.guild.settings; const i18n = this.client.i18n.get(settings.locale); - await channel.send( + await message.channel.send( new MessageEmbed() .setColor(settings.colour) .setTitle(i18n('bot.version', require('../../package.json').version)) .setDescription(args.topic) ); - // this.client.tickets.create(guild.id, member.id, args.category, args.topic); - this.client.tickets.create(guild.id, member.id, '825861413687787560'); + // console.log(this.aliases) + // console.log(args.category) + // console.log(args.topic) + + // this.client.tickets.create(message.guild.id, message.member.id, '825861413687787560', args.topic); } }; \ No newline at end of file diff --git a/src/commands/_settings.js b/src/commands/settings.js similarity index 79% rename from src/commands/_settings.js rename to src/commands/settings.js index c15d3da..8b80efe 100644 --- a/src/commands/_settings.js +++ b/src/commands/settings.js @@ -7,16 +7,15 @@ module.exports = class SettingsCommand extends Command { const i18n = client.i18n.get(client.config.locale); super(client, { internal: true, - slash: false, name: i18n('commands.settings.name'), description: i18n('commands.settings.description'), permissions: ['MANAGE_GUILD'] }); } - async execute({ guild, channel, member }, message) { + async execute(message) { - let settings = await guild.settings; + let settings = await message.guild.settings; const i18n = this.client.i18n.get(settings.locale); let attachments = [ ...message.attachments.values() ]; @@ -24,9 +23,10 @@ module.exports = class SettingsCommand extends Command { if (attachments.length >= 1) { // load settings from json - this.client.log.info(`Downloading settings for "${guild.name}"`); + this.client.log.info(`Downloading settings for "${message.guild.name}"`); let data = await (await fetch(attachments[0].url)).json(); settings.colour = data.colour; + settings.command_prefix = data.command_prefix; settings.error_colour = data.error_colour; settings.locale = data.locale; settings.log_messages = data.log_messages; @@ -51,7 +51,7 @@ module.exports = class SettingsCommand extends Command { let cat_channel = await this.client.channels.fetch(c.id); if (cat_channel.name !== c.name) - await cat_channel.setName(c.name, `Tickets category updated by ${member.user.tag}`); + await cat_channel.setName(c.name, `Tickets category updated by ${message.member.user.tag}`); for (let r of c.roles) { await cat_channel.updateOverwrite(r, { @@ -59,21 +59,21 @@ module.exports = class SettingsCommand extends Command { READ_MESSAGE_HISTORY: true, SEND_MESSAGES: true, ATTACH_FILES: true - }, `Tickets category updated by ${member.user.tag}`); + }, `Tickets category updated by ${message.member.user.tag}`); } } else { // create a new category const allowed_permissions = ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES']; - let cat_channel = await guild.channels.create(c.name, { + let cat_channel = await message.guild.channels.create(c.name, { type: 'category', - reason: `Tickets category created by ${member.user.tag}`, + reason: `Tickets category created by ${message.member.user.tag}`, position: 0, permissionOverwrites: [ ...[ { - id: guild.roles.everyone, + id: message.guild.roles.everyone, deny: ['VIEW_CHANNEL'] }, { @@ -94,14 +94,14 @@ module.exports = class SettingsCommand extends Command { max_per_member: c.max_per_member, name: c.name, name_format: c.name_format, - guild: guild.id, + guild: message.guild.id, roles: c.roles, }); } } - this.client.log.success(`Updated guild settings for "${guild.name}"`); - channel.send(i18n('commands.settings.response.updated')); + this.client.log.success(`Updated guild settings for "${message.guild.name}"`); + message.channel.send(i18n('commands.settings.response.updated')); } else { @@ -109,6 +109,7 @@ module.exports = class SettingsCommand extends Command { let data = { categories: [], colour: settings.colour, + command_prefix: settings.command_prefix, error_colour: settings.error_colour, locale: settings.locale, log_messages: settings.log_messages, @@ -117,7 +118,7 @@ module.exports = class SettingsCommand extends Command { let categories = await this.client.db.models.Category.findAll({ where: { - guild: guild.id + guild: message.guild.id } }); @@ -133,10 +134,10 @@ module.exports = class SettingsCommand extends Command { let attachment = new MessageAttachment( Buffer.from(JSON.stringify(data, null, 2)), - `Settings for ${guild.name}.json` + `Settings for ${message.guild.name}.json` ); - channel.send({ + message.channel.send({ files: [attachment] }); diff --git a/src/database/index.js b/src/database/index.js index 1e5b006..b23dee8 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -72,6 +72,10 @@ module.exports = async (log) => { type: DataTypes.STRING, defaultValue: config.locale }, + command_prefix: { + type: DataTypes.STRING, + defaultValue: config.defaults.command_prefix + }, colour: { type: DataTypes.STRING, defaultValue: config.defaults.colour diff --git a/src/listeners/interaction.js b/src/listeners/interaction.js deleted file mode 100644 index e9e26c8..0000000 --- a/src/listeners/interaction.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = { - event: 'INTERACTION_CREATE', - raw: true, - execute: async (client, interaction) => { - - switch (interaction.type) { - case 1: - client.log.info('Received interaction ping, responding with pong'); - await client.api.interactions(interaction.id, interaction.token).callback.post({ - data: { - type: 1, // PONG - } - }); - break; - case 2: - client.commands.handle(interaction, true); - break; - } - - } -}; \ No newline at end of file diff --git a/src/listeners/message.js b/src/listeners/message.js index 4020ed8..e1fbb11 100644 --- a/src/listeners/message.js +++ b/src/listeners/message.js @@ -31,8 +31,6 @@ module.exports = { } } - // non-slash commands - if (message.content.match(/^tickets\/(\S+)/mi)) - client.commands.handle(message, false); + client.commands.handle(message); } }; \ No newline at end of file diff --git a/src/locales/en-GB.json b/src/locales/en-GB.json index 08a02dd..c39d967 100644 --- a/src/locales/en-GB.json +++ b/src/locales/en-GB.json @@ -4,9 +4,11 @@ }, "commands": { "new": { - "name": "new", - "description": "Create a new support ticket", - "options": { + "aliases": { + "create": "create", + "open": "open" + }, + "args": { "category": { "name": "category", "description": "The category you would like to create a new ticket for" @@ -15,17 +17,19 @@ "name": "topic", "description": "The topic of the ticket" } - } + }, + "description": "Create a new support ticket", + "name": "new" }, "settings": { - "name": "settings", "description": "Configure Discord Tickets", + "name": "settings", "response": { "updated": "✅ Settings have been updated." } } }, "must_be_slash": "❌ This command must be invoked by a slash command interaction (`/%s`).", - "no_perm": "❌ You do not have the permissions required to use this command:\n%s", + "no_perm": "❌ You do not have the permissions required to use this command:\n%s", "staff_only": "❌ You must be a member of staff to use this command." } \ No newline at end of file diff --git a/src/modules/commands/command.js b/src/modules/commands/command.js index 274e52e..bf90c15 100644 --- a/src/modules/commands/command.js +++ b/src/modules/commands/command.js @@ -1,39 +1,7 @@ -/* eslint-disable no-unused-vars */ -const { - Client, - GuildMember, - Guild, - Channel, - Message -} = require('discord.js'); - -const { - createMessage, - flags -} = require('../../utils/discord'); - /** * A command */ module.exports = class Command { - /** - * A command option choice - * @typedef CommandOptionChoice - * @property {string} name - Choice name (1-100) - * @property {(string|number)} value - choice value - */ - - /** - * A command option - * @typedef CommandOption - * @property {number} type - [ApplicationCommandOptionType](https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype) - * @property {string} name - Option name (1-32) - * @property {string} description - Option description (1-100) - * @property {boolean} [required] - Required? - * @property {CommandOptionChoice[]} [choices] - Array of choices - * @property {CommandOption[]} [options] - Array of options if this option is a subcommand/subcommand group - */ - /** * Create a new Command * @param {Client} client - The Discord Client @@ -43,8 +11,7 @@ module.exports = class Command { * @param {boolean} [data.slash] - Register as a slash command? **Defaults to `true`** * @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.global] - Create a global command? - * @param {CommandOption[]} [data.options] - The command options (parameters), max of 10 + * @param {CommandArgument[]} [data.args] - The command's arguments */ constructor(client, data) { @@ -55,7 +22,7 @@ module.exports = class Command { this.manager = this.client.commands; if (typeof data !== 'object') { - throw new TypeError(`Expected type of data to be an object, got ${typeof data}`); + throw new TypeError(`Expected type of command "data" to be an object, got "${typeof data}"`); } /** @@ -64,18 +31,20 @@ 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} */ this.description = data.description; - /** - * Register as a slash command? - * @type {boolean} - */ - this.slash = data.slash === false ? false : true; - /** * Only allow staff to use this command? * @type {boolean} @@ -88,18 +57,11 @@ module.exports = class Command { */ this.permissions = data.permissions; - /** - * Is this a global command? - * @type {boolean} - * @default true - */ - this.global = data.global === false ? false : true; - /** * The command options - * @type {CommandOption[]} + * @type {CommandArgument[]} */ - this.options = data.options; + this.args = data.args; /** * True if command is internal, false if it is from a plugin @@ -113,9 +75,7 @@ module.exports = class Command { * @type {(undefined|Plugin)} */ this.plugin = this.client.plugins.plugins.find(p => p.commands?.includes(this.name)); - } - - this.manager.check(data); // validate + } try { this.manager.register(this); // register the command @@ -123,90 +83,15 @@ module.exports = class Command { return this.client.log.error(e); } - if (this.slash && this.global) - this.client.api.applications(this.client.user.id).commands.post({ data }); // post command to Discord - - let internal = this.internal ? 'internal ' : ''; - this.client.log.commands(`Loaded ${internal}"${this.name}" command`); } - /** - * [ApplicationCommandInteractionDataOption](https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondataoption) - * @typedef {Object} ApplicationCommandInteractionDataOption - * @property {string} name - Name of the parameter - * @property {*} value - The value - * @property {(undefined|ApplicationCommandInteractionDataOption[])} options - Present if the option is a subcommand/subcommand group - */ - - /** - * [Interaction](https://discord.com/developers/docs/interactions/slash-commands#interaction) object - * @typedef {Object} Interaction - * @property {string} interaction.id - ID of the interaction - * @property {number} interaction.type - Type of interaction - * @property {ApplicationCommandInteractionData} interaction.data - Interaction data - * @property {Object} interaction.guild- The guild object - * @property {Object} interaction.channel- The channel object - * @property {Object} interaction.member - The member object - * @property {string} interaction.token - The token used to respond to the interaction - */ - /** * The code to be executed when a command is invoked * @abstract - * @param {Object} data - Object containing data about the command invocation - * @param {Object} data.args - Command arguments - * @param {Channel} data.channel- The channel object - * @param {Guild} data.guild- The guild object - * @param {GuildMember} data.member - The member object - * @param {(Interaction|Message)} interaction_or_message - Interaction object + * @param {Message} message - The message that invoked this command + * @param {object?} args - Command arguments */ - async execute(data, interaction_or_message) { } + async execute(message, args) { } - /** - * Defer the response to respond later - * @param {Interaction} interaction - Interaction object - * @param {boolean} secret - Ephemeral? - */ - async acknowledge(interaction, secret) { - this.client.api.interactions(interaction.id, interaction.token).callback.post({ - data: { - type: 5, - // data: { - // flags: flags(secret) - // }, - } - }); - } - - /** - * Send an interaction response - * @param {Interaction} interaction - Interaction object - * @param {*} content - Message content - * @param {boolean} secret - Ephemeral message? **NOTE: EMBEDS AND ATTACHMENTS DO NOT RENDER IF TRUE** - */ - async respond(interaction, content, secret) { - let application = await this.client.fetchApplication(); - const send = this.client.api.webhooks(application.id, interaction.token).messages['@original'].patch; - if (typeof content === 'object') - await send({ - data: { - type: 4, - data: { - flags: flags(secret), - ...await createMessage(this.client, interaction.channel_id, content) - } - } - }); - else if (typeof content === 'string') - await send({ - data: { - type: 4, - data: { - flags: flags(secret), - content - } - } - }); - } }; \ No newline at end of file diff --git a/src/modules/commands/helpers.js b/src/modules/commands/helpers.js deleted file mode 100644 index 687fae5..0000000 --- a/src/modules/commands/helpers.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - OptionTypes: { - SUB_COMMAND: 1, - SUB_COMMAND_GROUP: 2, - STRING: 3, - INTEGER: 4, - BOOLEAN: 5, - USER: 6, - CHANNEL: 7, - ROLE: 8, - }, - ResponseTypes: { - Pong: 1, - ChannelMessageWithSource: 4, - DeferredChannelMessageWithSource: 5, - } -}; \ No newline at end of file diff --git a/src/modules/commands/manager.js b/src/modules/commands/manager.js index f399a8c..ceb0823 100644 --- a/src/modules/commands/manager.js +++ b/src/modules/commands/manager.js @@ -56,155 +56,61 @@ module.exports = class CommandManager { throw new Error(`A non-internal command with the name "${cmd.name}" already exists`); this.commands.set(cmd.name, cmd); - } - /** Check the command data */ - check(data) { - - if (typeof data.name !== 'string') - throw new TypeError(`Expected type of command name to be a string, got ${typeof data.name}`); - - if (data.name.length < 3 || data.name.length > 32) - throw new TypeError('Length of command name must be 3-32'); - - if (typeof data.description !== 'string') - throw new TypeError(`Expected type of command description to be a string, got ${typeof data.description}`); - - if (data.description.length < 1 || data.description.length > 100) - throw new TypeError('Length of description must be 3-32'); - - if (typeof data.options !== 'undefined' && !(data.options instanceof Array)) - throw new TypeError(`Expected type of command options to be undefined or an array, got ${typeof data.options}`); - - if (data.options) - this.checkOptions(data.options); - - } - - /** Check the command data's options */ - checkOptions(options) { - let num = 0; - options.forEach(o => { - if (typeof o.type !== 'number') - throw new TypeError(`Expected type of option ${num} type to be a number, got ${typeof o.type}`); - - if (typeof o.name !== 'string') - throw new TypeError(`Expected type of option ${num} name to be a string, got ${typeof o.name}`); - - if (o.name.length < 3 || o.name.length > 32) - throw new TypeError(`Length of option ${num} name must be 3-32`); - - if (typeof o.description !== 'string') - throw new TypeError(`Expected type of option ${num} description to be a string, got ${typeof o.description}`); - - if (o.description.length < 1 || o.description.length > 100) - throw new TypeError(`Length of option ${num} description must be 1-100`); - - if (typeof o.required !== 'undefined' && typeof o.required !== 'boolean') - throw new TypeError(`Expected type of option ${num} required to be undefined or a boolean, got ${typeof o.required}`); - - if (typeof o.choices !== 'undefined' && !(o.choices instanceof Array)) - throw new TypeError(`Expected type of option ${num} choices to be undefined or an array, got ${typeof o.choices}`); - - if (o.choices) - this.checkOptionChoices(o.choices); - - if (typeof o.options !== 'undefined' && !(o.options instanceof Array)) - throw new TypeError(`Expected type of option ${num} options to be undefined or an array, got ${typeof o.options}`); - - if (o.options) - this.checkOptions(o.options); - - num++; - }); - - } - - /** Check command option choices */ - checkOptionChoices(choices) { - let num = 0; - choices.forEach(c => { - if (typeof c.name !== 'string') - throw new TypeError(`Expected type of option choice ${num} name to be a string, got ${typeof c.name}`); - - if (c.name.length < 1 || c.name.length > 100) - throw new TypeError(`Length of option choice ${num} name must be 1-100`); - - if (typeof c.value !== 'string' && typeof c.value !== 'number') - throw new TypeError(`Expected type of option choice ${num} value to be a string or number, got ${typeof c.value}`); - - num++; - }); + let internal = cmd.internal ? 'internal ' : ''; + this.client.log.commands(`Loaded ${internal}"${cmd.name}" command`); } /** * Execute a command - * @param {(Interaction|Message)} interaction_or_message - Command interaction or message + * @param {Message} message - Command message */ - async handle(interaction_or_message, slash) { - slash = slash === false ? false : true; - let cmd_name, - args = {}, - data = {}, - guild_id, - channel_id, - member_id; + async handle(message) { - if (slash) { - cmd_name = interaction_or_message.data.name; - - guild_id = interaction_or_message.guild_id; - channel_id = interaction_or_message.channel_id; - member_id = interaction_or_message.member.user.id; - - if (interaction_or_message.data.options) - interaction_or_message.data.options.forEach(({ name, value }) => args[name] = value); - } else { - cmd_name = interaction_or_message.content.match(/^tickets\/(\S+)/mi); - if (cmd_name) cmd_name = cmd_name[1]; - - guild_id = interaction_or_message.guild.id; - channel_id = interaction_or_message.channel.id; - member_id = interaction_or_message.author.id; - } - - if (cmd_name === null || !this.commands.has(cmd_name)) - return this.client.log.warn(`Received "${cmd_name}" command invocation, but the command manager does not have a "${cmd_name}" command registered`); - - data.args = args; - data.guild = await this.client.guilds.fetch(guild_id); - data.channel = await this.client.channels.fetch(channel_id), - data.member = await data.guild.members.fetch(member_id); - - let settings = await data.guild.settings; - if (!settings) settings = await data.guild.createSettings(); + let settings = await message.guild.settings; + if (!settings) settings = await message.guild.createSettings(); + const prefix = settings.command_prefix; const i18n = this.client.i18n.get(settings.locale); - const cmd = this.commands.get(cmd_name); + let cmd_name = message.content.match(new RegExp(`^${prefix}(\\S+)`, 'mi')); + if (!cmd_name) return; - if (cmd.slash && !slash) { - this.client.log.commands(`Blocking command execution for the "${cmd_name}" command as it was invoked by a message, not a slash command interaction.`); - try { - data.channel.send(i18n('must_be_slash', cmd_name)); // interaction_or_message.reply - } catch (err) { - this.client.log.warn('Failed to reply to blocked command invocation message'); - } - return; - } + let raw_args = message.content.replace(cmd_name[0], '').trim(); + cmd_name = cmd_name[1]; - const no_perm = cmd.permissions instanceof Array - && !data.member.hasPermission(cmd.permissions); + const cmd = this.commands.find(cmd => cmd.aliases.includes(cmd_name)); + if (!cmd); + + let data = [...raw_args.matchAll(/(\w*)\s?:\s?("(.*)"|[\w<>@!#]*)/gmi)]; + let args = {}; + data.forEach(arg => args[arg[1]] = arg[3] || arg[2]); + + const no_perm = cmd.permissions instanceof Array && !message.member.hasPermission(cmd.permissions); if (no_perm) { let perms = cmd.permissions.map(p => `\`${p}\``).join(', '); - let msg = i18n('no_perm', perms); - if (slash) return await cmd.respond(interaction_or_message, msg, true); - else return await interaction_or_message.channel.send(msg); + return message.channel.send(i18n('no_perm', perms)); + } + + let guild_categories = await this.client.db.models.Category.findAll({ + where: { + guild: message.guild.id + } + }); + + if (cmd.staff_only) { + let staff_roles = new Set(); // eslint-disable-line no-undef + guild_categories.forEach(cat => { + cat.roles.forEach(r => staff_roles.add(r)); + }); + staff_roles = staff_roles.filter(r => message.member.roles.cache.has(r)); + if (staff_roles.length === 0) { + return message.channel.send(i18n('staff_only')); + } } try { - if (slash) await cmd.acknowledge(interaction_or_message, true); // respond to discord - this.client.log.commands(`Executing "${cmd_name}" command (invoked by ${data.member.user.tag})`); - await cmd.execute(data, interaction_or_message); // run the command + this.client.log.commands(`Executing "${cmd_name}" command (invoked by ${message.author.tag})`); + await cmd.execute(message, args, raw_args); // execute the command } catch (e) { this.client.log.warn(`An error occurred whilst executing the ${cmd_name} command`); this.client.log.error(e); diff --git a/src/modules/plugins/plugin.js b/src/modules/plugins/plugin.js index 0577b22..d54650c 100644 --- a/src/modules/plugins/plugin.js +++ b/src/modules/plugins/plugin.js @@ -1,10 +1,6 @@ /* eslint-disable no-unused-vars */ const { Client } = require('discord.js'); const Command = require('../commands/command'); -const { - OptionTypes, - ResponseTypes -} = require('../commands/helpers'); const fs = require('fs'); const { join } = require('path'); const { path } = require('../../utils/fs'); @@ -85,12 +81,6 @@ module.exports = class Plugin { name: clean, path: path(`./user/plugins/${clean}`) }; - - this.helpers = { - Command, - OptionTypes, - ResponseTypes - }; } /** diff --git a/user/example.config.js b/user/example.config.js index 7589201..1f04493 100644 --- a/user/example.config.js +++ b/user/example.config.js @@ -26,6 +26,7 @@ module.exports = { debug: false, defaults: { colour: '#009999', // https://discord.js.org/#/docs/main/stable/typedef/ColorResolvable + command_prefix: 'tickets/', log_messages: true, // transcripts/archives will be empty if false name_format: 'ticket-{number}', prefix: '-',