diff options
| author | Sin-MacBook <[email protected]> | 2020-08-10 23:44:20 +0200 |
|---|---|---|
| committer | Sin-MacBook <[email protected]> | 2020-08-10 23:44:20 +0200 |
| commit | 2a53887abba882bf7b63aeda8dfa55fdb3ab8792 (patch) | |
| tree | ad7a95eb41faa6ff13c3142285cdc0eb3ca92183 /src/config.js | |
| download | modmail-2a53887abba882bf7b63aeda8dfa55fdb3ab8792.tar.xz modmail-2a53887abba882bf7b63aeda8dfa55fdb3ab8792.zip | |
clean this up when home
Diffstat (limited to 'src/config.js')
| -rw-r--r-- | src/config.js | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..a2e3beb --- /dev/null +++ b/src/config.js @@ -0,0 +1,289 @@ +/** + * !!! 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 path = require('path'); + +let userConfig = {}; + +// Config files to search for, in priority order +const configFiles = [ + 'config.ini', + 'config.ini.ini', + 'config.ini.txt', + 'config.json', + 'config.json5', + 'config.json.json', + 'config.json.txt', + 'config.js' +]; + +let foundConfigFile; +for (const configFile of configFiles) { + try { + fs.accessSync(__dirname + '/../' + configFile); + foundConfigFile = configFile; + break; + } catch (e) {} +} + +// Load config file +if (foundConfigFile) { + console.log(`Loading configuration from ${foundConfigFile}...`); + try { + if (foundConfigFile.endsWith('.js')) { + userConfig = require(`../${foundConfigFile}`); + } else { + const raw = fs.readFileSync(__dirname + '/../' + foundConfigFile, {encoding: "utf8"}); + if (foundConfigFile.endsWith('.ini') || foundConfigFile.endsWith('.ini.txt')) { + userConfig = require('ini').decode(raw); + } else { + userConfig = require('json5').parse(raw); + } + } + } catch (e) { + throw new Error(`Error reading config file! The error given was: ${e.message}`); + } +} + +const required = ['token', 'mailGuildId', 'mainGuildId', 'logChannelId']; +const numericOptions = ['requiredAccountAge', 'requiredTimeOnServer', 'smallAttachmentLimit', 'port']; + +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, + + "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 +const envKeyPrefix = 'MM_'; +let loadedEnvValues = 0; + +for (const [key, value] of Object.entries(process.env)) { + if (! key.startsWith(envKeyPrefix)) continue; + + // MM_CLOSE_MESSAGE -> closeMessage + // MM_COMMAND_ALIASES__MV => commandAliases.mv + const configKey = key.slice(envKeyPrefix.length) + .toLowerCase() + .replace(/([a-z])_([a-z])/g, (m, m1, m2) => `${m1}${m2.toUpperCase()}`) + .replace('__', '.'); + + userConfig[configKey] = value.includes('||') + ? value.split('||') + : value; + + loadedEnvValues++; +} + +if (process.env.PORT && !process.env.MM_PORT) { + // Special case: allow common "PORT" environment variable without prefix + userConfig.port = process.env.PORT; + loadedEnvValues++; +} + +if (loadedEnvValues > 0) { + console.log(`Loaded ${loadedEnvValues} ${loadedEnvValues === 1 ? 'value' : 'values'} from environment variables`); +} + +// Convert config keys with periods to objects +// E.g. commandAliases.mv -> commandAliases: { mv: ... } +for (const [key, value] of Object.entries(userConfig)) { + if (! key.includes('.')) continue; + + const keys = key.split('.'); + let cursor = userConfig; + for (let i = 0; i < keys.length; i++) { + if (i === keys.length - 1) { + cursor[keys[i]] = value; + } else { + cursor[keys[i]] = cursor[keys[i]] || {}; + cursor = cursor[keys[i]]; + } + } + + delete userConfig[key]; +} + +// Combine user config with default config to form final config +const finalConfig = Object.assign({}, defaultConfig); + +for (const [prop, value] of Object.entries(userConfig)) { + if (! defaultConfig.hasOwnProperty(prop)) { + throw new Error(`Unknown option: ${prop}`); + } + + finalConfig[prop] = value; +} + +// Default knex config +if (! finalConfig['knex']) { + finalConfig['knex'] = { + client: 'sqlite', + connection: { + filename: path.join(finalConfig.dbDir, 'data.sqlite') + }, + useNullAsDefault: true + }; +} + +// Make sure migration settings are always present in knex config +Object.assign(finalConfig['knex'], { + migrations: { + directory: path.join(finalConfig.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 +if (! Array.isArray(finalConfig['mainGuildId'])) { + finalConfig['mainGuildId'] = [finalConfig['mainGuildId']]; +} + +// Make sure inboxServerPermission is always an array +if (! Array.isArray(finalConfig['inboxServerPermission'])) { + if (finalConfig['inboxServerPermission'] == null) { + finalConfig['inboxServerPermission'] = []; + } else { + finalConfig['inboxServerPermission'] = [finalConfig['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 +// greetings for specific servers in guildGreetings. +if (finalConfig.greetingMessage || finalConfig.greetingAttachment) { + for (const guildId of finalConfig.mainGuildId) { + if (finalConfig.guildGreetings[guildId]) continue; + finalConfig.guildGreetings[guildId] = { + message: finalConfig.greetingMessage, + attachment: finalConfig.greetingAttachment + }; + } +} + +// newThreadCategoryId is syntactic sugar for categoryAutomation.newThread +if (finalConfig.newThreadCategoryId) { + finalConfig.categoryAutomation.newThread = finalConfig.newThreadCategoryId; + delete finalConfig.newThreadCategoryId; +} + +// Turn empty string options to null (i.e. "option=" without a value in config.ini) +for (const [key, value] of Object.entries(finalConfig)) { + if (value === '') { + finalConfig[key] = null; + } +} + +// Cast numeric options to numbers +for (const numericOpt of numericOptions) { + if (finalConfig[numericOpt] != null) { + const number = parseFloat(finalConfig[numericOpt]); + if (Number.isNaN(number)) { + console.error(`Invalid numeric value for ${numericOpt}: ${finalConfig[numericOpt]}`); + 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 config.json value: ${opt}`); + process.exit(1); + } +} + +console.log("Configuration ok!"); + +module.exports = finalConfig; |