Use JSON Schema via AJV for config schema + validation
parent
ce8ebbfc2f
commit
468d1fc037
|
@ -55,14 +55,21 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "6.10.0",
|
"version": "6.12.3",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
|
||||||
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
|
"integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-deep-equal": "^2.0.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
"json-schema-traverse": "^0.4.1",
|
"json-schema-traverse": "^0.4.1",
|
||||||
"uri-js": "^4.2.2"
|
"uri-js": "^4.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ansi-align": {
|
"ansi-align": {
|
||||||
|
@ -1254,7 +1261,8 @@
|
||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-json-stable-stringify": {
|
"fast-json-stable-stringify": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
@ -1372,6 +1380,11 @@
|
||||||
"for-in": "^1.0.1"
|
"for-in": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"foreach": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
|
||||||
|
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
|
||||||
|
},
|
||||||
"forever-agent": {
|
"forever-agent": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||||
|
@ -2106,12 +2119,28 @@
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
|
||||||
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
|
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
|
||||||
},
|
},
|
||||||
|
"json-pointer": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz",
|
||||||
|
"integrity": "sha1-jlAFUKaqxUZKRzN32leqbMIoKNc=",
|
||||||
|
"requires": {
|
||||||
|
"foreach": "^2.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"json-schema": {
|
"json-schema": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
|
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"json-schema-to-jsdoc": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-to-jsdoc/-/json-schema-to-jsdoc-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xuP+10g5VOOTrA5ELnOVO1puiCYPQfx0GqmtDQh/OGGh+CbXyNLtJeEpKl6HPXQbiPPYm7NmMypkRlznZmfZbg==",
|
||||||
|
"requires": {
|
||||||
|
"json-pointer": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"json-schema-traverse": {
|
"json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
|
|
|
@ -8,16 +8,19 @@
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"watch": "nodemon -w src src/index.js",
|
"watch": "nodemon -w src src/index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"lint": "eslint ./src"
|
"lint": "eslint ./src",
|
||||||
|
"generate-config-jsdoc": "node src/data/generateCfgJsdoc.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Dragory/modmailbot"
|
"url": "https://github.com/Dragory/modmailbot"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": "^6.12.3",
|
||||||
"eris": "^0.11.1",
|
"eris": "^0.11.1",
|
||||||
"humanize-duration": "^3.12.1",
|
"humanize-duration": "^3.12.1",
|
||||||
"ini": "^1.3.5",
|
"ini": "^1.3.5",
|
||||||
|
"json-schema-to-jsdoc": "^1.0.0",
|
||||||
"json5": "^2.1.1",
|
"json5": "^2.1.1",
|
||||||
"knex": "^0.20.3",
|
"knex": "^0.20.3",
|
||||||
"knub-command-manager": "^6.1.0",
|
"knub-command-manager": "^6.1.0",
|
||||||
|
|
229
src/cfg.js
229
src/cfg.js
|
@ -1,29 +1,23 @@
|
||||||
/**
|
|
||||||
* !!! NOTE !!!
|
|
||||||
*
|
|
||||||
* If you're setting up the bot, DO NOT EDIT THIS FILE DIRECTLY!
|
|
||||||
*
|
|
||||||
* Create a configuration file in the same directory as the example file.
|
|
||||||
* You never need to edit anything under src/ to use the bot.
|
|
||||||
*
|
|
||||||
* !!! NOTE !!!
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const Ajv = require('ajv');
|
||||||
|
const schema = require('./data/cfg.schema.json');
|
||||||
|
|
||||||
let userConfig = {};
|
/** @type {ModmailConfig} */
|
||||||
|
let config = {};
|
||||||
|
|
||||||
// Config files to search for, in priority order
|
// Config files to search for, in priority order
|
||||||
const configFiles = [
|
const configFiles = [
|
||||||
'config.ini',
|
'config.ini',
|
||||||
'config.ini.ini',
|
|
||||||
'config.ini.txt',
|
|
||||||
'config.json',
|
'config.json',
|
||||||
'config.json5',
|
'config.json5',
|
||||||
|
'config.js',
|
||||||
|
|
||||||
|
// Possible config files when file extensions are hidden
|
||||||
|
'config.ini.ini',
|
||||||
|
'config.ini.txt',
|
||||||
'config.json.json',
|
'config.json.json',
|
||||||
'config.json.txt',
|
'config.json.txt',
|
||||||
'config.js'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let foundConfigFile;
|
let foundConfigFile;
|
||||||
|
@ -35,18 +29,18 @@ for (const configFile of configFiles) {
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config file
|
// Load config values from a config file (if any)
|
||||||
if (foundConfigFile) {
|
if (foundConfigFile) {
|
||||||
console.log(`Loading configuration from ${foundConfigFile}...`);
|
console.log(`Loading configuration from ${foundConfigFile}...`);
|
||||||
try {
|
try {
|
||||||
if (foundConfigFile.endsWith('.js')) {
|
if (foundConfigFile.endsWith('.js')) {
|
||||||
userConfig = require(`../${foundConfigFile}`);
|
config = require(`../${foundConfigFile}`);
|
||||||
} else {
|
} else {
|
||||||
const raw = fs.readFileSync(__dirname + '/../' + foundConfigFile, {encoding: "utf8"});
|
const raw = fs.readFileSync(__dirname + '/../' + foundConfigFile, {encoding: "utf8"});
|
||||||
if (foundConfigFile.endsWith('.ini') || foundConfigFile.endsWith('.ini.txt')) {
|
if (foundConfigFile.endsWith('.ini') || foundConfigFile.endsWith('.ini.txt')) {
|
||||||
userConfig = require('ini').decode(raw);
|
config = require('ini').decode(raw);
|
||||||
} else {
|
} else {
|
||||||
userConfig = require('json5').parse(raw);
|
config = require('json5').parse(raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -54,76 +48,9 @@ if (foundConfigFile) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const required = ['token', 'mailGuildId', 'mainGuildId', 'logChannelId'];
|
// Set dynamic default values which can't be set in the schema directly
|
||||||
const numericOptions = ['requiredAccountAge', 'requiredTimeOnServer', 'smallAttachmentLimit', 'port'];
|
config.dbDir = path.join(__dirname, '..', 'db');
|
||||||
|
config.logDir = path.join(__dirname, '..', 'logs'); // Only used for migrating data from older Modmail versions
|
||||||
const defaultConfig = {
|
|
||||||
"token": null,
|
|
||||||
"mailGuildId": null,
|
|
||||||
"mainGuildId": null,
|
|
||||||
"logChannelId": null,
|
|
||||||
|
|
||||||
"prefix": "!",
|
|
||||||
"snippetPrefix": "!!",
|
|
||||||
"snippetPrefixAnon": "!!!",
|
|
||||||
|
|
||||||
"status": "Message me for help!",
|
|
||||||
"responseMessage": "Thank you for your message! Our mod team will reply to you here as soon as possible.",
|
|
||||||
"closeMessage": null,
|
|
||||||
"allowUserClose": false,
|
|
||||||
|
|
||||||
"newThreadCategoryId": null,
|
|
||||||
"mentionRole": "here",
|
|
||||||
"pingOnBotMention": true,
|
|
||||||
"botMentionResponse": null,
|
|
||||||
|
|
||||||
"inboxServerPermission": null,
|
|
||||||
"alwaysReply": false,
|
|
||||||
"alwaysReplyAnon": false,
|
|
||||||
"useNicknames": false,
|
|
||||||
"ignoreAccidentalThreads": false,
|
|
||||||
"threadTimestamps": false,
|
|
||||||
"allowMove": false,
|
|
||||||
"syncPermissionsOnMove": true,
|
|
||||||
"typingProxy": false,
|
|
||||||
"typingProxyReverse": false,
|
|
||||||
"mentionUserInThreadHeader": false,
|
|
||||||
"rolesInThreadHeader": false,
|
|
||||||
"allowStaffEdit": false,
|
|
||||||
"allowStaffDelete": false,
|
|
||||||
|
|
||||||
"enableGreeting": false,
|
|
||||||
"greetingMessage": null,
|
|
||||||
"greetingAttachment": null,
|
|
||||||
|
|
||||||
"guildGreetings": {},
|
|
||||||
|
|
||||||
"requiredAccountAge": null, // In hours
|
|
||||||
"accountAgeDeniedMessage": "Your Discord account is not old enough to contact modmail.",
|
|
||||||
|
|
||||||
"requiredTimeOnServer": null, // In minutes
|
|
||||||
"timeOnServerDeniedMessage": "You haven't been a member of the server for long enough to contact modmail.",
|
|
||||||
|
|
||||||
"relaySmallAttachmentsAsAttachments": false,
|
|
||||||
"smallAttachmentLimit": 1024 * 1024 * 2,
|
|
||||||
"attachmentStorage": "local",
|
|
||||||
"attachmentStorageChannelId": null,
|
|
||||||
|
|
||||||
"categoryAutomation": {},
|
|
||||||
|
|
||||||
"updateNotifications": true,
|
|
||||||
"plugins": [],
|
|
||||||
|
|
||||||
"commandAliases": {},
|
|
||||||
|
|
||||||
"port": 8890,
|
|
||||||
"url": null,
|
|
||||||
|
|
||||||
"dbDir": path.join(__dirname, '..', 'db'),
|
|
||||||
"knex": null,
|
|
||||||
|
|
||||||
"logDir": path.join(__dirname, '..', 'logs'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load config values from environment variables
|
// Load config values from environment variables
|
||||||
const envKeyPrefix = 'MM_';
|
const envKeyPrefix = 'MM_';
|
||||||
|
@ -139,16 +66,16 @@ for (const [key, value] of Object.entries(process.env)) {
|
||||||
.replace(/([a-z])_([a-z])/g, (m, m1, m2) => `${m1}${m2.toUpperCase()}`)
|
.replace(/([a-z])_([a-z])/g, (m, m1, m2) => `${m1}${m2.toUpperCase()}`)
|
||||||
.replace('__', '.');
|
.replace('__', '.');
|
||||||
|
|
||||||
userConfig[configKey] = value.includes('||')
|
config[configKey] = value.includes('||')
|
||||||
? value.split('||')
|
? value.split('||')
|
||||||
: value;
|
: value;
|
||||||
|
|
||||||
loadedEnvValues++;
|
loadedEnvValues++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PORT && !process.env.MM_PORT) {
|
if (process.env.PORT && ! process.env.MM_PORT) {
|
||||||
// Special case: allow common "PORT" environment variable without prefix
|
// Special case: allow common "PORT" environment variable without prefix
|
||||||
userConfig.port = process.env.PORT;
|
config.port = process.env.PORT;
|
||||||
loadedEnvValues++;
|
loadedEnvValues++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,11 +85,11 @@ if (loadedEnvValues > 0) {
|
||||||
|
|
||||||
// Convert config keys with periods to objects
|
// Convert config keys with periods to objects
|
||||||
// E.g. commandAliases.mv -> commandAliases: { mv: ... }
|
// E.g. commandAliases.mv -> commandAliases: { mv: ... }
|
||||||
for (const [key, value] of Object.entries(userConfig)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
if (! key.includes('.')) continue;
|
if (! key.includes('.')) continue;
|
||||||
|
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let cursor = userConfig;
|
let cursor = config;
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
if (i === keys.length - 1) {
|
if (i === keys.length - 1) {
|
||||||
cursor[keys[i]] = value;
|
cursor[keys[i]] = value;
|
||||||
|
@ -172,60 +99,47 @@ for (const [key, value] of Object.entries(userConfig)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete userConfig[key];
|
delete config[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine user config with default config to form final config
|
// Cast boolean options (on, true, 1) (off, false, 0)
|
||||||
const finalConfig = Object.assign({}, defaultConfig);
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
if (typeof value !== "string") continue;
|
||||||
for (const [prop, value] of Object.entries(userConfig)) {
|
if (["on", "true", "1"].includes(value)) {
|
||||||
if (! defaultConfig.hasOwnProperty(prop)) {
|
config[key] = true;
|
||||||
throw new Error(`Unknown option: ${prop}`);
|
} else if (["off", "false", "0"].includes(value)) {
|
||||||
|
config[key] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
finalConfig[prop] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default knex config
|
if (! config['knex']) {
|
||||||
if (! finalConfig['knex']) {
|
config.knex = {
|
||||||
finalConfig['knex'] = {
|
|
||||||
client: 'sqlite',
|
client: 'sqlite',
|
||||||
connection: {
|
connection: {
|
||||||
filename: path.join(finalConfig.dbDir, 'data.sqlite')
|
filename: path.join(config.dbDir, 'data.sqlite')
|
||||||
},
|
},
|
||||||
useNullAsDefault: true
|
useNullAsDefault: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure migration settings are always present in knex config
|
// Make sure migration settings are always present in knex config
|
||||||
Object.assign(finalConfig['knex'], {
|
Object.assign(config['knex'], {
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: path.join(finalConfig.dbDir, 'migrations')
|
directory: path.join(config.dbDir, 'migrations')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (finalConfig.smallAttachmentLimit > 1024 * 1024 * 8) {
|
|
||||||
finalConfig.smallAttachmentLimit = 1024 * 1024 * 8;
|
|
||||||
console.warn('[WARN] smallAttachmentLimit capped at 8MB');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specific checks
|
|
||||||
if (finalConfig.attachmentStorage === 'discord' && ! finalConfig.attachmentStorageChannelId) {
|
|
||||||
console.error('Config option \'attachmentStorageChannelId\' is required with attachment storage \'discord\'');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure mainGuildId is internally always an array
|
// Make sure mainGuildId is internally always an array
|
||||||
if (! Array.isArray(finalConfig['mainGuildId'])) {
|
if (! Array.isArray(config['mainGuildId'])) {
|
||||||
finalConfig['mainGuildId'] = [finalConfig['mainGuildId']];
|
config['mainGuildId'] = [config['mainGuildId']];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure inboxServerPermission is always an array
|
// Make sure inboxServerPermission is always an array
|
||||||
if (! Array.isArray(finalConfig['inboxServerPermission'])) {
|
if (! Array.isArray(config['inboxServerPermission'])) {
|
||||||
if (finalConfig['inboxServerPermission'] == null) {
|
if (config['inboxServerPermission'] == null) {
|
||||||
finalConfig['inboxServerPermission'] = [];
|
config['inboxServerPermission'] = [];
|
||||||
} else {
|
} else {
|
||||||
finalConfig['inboxServerPermission'] = [finalConfig['inboxServerPermission']];
|
config['inboxServerPermission'] = [config['inboxServerPermission']];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,59 +147,44 @@ if (! Array.isArray(finalConfig['inboxServerPermission'])) {
|
||||||
// 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
|
||||||
// greetings for specific servers in guildGreetings.
|
// greetings for specific servers in guildGreetings.
|
||||||
if (finalConfig.greetingMessage || finalConfig.greetingAttachment) {
|
if (config.greetingMessage || config.greetingAttachment) {
|
||||||
for (const guildId of finalConfig.mainGuildId) {
|
for (const guildId of config.mainGuildId) {
|
||||||
if (finalConfig.guildGreetings[guildId]) continue;
|
if (config.guildGreetings[guildId]) continue;
|
||||||
finalConfig.guildGreetings[guildId] = {
|
config.guildGreetings[guildId] = {
|
||||||
message: finalConfig.greetingMessage,
|
message: config.greetingMessage,
|
||||||
attachment: finalConfig.greetingAttachment
|
attachment: config.greetingAttachment
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newThreadCategoryId is syntactic sugar for categoryAutomation.newThread
|
// newThreadCategoryId is syntactic sugar for categoryAutomation.newThread
|
||||||
if (finalConfig.newThreadCategoryId) {
|
if (config.newThreadCategoryId) {
|
||||||
finalConfig.categoryAutomation.newThread = finalConfig.newThreadCategoryId;
|
config.categoryAutomation.newThread = config.newThreadCategoryId;
|
||||||
delete finalConfig.newThreadCategoryId;
|
delete config.newThreadCategoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn empty string options to null (i.e. "option=" without a value in config.ini)
|
// Turn empty string options to null (i.e. "option=" without a value in config.ini)
|
||||||
for (const [key, value] of Object.entries(finalConfig)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
finalConfig[key] = null;
|
config[key] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast numeric options to numbers
|
// Validate config and assign defaults (if missing)
|
||||||
for (const numericOpt of numericOptions) {
|
const ajv = new Ajv({ useDefaults: true });
|
||||||
if (finalConfig[numericOpt] != null) {
|
const validate = ajv.compile(schema);
|
||||||
const number = parseFloat(finalConfig[numericOpt]);
|
const configIsValid = validate(config);
|
||||||
if (Number.isNaN(number)) {
|
|
||||||
console.error(`Invalid numeric value for ${numericOpt}: ${finalConfig[numericOpt]}`);
|
if (! configIsValid) {
|
||||||
|
console.error('Issues with configuration options:');
|
||||||
|
for (const error of validate.errors) {
|
||||||
|
console.error(`The "${error.dataPath.slice(1)}" option ${error.message}`);
|
||||||
|
}
|
||||||
|
console.error('');
|
||||||
|
console.error('Please restart the bot after fixing the issues mentioned above.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
|
||||||
finalConfig[numericOpt] = number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast boolean options (on, true, 1) (off, false, 0)
|
|
||||||
for (const [key, value] of Object.entries(finalConfig)) {
|
|
||||||
if (typeof value !== "string") continue;
|
|
||||||
if (["on", "true", "1"].includes(value)) {
|
|
||||||
finalConfig[key] = true;
|
|
||||||
} else if (["off", "false", "0"].includes(value)) {
|
|
||||||
finalConfig[key] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure all of the required config options are present
|
|
||||||
for (const opt of required) {
|
|
||||||
if (! finalConfig[opt]) {
|
|
||||||
console.error(`Missing required configuration value: ${opt}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Configuration ok!");
|
console.log("Configuration ok!");
|
||||||
|
|
||||||
module.exports = finalConfig;
|
module.exports = config;
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
/**
|
||||||
|
* @typedef {object} ModmailConfig
|
||||||
|
* @property {string} [token]
|
||||||
|
* @property {*} [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 {boolean} [allowUserClose=false]
|
||||||
|
* @property {string} [newThreadCategoryId]
|
||||||
|
* @property {string} [mentionRole="here"]
|
||||||
|
* @property {boolean} [pingOnBotMention=true]
|
||||||
|
* @property {*} [botMentionResponse]
|
||||||
|
* @property {*} [inboxServerPermission]
|
||||||
|
* @property {boolean} [alwaysReply=false]
|
||||||
|
* @property {boolean} [alwaysReplyAnon=false]
|
||||||
|
* @property {boolean} [useNicknames=false]
|
||||||
|
* @property {boolean} [ignoreAccidentalThreads=false]
|
||||||
|
* @property {boolean} [threadTimestamps=false]
|
||||||
|
* @property {boolean} [allowMove=false]
|
||||||
|
* @property {boolean} [syncPermissionsOnMove=true]
|
||||||
|
* @property {boolean} [typingProxy=false]
|
||||||
|
* @property {boolean} [typingProxyReverse=false]
|
||||||
|
* @property {boolean} [mentionUserInThreadHeader=false]
|
||||||
|
* @property {boolean} [rolesInThreadHeader=false]
|
||||||
|
* @property {boolean} [allowStaffEdit=false]
|
||||||
|
* @property {boolean} [allowStaffDelete=false]
|
||||||
|
* @property {boolean} [enableGreeting=false]
|
||||||
|
* @property {*} [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 {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 {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 {boolean} [updateNotifications=true]
|
||||||
|
* @property {array} [plugins=[]]
|
||||||
|
* @property {*} [commandAliases]
|
||||||
|
* @property {number} [port=8890]
|
||||||
|
* @property {string} [url]
|
||||||
|
* @property {string} [dbDir]
|
||||||
|
* @property {object} [knex]
|
||||||
|
* @property {string} [logDir]
|
||||||
|
*/
|
|
@ -0,0 +1,268 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "ModmailConfig",
|
||||||
|
"type": "object",
|
||||||
|
"definitions": {
|
||||||
|
"stringOrArrayOfStrings": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"stringOrMultilineString": {
|
||||||
|
"$ref": "#/definitions/stringOrArrayOfStrings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mainGuildId": {
|
||||||
|
"$ref": "#/definitions/stringOrArrayOfStrings"
|
||||||
|
},
|
||||||
|
"mailGuildId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logChannelId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
|
||||||
|
"prefix": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "!"
|
||||||
|
},
|
||||||
|
"snippetPrefix": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "!!"
|
||||||
|
},
|
||||||
|
"snippetPrefixAnon": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "!!!"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Message me for help!"
|
||||||
|
},
|
||||||
|
"responseMessage": {
|
||||||
|
"$ref": "#/definitions/stringOrMultilineString",
|
||||||
|
"default": "Thank you for your message! Our mod team will reply to you here as soon as possible."
|
||||||
|
},
|
||||||
|
"closeMessage": {
|
||||||
|
"$ref": "#/definitions/stringOrMultilineString"
|
||||||
|
},
|
||||||
|
"allowUserClose": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"newThreadCategoryId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mentionRole": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "here"
|
||||||
|
},
|
||||||
|
"pingOnBotMention": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"botMentionResponse": {
|
||||||
|
"$ref": "#/definitions/stringOrMultilineString"
|
||||||
|
},
|
||||||
|
|
||||||
|
"inboxServerPermission": {
|
||||||
|
"$ref": "#/definitions/stringOrArrayOfStrings"
|
||||||
|
},
|
||||||
|
"alwaysReply": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"alwaysReplyAnon": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"useNicknames": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"ignoreAccidentalThreads": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"threadTimestamps": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"allowMove": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"syncPermissionsOnMove": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"typingProxy": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"typingProxyReverse": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"mentionUserInThreadHeader": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"rolesInThreadHeader": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"allowStaffEdit": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"allowStaffDelete": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"enableGreeting": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"greetingMessage": {
|
||||||
|
"$ref": "#/definitions/stringOrMultilineString"
|
||||||
|
},
|
||||||
|
"greetingAttachment": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"guildGreetings": {
|
||||||
|
"patternProperties": {
|
||||||
|
"^\\d+$": {
|
||||||
|
"$ref": "#/definitions/stringOrMultilineString"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
"requiredAccountAge": {
|
||||||
|
"description": "Required account age to message Modmail, in hours",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"accountAgeDeniedMessage": {
|
||||||
|
"$ref": "#/definitions/stringOrMultilineString",
|
||||||
|
"default": "Your Discord account is not old enough to contact modmail."
|
||||||
|
},
|
||||||
|
|
||||||
|
"requiredTimeOnServer": {
|
||||||
|
"description": "Required time on server to message Modmail, in minutes",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"timeOnServerDeniedMessage": {
|
||||||
|
"$ref": "#/definitions/stringOrMultilineString",
|
||||||
|
"default": "You haven't been a member of the server for long enough to contact modmail."
|
||||||
|
},
|
||||||
|
|
||||||
|
"relaySmallAttachmentsAsAttachments": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"smallAttachmentLimit": {
|
||||||
|
"description": "Max size of attachment to relay directly. Default is 2MB.",
|
||||||
|
"type": "number",
|
||||||
|
"default": 2097152,
|
||||||
|
"maximum": 8388608,
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
|
||||||
|
"attachmentStorage": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "local"
|
||||||
|
},
|
||||||
|
"attachmentStorageChannelId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
|
||||||
|
"categoryAutomation": {
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d+$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
"updateNotifications": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
|
||||||
|
"commandAliases": {
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"port": {
|
||||||
|
"type": "number",
|
||||||
|
"maximum": 65535,
|
||||||
|
"minimum": 1,
|
||||||
|
"default": 8890
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
|
||||||
|
"dbDir": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"knex": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
|
||||||
|
"logDir": {
|
||||||
|
"type": "string",
|
||||||
|
"deprecationMessage": "This option is no longer used"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$comment": "Base required values",
|
||||||
|
"required": ["token", "mainGuildId", "mailGuildId", "logChannelId"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$comment": "Make attachmentStorageChannelId required if attachmentStorage is set to 'discord'",
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"attachmentStorage": {
|
||||||
|
"const": "discord"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["attachmentStorage"]
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["attachmentStorageChannelId"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
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);
|
||||||
|
fs.writeFileSync(target, result, { encoding: 'utf8' });
|
Loading…
Reference in New Issue