290 lines
8.2 KiB
JavaScript
290 lines
8.2 KiB
JavaScript
|
/**
|
||
|
* !!! 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 configuration value: ${opt}`);
|
||
|
process.exit(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log("Configuration ok!");
|
||
|
|
||
|
module.exports = finalConfig;
|