diff --git a/src/commands/new.js b/src/commands/new.js index 9a9b4b3..133f107 100644 --- a/src/commands/new.js +++ b/src/commands/new.js @@ -108,10 +108,10 @@ module.exports = class NewCommand extends Command { .setFooter(settings.footer, message.guild.iconURL()) ); } else if (categories.count === 1) { - create(categories.rows[0]); + create(categories.rows[0]); // skip the category selection } else { - let letters_array = Object.values(letters); - let category_list = categories.rows.map((category, i) => `${letters_array[i]} » ${category.name}`); + let letters_array = Object.values(letters); // convert the A-Z emoji object to an array + let category_list = categories.rows.map((category, i) => `${letters_array[i]} » ${category.name}`); // list category names with an A-Z emoji let collector_message = await message.channel.send( new MessageEmbed() .setColor(settings.colour) @@ -122,11 +122,11 @@ module.exports = class NewCommand extends Command { ); for (let i in categories.rows) { - await collector_message.react(letters_array[i]); + await collector_message.react(letters_array[i]); // add the correct number of letter reactions } const collector_filter = (reaction, user) => { - let allowed = letters_array.slice(0, categories.count); + let 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); }; @@ -135,10 +135,10 @@ module.exports = class NewCommand extends Command { }); collector.on('collect', async (reaction) => { - let index = letters_array.findIndex(value => value === reaction.emoji.name); + let index = letters_array.findIndex(value => value === reaction.emoji.name); // find where the letter is in the alphabet if (index === -1) return await collector_message.delete({ timeout: 15000 }); await collector_message.reactions.removeAll(); - create(categories.rows[index], collector_message); + 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) => { @@ -152,7 +152,10 @@ module.exports = class NewCommand extends Command { .setDescription(i18n('commands.new.response.select_category_timeout.description', category_list.join('\n'))) .setFooter(footer(settings.footer, i18n('message_will_be_deleted_in', 15)), message.guild.iconURL()) ); - collector_message.delete({ timeout: 15000 }); + setTimeout(async () => { + await collector_message.delete(); + await message.delete(); + }, 15000); } }); } diff --git a/src/commands/settings.js b/src/commands/settings.js index 2e04145..bc2bc18 100644 --- a/src/commands/settings.js +++ b/src/commands/settings.js @@ -46,10 +46,12 @@ module.exports = class SettingsCommand extends Command { id: c.id } }); - category.name = c.name; - category.roles = c.roles; category.max_per_member = c.max_per_member; + category.name = c.name; category.name_format = c.name_format; + category.opening_message = c.opening_message; + category.require_topic = c.require_topic; + category.roles = c.roles; category.save(); let cat_channel = await this.client.channels.fetch(c.id); @@ -99,6 +101,8 @@ module.exports = class SettingsCommand extends Command { name: c.name, name_format: c.name_format, guild: message.guild.id, + opening_message: c.opening_message, + require_topic: c.require_topic, roles: c.roles, }); @@ -133,6 +137,8 @@ module.exports = class SettingsCommand extends Command { max_per_member: c.max_per_member, name: c.name, name_format: c.name_format, + opening_message: c.opening_message, + require_topic: c.require_topic, roles: c.roles }; }); diff --git a/src/database/index.js b/src/database/index.js index d9e8761..9a637bf 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -68,33 +68,33 @@ module.exports = async (client) => { primaryKey: true, allowNull: false, }, - locale: { + colour: { type: DataTypes.STRING, - defaultValue: config.locale + defaultValue: config.defaults.colour }, command_prefix: { type: DataTypes.STRING, defaultValue: config.defaults.command_prefix }, - colour: { - type: DataTypes.STRING, - defaultValue: config.defaults.colour - }, - success_colour: { - type: DataTypes.STRING, - defaultValue: 'GREEN' - }, error_colour: { type: DataTypes.STRING, defaultValue: 'RED' }, + footer: { + type: DataTypes.STRING, + defaultValue: 'Discord Tickets by eartharoid' + }, + locale: { + type: DataTypes.STRING, + defaultValue: config.locale + }, log_messages: { type: DataTypes.BOOLEAN, defaultValue: config.defaults.log_messages }, - footer: { + success_colour: { type: DataTypes.STRING, - defaultValue: 'Discord Tickets by eartharoid' + defaultValue: 'GREEN' }, }, { tableName: DB_TABLE_PREFIX + 'guilds' @@ -106,11 +106,6 @@ module.exports = async (client) => { primaryKey: true, allowNull: false, }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: 'name-guild' - }, guild: { type: DataTypes.CHAR(18), allowNull: false, @@ -120,18 +115,36 @@ module.exports = async (client) => { }, unique: 'name-guild' }, - roles: { - type: DataTypes.JSON - }, max_per_member: { type: DataTypes.INTEGER, defaultValue: 1 }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: 'name-guild' + }, name_format: { type: DataTypes.STRING, allowNull: false, defaultValue: config.defaults.name_format - } + }, + opening_message: { + type: DataTypes.STRING, + defaultValue: config.defaults.opening_message, + }, + require_topic: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + roles: { + type: DataTypes.JSON, + allowNull: false, + }, + questions: { + type: DataTypes.JSON, + allowNull: true, + }, }, { tableName: DB_TABLE_PREFIX + 'categories' }); @@ -142,10 +155,21 @@ module.exports = async (client) => { primaryKey: true, allowNull: false, }, - number: { - type: DataTypes.INTEGER, + category: { + type: DataTypes.CHAR(18), + allowNull: false, + references: { + model: Category, + key: 'id' + }, + }, + closed_by: { + type: DataTypes.CHAR(18), + allowNull: true, + }, + creator: { + type: DataTypes.CHAR(18), allowNull: false, - unique: 'number-guild' }, guild: { type: DataTypes.CHAR(18), @@ -156,30 +180,19 @@ module.exports = async (client) => { }, unique: 'number-guild' }, - category: { - type: DataTypes.CHAR(18), - allowNull: false, - references: { - model: Category, - key: 'id' - }, - }, - topic: { - type: DataTypes.STRING, - allowNull: true, - }, - creator: { - type: DataTypes.CHAR(18), + number: { + type: DataTypes.INTEGER, allowNull: false, + unique: 'number-guild' }, open: { type: DataTypes.BOOLEAN, defaultValue: true }, - closed_by: { - type: DataTypes.CHAR(18), + topic: { + type: DataTypes.STRING, allowNull: true, - } + }, }, { tableName: DB_TABLE_PREFIX + 'tickets' }); @@ -191,6 +204,22 @@ module.exports = async (client) => { primaryKey: true, allowNull: false, }, + author: { + type: DataTypes.CHAR(18), + allowNull: false, + }, + data: { + type: DataTypes.JSON, + allowNull: false, + }, + deleted: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + edited: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, ticket: { type: DataTypes.CHAR(18), allowNull: false, @@ -199,31 +228,31 @@ module.exports = async (client) => { key: 'id' }, }, - author: { - type: DataTypes.CHAR(18), - allowNull: false, - }, - edited: { - type: DataTypes.BOOLEAN, - defaultValue: false, - }, - deleted: { - type: DataTypes.BOOLEAN, - defaultValue: false, - }, - data: { - type: DataTypes.JSON - }, }, { tableName: DB_TABLE_PREFIX + 'messages' }); // eslint-disable-next-line no-unused-vars const UserEntity = sequelize.define('UserEntity', { - user: { - type: DataTypes.CHAR(18), + avatar: { + type: DataTypes.STRING, + allowNull: false, + }, + bot: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + colour: { + type: DataTypes.CHAR(6), + allowNull: true, + }, + discriminator: { + type: DataTypes.STRING, + allowNull: false, + }, + display_name: { + type: DataTypes.STRING, allowNull: false, - unique: 'id-ticket' }, ticket: { type: DataTypes.CHAR(18), @@ -232,14 +261,17 @@ module.exports = async (client) => { references: { model: Ticket, key: 'id' - }, + }, + }, + user: { + type: DataTypes.CHAR(18), + allowNull: false, + unique: 'id-ticket' + }, + username: { + type: DataTypes.STRING, + allowNull: false, }, - avatar: DataTypes.STRING, - username: DataTypes.STRING, - discriminator: DataTypes.STRING, - display_name: DataTypes.STRING, - colour: DataTypes.CHAR(6), - bot: DataTypes.BOOLEAN }, { tableName: DB_TABLE_PREFIX + 'user_entities' }); @@ -251,6 +283,10 @@ module.exports = async (client) => { allowNull: false, unique: 'id-ticket' }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, ticket: { type: DataTypes.CHAR(18), allowNull: false, @@ -260,13 +296,20 @@ module.exports = async (client) => { key: 'id' }, }, - name: DataTypes.STRING, }, { tableName: DB_TABLE_PREFIX + 'channel_entities' }); // eslint-disable-next-line no-unused-vars const RoleEntity = sequelize.define('RoleEntity', { + colour: { + type: DataTypes.CHAR(6), + defaultValue: '7289DA', + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, role: { type: DataTypes.CHAR(18), allowNull: false, @@ -281,8 +324,6 @@ module.exports = async (client) => { key: 'id' }, }, - name: DataTypes.STRING, - colour: DataTypes.CHAR(6), }, { tableName: DB_TABLE_PREFIX + 'role_entities' }); diff --git a/src/listeners/debug.js b/src/listeners/debug.js index 607f7d4..7726a75 100644 --- a/src/listeners/debug.js +++ b/src/listeners/debug.js @@ -1,7 +1,6 @@ module.exports = { event: 'debug', execute: (client, data) => { - if (client.config.debug) - client.log.debug(data); + if (client.config.debug) client.log.debug(data); } }; \ No newline at end of file diff --git a/src/listeners/guildCreate.js b/src/listeners/guildCreate.js index 375722a..2c4bd18 100644 --- a/src/listeners/guildCreate.js +++ b/src/listeners/guildCreate.js @@ -1,7 +1,7 @@ module.exports = { event: 'guildCreate', execute: async (client, guild) => { - client.log.info(`Added to ${guild.name}`); + client.log.info(`Added to "${guild.name}"`); await guild.createSettings(); } }; \ No newline at end of file diff --git a/src/listeners/guildDelete.js b/src/listeners/guildDelete.js index 9f202b9..b184e37 100644 --- a/src/listeners/guildDelete.js +++ b/src/listeners/guildDelete.js @@ -1,7 +1,7 @@ module.exports = { event: 'guildDelete', execute: async (client, guild) => { - client.log.info(`Removed from ${guild.name}`); + client.log.info(`Removed from "${guild.name}"`); await guild.deleteSettings(); } }; \ No newline at end of file diff --git a/src/listeners/message.js b/src/listeners/message.js index 032a003..67fab76 100644 --- a/src/listeners/message.js +++ b/src/listeners/message.js @@ -1,12 +1,13 @@ module.exports = { event: 'message', execute: async (client, message) => { + if (!message.guild) return; - let settings = await message.guild?.settings; + let settings = await message.guild.settings; + if (!settings) settings = await message.guild.createSettings(); - if (settings?.log_messages && !message.system) - client.tickets.archives.addMessage(message); + if (settings.log_messages && !message.system) client.tickets.archives.addMessage(message); // add the message to the archives (if it is in a ticket channel) - client.commands.handle(message); + client.commands.handle(message); // pass the message to the command handler } }; \ No newline at end of file diff --git a/src/listeners/messageDelete.js b/src/listeners/messageDelete.js index 1fed7fc..6ff878a 100644 --- a/src/listeners/messageDelete.js +++ b/src/listeners/messageDelete.js @@ -1,12 +1,11 @@ module.exports = { event: 'messageDelete', execute: async (client, message) => { + if (!message.guild) return; - let settings = await message.guild?.settings; + let settings = await message.guild.settings; + if (!settings) settings = await message.guild.createSettings(); - if (settings?.log_messages) { - if (message.system) return; - client.tickets.archives.deleteMessage(message); - } + if (settings.log_messages && !message.system) client.tickets.archives.deleteMessage(message); // mark the message as deleted in the database (if it exists) } }; \ No newline at end of file diff --git a/src/listeners/messageUpdate.js b/src/listeners/messageUpdate.js index 6f2cd21..873152c 100644 --- a/src/listeners/messageUpdate.js +++ b/src/listeners/messageUpdate.js @@ -10,11 +10,11 @@ module.exports = { } } - let settings = await newm.guild?.settings; + if (!newm.guild) return; - if (settings?.log_messages) { - if (newm.system) return; - client.tickets.archives.updateMessage(newm); - } + let settings = await newm.guild.settings; + if (!settings) settings = await newm.guild.createSettings(); + + if (settings.log_messages && !newm.system) client.tickets.archives.updateMessage(newm); // update the message in the database } }; \ No newline at end of file diff --git a/src/modules/commands/manager.js b/src/modules/commands/manager.js index 50cd9cd..db285b3 100644 --- a/src/modules/commands/manager.js +++ b/src/modules/commands/manager.js @@ -1,7 +1,5 @@ // eslint-disable-next-line no-unused-vars const { Collection, Client, Message, MessageEmbed } = require('discord.js'); -// eslint-disable-next-line no-unused-vars -const Command = require('./command'); const fs = require('fs'); const { path } = require('../../utils/fs'); @@ -41,8 +39,7 @@ 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); + const is_internal = (exists && cmd.internal) || (exists && this.commands.get(cmd.name).internal); if (is_internal) { let plugin = this.client.plugins.plugins.find(p => p.commands.includes(cmd.name)); @@ -67,16 +64,18 @@ module.exports = class CommandManager { */ async handle(message) { 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); - let cmd_name = message.content.match(new RegExp(`^${prefix.replace(/(?=\W)/g, '\\')}(\\S+)`, 'mi')); - if (!cmd_name) return; + 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 let raw_args = message.content.replace(cmd_name[0], '').trim(); // remove the prefix and command - cmd_name = cmd_name[1]; // set cmd_name to the actual string + 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; @@ -85,16 +84,16 @@ module.exports = class CommandManager { if (cmd.process_args) { args = {}; - let data = [...raw_args.matchAll(/(?\w+)\??\s?:\s?(?([^;]|;{2})*);/gmi)]; - data.forEach(arg => args[arg.groups.key] = arg.groups.value.replace(/;{2}/gm, ';')); + let data = [...raw_args.matchAll(/(?\w+)\??\s?:\s?(?([^;]|;{2})*);/gmi)]; // an array of argument objects + data.forEach(arg => args[arg.groups.key] = arg.groups.value.replace(/;{2}/gm, ';')); // put the data into a useful format for (let arg of cmd.args) { if (arg.required && !args[arg]) { - return await cmd.sendUsage(message.channel, cmd_name); + return await cmd.sendUsage(message.channel, cmd_name); // send usage if any required arg is missing } } } else { - const args_num = raw_args.split(' ').filter(arg => arg.length !== 0).length; - const required_args = cmd.args.reduce((acc, arg) => arg.required ? acc + 1 : acc, 0); + const args_num = raw_args.split(' ').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); } @@ -120,8 +119,8 @@ module.exports = class CommandManager { }); guild_categories.forEach(cat => { cat.roles.forEach(r => staff_roles.add(r)); - }); - staff_roles = staff_roles.filter(r => message.member.roles.cache.has(r)); + }); // add all of the staff role IDs to the Set + staff_roles = staff_roles.filter(r => message.member.roles.cache.has(r)); // filter out any roles that the member does not have const not_staff = staff_roles.length === 0; if (not_staff) { return await message.channel.send( @@ -144,7 +143,7 @@ module.exports = class CommandManager { .setColor('ORANGE') .setTitle(i18n('command_execution_error.title')) .setDescription(i18n('command_execution_error.description')) - ); + ); // hopefully no user will ever see this message } } diff --git a/user/example.config.js b/user/example.config.js index 80d53c8..6aca452 100644 --- a/user/example.config.js +++ b/user/example.config.js @@ -25,6 +25,7 @@ module.exports = { command_prefix: prefix, log_messages: true, // transcripts/archives will be empty if false 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.__', }, locale: 'en-GB', // used for globals (such as commands) and the default guild locale logs: {