From 339eb93bf875ead7653656d535a7d2c9def5e7a8 Mon Sep 17 00:00:00 2001 From: nolan Date: Tue, 5 May 2020 19:40:46 -0400 Subject: [PATCH 1/2] Settings command base, localization fixes, guild storage wrapper, and guild prefix setting. --- language/LocaleLoader.js | 8 +- .../en_us_utility.lang} | 0 .../en_us/commands/en_us_utility.lang | 47 +++-- language/languages/en_us/en_us_general.lang | 12 ++ language/languages/en_us/en_us_modules.lang | 17 ++ .../en_us/settings/en_us_utility.lang | 11 ++ storage/providers/Mongodb.js | 175 +++++++++++++++--- structure/client/DiscordClient.js | 20 +- structure/client/Resolver.js | 22 +++ structure/client/TransactionHandler.js | 30 +-- .../components/commands/utility/Ping.js | 3 +- .../components/commands/utility/Settings.js | 111 +++++++++-- .../commands/utility/TestStorage.js | 2 +- .../inhibitors/ClientPermissions.js | 2 +- .../components/observers/CommandHandler.js | 17 +- .../settings/utility/GuildPrefix.js | 54 ++++++ structure/extensions/Guild.js | 120 +++++++++++- structure/extensions/Message.js | 24 ++- structure/extensions/User.js | 2 +- structure/interfaces/Setting.js | 31 +++- structure/moderation/interfaces/Infraction.js | 2 +- 21 files changed, 620 insertions(+), 90 deletions(-) rename language/languages/en_us/{commands/en_us_settings.lang => arguments/en_us_utility.lang} (100%) create mode 100644 language/languages/en_us/en_us_general.lang create mode 100644 language/languages/en_us/en_us_modules.lang create mode 100644 language/languages/en_us/settings/en_us_utility.lang create mode 100644 structure/client/components/settings/utility/GuildPrefix.js diff --git a/language/LocaleLoader.js b/language/LocaleLoader.js index 79ca0cb..403b59d 100644 --- a/language/LocaleLoader.js +++ b/language/LocaleLoader.js @@ -15,7 +15,8 @@ class LocaleLoader { template(locale, index) { - return this.languages[locale] ? this.languages[locale][index] : `Locale ${locale} does not exist.` || `Entry ${locale}:${index} does not exist.`; + // console.log(String.raw`${this.languages[locale][index]}`); + return (this.languages[locale] ? this.languages[locale][index] : `Locale ${locale} does not exist.`) || `Entry ${locale}:${index} does not exist.`; } @@ -55,6 +56,7 @@ class LocaleLoader { let matched = null; let text = ''; for(const line of lines) { + if(line.startsWith('//')) continue; const matches = line.match(/\[([_A-Z0-9]{1,})]/, 'gi'); if(matches) { if(matched) { @@ -73,8 +75,8 @@ class LocaleLoader { } } - if(matched) { - parsed[matched] = text; + for(const [ key, value ] of Object.entries(parsed)) { + parsed[key] = value.replace(/^(\r)|(\r){1,}$/g, '') } return parsed; diff --git a/language/languages/en_us/commands/en_us_settings.lang b/language/languages/en_us/arguments/en_us_utility.lang similarity index 100% rename from language/languages/en_us/commands/en_us_settings.lang rename to language/languages/en_us/arguments/en_us_utility.lang diff --git a/language/languages/en_us/commands/en_us_utility.lang b/language/languages/en_us/commands/en_us_utility.lang index f36c4cc..5b5335a 100644 --- a/language/languages/en_us/commands/en_us_utility.lang +++ b/language/languages/en_us/commands/en_us_utility.lang @@ -1,21 +1,36 @@ -[C_PING] -Pong! {number} +//Ping Command -[C_MODE_TEST] -switch('{mode}') { - case 'explicit': - 'works1'; - break; - case 'fuzzy': - 'works2'; - break; -} +[C_PING_RESPONSE] +Pong! -[C_PING_HELP] -what? +//Settings Command -[C_SETTING_RESET] -Are you sure you want to reset __all__ of your guild settings to the default values? +[C_SETTINGS_RESET] +Are you sure you want to reset **all** of your {type} settings to the default values? This cannot be undone. *(__y__es, __n__o)* +This prompt will time out in __30 seconds__. + +[C_SETTINGS_RESETERROR] +You provided an invalid input, please try again. + +[C_SETTINGS_RESETSUCCESS] +All {type} settings were successfully deleted and set to default. + +[C_SETTINGS_USERSETTINGSTITLE] +User Settings + +[C_SETTINGS_GUILDSETTINGSTITLE] +Guild Settings + +[C_SETTINGS_LISTSETTINGS] +Use `{prefix}setting [setting-name]` to view a description on the setting. Each setting is unique, so make sure to read carefully. + +[C_SETTINGS_LISTSETTINGSALT] +Alternatively, you can view user settings by using the command `{prefix}settings -u` + +[C_SETTINGS_NONEXIST] +That setting does not exist! + +//User Command [C_USER] **Nickname:** {nickname} @@ -35,4 +50,4 @@ Search result for keyword: `{key}` Found {matches} matches, displaying {count} [C_USER_HELP] -To search server members with similar names use `{prefix}user search < arguments >` \ No newline at end of file +To search server members with similar names use `{prefix}user search ` \ No newline at end of file diff --git a/language/languages/en_us/en_us_general.lang b/language/languages/en_us/en_us_general.lang new file mode 100644 index 0000000..296e02f --- /dev/null +++ b/language/languages/en_us/en_us_general.lang @@ -0,0 +1,12 @@ +[GENERAL_COMPONENTS] +switch({component}) { + case "command": + "command"; + break; + case "setting": + "setting"; + break; + case "inhibitor": + "inhibitor": + break; +} \ 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 new file mode 100644 index 0000000..a100689 --- /dev/null +++ b/language/languages/en_us/en_us_modules.lang @@ -0,0 +1,17 @@ +[M_UTILITY_NAME] +Utility + +[M_MODERATION_NAME] +Moderation + +[M_DEVELOPER_NAME] +Developer + +[M_ADMINISTRATOR_NAME] +Administrator + +[M_INFORMATION_NAME] +Information + +[M_MUSIC_NAME] +Music diff --git a/language/languages/en_us/settings/en_us_utility.lang b/language/languages/en_us/settings/en_us_utility.lang new file mode 100644 index 0000000..da2685c --- /dev/null +++ b/language/languages/en_us/settings/en_us_utility.lang @@ -0,0 +1,11 @@ +[S_GUILDPREFIX_DESCRIPTION] +Customizes your prefix in the guild. + +[S_GUILDPREFIX_SUCCESS] +Successfully set the guild prefix to `{prefix}`. + +[S_GUILDPREFIX_LENGTH] +The guild prefix cannot exceed {max} characters. `[{length}/{max}]`. + +[S_GUILDPREFIX_RESET] +Successfully reset the guild prefix to `{prefix}`. \ No newline at end of file diff --git a/storage/providers/Mongodb.js b/storage/providers/Mongodb.js index 572c68a..a936917 100644 --- a/storage/providers/Mongodb.js +++ b/storage/providers/Mongodb.js @@ -33,26 +33,86 @@ class MongoDBProvider extends Provider { type: '', -- The function to use from this class, ex. findOne collection: '', -- Which collection to query from query: { }, -- Actual search query - data: { } -- If the operation is to update or insert something, this is what to insert + data: { }, -- If the operation is to update or insert something, this is what to insert + upsert: bool, -- If true and no matching documents are found, insert a new one + options: { }, + projection: { } -- The fields that should be returned } */ if (!this._initialized) return { error: true, message: 'MongoDB not connected' }; if (!this[request.type]) return { error: true, message: `Invalid request type, got '${request.type}'` }; - if (!request.collection) return { error: true, message: `You must specify a collection to query!` }; + if (!request.collection && request.type !== 'stats') return { error: true, message: `You must specify a collection to query!` }; return this[request.type](request); } - remove({ collection, query }) { - + /** + * Fetches basic statistics about the database or collection + * + * @param {String} collection The collection to query, optional + * @param {Object} options Optional options for the stats method + * @returns + * @memberof MongoDBProvider + */ + stats({ collection, options = { } }) { + return new Promise((resolve, reject) => { - - this.db.collection(collection).remove(query, (err, result) => { - - if (err) reject(err); - resolve(result); + + if (!collection || collection.length) { + + this.db.stats(options, (error, stats) => { + + if (err) return reject(err); + else { + + let { db, collections, objects, averageSize: avgObjSize, dataSize } = stats; + return resolve({ db, collections, objects, averageSize, dataSize }); + + } + + }); + + } else { + + this.db.collection(collection).stats(options, (err, stats) => { + + if (err) return reject(err); + else { + + let { index: ns, size, count, averageSize: avgObjSize } = stats; + return resolve({ index, size, count, averageSize }); + + } + + }); + + } + + }); + + } + + /** + * Count the objects in + * + * @param {String} collection The collection to query + * @param {Object} query Only documents matching this will be counted, optional + * @param {Object} options Optional options, see mongo docs for these + * @returns + * @memberof MongoDBProvider + */ + count({ collection, query, options }) { + + return new Promise((resolve, reject) => { + + this.db.collection(collection).countDocuments(query, options, (error, result) => { + + if (error) + return reject(error); + else + return resolve(result); }); @@ -60,6 +120,38 @@ class MongoDBProvider extends Provider { } + + /** + * Remove a document from a collection + * + * @param {String} collection The collection to remove from + * @param {Object} query Any documents matching this will be removed + * @returns + * @memberof MongoDBProvider + */ + remove({ collection, query }) { + + return new Promise((resolve, reject) => { + + this.db.collection(collection).deleteOne(query, (err, result) => { + + if (err) return reject(err); + return resolve(result); + + }); + + }); + + } + + /** + * Insert one document to the collection + * + * @param {String} collection The collection to insert into + * @param {Object} data The document to insert + * @returns + * @memberof MongoDBProvider + */ insertOne({ collection, data }) { return new Promise((resolve, reject) => { @@ -77,17 +169,21 @@ class MongoDBProvider extends Provider { /** * Find and return the first match + * + * @param {String} collection The collection to find from + * @param {Object} query Documents matching this will be returned + * @param {Object} projection Defines which fields to return, { 'key': 1, 'key2': 0 } -- key will return, key2 won't, optional * @memberof Database */ - find({ collection, query }) { + find({ collection, query, projection }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming find query for ${db} with parameters ${JSON.stringify(query)}`); return new Promise((resolve, reject) => { if(!this._initialized) reject(new Error('MongoDB not connected')); - this.db.collection(collection).find(query, async (error, cursor) => { + this.db.collection(collection).find(query, { projection }, async (error, cursor) => { if(error) return reject(error); return resolve(await cursor.toArray()); @@ -101,20 +197,21 @@ class MongoDBProvider extends Provider { /** * Find and return the first match * - * @param {String} db The collection in which the data is to be updated + * @param {String} collection The collection in which the data is to be updated * @param {Object} query The filter that is used to find the data + * @param {Object} projection Defines which fields to return, { 'key': 1, 'key2': 0 } -- key will return, key2 won't, optional * @returns {Object} An object containing the queried data * @memberof Database */ - findOne({ collection, query }) { + findOne({ collection, query, projection }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming findOne query for ${db} with parameters ${JSON.stringify(query)}`); return new Promise((resolve, reject) => { if(!this._initialized) reject(new Error('MongoDB not connected')); - this.db.collection(collection).findOne(query, async (error, item) => { + this.db.collection(collection).findOne(query, { projection }, async (error, item) => { if(error) return reject(error); return resolve(item); @@ -128,14 +225,14 @@ class MongoDBProvider extends Provider { /** * Update the first filter match. * - * @param {String} db The collection in which the data is to be updated - * @param {Object} filter The filter that is used to find the data + * @param {String} collection The collection in which the data is to be updated + * @param {Object} query The filter that is used to find the data * @param {Object} data The updated data - * @param {Boolean} [upsert=false] Create a new entry if no match is found + * @param {Boolean} [upsert=true] Create a new entry if no match is found * @returns {WriteResult} Object containing the followint counts: Matched, Upserted, Modified * @memberof Database */ - updateOne({ collection, query, data, upsert = false }) { + updateOne({ collection, query, data, upsert = true }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming updateOne query for ${db} with parameters ${JSON.stringify(filter)}`); return new Promise((resolve, reject) => { @@ -156,17 +253,47 @@ class MongoDBProvider extends Provider { } + /** + * Remove a document property + * + * @param {String} collection The collection to query + * @param {Object} query The filter that is used to find the data + * @param {Array} data Array of fields to remove + * @returns + * @memberof MongoDBProvider + */ + removeProperty({ collection, query, data }) { + return new Promise((resolve, reject) => { + + let unset = {}; + for (let field in data) unset[field] = ''; + this.db.collection(collection).updateOne(query, { $unset: unset }, async (error, result) => { + + if(error) return reject(error); + else { + + let { matchedCount, modifiedCount } = result; + return resolve({ matched: matchedCount, modified: modifiedCount }); + + } + + }); + + }); + + } + /** * Push data to an array * - * @param {string} db The collection to query - * @param {object} filter The filter to find the document to update + * @param {string} collection The collection to query + * @param {object} query The filter to find the document to update * @param {object} data The data to be pushed - * @param {boolean} [upsert=false] Create a new entry if no match is found + * @param {boolean} [upsert=true] Create a new entry if no match is found * @returns * @memberof Database */ - push({ collection, query, data, upsert = false }) { + push({ collection, query, data, upsert = true }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming push query for ${db}, with upsert ${upsert} and with parameters ${JSON.stringify(filter)} and data ${JSON.stringify(data)}`); return new Promise((resolve, reject) => { @@ -187,8 +314,8 @@ class MongoDBProvider extends Provider { /** * Find a random element from a database * - * @param {string} db The collection to query - * @param {object} [filter={}] The filtering object to narrow down the sample pool + * @param {string} collection The collection to query + * @param {object} [query={}] The filtering object to narrow down the sample pool * @param {number} [amount=1] Amount of items to return * @returns {object} * @memberof Database diff --git a/structure/client/DiscordClient.js b/structure/client/DiscordClient.js index 7b3956d..3d30bd4 100644 --- a/structure/client/DiscordClient.js +++ b/structure/client/DiscordClient.js @@ -28,6 +28,7 @@ class DiscordClient extends Client { this.localeLoader = new LocaleLoader(this); this._options = options; + this._defaultConfig = null; this._built = false; process.on('message', this._handleMessage.bind(this)); @@ -51,7 +52,7 @@ class DiscordClient extends Client { await this.registry.loadComponents('components/inhibitors/', Inhibitor); await this.registry.loadComponents('components/commands/', Command); await this.registry.loadComponents('components/observers/', Observer); - // await this.registry.loadComponents('components/settings/', Setting); + await this.registry.loadComponents('components/settings/', Setting); await this.dispatcher.dispatch(); @@ -59,6 +60,23 @@ class DiscordClient extends Client { } + get defaultConfig() { + if(this._defaultConfig) return this._defaultConfig; + const settings = this.registry.components.filter(c=>c.type === 'setting' && c.resolve === 'GUILD'); + let def = {}; + for(const setting of settings.values()) { + if(setting.default !== null) { + def = { + ...def, + ...setting.default + } + } + } + console.log(def); + this._defaultConfig = def; + return def; + } + } module.exports = DiscordClient; diff --git a/structure/client/Resolver.js b/structure/client/Resolver.js index cebd8a6..156cee0 100644 --- a/structure/client/Resolver.js +++ b/structure/client/Resolver.js @@ -17,6 +17,28 @@ class Resolver { return components || []; } + + resolveBoolean(input) { + input = input.toLowerCase(); + const truthy = [ 'on', 'true', 'yes', 'enable', 'y' ]; + const falsey = [ 'off', 'false', 'no', 'disable', 'n' ]; + + if(truthy.includes(input)) { + return true; + } else if (falsey.includes(input)) { + return false; + } else { + return null; + } + } + + // resolveTrue(input) { + // return [ 'on', 'true' ].includes(input.toLowerCase()); + // } + + // resolveFalse(input) { + // return [].includes(input.toLowerCase()); + // } async resolveMemberAndUser(string, guild) { diff --git a/structure/client/TransactionHandler.js b/structure/client/TransactionHandler.js index 11e3019..24c1713 100644 --- a/structure/client/TransactionHandler.js +++ b/structure/client/TransactionHandler.js @@ -9,21 +9,7 @@ class TransactionHandler { } - _receive(message) { - - if(!message.transactionID) return undefined; - const transaction = this.transactions.get(message.transactionID); - - if(!transaction) return undefined; - - if(message.error) transaction.reject(message.message); - else transaction.resolve(message.result); - - this.transactions.delete(message.transactionID); - - } - - _send(message, options = {}) { + send(message, options = {}) { /** Message structure * { @@ -48,6 +34,20 @@ class TransactionHandler { } + _receive(message) { + + if(!message.transactionID) return undefined; + const transaction = this.transactions.get(message.transactionID); + + if(!transaction) return undefined; + + if(message.error) transaction.reject(message.message); + else transaction.resolve(message.result); + + this.transactions.delete(message.transactionID); + + } + get transactionID() { return `${[ this.client.shard.ids ]}-${Date.now().toString(36)}`; } diff --git a/structure/client/components/commands/utility/Ping.js b/structure/client/components/commands/utility/Ping.js index 30187de..8157a8d 100644 --- a/structure/client/components/commands/utility/Ping.js +++ b/structure/client/components/commands/utility/Ping.js @@ -17,7 +17,8 @@ class PingCommand extends Command { async execute(message) { const ping = this.client.ws.ping.toFixed(0); - return message.respond(`${message.format('PING_COMMAND_RESPONSE')} \`${ping}ms\``, { emoji: 'success' }); + //console.log(message.format('C_PING_RESPONSE')) + return message.respond(`${message.format('C_PING_RESPONSE')} \`${ping}ms\``, { emoji: 'success' }); } } diff --git a/structure/client/components/commands/utility/Settings.js b/structure/client/components/commands/utility/Settings.js index ebe6de3..e11932e 100644 --- a/structure/client/components/commands/utility/Settings.js +++ b/structure/client/components/commands/utility/Settings.js @@ -1,4 +1,6 @@ -const { Command } = require('../../../../interfaces/'); +const { Command, Argument } = require('../../../../interfaces/'); + +const { stripIndents } = require('common-tags'); class SettingCommand extends Command { @@ -12,7 +14,23 @@ class SettingCommand extends Command { 'settings', 'set' ], - showUsage: true + arguments: [ + new Argument(client, { + name: 'user', + type: 'BOOLEAN', + types: ['VERBAL', 'FLAG'], + default: true + }), + new Argument(client, { + name: 'all', + type: 'BOOLEAN', + types: ['VERBAL', 'FLAG'], + default: true + }) + ], + memberPermissions: ['ADMINISTRATOR'], + showUsage: true, + }); this.client = client; @@ -20,31 +38,94 @@ class SettingCommand extends Command { } - //-settings list - //-settings reset - //-settings walkthrough - //-settings [setting] + async execute(message, { params, args }) { - async execute(message, { params }) { + const type = (!message.guild || args.user) ? 'USER' : 'GUILD'; const target = params[0].toLowerCase(); // params[0] should never be null, see showUsage if(target === 'list') { - this._listSettings(message); - return; + this._listSettings(message, type, Boolean(args.all)); + return undefined; } else if(target === 'reset') { - const prompt = await this.message.prompt(message.format('C_SETTING_RESET'), { emoji: 'warning' }); - return this._handleReset(prompt, message); + const prompt = await message.prompt(message.format('C_SETTINGS_RESET', { type: type.toLowerCase() }), { emoji: 'warning' }); + return await this._handleReset(prompt, message, type); } else if(target === 'walkthrough') { - return; + //TODO + return undefined; } - const [ setting ] = this.client.resolver.components(params, 'setting', false); + const settings = this.client.resolver.components(target, 'setting', false).sort(c=>c.resolve === type); + const [ setting ] = settings; if(!setting) { - + await message.respond(message.format('C_SETTINGS_NONEXISTANT'), { emoji: 'failure' }); + return undefined; } + const response = await setting.handle(message, params.splice(1)); + message.respond(response.msg, { emoji: response.error ? 'failure' : 'success' }); + + } + + _listSettings(message, type, all) { + if(!message.guild && type === 'GUILD') type = 'USER'; + + const prefix = message.guild.prefix; + + let fields = []; + const sorted = this.client.registry.components + .filter(c=>c.type === 'module') + .sort((a, b) => { + const filter = c=>c.type === 'setting'; + return b.components.filter(filter) - a.components.filter(filter); + }); + + for(const module of sorted.values()) { + let field = { + name: module.id, + value: '', + inline: true + }; + + for(const setting of module.components.values()) { + if(setting.type !== 'setting' + || (setting.resolve !== type && !all) + || (setting.restricted && !all)) continue; + field.value += `\`${setting.display}\`\n`; + } + if(field.value) fields.push(field); + } + + const embed = { + author: { + name: `${type === 'GUILD' ? message.format('C_SETTINGS_GUILDSETTINGSTITLE') : message.format('C_SETTINGS_USERSETTINGSTITLE')}`, + icon_url: type === 'GUILD' ? message.guild.iconURL() : message.author.avatarURL() + }, + description: stripIndents`${message.format('C_SETTINGS_LISTSETTINGS', { prefix })} + + ${type === 'USER' ? '' : message.format('C_SETTINGS_LISTSETTINGSALT', { prefix })}`, + fields + }; + + return message.embed(embed); + + } + + async _handleReset(prompt, message, type) { + + if(!prompt) return; + const response = prompt.content.toLowerCase(); + const bool = this.client.resolver.resolveBoolean(response); + if(bool === null) return message.respond(message.format('C_SETTINGS_RESETERROR'), { emoji: 'failure' }); + if(!bool) return message.respond(message.format('C_SETTINGS_RESETABORT'), { emoji: 'success' }); + + type === 'USER' + ? await message.author._deleteSettings() + : await message.guild._deleteSettings(); + + return message.respond(message.format('C_SETTINGS_RESETSUCCESS', { type: type.toLowerCase() }), { emoji: 'success' }) + } } -module.exports = SettingCommand; \ No newline at end of file +module.exports = SettingCommand; diff --git a/structure/client/components/commands/utility/TestStorage.js b/structure/client/components/commands/utility/TestStorage.js index ac47d20..8baf0a3 100644 --- a/structure/client/components/commands/utility/TestStorage.js +++ b/structure/client/components/commands/utility/TestStorage.js @@ -21,7 +21,7 @@ class PingCommand extends Command { 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; }); + 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); diff --git a/structure/client/components/inhibitors/ClientPermissions.js b/structure/client/components/inhibitors/ClientPermissions.js index 317151a..9d6c0e3 100644 --- a/structure/client/components/inhibitors/ClientPermissions.js +++ b/structure/client/components/inhibitors/ClientPermissions.js @@ -15,7 +15,7 @@ class ClientPermissions extends Inhibitor { } execute(message, command) { - const missing = message.channel.permissionsFor(message.guild.me).missing(command.memberPermissions); + const missing = message.channel.permissionsFor(message.guild.me).missing(command.clientPermissions); if(missing.length > 0) { return super._fail(stripIndents`The command **${command.resolveable}** requires the bot to have permissions to use. *Missing: ${missing.join(', ')}*`); diff --git a/structure/client/components/observers/CommandHandler.js b/structure/client/components/observers/CommandHandler.js index bfe428d..7f1367e 100644 --- a/structure/client/components/observers/CommandHandler.js +++ b/structure/client/components/observers/CommandHandler.js @@ -53,12 +53,13 @@ class CommandHandler extends Observer { async _getCommand(message, [arg1, arg2, ...args]) { - const prefix = this.client._options.bot.prefix; //Change this for guild prefix settings. + const settings = await message.guild.settings(); + const prefix = message.guild.prefix; let command = null; let remains = []; if(arg1 && arg1.startsWith(prefix)) { - const commandName = arg1.slice(1); + const commandName = arg1.slice(prefix.length); command = await this._matchCommand(message, commandName); remains = [arg2, ...args]; } else if(arg1 && arg2 && arg1.startsWith('<@')){ @@ -165,7 +166,10 @@ class CommandHandler extends Observer { const [one,two,...chars] = word.split(''); if(one === '-' && two !== '-') { const name = [ two, ...chars ].join(''); - if(!keys.includes(name)) continue; + if(!keys.includes(name)) { + params.push(word); + continue; + } currentArgument = shortFlags[name]; if(currentArgument.type === 'BOOLEAN') { currentArgument.value = currentArgument.default; @@ -177,11 +181,14 @@ class CommandHandler extends Observer { return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: true }, message }); } continue; - } else if((one === '-' && two === '-') || one === '—') { //Handling for "long dash" on mobile phones x_x + } else if((one === '-' && two === '-') || (one === '—')) { //Handling for "long dash" on mobile phones x_x const name = one === '—' ? [ two, ...chars ].join('').toLowerCase() : chars.join('').toLowerCase(); //can convert to lowercase now that shortFlags are out of the way. - if(!keys.includes(name)) continue; + if(!keys.includes(name)) { + params.push(word); + continue; + } currentArgument = longFlags[name]; if(currentArgument.type === 'BOOLEAN') { currentArgument.value = currentArgument.default; diff --git a/structure/client/components/settings/utility/GuildPrefix.js b/structure/client/components/settings/utility/GuildPrefix.js new file mode 100644 index 0000000..e0dd1a9 --- /dev/null +++ b/structure/client/components/settings/utility/GuildPrefix.js @@ -0,0 +1,54 @@ +const { Setting } = require('../../../../interfaces/'); + +class GuildPrefixSetting extends Setting { + + constructor(client, opts = {}) { + + super(client, { + name: 'guildPrefix', + index: 'prefix', + module: 'utility', + display: 'prefix', + aliases: [ + 'prefix' + ], + guarded: true, + resolve: 'GUILD', + default: { + prefix: "-" + }, + custom: true + }); + + this.client = client; + + } + + async handle(message, params, operator) { + + if (operator === 'RESET') { + return await super._handleReset(message, params); + } + + // let { params, parsedArguments } = await this._parseArguments(params); + + let [ prefix ] = params; + + const MaxCharacters = 6; + if(prefix.length > MaxCharacters) return { + msg: message.format('S_GPREFIX_LENGTH', { length: prefix.length, max: MaxCharacters }), + error: true + }; + + await message.guild._updateSettings({ prefix }); + return { + msg: message.format(`S_${this.name.toUpperCase()}_SUCCESS`, { prefix }), + error: false + } + + } + + +} + +module.exports = GuildPrefixSetting; \ No newline at end of file diff --git a/structure/extensions/Guild.js b/structure/extensions/Guild.js index d5e777d..a21a84c 100644 --- a/structure/extensions/Guild.js +++ b/structure/extensions/Guild.js @@ -15,12 +15,110 @@ const Guild = Structures.extend('Guild', (Guild) => { async settings() { - if (!this._settings) this._settings = this.client.transactionHandler._send({ provider: 'mongodb', request: { collection: 'guild_settings', type: 'findOne', query: { guild: this.id } } }); - if (this._settings instanceof Promise) this._settings = await this._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) { + this._settings = this.client.defaultConfig; + } + return this._settings; } + /* Database Shortcuts */ + + async _deleteSettings() { //Delete whole entry - remove + try { + await this.client.transactionHandler.send({ + provider: 'mongodb', + request: { + type: 'remove', + collection: 'guilds', + query: { + guildId: this.id + } + } + }); + this._settings = this.client.defaultConfig; + this._storageLog(`Database Delete (guild:${this.id}).`); + } catch(error) { + this._storageError(error); + } + } + + async _updateSettings(data) { //Update property (upsert true) - updateOne + try { + await this.client.transactionHandler.send({ + provider: 'mongodb', + request: { + type: 'updateOne', + collection: 'guilds', + query: { + guildId: this.id + }, + data + } + }); + this._settings = { + ...this._settings, + ...data + }; + this._storageLog(`Database Update (guild:${this.id}).`); + } catch(error) { + this._storageError(error); + } + } + + async _removeSettings(value) { //Remove property + if(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', + query: { + guildId: this.id + }, + data: [ + value + ] + } + }); + delete this._settings[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); + } + } + */ + + /* Language Formatting */ + format(key, parameters = {}) { const language = this._settings.locale || "en_us"; //this._settings.language or something idk @@ -33,6 +131,8 @@ const Guild = Structures.extend('Guild', (Guild) => { } + /* Resolver Shortcuts */ + async resolveMembers(members, strict = false) { return await this.client.resolver.resolveMembers(members, this, strict); @@ -72,6 +172,22 @@ const Guild = Structures.extend('Guild', (Guild) => { } + + + /* Logging */ + + _storageLog(log) { + this.client.logger.debug(log) + } + + _storageError(error) { + this.client.logger.error(`Database Error (guild:${this.id}) : \n${error.stack || error}`) + } + + get prefix() { + return this._settings.prefix; + } + } return ExtendedGuild; diff --git a/structure/extensions/Message.js b/structure/extensions/Message.js index c4e2ae3..a030d0b 100644 --- a/structure/extensions/Message.js +++ b/structure/extensions/Message.js @@ -26,15 +26,17 @@ 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._settings.prefix ? this.guild._settings.prefix : this.client._options.bot.prefix ; + parameters.prefix = this.guild ? this.guild.prefix : this.client._config.bot.prefix; let template = this.client.localeLoader.template(language, index); //.languages[language][index]; + if(!template) { + return `**Missing language index \`${language} [${index}]\` in languages. Contact a bot developer about this.**`; + } + for (const [param, val] of Object.entries(parameters)) { template = template.replace(new RegExp(`{${escapeRegex(param.toLowerCase())}}`, 'gi'), val); } - template = template.replace(new RegExp(`\r`, 'gi'), '\n'); - if(code) { try { template = eval(template); @@ -86,6 +88,7 @@ const Message = Structures.extend('Message', (Message) => { if(opts.reply) str = `<@!${this.author.id}> ${str}`; } + //console.log(str) this._pending = await this.channel.send(str); return this._pending; @@ -100,6 +103,21 @@ const Message = Structures.extend('Message', (Message) => { return super.edit(str); } + async prompt(str, opts) { + if(typeof str === 'string') { + if(opts.emoji) str = `${emojis[opts.emoji]} ${str}`; + if(opts.reply) str = `<@!${this.author.id}> ${str}`; + } + const message = await this.channel.send(str); + return await this.channel.awaitMessages(m => m.author.id === this.author.id, { max: 1, time: 30000, errors: ['time'] }) + .then((collected) => { + return collected.first(); + }) + .catch((error) => { + return null; + }); + } + async _showUsage() { //TODO: format this return await this.embed({ diff --git a/structure/extensions/User.js b/structure/extensions/User.js index 9462362..3feeaa4 100644 --- a/structure/extensions/User.js +++ b/structure/extensions/User.js @@ -20,7 +20,7 @@ const User = Structures.extend('User', (User) => { async settings() { - if (!this._settings) this._settings = this.client.transactionHandler._send({ provider: 'mongodb', request: { collection: 'user_settings', type: 'findOne', query: { user: this.id } } }); + if (!this._settings) this._settings = this.client.transactionHandler.send({ provider: 'mongodb', request: { collection: 'users', type: 'findOne', query: { user: this.id } } }); if (this._settings instanceof Promise) this._settings = await this._settings || {}; return this._settings; diff --git a/structure/interfaces/Setting.js b/structure/interfaces/Setting.js index 304e890..582d127 100644 --- a/structure/interfaces/Setting.js +++ b/structure/interfaces/Setting.js @@ -25,13 +25,42 @@ class Setting extends Component { this.aliases = opts.aliases || []; this.resolve = (opts.resolve && Constants.Resolves.includes(opts.resolve)) ? opts.resolve : 'GUILD'; this.default = opts.default; + this.arguments = opts.arguments || []; + this.custom = Boolean(opts.custom); + this.display = opts.display || opts.name; this.memberPermissions = opts.memberPermissions || []; this.clientPermissions = opts.clientPermissions || []; + this._commandHandler = null; + } - + async handle() { + this.client.logger.error(`No handle function found in ${this.moduleResolveable}.`); + } + + 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() { + if(this._commandHandler) return this._commandHandler; + else return this._commandHandler = this.client.registry.components.get('observer:commandHandler'); + } + + + get moduleResolveable() { + return `${this.module.id}:${this.id}`; + } } diff --git a/structure/moderation/interfaces/Infraction.js b/structure/moderation/interfaces/Infraction.js index b38f045..5db5305 100644 --- a/structure/moderation/interfaces/Infraction.js +++ b/structure/moderation/interfaces/Infraction.js @@ -36,7 +36,7 @@ class Infraction { async save() { - await this.client.transactionHandler._send({ + await this.client.transactionHandler.send({ provider: 'mongodb', request: { type: 'insertOne', From 5f23bfe4f3d5ee69b2c48110946c4c934732ecff Mon Sep 17 00:00:00 2001 From: nolan Date: Wed, 6 May 2020 03:11:00 -0400 Subject: [PATCH 2/2] settings permission handling that will need to be changed later --- .../en_us/commands/en_us_utility.lang | 13 +++++++++++- structure/client/DiscordClient.js | 1 - .../components/commands/utility/Settings.js | 21 ++++++++++++++++++- .../components/observers/CommandHandler.js | 5 +++-- structure/extensions/Message.js | 2 +- 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/language/languages/en_us/commands/en_us_utility.lang b/language/languages/en_us/commands/en_us_utility.lang index 5b5335a..507e6d5 100644 --- a/language/languages/en_us/commands/en_us_utility.lang +++ b/language/languages/en_us/commands/en_us_utility.lang @@ -5,6 +5,17 @@ Pong! //Settings Command +[C_SETTINGS_ADMINISTRATORERROR] +You must have the `ADMINISTRATOR` permission to reset the {type} settings. + +[C_SETTINGS_CLIENTPERMISSIONERROR] +The setting **{setting}** requires __the bot__ to have permissions to use. +*Missing: {missing}* + +[C_SETTINGS_MEMBERPERMISSIONERROR] +The setting **{setting}** requires __you__ to have permissions to use. +*Missing: {missing}* + [C_SETTINGS_RESET] Are you sure you want to reset **all** of your {type} settings to the default values? This cannot be undone. *(__y__es, __n__o)* This prompt will time out in __30 seconds__. @@ -27,7 +38,7 @@ Use `{prefix}setting [setting-name]` to view a description on the setting. Each [C_SETTINGS_LISTSETTINGSALT] Alternatively, you can view user settings by using the command `{prefix}settings -u` -[C_SETTINGS_NONEXIST] +[C_SETTINGS_NONEXISTANT] That setting does not exist! //User Command diff --git a/structure/client/DiscordClient.js b/structure/client/DiscordClient.js index 3d30bd4..0b74b41 100644 --- a/structure/client/DiscordClient.js +++ b/structure/client/DiscordClient.js @@ -72,7 +72,6 @@ class DiscordClient extends Client { } } } - console.log(def); this._defaultConfig = def; return def; } diff --git a/structure/client/components/commands/utility/Settings.js b/structure/client/components/commands/utility/Settings.js index e11932e..4323d27 100644 --- a/structure/client/components/commands/utility/Settings.js +++ b/structure/client/components/commands/utility/Settings.js @@ -47,6 +47,10 @@ class SettingCommand extends Command { this._listSettings(message, type, Boolean(args.all)); return undefined; } else if(target === 'reset') { + if(message.channel.permissionsFor(message.member).missing('ADMINISTRATOR').length > 0) { + await message.respond(message.format('C_SETTINGS_ADMINISTRATORERROR', { type: type.toLowerCase() }), { emoji: 'failure' }); + return undefined; + } const prompt = await message.prompt(message.format('C_SETTINGS_RESET', { type: type.toLowerCase() }), { emoji: 'warning' }); return await this._handleReset(prompt, message, type); } else if(target === 'walkthrough') { @@ -61,6 +65,21 @@ class SettingCommand extends Command { return undefined; } + //Setting permission handling + if(setting.clientPermissions.length > 0) { + const missing = message.channel.permissionsFor(message.guild.me).missing(command.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; + } + } + const response = await setting.handle(message, params.splice(1)); message.respond(response.msg, { emoji: response.error ? 'failure' : 'success' }); @@ -69,7 +88,7 @@ class SettingCommand extends Command { _listSettings(message, type, all) { if(!message.guild && type === 'GUILD') type = 'USER'; - const prefix = message.guild.prefix; + const prefix = message.guild?.prefix || this.client._options.bot.prefix; let fields = []; const sorted = this.client.registry.components diff --git a/structure/client/components/observers/CommandHandler.js b/structure/client/components/observers/CommandHandler.js index 7f1367e..9f47065 100644 --- a/structure/client/components/observers/CommandHandler.js +++ b/structure/client/components/observers/CommandHandler.js @@ -53,8 +53,9 @@ class CommandHandler extends Observer { async _getCommand(message, [arg1, arg2, ...args]) { - const settings = await message.guild.settings(); - const prefix = message.guild.prefix; + if(message.guild) await message.guild.settings(); + const prefix = message.guild?.prefix || this.client._options.bot.prefix; + let command = null; let remains = []; diff --git a/structure/extensions/Message.js b/structure/extensions/Message.js index a030d0b..88bdead 100644 --- a/structure/extensions/Message.js +++ b/structure/extensions/Message.js @@ -26,7 +26,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._config.bot.prefix; + parameters.prefix = this.guild ? this.guild.prefix : this.client._options.bot.prefix; let template = this.client.localeLoader.template(language, index); //.languages[language][index]; if(!template) {