diff --git a/.gitignore b/.gitignore index 557cbda..bb57796 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules yarn-error.log .eslintrc.json logs +permissionExample.json \ No newline at end of file diff --git a/language/LocaleLoader.js b/language/LocaleLoader.js index 403b59d..7a1b878 100644 --- a/language/LocaleLoader.js +++ b/language/LocaleLoader.js @@ -25,11 +25,11 @@ class LocaleLoader { const _directory = path.join(process.cwd(), 'language/languages'); const directories = fs.readdirSync(_directory); //locale directories, ex. en_us, fi - for (const directory of directories) await this.loadLanguage(directory); + for (const directory of directories) this.loadLanguage(directory); } - async loadLanguage(language) { + loadLanguage(language) { this.client.logger.info(`Loading locale ${chalk.bold(language)}`); const directory = path.join(process.cwd(), `language/languages/${language}`); @@ -76,7 +76,7 @@ class LocaleLoader { } for(const [ key, value ] of Object.entries(parsed)) { - parsed[key] = value.replace(/^(\r)|(\r){1,}$/g, '') + parsed[key] = value.replace(/^(\r)|(\r){1,}$/g, ''); } return parsed; diff --git a/language/languages/en_us/commands/en_us_administrator.lang b/language/languages/en_us/commands/en_us_administrator.lang new file mode 100644 index 0000000..7ff1fce --- /dev/null +++ b/language/languages/en_us/commands/en_us_administrator.lang @@ -0,0 +1,20 @@ +//Grant Command + +[C_GRANT_DESCRIPTION] +Grant roles or users permissions for commands or modules. + +[C_GRANT_RESOLVEERROR] +Unable to find any roles or members, view `{prefix}cmd grant` for more help. + +[C_GRANT_MISSINGPERMPARAM] +You must provide permissions to grant, view `{prefix}cmd grant` for more help. + +//Revoke Command + +[C_REVOKE_DESCRIPTION] +Revoke permissions granted to roles or users. + +//Permissions Command + +[C_PERMISSIONS_DESCRIPTION] +View permissions granted to roles or users. \ No newline at end of file diff --git a/language/languages/en_us/commands/en_us_developer.lang b/language/languages/en_us/commands/en_us_developer.lang new file mode 100644 index 0000000..23553f1 --- /dev/null +++ b/language/languages/en_us/commands/en_us_developer.lang @@ -0,0 +1,5 @@ +[C_EVALUATE_DESCRIPTION] +Evaluates javascript code. + +[C_RELOAD_DESCRIPTION] +Reloads components and language files. \ No newline at end of file diff --git a/language/languages/en_us/commands/en_us_errors.lang b/language/languages/en_us/commands/en_us_errors.lang deleted file mode 100644 index f19b3f8..0000000 --- a/language/languages/en_us/commands/en_us_errors.lang +++ /dev/null @@ -1,3 +0,0 @@ -[C_HELP_404] -**ERROR:** -`{component}` was not found! \ No newline at end of file diff --git a/language/languages/en_us/commands/en_us_information.lang b/language/languages/en_us/commands/en_us_information.lang index 2d63977..8fbf2c3 100644 --- a/language/languages/en_us/commands/en_us_information.lang +++ b/language/languages/en_us/commands/en_us_information.lang @@ -1,4 +1,9 @@ -[C_HELP_USAGE] +//Help Command + +[C_HELP_DESCRIPTION] +Shows helpful information for commands, settings, and moderation. + +[C_HELP] **__HELP MENU__** For a list of all available commands, use `{prefix}commands [ group ]`. @@ -12,6 +17,8 @@ The bot splits arguments by space unless specified otherwise. To pass an argumen **❯ Documentation notation** **Optional** arguments are denoted by being encapsulated in brackets `[ ]` - means that the command will run either with default values or show usage prompt. **Required** arguments are denoted by less and greater than `< >` - means that the command will not run and return an error. +**Infinite** arguments (ones you can list several) are denoted by `..` after the argument. Ex `< argument.. >` - means you can pass more than one argument. +**Alternatives** are denoted by being separated by a `|`. **❯ Moderation** For help with moderation, see `{prefix}help modhelp`. @@ -35,4 +42,23 @@ __**{component} HELP**__ [C_HELP_HELP] Shows information about the bot and component usage. -To show help for commands `{prefix}help [ command ]` \ No newline at end of file +To show help for commands `{prefix}help [command]` + +//Commands Command + +[C_COMMANDS_DESCRIPTION] +Displays all commands and help. + +[C_COMMANDS_TITLE] +Available commands + +[C_COMMANDS_TEMPLATE] +Displaying commands for module `{mod}`. + +{text} + +[C_COMMANDS] +__Commands that are struck through are disabled.__ + +[C_COMMANDS_FOOTER] +To search by specific module, use {prefix}commands [ module ] \ No newline at end of file diff --git a/language/languages/en_us/commands/en_us_utility.lang b/language/languages/en_us/commands/en_us_utility.lang index 507e6d5..9ecacbb 100644 --- a/language/languages/en_us/commands/en_us_utility.lang +++ b/language/languages/en_us/commands/en_us_utility.lang @@ -3,8 +3,14 @@ [C_PING_RESPONSE] Pong! +[C_PING_DESCRIPTION] +Shows the millisecond delay between the bot and the discord server. + //Settings Command +[C_SETTINGS_DESCRIPTION] +Configure your guild and user settings. + [C_SETTINGS_ADMINISTRATORERROR] You must have the `ADMINISTRATOR` permission to reset the {type} settings. @@ -43,6 +49,9 @@ That setting does not exist! //User Command +[C_USER_DESCRIPTION] +Search for users or view user information. + [C_USER] **Nickname:** {nickname} **User:** <@{id}> diff --git a/language/languages/en_us/en_us_general.lang b/language/languages/en_us/en_us_general.lang index e3b9619..5b78486 100644 --- a/language/languages/en_us/en_us_general.lang +++ b/language/languages/en_us/en_us_general.lang @@ -11,5 +11,11 @@ switch({component}) { break; } -[EXAMPLES] -Example usage \ No newline at end of file +[GENERAL_EXAMPLES] +Example Usage + +[GENERAL_ALIASES] +Aliases + +[GENERAL_ARGUMENTS] +Arguments \ No newline at end of file diff --git a/language/languages/en_us/en_us_modules.lang b/language/languages/en_us/en_us_modules.lang index a100689..cf67afd 100644 --- a/language/languages/en_us/en_us_modules.lang +++ b/language/languages/en_us/en_us_modules.lang @@ -15,3 +15,5 @@ Information [M_MUSIC_NAME] Music + +[ diff --git a/middleware/logger/transports/DiscordWebhook.js b/middleware/logger/transports/DiscordWebhook.js index 97c13c7..d6d9f0d 100644 --- a/middleware/logger/transports/DiscordWebhook.js +++ b/middleware/logger/transports/DiscordWebhook.js @@ -19,7 +19,7 @@ class DiscordWebhook extends Transport { log(info, callback) { setImmediate(() => { - this.emit('logged', info); + this.emit('logged', info); }); const message = info.message.replace(regex, '') diff --git a/structure/client/DiscordClient.js b/structure/client/DiscordClient.js index 0b74b41..6ec771d 100644 --- a/structure/client/DiscordClient.js +++ b/structure/client/DiscordClient.js @@ -10,7 +10,7 @@ const Logger = require('./Logger.js'); const TransactionHandler = require('./TransactionHandler.js'); const LocaleLoader = require('../../language/LocaleLoader.js'); -const { Guild, User, Message } = require('../../structure/extensions/'); //eslint-disable-line +const { Guild, GuildMember, User, Message } = require('../../structure/extensions/'); //eslint-disable-line no-unused-vars const { Command, Observer, Inhibitor, Setting } = require('../../structure/interfaces/'); class DiscordClient extends Client { @@ -47,7 +47,7 @@ class DiscordClient extends Client { await super.login(this._options.bot.token); - await this.localeLoader.loadLanguages(); + this.localeLoader.loadLanguages(); await this.registry.loadComponents('components/inhibitors/', Inhibitor); await this.registry.loadComponents('components/commands/', Command); @@ -69,7 +69,7 @@ class DiscordClient extends Client { def = { ...def, ...setting.default - } + }; } } this._defaultConfig = def; diff --git a/structure/client/Registry.js b/structure/client/Registry.js index 4431147..0f18775 100644 --- a/structure/client/Registry.js +++ b/structure/client/Registry.js @@ -24,12 +24,14 @@ class Registry { const func = require(path); if(typeof func !== 'function') { this.client.logger.warn("Attempted to index an invalid function as a component."); + delete require.cache[path]; continue; } const component = new func(this.client); //Instantiates the component class. if(classToHandle && !(component instanceof classToHandle)) { this.client.logger.warn("Attempted to load an invalid class."); + delete require.cache[path]; continue; } @@ -44,6 +46,7 @@ class Registry { async loadComponent(component, directory) { if(!(component instanceof Component)) { + delete require.cache[directory]; this.client.logger.warn("Attempted to load an invalid component."); return null; } @@ -69,7 +72,10 @@ class Registry { } async unloadComponent(component) { - this.components.delete(component.id); + if(component.module) { + component.module.components.delete(component.resolveable); + } + this.components.delete(component.resolveable); } } diff --git a/structure/client/Resolver.js b/structure/client/Resolver.js index 156cee0..99696be 100644 --- a/structure/client/Resolver.js +++ b/structure/client/Resolver.js @@ -161,13 +161,13 @@ class Resolver { if(/<@!?([0-9]{17,21})>/.test(resolveable)) { let id = resolveable.match(/<@!?([0-9]{17,21})>/)[1]; - let member = await members.fetch(id).catch(err => { if(err.code === 10007) return false; else { console.warn(err); return false; } }); + let member = await members.fetch(id).catch(err => { if(err.code === 10007) return false; else { this.client.logger.warn(err); return false; } }); if(member) resolved.push(member); } else if(/(id\:)?([0-9]{17,21})/.test(resolveable)) { let id = resolveable.match(/(id\:)?([0-9]{17,21})/)[2]; - let member = await members.fetch(id).catch(err => { if(err.code === 10007) return false; else { console.warn(err); return false; } }); + let member = await members.fetch(id).catch(err => { if(err.code === 10007) return false; else { this.client.logger.warn(err); return false; } }); if(member) resolved.push(member); } else if(/^\@?([\S\s]{1,32})\#([0-9]{4})/.test(resolveable)) { @@ -175,21 +175,21 @@ class Resolver { let m = resolveable.match(/^\@?([\S\s]{1,32})\#([0-9]{4})/); let username = m[1].toLowerCase(); let discrim = m[2].toLowerCase(); - let [ member ] = members.cache.filter(m => { + let member = members.cache.filter(m => { return m.user.username.toLowerCase() === username && m.user.discriminator === discrim; - }).first(1); + }).first(); if(member) resolved.push(member); } else if(/^\@?([\S\s]{1,32})/.test(resolveable) && guild && !strict) { let nickname = resolveable.match(/^\@?([\S\s]{1,32})/)[0].toLowerCase(); - let [ member ] = members.cache.filter((m) => { + let member = members.cache.filter((m) => { return (m && m.user) && ((!m.nickname ? false : m.nickname.toLowerCase() == nickname ) || (!m.nickname ? false : m.nickname.toLowerCase().includes(nickname)) || m.user.username.toLowerCase().includes(nickname) || m.user.username.toLowerCase() == nickname); - }).first(1); + }).first(); if(member) resolved.push(member); } @@ -200,6 +200,13 @@ class Resolver { } + async resolveMember(resolveable, guild, strict) { + + let result = await this.resolveMembers([ resolveable ], guild, strict); + return result; + + } + /** * Resolve multiple channels * @@ -227,12 +234,12 @@ class Resolver { let name = /^\#?([a-z0-9\-\_0]*)/i; let id = /^\<\#([0-9]*)\>/i; - if(name.test(resolveable)) { + if (name.test(resolveable)) { let match = resolveable.match(name); let ch = match[1].toLowerCase(); - let channel = channels.cache.filter(c => { + let [ channel ] = channels.cache.filter(c => { if(!strict) return c.name.toLowerCase().includes(ch); return c.name.toLowerCase() === ch; }).first(1); @@ -287,7 +294,7 @@ class Resolver { } else { - let role = roles.cache.filter(r => { + let [ role ] = roles.cache.filter(r => { if(!strict) return r.name.toLowerCase().includes(resolveable.toLowerCase()); return r.name.toLowerCase() === resolveable.toLowerCase(); }).first(1); diff --git a/structure/client/components/commands/administrator/Grant.js b/structure/client/components/commands/administrator/Grant.js new file mode 100644 index 0000000..aac86ac --- /dev/null +++ b/structure/client/components/commands/administrator/Grant.js @@ -0,0 +1,101 @@ +const { Command, Argument } = require('../../../../interfaces/'); + +class GrantCommand extends Command { + + constructor(client) { + + super(client, { + name: 'grant', + module: 'administrator', + usage: " ", + examples: [ + "\"Server Moderators\" module:moderation", + "@nolan#2887 command:kick" + ], + memberPermissions: ['ADMINISTRATOR'], + showUsage: true, + guildOnly: true, + arguments: [ + new Argument(client, { + name: 'channel', + aliases: [ + 'channels' + ], + type: 'CHANNEL', + types: ['FLAG', 'VERBAL'], + infinite: true + }) + ] + }); + + } + + async execute(message, { params, args }) { + + const _permissions = await message.guild.permissions_(); + + const [ parse, ...perms ] = params; + const resolveable = await this._parseResolveable(message, parse); + if(perms.length === 0) { + await message.respond(message.format('C_GRANT_MISSINGPERMPARAM'), { emoji: 'failure' }); + return undefined; + } + + const permissions = this.client.registry.components.filter(c=> + c.type === 'command' + || c.type === 'module' + ); + + let parsed = []; + let failed = []; + for(const perm of perms) { + const search = permissions.filter(filterInexact(perm)).first(); + if(!search) failed.push(perm); + if(search.type === 'module') { + for(const component of search.components.values()) { + if(component.type === 'command') parsed.push(component.resolveable); + //add check for grantable + } + } else { + //add check for grantable + parsed.push(search.resolveable); + } + } + + let data = {}; + let existing = _permissions[resolveable.id]; + if(existing) { + for(let perm of parsed) { + if(existing.includes(perm)) failed.push(perm); + else existing.push(perm); + } + } else { + existing = parsed; + } + data = existing; + + + } + + async _parseResolveable(message, resolveable) { + let parsed = await this.client.resolver.resolveRoles(resolveable, message.guild); + if(!parsed) { + parsed = await this.client.resolver.resolveMembers(resolveable, message.guild); + if(!parsed) { + await message.respond(message.format('C_GRANT_RESOLVEERROR'), { emoji: 'failure' }); + return null; + } + } + return parsed[0]; + } + +} + +module.exports = GrantCommand; + +const filterInexact = (search) => { + return comp => comp.id.toLowerCase().includes(search) || + comp.resolveable.toLowerCase().includes(search) || + (comp.aliases && (comp.aliases.some(ali => `${comp.type}:${ali}`.toLowerCase().includes(search)) || + comp.aliases.some(ali => ali.toLowerCase().includes(search)))); +}; \ No newline at end of file diff --git a/structure/client/components/commands/administrator/Revoke.js b/structure/client/components/commands/administrator/Revoke.js new file mode 100644 index 0000000..b04925d --- /dev/null +++ b/structure/client/components/commands/administrator/Revoke.js @@ -0,0 +1,37 @@ +const { Command, Argument } = require('../../../../interfaces/'); + +class RevokeCommand extends Command { + + constructor(client) { + + super(client, { + name: 'revoke', + module: 'administrator', + usage: " ", + examples: [ + "\"Server Moderators\" module:moderation", + "@nolan#2887 command:kick" + ], + memberPermissions: ['ADMINISTRATOR'], + showUsage: true, + guildOnly: true, + arguments: [ + new Argument(client, { + name: 'channel', + type: 'CHANNEL', + types: ['FLAG', 'VERBAL'], + }) + ] + }); + + } + + async execute(message, { params, args }) { + + + + } + +} + +module.exports = RevokeCommand; \ No newline at end of file diff --git a/structure/client/components/commands/developer/Component.js b/structure/client/components/commands/developer/Component.js new file mode 100644 index 0000000..55327cc --- /dev/null +++ b/structure/client/components/commands/developer/Component.js @@ -0,0 +1,158 @@ +const { Command } = require('../../../../interfaces/'); +const path = require('path'); +const fs = require('fs'); + +class ComponentCommand extends Command { + + constructor(client) { + + super(client, { + name: 'component', + module: 'developer', + restricted: true, + aliases: [ + 'c', + 'comp' + ], + usage: '[component..]', + arguments: [ + // new Argument(client, { + // name: 'reload', + // type: 'STRING', + // types: ['FLAG'], + // aliases: [ 'r' ], + // description: "Reloads the language library", + // default: 'all' + // }) + ], + }); + + this.client = client; + + + } + + async execute(message, { params }) { + + // + const method = params.shift().toLowerCase(); + let response; + + if (method === 'reload') + response = this._handleReload(params); + else if (method === 'enable') + response = this._handleDisableEnable(params.shift().toLowerCase(), true); + else if (method === 'disable') + response = this._handleDisableEnable(params.shift().toLowerCase(), false); + else if (method === 'load') + response = this._handleLoadUnload(params.shift().toLowerCase(), true); + else if (method === 'unload') + response = this._handleLoadUnload(params.shift().toLowerCase(), false); + else return await message.respond('Invalid method. Can only be `reload`, `enable`, `disable`, `load`, `unload`', 'failure'); + + return await message.respond(response.msg, { emoji: response.error ? 'failure' : 'success' }); + + } + + _handleReload(params) { + + const name = params.length ? params.shift().toLowerCase() : 'all'; //ex. language + const value = params.length ? params.shift().toLowerCase() : 'all'; //ex. en_us --> combined: -component reload language en_us + + if (name === 'language' || name === 'lang' || name === 'l') { + + if (value === 'all') { + this.client.localeLoader.loadLanguages(); + return { msg: 'Reloaded all languages' }; + } else { + try { + this.client.localeLoader.loadLanguage(value); + return { msg: `Reloaded locale \`${value}\`` }; + } catch (err) { + return { error: true, msg: err.message }; + } + + } + + } else { + + if (name === 'all') { + + const errors = []; + const components = this.client.registry.components; + for (let component of components.values()) { + const result = component.reload(); + if (result.error) errors.push(`Component ${component.id} errored while reloading with code \`${result.code}\``); + } + if (errors.length) return { error: true, msg: `The following errors occurred during reload:\n${errors.join('\n')}` }; + return { msg: `Successfully reloaded all components` }; + + } else { + + const component = this.client.registry.components.get(name); + if (!component) return { error: true, msg: `Component ${name} doesn't exist.` }; + const result = component.reload(); + if (result.error) return { error: true, msg: `Component ${name} errored while reloading with code \`${result.code}\`` }; + else return { msg: `Successfully reloaded ${name}` }; + + } + + } + + } + + _handleDisableEnable(name, enable) { + + const component = this.client.registry.components.get(name); + let result; + if (!component) return { error: true, msg: `Component ${name} doesn't exist.` }; + if (enable) result = component.enable(); + else result = component.disable(); + + if (result.error) return { error: true, msg: `Cannot ${enable ? 'enable' : 'disable'} ${name} due to ${result.code}` }; + else return { msg: `Successfully ${enable ? 'enabled' : 'disabled'} component ${name}` }; + + } + + _handleLoadUnload(name, load) { + + let result; + if (load) { + const directory = path.join(process.cwd(), 'structure/client/components', name); + try { + fs.accessSync(directory); + } catch(err) { + return { error: true, msg: `\`${name}\` is an invalid path!` }; + } + // components/commands/utility/Ping.js + const func = require(directory); //directory + if (typeof func !== 'function') { + delete require.cache[directory]; + return { error: true, msg: 'Attempted to index an invalid function as a component.' }; + } + const component = new func(this.client); + result = this.client.registry.loadComponent(component, directory); + if (!result) return { error: true, msg: `Failed to load component ${name}, see console.` }; + return { msg: `Successfully loaded component: ${component.resolveable}` }; + + } else { + const component = this.client.registry.components.filter(filterInexact(name)).first(); + if (!component) return { error: true, msg: `Component ${name} doesn't exist.` }; + result = component.unload(); + } + + if (result.error) return { error: true, msg: `Cannot ${load ? 'load' : 'unload'} ${name} due to ${result.code}` }; + else return { msg: `Successfully ${load ? 'loaded' : 'unloaded'} component ${name}` }; + + } + +} + +module.exports = ComponentCommand; + +const filterInexact = (search) => { + return comp => comp.id.toLowerCase().includes(search) || + comp.resolveable.toLowerCase().includes(search) || + (comp.aliases && (comp.aliases.some(ali => `${comp.type}:${ali}`.toLowerCase().includes(search)) || + comp.aliases.some(ali => ali.toLowerCase().includes(search)))); +}; \ No newline at end of file diff --git a/structure/client/components/commands/developer/Evaluate.js b/structure/client/components/commands/developer/Evaluate.js index 1659cd9..300fad1 100644 --- a/structure/client/components/commands/developer/Evaluate.js +++ b/structure/client/components/commands/developer/Evaluate.js @@ -18,8 +18,8 @@ class Evaluate extends Command { 'eval', 'e' ], + usage: '', restricted: true, - description: "Evaluates javascript code.", arguments: [ new Argument(client, { name: 'log', @@ -33,12 +33,12 @@ class Evaluate extends Command { types: ['FLAG'], description: "Hides the output from the channel." }) - ] + ], + showUsage: true }); } - async execute(message, { params, args }) { params = params.join(' '); diff --git a/structure/client/components/commands/developer/Reload.js b/structure/client/components/commands/developer/Reload.js deleted file mode 100644 index 33d4d2c..0000000 --- a/structure/client/components/commands/developer/Reload.js +++ /dev/null @@ -1,52 +0,0 @@ -const { Command, Argument } = require('../../../../interfaces/'); - -class ReloadCommand extends Command { - - constructor(client) { - - super(client, { - name: 'reload', - module: 'developer', - description: 'Reloads components and locales.', - restricted: true, - aliases: ['r'], - arguments: [ - new Argument(client, { - name: 'language', - type: 'STRING', - types: ['FLAG'], - aliases: [ 'lang' ], - description: "Reloads the language library", - default: 'all' - }) - ] - }); - - this.client = client; - - - } - - async execute(message, { args }) { - - if (args.language) { - if (args.language.value === 'all') { - await this.client.localeLoader.loadLanguages(); - return message.respond('Reloaded all languages'); - } else { - try { - await this.client.localeLoader.loadLanguage(args.language.value); - return message.respond(`Reloaded locale \`${args.language.value}\``); - } catch (err) { - return message.respond(err.message); - } - - } - - } - - } - -} - -module.exports = ReloadCommand; \ No newline at end of file diff --git a/structure/client/components/commands/information/Commands.js b/structure/client/components/commands/information/Commands.js new file mode 100644 index 0000000..faab6a5 --- /dev/null +++ b/structure/client/components/commands/information/Commands.js @@ -0,0 +1,110 @@ +const { Command } = require('../../../../interfaces/'); + +class CommandsCommand extends Command { + + constructor(client) { + + super(client, { + name: 'commands', + module: 'information', + aliases: [ + 'cmd', + 'cmds', + 'command' + ], + usage: '[module]', + arguments: [ + // new Argument(client, { + // name: 'user', + // type: 'BOOLEAN', + // types: ['VERBAL', 'FLAG'], + // default: true + // }), + ] + }); + + this.client = client; + + } + + async execute(message, { params }) { + + if (!params.length) // list all commands + return this._listCommands(message); + + params = params.join(' '); + + const [ mod ] = this.client.resolver.components(params, 'module', false); + if(!mod) { + const [ command ] = this.client.resolver.components(params, 'command', false); + if (!command) return message.format('C_COMMAND_INVALID'); + return await message._showUsage(command); + } + //list module's commands + + const commands = mod.components.filter(c=>c.type === 'command'); + let text = ''; + + for(let command of commands.values()) { + text += command.disabled ? `~~${command.name}~~\n` : `${command.name}`; //TODO: Denote disabled commands somehow + } + + const embed = { + author: { + name: message.format('C_COMMANDS_TITLE'), + icon_url: this.client.user.avatarURL() + }, + description: message.format('C_COMMANDS') + '\n' + message.format('C_COMMANDS_TEMPLATE', { mod: mod.name, text }), + footer: { + text: message.format('C_COMMANDS_FOOTER') + } + }; + + return message.embed(embed); + + } + + _listCommands(message) { + + let fields = []; + const sortedModules = this.client.registry.components + .filter(c => c.type === 'module') + .sort((a, b) => { + const filter = c => c.type === 'command'; + return b.components.filter(filter) - a.components.filter(filter); + }); + + for (const mod of sortedModules.values()) { + let field = { + name: mod.id, + value: '', + inline: true + }; + + for (const command of mod.components.values()) { + if (command.type !== 'command' + || (command.restricted && !this.client._options.bot.owners.includes(message.author.id))) continue; + field.value += `${command.name}\n`; + } + if (field.value) fields.push(field); + } + + const embed = { + author: { + name: message.format('C_COMMANDS_TITLE'), + icon_url: this.client.user.avatarURL() + }, + description: message.format('C_COMMANDS'), + fields, + footer: { + text: message.format('C_COMMANDS_FOOTER') + } + }; + + return message.embed(embed); + + } + +} + +module.exports = CommandsCommand; \ No newline at end of file diff --git a/structure/client/components/commands/information/Help.js b/structure/client/components/commands/information/Help.js index 24887a7..b1b89d8 100644 --- a/structure/client/components/commands/information/Help.js +++ b/structure/client/components/commands/information/Help.js @@ -6,18 +6,20 @@ class HelpCommand extends Command { super(client, { name: 'help', - module: 'information', - description: 'Get help!', - showUsage: true + module: 'information' }); this.client = client; - } async execute(message, { params }) { + if (!params.length) + return await message.embed({ + description: message.format('C_HELP') + }); + const [ key ] = params; let [ result ] = this.client.resolver.components(key, 'command'); if (!result) [ result ] = this.client.resolver.components(key, 'setting'); diff --git a/structure/client/components/commands/utility/Arguments.js b/structure/client/components/commands/utility/Arguments.js index 837586e..8c725f3 100644 --- a/structure/client/components/commands/utility/Arguments.js +++ b/structure/client/components/commands/utility/Arguments.js @@ -9,7 +9,6 @@ class PingCommand extends Command { super(client, { name: 'arguments', module: 'utility', - description: "Tests the argument parsing of the command handler.", aliases: ['args', 'arg', 'argument'], arguments: [ new Argument(client, { @@ -32,7 +31,9 @@ class PingCommand extends Command { required: true, types: ['FLAG', 'VERBAL'] }) - ] + ], + restricted: true, + archivable: false }); this.client = client; diff --git a/structure/client/components/commands/utility/Settings.js b/structure/client/components/commands/utility/Settings.js index 4323d27..8f63ee0 100644 --- a/structure/client/components/commands/utility/Settings.js +++ b/structure/client/components/commands/utility/Settings.js @@ -28,9 +28,7 @@ class SettingCommand extends Command { default: true }) ], - memberPermissions: ['ADMINISTRATOR'], - showUsage: true, - + showUsage: true }); this.client = client; @@ -67,17 +65,16 @@ class SettingCommand extends Command { //Setting permission handling if(setting.clientPermissions.length > 0) { - const missing = message.channel.permissionsFor(message.guild.me).missing(command.clientPermissions); + const missing = message.channel.permissionsFor(message.guild.me).missing(setting.clientPermissions); if(missing.length > 0) { await message.respond(message.format('C_SETTINGS_CLIENTPERMISSIONERROR', { setting: setting.moduleResolveable, missing: missing.join(', ')}), { emoji: 'failure' }); return undefined; } - } else if(setting.memberPermissions.length > 0) { - const missing = message.channel.permissionsFor(message.member).missing(command.memberPermissions); - if(missing.length > 0) { - await message.respond(message.format('C_SETTINGS_MEMBERPERMISSIONERROR', { setting: setting.moduleResolveable, missing: missing.join(', ')}), { emoji: 'failure' }); - return undefined; - } + } + + if(message.channel.permissionsFor(message.member).missing('ADMINISTRATOR').length > 0 && setting.resolve === 'GUILD') { + await message.respond(message.format('C_SETTINGS_ADMINISTRATORERROR', { type: type.toLowerCase() }), { emoji: 'failure' }); + return undefined; } const response = await setting.handle(message, params.splice(1)); @@ -138,8 +135,8 @@ class SettingCommand extends Command { if(!bool) return message.respond(message.format('C_SETTINGS_RESETABORT'), { emoji: 'success' }); type === 'USER' - ? await message.author._deleteSettings() - : await message.guild._deleteSettings(); + ? await message.author._delete() + : await message.guild._delete('guilds'); return message.respond(message.format('C_SETTINGS_RESETSUCCESS', { type: type.toLowerCase() }), { emoji: 'success' }) diff --git a/structure/client/components/commands/utility/TestStorage.js b/structure/client/components/commands/utility/TestStorage.js deleted file mode 100644 index 8baf0a3..0000000 --- a/structure/client/components/commands/utility/TestStorage.js +++ /dev/null @@ -1,33 +0,0 @@ -const { Command } = require('../../../../interfaces/'); - -class PingCommand extends Command { - - constructor(client) { - - super(client, { - name: 'test', - module: 'utility', - description: "Determines the ping of the bot.", - arguments: [ - - ] - }); - - this.client = client; - - - } - - async execute(message) { - - const time1 = new Date().getTime(); - let response = await this.client.transactionHandler.send({ provider: 'mongodb', request: { collection: 'infractions', type: 'find', query: { case: 1 } } }).catch(err => { return err; }); - const time2 = new Date().getTime(); - console.log(time2-time1); - - message.reply(JSON.stringify(response)); - } - -} - -module.exports = PingCommand; \ No newline at end of file diff --git a/structure/extensions/Guild.js b/structure/extensions/Guild.js index a21a84c..e1c34f5 100644 --- a/structure/extensions/Guild.js +++ b/structure/extensions/Guild.js @@ -10,30 +10,37 @@ const Guild = Structures.extend('Guild', (Guild) => { super(...args); this._settings = null; //internal cache of current guild's settings; should ALWAYS stay the same as database. + this._permissions = null; //internal cache, should always match database. } async settings() { - if(!this._settings) this._settings = this.client.transactionHandler.send({ provider: 'mongodb', request: { collection: 'guilds', type: 'findOne', query: { guildId: this.id } } }); - if(this._settings instanceof Promise) this._settings = await this._settings || null + if(this._settings instanceof Promise) this._settings = await this._settings || null; if(!this._settings) { this._settings = this.client.defaultConfig; } - return this._settings; - } - /* Database Shortcuts */ + async permissions_() { + if(!this._permissions) this._permissions = this.client.transactionHandler.send({ provider: 'mongodb', request: { collection: 'permissions', type: 'findOne', query: { guildId: this.id } } }); + if(this._permissions instanceof Promise) this._permissions = await this._permissions || null; + if(!this._permissions) { + this._permissions = {}; + } + return this._permissions; + } - async _deleteSettings() { //Delete whole entry - remove + /* Settings Wrapper */ + + async _dbDelete(collection) { //Delete whole entry - remove try { await this.client.transactionHandler.send({ provider: 'mongodb', request: { type: 'remove', - collection: 'guilds', + collection, query: { guildId: this.id } @@ -46,21 +53,22 @@ const Guild = Structures.extend('Guild', (Guild) => { } } - async _updateSettings(data) { //Update property (upsert true) - updateOne + async _dbUpdateOne(data, collection) { //Update property (upsert true) - updateOne + var index = this.dbIndex(collection); try { await this.client.transactionHandler.send({ provider: 'mongodb', request: { type: 'updateOne', - collection: 'guilds', + collection, query: { guildId: this.id }, data } }); - this._settings = { - ...this._settings, + index = { + ...index, ...data }; this._storageLog(`Database Update (guild:${this.id}).`); @@ -69,18 +77,18 @@ const Guild = Structures.extend('Guild', (Guild) => { } } - async _removeSettings(value) { //Remove property - if(this.client.defaultConfig[value]) { + async _dbRemoveProperty(value, collection) { //Remove property + const index = this.dbIndex(collection); + if(collection === 'guild' && this.client.defaultConfig[value]) { await this._updateSettings(this.client.defaultConfig[value]); return undefined; } - try { await this.client.transactionHandler.send({ provider: 'mongodb', request: { type: 'removeProperty', - collection: 'guilds', + collection, query: { guildId: this.id }, @@ -89,33 +97,23 @@ const Guild = Structures.extend('Guild', (Guild) => { ] } }); - delete this._settings[value]; + delete index[value]; this._storageLog(`Database Remove (guild:${this.id}).`); } catch(error) { this._storageError(error); } } - - /* - async _createSettings(data) { - try { - this.client.transactionHandler.send({ - provider: 'mongodb', - request: { - type: 'insertOne', - collection: 'guilds', - data: { - guildId: this.id, - ...data - } - } - }); - this._storageLog(`Database Create (guild:${this.id}).`); - } catch(error) { - this._storageError(error); - } + + dbIndex(collection) { + return { + 'guilds': this._settings, + 'permissions': this._permissions + }[collection]; } - */ + + /* Permissions Wrapper */ + + /* Language Formatting */ diff --git a/structure/extensions/GuildMember.js b/structure/extensions/GuildMember.js new file mode 100644 index 0000000..a5ca615 --- /dev/null +++ b/structure/extensions/GuildMember.js @@ -0,0 +1,26 @@ +const { Structures } = require('discord.js'); + +const GuildMember = Structures.extend('GuildMember', (GuildMember) => { + + class ExtendedGuildMember extends GuildMember { + + constructor(...args) { + + super(...args); + + this._cached = Date.now(); + + } + + get timeSinceCached() { + return Date.now()-this._cached; + } + + + } + + return ExtendedGuildMember; + +}); + +module.exports = GuildMember; \ No newline at end of file diff --git a/structure/extensions/Message.js b/structure/extensions/Message.js index c8934bc..fcb9cc1 100644 --- a/structure/extensions/Message.js +++ b/structure/extensions/Message.js @@ -4,6 +4,7 @@ const escapeRegex = require('escape-string-regexp'); const emojis = require('../../util/emojis.json'); const { Util } = require('../../util/'); +const { stripIndents } = require('common-tags') const Message = Structures.extend('Message', (Message) => { @@ -26,7 +27,7 @@ const Message = Structures.extend('Message', (Message) => { let language = this.author._settings.locale || 'en_us'; if(this.guild && this.guild._settings.locale) language = this.guild._settings.locale; - parameters.prefix = this.guild ? this.guild.prefix : this.client._options.bot.prefix; + parameters.prefix = this.guild?.prefix || this.client._options.bot.prefix; let template = this.client.localeLoader.template(language, index); //.languages[language][index]; if(!template) { @@ -118,18 +119,46 @@ const Message = Structures.extend('Message', (Message) => { }); } - async _showUsage() { - //TODO: format this - return await this.embed({ - title: `**${this.command.name.toUpperCase()} USAGE**`, - description: this.format(`C_${this.command.name.toUpperCase()}_USAGE`), - fields: [ - { - name: this.format('EXAMPLES'), - value: this.format(`C_${this.command.name.toUpperCase()}_EXAMPLES`) - } - ] - }); + async _showUsage(comp = null) { + + const component = comp + ? comp + : this.command; + + const prefix = this.guild?.prefix + || this.client._options.bot.prefix; + + let fields = []; + if(component.examples.length > 0) { + fields.push({ + name: `》${this.format('GENERAL_EXAMPLES')}`, + value: component.examples.map(e=>`\`${prefix}${component.name} ${e}\``).join('\n') + }); + } + if(component.aliases.length > 0) { + fields.push({ + name: `》${this.format('GENERAL_ALIASES')}`, + value: component.aliases.map(a=>`\`${a}\``).join(', ') + }); + } + if(component.arguments.length > 0) { + fields.push({ + name: `》${this.format('GENERAL_ARGUMENTS')}`, + value: component.arguments.map(a=>`${a.name}: ${this.format(`A_${a.name.toUpperCase}_DESCRIPTION`)}`) + }); + } + + let embed = { + author: { + name: `${component.name}${component.module ? ` (${component.module.resolveable})` : ''}`, + icon_url: this.client.user.avatarURL() + }, + description: stripIndents`\`${prefix}${component.name}${component.usage ? ` ${component.usage}` : ''}\` + ${this.format(`C_${component.name.toUpperCase()}_DESCRIPTION`)}${component.guildOnly ? ' *(guild-only)*' : ''}`, + fields + }; + + return await this.embed(embed); } } diff --git a/structure/extensions/index.js b/structure/extensions/index.js index 153bfe4..bbaf634 100644 --- a/structure/extensions/index.js +++ b/structure/extensions/index.js @@ -1,5 +1,6 @@ module.exports = { Message: require('./Message.js'), Guild: require('./Guild.js'), + GuildMember: require('./GuildMember.js'), User: require('./User.js') }; \ No newline at end of file diff --git a/structure/interfaces/Command.js b/structure/interfaces/Command.js index 463a4fe..27ed734 100644 --- a/structure/interfaces/Command.js +++ b/structure/interfaces/Command.js @@ -19,14 +19,15 @@ class Command extends Component { this.aliases = opts.aliases || []; this.description = `C_${opts.name}_DESCRIPTION`; - this.examples = `C_${opts.name}_EXAMPLES`; - this.usage = `C_${opts.name}_USAGE`; + this.usage = opts.usage || ''; + this.examples = opts.examples || []; this.restricted = Boolean(opts.restricted); - this.archivable = opts.archivable === undefined ? false : Boolean(opts.archivable); - this.guildOnly = Boolean(opts.guildOnly); - this.arguments = opts.arguments || []; this.showUsage = Boolean(opts.showUsage); + this.guildOnly = Boolean(opts.guildOnly); + + this.archivable = opts.archivable === undefined ? false : Boolean(opts.archivable); + this.arguments = opts.arguments || []; this.clientPermissions = opts.clientPermissions || []; this.memberPermissions = opts.memberPermissions || []; diff --git a/structure/interfaces/Setting.js b/structure/interfaces/Setting.js index 582d127..00c3545 100644 --- a/structure/interfaces/Setting.js +++ b/structure/interfaces/Setting.js @@ -41,15 +41,8 @@ class Setting extends Component { } async _parseArguments(params) { - const { parsedArguments, newArgs } = await this.commandHandler._parseArguments(this.arguments, params); return { parsedArguments, params: newArgs }; - - } - - async _handleReset(message, params) { - const response = await message.prompt(message.format('UHHH'), { error: 'warning' }); - } get commandHandler() {