diff --git a/options.json b/options.json index dd8df36..28f86b8 100644 --- a/options.json +++ b/options.json @@ -9,6 +9,7 @@ "GUILD_MESSAGES" ] }, + "invite": "https://discord.gg/49u6cHu", "shardOptions": { "totalShards": "auto" }, diff --git a/package.json b/package.json index b4a2ef7..77cb240 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "chalk": "^4.1.2", "discord.js": "^13.2.0-dev.1629115738.9a833b1", "dotenv": "^10.0.0", + "escape-string-regexp": "^5.0.0", "eslint": "^7.32.0", "moment": "^2.29.1", "mongodb": "^3.5.9", diff --git a/src/Util.js b/src/Util.js index f76667f..68557bb 100644 --- a/src/Util.js +++ b/src/Util.js @@ -104,6 +104,16 @@ class Util { return ['.', '+', '*', '?', '\\[', '\\]', '^', '$', '(', ')', '{', '}', '|', '\\\\', '-']; } + static escapeRegex(string) { + if(typeof string !== 'string') { + throw new Error("Invalid type sent to escapeRegex."); + } + + return string + .replace(/[|\\{}()[\]^$+*?.]/gu, '\\$&') + .replace(/-/gu, '\\x2d'); + } + static duration(seconds) { const { plural } = this; let s = 0, @@ -129,7 +139,7 @@ class Util { } static get date() { - return moment().format("YYYY-MM-DD hh:mm:ss"); + return moment().format("YYYY-MM-DD HH:mm:ss"); } } diff --git a/src/constants/index.js b/src/constants/index.js index 99a8f8b..be378c4 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,4 +1,4 @@ module.exports = { Commands: require('./Commands.json'), Emojis: require('./Emojis.json') -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/localization/en_us/observers/en_us_commandHandler.lang b/src/localization/en_us/observers/en_us_commandHandler.lang index ac86902..9daa24f 100644 --- a/src/localization/en_us/observers/en_us_commandHandler.lang +++ b/src/localization/en_us/observers/en_us_commandHandler.lang @@ -1,3 +1,30 @@ [O_COMMANDHANDLER_COMMANDNOTSYNCED] It appears as if the command does not exist on the client. This is an issue that should be reported to a bot developer. + +[O_COMMANDHANDLER_GUILDONLY] +This command can only be run in servers. + +[O_COMMANDHANDLER_TYPEINTEGER] +The command option {option} requires an integer between `{min}` and `{max}`. + +[O_COMMANDHANDLER_TYPEMEMBER] +The command option {option} requires a server member. + +[O_COMMANDHANDLER_TYPETEXT_CHANNEL] +The command option {option} requires a text channel. + +[O_COMMANDHANDLER_TYPEVOICE_CHANNEL] +The command option {option} requires a voice channel. + +[O_COMMANDHANDLER_TYPENUMBER] +The command option {option} requires a number between `{min}` and `{max}`. + +[O_COMMANDHANDLER_TYPEFLOAT] +The command option {option} requires a float between `{min}` and `{max}`. + +[O_COMMANDHANDLER_ERROR] +An error occured while executing that command. +It is recommended to contact a developer about this issue. + +You can join the support server by clicking on the button below. \ No newline at end of file diff --git a/src/middleware/BaseClient.js b/src/middleware/BaseClient.js index 9a2a3af..160d5f6 100644 --- a/src/middleware/BaseClient.js +++ b/src/middleware/BaseClient.js @@ -28,13 +28,13 @@ class BaseClient extends EventEmitter { async build() { - await this.shardingManager.spawn().catch((err) => { - this.error(`Fatal error during shard spawning:\n${err.stack}`); + await this.shardingManager.spawn().catch((error) => { + this.logger.error(`Fatal error during shard spawning:\n${error.stack || error}`); // eslint-disable-next-line no-process-exit process.exit(); // Prevent a boot loop when shards die due to an error in the client }); - const API = await import('./api/index.js').catch((err) => this.warn(`Error importing API files:\n${err.stack}`)); + const API = await import('./api/index.js').catch((error) => this.logger.warn(`Error importing API files:\n${error.stack || error}`)); if (API) { this.info('Booting up API'); const { default: APIManager } = API; @@ -135,7 +135,7 @@ class BaseClient extends EventEmitter { } - async getLiveGuild(shard, message) { + async getLiveGuild(shard, message) { //eslint-disable-line no-unused-vars // TODO: Figure out what exactly this should return, waiting for further client implementation // const result = await this.shardingManager.broadcastEval((client) => { @@ -144,18 +144,6 @@ class BaseClient extends EventEmitter { } - warn(message) { - this.logger.write('warn', message); - } - - info(message) { - this.logger.write('info', message); - } - - error(message) { - this.logger.write('error', message); - } - } module.exports = BaseClient; \ No newline at end of file diff --git a/src/middleware/Logger.js b/src/middleware/Logger.js index 7c9e25d..2d8e65e 100644 --- a/src/middleware/Logger.js +++ b/src/middleware/Logger.js @@ -4,6 +4,7 @@ const path = require('path'); const fs = require('fs'); const Util = require('../Util.js'); +const { runInThisContext } = require('vm'); const Constants = { Types: [ @@ -105,6 +106,26 @@ class Logger { return `${id}`.length === 1 ? `0${id}` : `${id}`; } + error(message) { + this.write('error', message); + } + + warn(message) { + this.write('warn', message); + } + + debug(message) { + this.write('debug', message); + } + + info(message) { + this.write('info', message); + } + + status(message) { + this.write('status', message); + } + } module.exports = Logger; \ No newline at end of file diff --git a/src/structure/DiscordClient.js b/src/structure/DiscordClient.js index ae3948f..cabad56 100644 --- a/src/structure/DiscordClient.js +++ b/src/structure/DiscordClient.js @@ -24,16 +24,17 @@ class DiscordClient extends Client { this.resolver = new Resolver(this); + this._activity = 0; this._options = options; this._built = false; - this.once('ready', () => { - this._setActivity(); + // this.once('ready', () => { + // this._setActivity(); - setInterval(() => { - this._setActivity(); - }, 1800000); // I think this is 30 minutes. I could be wrong. - }); + // setInterval(() => { + // this._setActivity(); + // }, 1800000); // I think this is 30 minutes. I could be wrong. + // }); } @@ -87,7 +88,6 @@ class DiscordClient extends Client { return Boolean(this.shard.ids[0] === 0); } - } module.exports = DiscordClient; diff --git a/src/structure/client/LocaleLoader.js b/src/structure/client/LocaleLoader.js index bdabb87..0826d2b 100644 --- a/src/structure/client/LocaleLoader.js +++ b/src/structure/client/LocaleLoader.js @@ -23,6 +23,27 @@ class LocaleLoader { } + format(language, index, parameters = {}, code = false) { + + let string = this.languages[language][index]; + if(!string) return `< Missing Locale: ${language}.${index} >`; + + for(const [ parameter, value ] of Object.entries(parameters)) { + string = string.replace(new RegExp(`{${Util.escapeRegex(parameter.toLowerCase())}}`, 'giu'), value); + } + + if(code) { + try { + string = eval(string); //eslint-disable-line no-eval + } catch(error) { + this.client.logger.error(`Locale [${language}.${index}] failed to execute code.\n${error.stack || error}`); + } + } + + return string; + + } + _loadLanguage(root, language) { const directory = path.join(root, language); diff --git a/src/structure/components/commands/administration/SettingsCommand.js b/src/structure/components/commands/administration/SettingsCommand.js new file mode 100644 index 0000000..5936bf3 --- /dev/null +++ b/src/structure/components/commands/administration/SettingsCommand.js @@ -0,0 +1,44 @@ +const { SlashCommand, CommandOption } = require("../../../interfaces"); + +class SettingsCommand extends SlashCommand { + + constructor(client) { + super(client, { + name: 'settings', + description: "Invoke to display a settings menu.", + module: 'administration', + options: [ + new CommandOption({ + name: 'category', + description: "Select a category to view settings for.", + type: 'STRING', + choices: [ + { + name: 'Administration', + value: 'administrator' + }, + { + name: 'Moderation', + value: 'moderation' + }, + { + name: 'Utility', + value: 'utility' + } + ], + required: true + }) + ], + guildOnly: true + }); + } + + async execute(thing, options) { + + + + } + +} + +module.exports = SettingsCommand; \ No newline at end of file diff --git a/src/structure/components/commands/moderation/MuteCommand.js b/src/structure/components/commands/moderation/MuteCommand.js index e2cf705..c51c4cc 100644 --- a/src/structure/components/commands/moderation/MuteCommand.js +++ b/src/structure/components/commands/moderation/MuteCommand.js @@ -33,10 +33,12 @@ class MuteCommand extends SlashCommand { }); } - async execute(interaction) { + async execute(thing, options) { + + console.log(options); // console.log(interaction, interaction.options); - interaction.reply(this.resolveable); + thing.reply({ content: this.resolveable }); } diff --git a/src/structure/components/observers/CommandHandler.js b/src/structure/components/observers/CommandHandler.js index c2a13e9..7fee260 100644 --- a/src/structure/components/observers/CommandHandler.js +++ b/src/structure/components/observers/CommandHandler.js @@ -45,11 +45,29 @@ class CommandHandler extends Observer { const command = this._matchCommand(interaction.commandName); const thing = new Thing(this.client, command, interaction); - if(!command) return thing.reply({ locale: 'O_COMMANDHANDLER_COMMANDNOTSYNCED', emoji: 'failure', ephemeral: true }); + if(!command) return thing.reply({ content: thing.format('O_COMMANDHANDLER_COMMANDNOTSYNCED'), emoji: 'failure', ephemeral: true }); const response = await this._parseInteraction(thing); if(response.error) { - + return thing.reply({ + content: thing.format(`O_COMMANDHANDLER_TYPE${response.option.type}`, { option: response.option.name, min: response.option.minimum, max: response.option.maximum }), + emoji: 'failure', + ephemeral: true + }); + } + + return this._executeCommand(thing, response.options); + + } + + async _executeCommand(thing, options) { + + try { + const resolved = thing.command.execute(thing, options); + if(resolved instanceof Promise) await resolved; + } catch(error) { + this.client.logger.error(error.stack || error); + this._generateError(thing); } } @@ -63,9 +81,12 @@ class CommandHandler extends Observer { } let error = null; + const options = {}; + for(const option of interaction.options._hoistedOptions) { const matched = command.options.find((o) => o.name === option.name); - const newOption = new CommandOption({ name: matched.name, type: matched.type, _rawValue: option.value }); + console.log('matched', matched) + const newOption = new CommandOption({ name: matched.name, type: matched.type, minimum: matched.minimum, maximum: matched.maximum, _rawValue: option.value }); const parsed = await this._parseOption(thing, newOption); if(parsed.error) { @@ -77,43 +98,43 @@ class CommandHandler extends Observer { } newOption.value = parsed.value; - thing.options.push(newOption); + options[matched.name] = newOption; } if(error) return error; return { error: false, - //uhhh.. - } - console.log(options); + options + }; } async _parseOption(thing, option) { const types = { - ROLES: (string) => { + // ROLES: (string) => { - }, - MEMBERS: (string) => { + // }, + // MEMBERS: (string) => { - }, - USERS: (string) => { + // }, + // USERS: (string) => { - }, - CHANNELS: (string) => { + // }, + // CHANNELS: (string) => { - }, - TEXT_CHANNELS: (string) => { + // }, + // TEXT_CHANNELS: (string) => { - }, - VOICE_CHANNELS: (string) => { + // }, + // VOICE_CHANNELS: (string) => { - }, + // }, STRING: (string) => { return { error: false, value: string }; }, INTEGER: (integer) => { + console.log(option); if(option.minimum !== undefined && integer < option.minimum) return { error: true }; if(option.maximum !== undefined && integer > option.maximum) return { error: true }; return { error: false, value: parseInt(integer) }; @@ -188,7 +209,26 @@ class CommandHandler extends Observer { return command || null; } - _generateError() { + _generateError(thing) { + + thing.reply({ + content: thing.format('O_COMMANDHANDLER_ERROR'), + emoji: 'failure', + ephemeral: true, + components: [ + { + type: 'ACTION_ROW', + components: [ + { + label: 'Support', + type: 'BUTTON', + style: 'LINK', + url: this.client._options.discord.invite + } + ] + } + ] + }); } diff --git a/src/structure/components/settings/moderation/MuteSetting.js b/src/structure/components/settings/moderation/MuteSetting.js new file mode 100644 index 0000000..cbd737c --- /dev/null +++ b/src/structure/components/settings/moderation/MuteSetting.js @@ -0,0 +1,18 @@ +const { Setting } = require('../../../interfaces/'); + +class MuteSetting extends Setting { + + constructor(client) { + + super(client, { + name: 'mute', + description: 'uhhhhh' + }); + + } + + + +} + +module.exports = MuteSetting; \ No newline at end of file diff --git a/src/structure/interfaces/Argument.js b/src/structure/interfaces/Argument.js deleted file mode 100644 index 5bdfc56..0000000 --- a/src/structure/interfaces/Argument.js +++ /dev/null @@ -1,63 +0,0 @@ -const Constants = { - ArgumentOptionTypes: { //https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type - "SUB_COMMAND": 1, - "SUB_COMMAND_GROUP": 2, - "STRING": 3, - "INTEGER": 4, - "BOOLEAN": 5, - "USER": 6, - "CHANNEL": 7, - "ROLE": 8, - "MENTIONABLE": 9, - "NUMBER": 10 - } -}; - -/* -ApplicationCommandOption { - type - name - description - required - choices - options (only used for subcommands/groups) -} - -*/ - -class Argument { - - constructor(client, options = {}) { - if(!options) return null; - - this.client = client; - - this.key = options.key?.toLowerCase(); - this.description = options.description; - - this.aliases = options.aliases || null; - - this.type = options.type || 'STRING'; - - this.aliases = options.aliases || []; - this.types = options.types || ['FLAG', 'VERBAL']; - this.choices = options.choices || []; - - this.required = Boolean(options.required); - this.infinite = Boolean(options.infinite); - - this.parser = typeof options.parser === 'function' ? options.parser : null; - - this.value = undefined; //Defined in the Command Handler. - - } - - get structure() { - return { - name: this.key, - description: this.description, - type: - } - } - -} \ No newline at end of file diff --git a/src/structure/interfaces/CommandOption.js b/src/structure/interfaces/CommandOption.js index db6f1eb..482d2cc 100644 --- a/src/structure/interfaces/CommandOption.js +++ b/src/structure/interfaces/CommandOption.js @@ -35,8 +35,8 @@ class CommandOption { this.choices = options.choices || []; //Used for STRING/INTEGER/NUMBER types. this.options = options.options || []; //Used for SUB_COMMAND/SUB_COMMAND_GROUP types. - this.minimum = options.minimum || undefined; //Used for INTEGER/NUMBER/FLOAT types. - this.maxiumum = options.maximum || undefined; + this.minimum = typeof options.minimum === 'number' ? options.minimum : undefined; //Used for INTEGER/NUMBER/FLOAT types. + this.maximum = typeof options.maximum === 'number' ? options.maximum : undefined; this.value = undefined; diff --git a/src/structure/interfaces/Observer.js b/src/structure/interfaces/Observer.js index 8970134..413977f 100644 --- a/src/structure/interfaces/Observer.js +++ b/src/structure/interfaces/Observer.js @@ -18,18 +18,6 @@ class Observer extends Component { } - execute() { - return this._continue(); - } - - _continue() { - return { error: false, observer: this }; - } - - _stop() { - return { error: true, observer: this }; - } - } module.exports = Observer; \ No newline at end of file diff --git a/src/structure/interfaces/Setting.js b/src/structure/interfaces/Setting.js new file mode 100644 index 0000000..b7e03a6 --- /dev/null +++ b/src/structure/interfaces/Setting.js @@ -0,0 +1,24 @@ +const Component = require("./Component.js"); + +class Setting extends Component { + + constructor(client, options = {}) { + if(!options) return null; + + super(client, { + id: options.name, + _type: 'setting', + disabled: options.disabled, + guarded: options.guarded + }); + + this.name = options.name; + this.module = options.module; + + this.description = options.description || ""; + + } + +} + +module.exports = Setting; \ No newline at end of file diff --git a/src/structure/interfaces/Thing.js b/src/structure/interfaces/Thing.js index abbc923..79c0b26 100644 --- a/src/structure/interfaces/Thing.js +++ b/src/structure/interfaces/Thing.js @@ -19,11 +19,6 @@ class Thing { async reply(options = {}) { - if(options.locale) { - options.content = this.format(options.locale); - // delete options.locale; - } - if(options.emoji) { if(!Emojis[options.emoji]) this.client.logger.warn(`Invalid emoji provided to command ${this.command.resolveable}: "${options.emoji}"`); options.content = `${Emojis[options.emoji]} ${options.content}`; @@ -34,10 +29,10 @@ class Thing { return this._pending; } - format(locale) { + format(locale, parameters = {}, code = false) { const language = 'en_us'; //Default language. //TODO: Fetch guild/user settings and switch localization. - return this.client.localeLoader.languages[language][locale]; + return this.client.localeLoader.format(language, locale, parameters, code); } get guild() { diff --git a/src/structure/interfaces/index.js b/src/structure/interfaces/index.js index a8b018d..83ad16a 100644 --- a/src/structure/interfaces/index.js +++ b/src/structure/interfaces/index.js @@ -5,5 +5,6 @@ module.exports = { SlashCommand: require('./commands/SlashCommand.js'), Command: require('./commands/Command.js'), CommandOption: require('./CommandOption.js'), - Thing: require('./Thing.js') + Thing: require('./Thing.js'), + Setting: require('./Setting.js') }; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 426674d..112c2d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -679,6 +679,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"