diff --git a/src/commands/_settings.js b/src/commands/_settings.js index 2bef7c0..3d99e56 100644 --- a/src/commands/_settings.js +++ b/src/commands/_settings.js @@ -1,4 +1,6 @@ const Command = require('../modules/commands/command'); +const fetch = require('node-fetch'); +const { MessageAttachment } = require('discord.js'); module.exports = class SettingsCommand extends Command { constructor(client) { @@ -12,11 +14,108 @@ module.exports = class SettingsCommand extends Command { }); } - async execute({ guild, member, channel, args }, message) { + async execute({ guild, channel, member }, message) { let settings = await guild.settings; const i18n = this.client.i18n.get(settings.locale); - channel.send('Settings!'); + let attachments = [ ...message.attachments.values() ]; + + if (attachments.length >= 1) { + // load settings from json + let data = await (await fetch(attachments[0].url)).json(); + + settings.colour = data.colour; + settings.error_colour = data.error_colour; + settings.locale = data.locale; + settings.log_messages = data.log_messages; + settings.success_colour = data.success_colour; + await settings.save(); + + for (let c of data.categories) { + let permissions = [ + ...[ + { + id: guild.roles.everyone, + deny: ['VIEW_CHANNEL'] + } + ], + ...c.roles.map(r => { + return { + id: r, + allow: ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'ATTACH_FILES'] + }; + }) + ]; + + if (c.id) { + // existing category + let category = await this.client.db.models.Category.findOne({ + where: { + id: c.id + } + }); + category.name = c.name; + category.roles = c.roles; + category.save(); + + let cat_channel = await this.client.channels.fetch(c.id); + await cat_channel.edit({ + name: c.name, // await cat_channel.setName(c.name); + permissionOverwrites: permissions // await cat_channel.overwritePermissions(permissions); + }, + `Tickets category updated by ${member.user.tag}` + ); + } else { + // create a new category + let created = await guild.channels.create(c.name, { + type: 'category', + reason: `Tickets category created by ${member.user.tag}`, + permissionOverwrites: permissions + }); + await this.client.db.models.Category.create({ + id: created.id, + name: c.name, + guild: guild.id, + roles: c.roles + }); + } + } + + channel.send(`\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``); + } else { + // upload settings as json to be modified + let data = { + categories: [], + colour: settings.colour, + error_colour: settings.error_colour, + locale: settings.locale, + log_messages: settings.log_messages, + success_colour: settings.success_colour, + }; + + let categories = await this.client.db.models.Category.findAll({ + where: { + guild: guild.id + } + }); + + for (let c of categories) { + data.categories.push({ + id: c.id, + name: c.name, + roles: c.roles + }); + } + + let attachment = new MessageAttachment( + Buffer.from(JSON.stringify(data, null, 2)), + `Settings for ${guild.name}.json` + ); + + channel.send(i18n('commands.settings.response'), { + files: [attachment] + }); + } } }; \ No newline at end of file diff --git a/src/database/index.js b/src/database/index.js index 4de53ea..bf97e5c 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -5,7 +5,6 @@ const { const { path } = require('../utils/fs'); const config = require('../../user/config'); const types = require('./dialects'); -const supported = Object.keys(types); module.exports = async (log) => { @@ -21,6 +20,7 @@ module.exports = async (log) => { let type = (DB_TYPE || 'sqlite').toLowerCase(); + const supported = Object.keys(types); if (!supported.includes(type)) { log.error(new Error(`DB_TYPE (${type}) is not a valid type`)); return process.exit(); @@ -68,10 +68,22 @@ module.exports = async (log) => { primaryKey: true, allowNull: false, }, + locale: { + type: DataTypes.STRING, + defaultValue: config.locale + }, colour: { type: DataTypes.STRING, defaultValue: config.defaults.colour }, + success_colour: { + type: DataTypes.STRING, + defaultValue: 'GREEN' + }, + error_colour: { + type: DataTypes.STRING, + defaultValue: 'RED' + }, log_messages: { type: DataTypes.BOOLEAN, defaultValue: config.defaults.log_messages @@ -100,6 +112,9 @@ module.exports = async (log) => { }, unique: 'name_guild' }, + roles: { + type: DataTypes.JSON + } }, { tableName: DB_TABLE_PREFIX + 'categories' }); diff --git a/src/listeners/interaction.js b/src/listeners/interaction.js index a327fe1..e9e26c8 100644 --- a/src/listeners/interaction.js +++ b/src/listeners/interaction.js @@ -5,7 +5,7 @@ module.exports = { switch (interaction.type) { case 1: - client.log.debug('Received interaction ping, responding with pong'); + client.log.info('Received interaction ping, responding with pong'); await client.api.interactions(interaction.id, interaction.token).callback.post({ data: { type: 1, // PONG @@ -13,7 +13,7 @@ module.exports = { }); break; case 2: - client.commands.handle(interaction); + client.commands.handle(interaction, true); break; } diff --git a/src/listeners/messageDelete.js b/src/listeners/messageDelete.js index f4d467e..421d51f 100644 --- a/src/listeners/messageDelete.js +++ b/src/listeners/messageDelete.js @@ -2,13 +2,6 @@ module.exports = { event: 'messageDelete', execute: async (client, message) => { - if (message.partial) - try { - await message.fetch(); - } catch (err) { - return client.log.error(err); - } - let settings = await message.guild?.settings; if (settings?.log_messages) { diff --git a/src/listeners/messageUpdate.js b/src/listeners/messageUpdate.js index d934a37..d6e8f0f 100644 --- a/src/listeners/messageUpdate.js +++ b/src/listeners/messageUpdate.js @@ -2,12 +2,13 @@ module.exports = { event: 'msgUpdate', execute: async (client, oldm, newm) => { - if (newm.partial) + if (newm.partial) { try { await newm.fetch(); } catch (err) { return client.log.error(err); } + } let settings = await newm.guild?.settings; diff --git a/src/locales/en-GB.json b/src/locales/en-GB.json index 8f53bdb..bcd389c 100644 --- a/src/locales/en-GB.json +++ b/src/locales/en-GB.json @@ -23,6 +23,7 @@ "description": "Configure Discord Tickets" } }, + "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", "support_only": "❌ You must be a member of staff to use this command." } \ No newline at end of file diff --git a/src/modules/commands/manager.js b/src/modules/commands/manager.js index 2370c9e..6aa3ac0 100644 --- a/src/modules/commands/manager.js +++ b/src/modules/commands/manager.js @@ -70,7 +70,7 @@ module.exports = class CommandManager { if (typeof data.description !== 'string') throw new TypeError(`Expected type of command description to be a string, got ${typeof data.description}`); - if (data.name.length < 1 || data.name.length > 100) + 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)) @@ -139,9 +139,9 @@ module.exports = class CommandManager { /** * Execute a command - * @param {(Interaction|Message)} interaction - Command interaction, or message + * @param {(Interaction|Message)} interaction_or_message - Command interaction or message */ - async handle(interaction, slash) { + async handle(interaction_or_message, slash) { slash = slash === false ? false : true; let cmd_name, args = {}, @@ -151,54 +151,62 @@ module.exports = class CommandManager { member_id; if (slash) { - cmd_name = interaction.data.name; + cmd_name = interaction_or_message.data.name; - guild_id = interaction.guild_id; - channel_id = interaction.channel_id; - member_id = interaction.member.user.id; + guild_id = interaction_or_message.guild_id; + channel_id = interaction_or_message.channel_id; + member_id = interaction_or_message.member.user.id; - if (interaction.data.options) - interaction.data.options.forEach(({ name, value }) => args[name] = value); + if (interaction_or_message.data.options) + interaction_or_message.data.options.forEach(({ name, value }) => args[name] = value); } else { - cmd_name = interaction.content.match(/^tickets\/(\S+)/mi); + cmd_name = interaction_or_message.content.match(/^tickets\/(\S+)/mi); if (cmd_name) cmd_name = cmd_name[1]; - guild_id = interaction.guild.id; - channel_id = interaction.channel.id; - member_id = interaction.author.id; + 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)) - throw new Error(`Received "${cmd_name}" command invocation, but the command manager does not have a "${cmd_name}" command`); + 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); - const cmd = this.commands.get(cmd_name); - let settings = await data.guild.settings; if (!settings) settings = await data.guild.createSettings(); const i18n = this.client.i18n.get(settings.locale); - // if (cmd.staff_only) {} + const cmd = this.commands.get(cmd_name); + + 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_or_message.`); + 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; + } const no_perm = cmd.permissions instanceof Array && !data.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.sendResponse(interaction, msg, true); - else return await interaction.channel.send(msg); + if (slash) return await cmd.respond(interaction_or_message, msg, true); + else return await interaction_or_message.channel.send(msg); } try { - if (slash) await cmd.acknowledge(interaction, true); // respond to discord + 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); // run the command + await cmd.execute(data, interaction_or_message); // run the command } catch (e) { - this.client.log.warn(`(COMMANDS) An error occurred whilst executed the ${cmd_name} command`); + this.client.log.warn(`An error occurred whilst executing the ${cmd_name} command`); this.client.log.error(e); } diff --git a/src/modules/tickets.js b/src/modules/tickets.js index 1b60851..a2cbced 100644 --- a/src/modules/tickets.js +++ b/src/modules/tickets.js @@ -39,16 +39,36 @@ module.exports = class TicketManager extends EventEmitter { /** * Close a ticket * @param {string} ticket - The channel ID, or the ticket number + * @param {string} [closer] - ID of the member who is closing the ticket */ - async close(ticket) { + async close(ticket, closer) { + if (!this.client.channels.resolve(ticket)) { + let row = await this.client.db.Models.Ticket.findOne({ + where: { + number: ticket + } + }); + if (!row) throw new Error(`Could not find a ticket with number ${ticket}`); + ticket = row.id; + } + + let row = await this.client.db.Models.Ticket.findOne({ + where: { + id: ticket + } + }); + if (!row) throw new Error(`Could not find a ticket with ID ${ticket}`); + + this.emit('beforeClose', ticket, closer); + + /** + * + * + * for each message in table, create entities + * + * + */ } - /** - * Close multiple tickets - * @param {string[]} tickets - An array of channel IDs to close **(does not accept ticket numbers)** - */ - async closeMultiple(tickets) { - - } }; \ No newline at end of file diff --git a/user/example.config.js b/user/example.config.js index 4d4986b..48dfc7d 100644 --- a/user/example.config.js +++ b/user/example.config.js @@ -25,12 +25,12 @@ module.exports = { debug: false, defaults: { - colour: '#009999', + colour: '#009999', // https://discord.js.org/#/docs/main/stable/typedef/ColorResolvable log_messages: true, // transcripts/archives will be empty if false prefix: '-', ticket_welcome: '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.__', }, - locale: 'en-GB', + locale: 'en-GB', // used for globals (such as commands) and the default guild locale logs: { enabled: true, keep_for: 30