Fixes and tweaks to new config validation

cshd
Dragory 2020-08-13 00:03:01 +03:00
parent f7b8a312f9
commit bd8dcc6129
No known key found for this signature in database
GPG Key ID: 5F387BA66DF8AAC1
4 changed files with 132 additions and 55 deletions

View File

@ -102,16 +102,6 @@ for (const [key, value] of Object.entries(config)) {
delete config[key]; 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']) { if (! config['knex']) {
config.knex = { config.knex = {
client: 'sqlite', 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 // 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 // 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 // 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; 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)) { for (const [key, value] of Object.entries(config)) {
if (value === '') { if (value === '') {
config[key] = null; delete config[key];
} }
} }
// Validate config and assign defaults (if missing) // Validate config and assign defaults (if missing)
const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" }); 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 validate = ajv.compile(schema);
const configIsValid = validate(config); const configIsValid = validate(config);
@ -186,5 +218,6 @@ if (! configIsValid) {
} }
console.log("Configuration ok!"); console.log("Configuration ok!");
process.exit(0);
module.exports = config; module.exports = config;

View File

@ -1,21 +1,21 @@
/** /**
* @typedef {object} ModmailConfig * @typedef {object} ModmailConfig
* @property {string} [token] * @property {string} [token]
* @property {*} [mainGuildId] * @property {array} [mainGuildId]
* @property {string} [mailGuildId] * @property {string} [mailGuildId]
* @property {string} [logChannelId] * @property {string} [logChannelId]
* @property {string} [prefix="!"] * @property {string} [prefix="!"]
* @property {string} [snippetPrefix="!!"] * @property {string} [snippetPrefix="!!"]
* @property {string} [snippetPrefixAnon="!!!"] * @property {string} [snippetPrefixAnon="!!!"]
* @property {string} [status="Message me for help!"] * @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 {string} [responseMessage="Thank you for your message! Our mod team will reply to you here as soon as possible."]
* @property {*} [closeMessage] * @property {string} [closeMessage]
* @property {boolean} [allowUserClose=false] * @property {boolean} [allowUserClose=false]
* @property {string} [newThreadCategoryId] * @property {string} [newThreadCategoryId]
* @property {string} [mentionRole="here"] * @property {string} [mentionRole="here"]
* @property {boolean} [pingOnBotMention=true] * @property {boolean} [pingOnBotMention=true]
* @property {*} [botMentionResponse] * @property {string} [botMentionResponse]
* @property {*} [inboxServerPermission] * @property {array} [inboxServerPermission]
* @property {boolean} [alwaysReply=false] * @property {boolean} [alwaysReply=false]
* @property {boolean} [alwaysReplyAnon=false] * @property {boolean} [alwaysReplyAnon=false]
* @property {boolean} [useNicknames=false] * @property {boolean} [useNicknames=false]
@ -30,18 +30,18 @@
* @property {boolean} [allowStaffEdit=false] * @property {boolean} [allowStaffEdit=false]
* @property {boolean} [allowStaffDelete=false] * @property {boolean} [allowStaffDelete=false]
* @property {boolean} [enableGreeting=false] * @property {boolean} [enableGreeting=false]
* @property {*} [greetingMessage] * @property {string} [greetingMessage]
* @property {string} [greetingAttachment] * @property {string} [greetingAttachment]
* @property {*} [guildGreetings={}] * @property {*} [guildGreetings={}]
* @property {number} [requiredAccountAge] Required account age to message Modmail, in hours * @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 {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 {boolean} [relaySmallAttachmentsAsAttachments=false]
* @property {number} [smallAttachmentLimit=2097152] Max size of attachment to relay directly. Default is 2MB. * @property {number} [smallAttachmentLimit=2097152] Max size of attachment to relay directly. Default is 2MB.
* @property {string} [attachmentStorage="local"] * @property {string} [attachmentStorage="local"]
* @property {string} [attachmentStorageChannelId] * @property {string} [attachmentStorageChannelId]
* @property {*} [categoryAutomation] * @property {*} [categoryAutomation={}]
* @property {boolean} [updateNotifications=true] * @property {boolean} [updateNotifications=true]
* @property {array} [plugins=[]] * @property {array} [plugins=[]]
* @property {*} [commandAliases] * @property {*} [commandAliases]

View File

@ -10,7 +10,36 @@
} }
}, },
"multilineString": { "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": { "properties": {
@ -51,7 +80,7 @@
"$ref": "#/definitions/multilineString" "$ref": "#/definitions/multilineString"
}, },
"allowUserClose": { "allowUserClose": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
@ -63,7 +92,7 @@
"default": "here" "default": "here"
}, },
"pingOnBotMention": { "pingOnBotMention": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": true "default": true
}, },
"botMentionResponse": { "botMentionResponse": {
@ -74,60 +103,60 @@
"$ref": "#/definitions/stringArray" "$ref": "#/definitions/stringArray"
}, },
"alwaysReply": { "alwaysReply": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"alwaysReplyAnon": { "alwaysReplyAnon": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"useNicknames": { "useNicknames": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"ignoreAccidentalThreads": { "ignoreAccidentalThreads": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"threadTimestamps": { "threadTimestamps": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"allowMove": { "allowMove": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"syncPermissionsOnMove": { "syncPermissionsOnMove": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": true "default": true
}, },
"typingProxy": { "typingProxy": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"typingProxyReverse": { "typingProxyReverse": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"mentionUserInThreadHeader": { "mentionUserInThreadHeader": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"rolesInThreadHeader": { "rolesInThreadHeader": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"allowStaffEdit": { "allowStaffEdit": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"allowStaffDelete": { "allowStaffDelete": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"enableGreeting": { "enableGreeting": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"greetingMessage": { "greetingMessage": {
@ -164,7 +193,7 @@
}, },
"relaySmallAttachmentsAsAttachments": { "relaySmallAttachmentsAsAttachments": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": false "default": false
}, },
"smallAttachmentLimit": { "smallAttachmentLimit": {
@ -194,7 +223,7 @@
}, },
"updateNotifications": { "updateNotifications": {
"type": "boolean", "$ref": "#/definitions/customBoolean",
"default": true "default": true
}, },
"plugins": { "plugins": {

View File

@ -4,5 +4,20 @@ const toJsdoc = require('json-schema-to-jsdoc');
const schema = require('./cfg.schema.json'); const schema = require('./cfg.schema.json');
const target = path.join(__dirname, 'cfg.jsdoc.js'); 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' }); fs.writeFileSync(target, result, { encoding: 'utf8' });