From bd8dcc61294ace52eeea489c7382cef7b79ba10b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 13 Aug 2020 00:03:01 +0300 Subject: [PATCH] Fixes and tweaks to new config validation --- src/cfg.js | 85 +++++++++++++++++++++++++----------- src/data/cfg.jsdoc.js | 18 ++++---- src/data/cfg.schema.json | 67 ++++++++++++++++++++-------- src/data/generateCfgJsdoc.js | 17 +++++++- 4 files changed, 132 insertions(+), 55 deletions(-) diff --git a/src/cfg.js b/src/cfg.js index dc38d7f..0d86065 100644 --- a/src/cfg.js +++ b/src/cfg.js @@ -102,16 +102,6 @@ for (const [key, value] of Object.entries(config)) { delete config[key]; } -// Cast boolean options (on, true, 1) (off, false, 0) -for (const [key, value] of Object.entries(config)) { - if (typeof value !== "string") continue; - if (["on", "true", "1"].includes(value)) { - config[key] = true; - } else if (["off", "false", "0"].includes(value)) { - config[key] = false; - } -} - if (! config['knex']) { config.knex = { client: 'sqlite', @@ -129,20 +119,6 @@ Object.assign(config['knex'], { } }); -// Make sure mainGuildId is internally always an array -if (! Array.isArray(config['mainGuildId'])) { - config['mainGuildId'] = [config['mainGuildId']]; -} - -// Make sure inboxServerPermission is always an array -if (! Array.isArray(config['inboxServerPermission'])) { - if (config['inboxServerPermission'] == null) { - config['inboxServerPermission'] = []; - } else { - config['inboxServerPermission'] = [config['inboxServerPermission']]; - } -} - // Move greetingMessage/greetingAttachment to the guildGreetings object internally // Or, in other words, if greetingMessage and/or greetingAttachment is set, it is applied for all servers that don't // already have something set up in guildGreetings. This retains backwards compatibility while allowing you to override @@ -163,15 +139,71 @@ if (config.newThreadCategoryId) { delete config.newThreadCategoryId; } -// Turn empty string options to null (i.e. "option=" without a value in config.ini) +// Delete empty string options (i.e. "option=" without a value in config.ini) for (const [key, value] of Object.entries(config)) { if (value === '') { - config[key] = null; + delete config[key]; } } // Validate config and assign defaults (if missing) const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" }); + +// https://github.com/ajv-validator/ajv/issues/141#issuecomment-270692820 +const truthyValues = ["1", "true", "on"]; +const falsyValues = ["0", "false", "off"]; +ajv.addKeyword('coerceBoolean', { + compile(value) { + return (data, dataPath, parentData, parentKey) => { + if (! value) { + // Disabled -> no coercion + return true; + } + + // https://github.com/ajv-validator/ajv/issues/141#issuecomment-270777250 + // The "data" argument doesn't update within the same set of schemas inside "allOf", + // so we're referring to the original property instead. + // This also means we can't use { "type": "boolean" }, as it would test the un-updated data value. + const realData = parentData[parentKey]; + + if (typeof realData === "boolean") { + return true; + } + + if (truthyValues.includes(realData)) { + parentData[parentKey] = true; + } else if (falsyValues.includes(realData)) { + parentData[parentKey] = false; + } else { + return false; + } + + return true; + }; + }, +}); + +ajv.addKeyword('multilineString', { + compile(value) { + return (data, dataPath, parentData, parentKey) => { + if (! value) { + // Disabled -> no coercion + return true; + } + + const realData = parentData[parentKey]; + + if (typeof realData === "string") { + return true; + } + + parentData[parentKey] = realData.join("\n"); + + return true; + }; + }, +}); + const validate = ajv.compile(schema); const configIsValid = validate(config); @@ -186,5 +218,6 @@ if (! configIsValid) { } console.log("Configuration ok!"); +process.exit(0); module.exports = config; diff --git a/src/data/cfg.jsdoc.js b/src/data/cfg.jsdoc.js index a581fe1..e3de128 100644 --- a/src/data/cfg.jsdoc.js +++ b/src/data/cfg.jsdoc.js @@ -1,21 +1,21 @@ /** * @typedef {object} ModmailConfig * @property {string} [token] - * @property {*} [mainGuildId] + * @property {array} [mainGuildId] * @property {string} [mailGuildId] * @property {string} [logChannelId] * @property {string} [prefix="!"] * @property {string} [snippetPrefix="!!"] * @property {string} [snippetPrefixAnon="!!!"] * @property {string} [status="Message me for help!"] - * @property {*} [responseMessage="Thank you for your message! Our mod team will reply to you here as soon as possible."] - * @property {*} [closeMessage] + * @property {string} [responseMessage="Thank you for your message! Our mod team will reply to you here as soon as possible."] + * @property {string} [closeMessage] * @property {boolean} [allowUserClose=false] * @property {string} [newThreadCategoryId] * @property {string} [mentionRole="here"] * @property {boolean} [pingOnBotMention=true] - * @property {*} [botMentionResponse] - * @property {*} [inboxServerPermission] + * @property {string} [botMentionResponse] + * @property {array} [inboxServerPermission] * @property {boolean} [alwaysReply=false] * @property {boolean} [alwaysReplyAnon=false] * @property {boolean} [useNicknames=false] @@ -30,18 +30,18 @@ * @property {boolean} [allowStaffEdit=false] * @property {boolean} [allowStaffDelete=false] * @property {boolean} [enableGreeting=false] - * @property {*} [greetingMessage] + * @property {string} [greetingMessage] * @property {string} [greetingAttachment] * @property {*} [guildGreetings={}] * @property {number} [requiredAccountAge] Required account age to message Modmail, in hours - * @property {*} [accountAgeDeniedMessage="Your Discord account is not old enough to contact modmail."] + * @property {string} [accountAgeDeniedMessage="Your Discord account is not old enough to contact modmail."] * @property {number} [requiredTimeOnServer] Required time on server to message Modmail, in minutes - * @property {*} [timeOnServerDeniedMessage="You haven't been a member of the server for long enough to contact modmail."] + * @property {string} [timeOnServerDeniedMessage="You haven't been a member of the server for long enough to contact modmail."] * @property {boolean} [relaySmallAttachmentsAsAttachments=false] * @property {number} [smallAttachmentLimit=2097152] Max size of attachment to relay directly. Default is 2MB. * @property {string} [attachmentStorage="local"] * @property {string} [attachmentStorageChannelId] - * @property {*} [categoryAutomation] + * @property {*} [categoryAutomation={}] * @property {boolean} [updateNotifications=true] * @property {array} [plugins=[]] * @property {*} [commandAliases] diff --git a/src/data/cfg.schema.json b/src/data/cfg.schema.json index da29db6..70c1a70 100644 --- a/src/data/cfg.schema.json +++ b/src/data/cfg.schema.json @@ -10,7 +10,36 @@ } }, "multilineString": { - "$ref": "#/definitions/stringArray" + "allOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + { + "$comment": "See definition of multilineString in cfg.js", + "multilineString": true + } + ] + }, + "customBoolean": { + "allOf": [ + { + "type": ["boolean", "string"] + }, + { + "$comment": "See definition of coerceBoolean in cfg.js", + "coerceBoolean": true + } + ] } }, "properties": { @@ -51,7 +80,7 @@ "$ref": "#/definitions/multilineString" }, "allowUserClose": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, @@ -63,7 +92,7 @@ "default": "here" }, "pingOnBotMention": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": true }, "botMentionResponse": { @@ -74,60 +103,60 @@ "$ref": "#/definitions/stringArray" }, "alwaysReply": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "alwaysReplyAnon": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "useNicknames": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "ignoreAccidentalThreads": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "threadTimestamps": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "allowMove": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "syncPermissionsOnMove": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": true }, "typingProxy": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "typingProxyReverse": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "mentionUserInThreadHeader": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "rolesInThreadHeader": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "allowStaffEdit": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "allowStaffDelete": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "enableGreeting": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "greetingMessage": { @@ -164,7 +193,7 @@ }, "relaySmallAttachmentsAsAttachments": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": false }, "smallAttachmentLimit": { @@ -194,7 +223,7 @@ }, "updateNotifications": { - "type": "boolean", + "$ref": "#/definitions/customBoolean", "default": true }, "plugins": { diff --git a/src/data/generateCfgJsdoc.js b/src/data/generateCfgJsdoc.js index 219615c..096ddb8 100644 --- a/src/data/generateCfgJsdoc.js +++ b/src/data/generateCfgJsdoc.js @@ -4,5 +4,20 @@ const toJsdoc = require('json-schema-to-jsdoc'); const schema = require('./cfg.schema.json'); const target = path.join(__dirname, 'cfg.jsdoc.js'); -const result = toJsdoc(schema); +// Fix up some custom types for the JSDoc conversion +const schemaCopy = JSON.parse(JSON.stringify(schema)); +for (const propertyDef of Object.values(schemaCopy.properties)) { + if (propertyDef.$ref === "#/definitions/stringArray") { + propertyDef.type = "array"; + delete propertyDef.$ref; + } else if (propertyDef.$ref === "#/definitions/customBoolean") { + propertyDef.type = "boolean"; + delete propertyDef.$ref; + } else if (propertyDef.$ref === "#/definitions/multilineString") { + propertyDef.type = "string"; + delete propertyDef.$ref; + } +} + +const result = toJsdoc(schemaCopy); fs.writeFileSync(target, result, { encoding: 'utf8' });