Compare commits
24 Commits
d24e4b046a
...
c5f2cc0cfd
Author | SHA1 | Date |
---|---|---|
Matthew | c5f2cc0cfd | |
Matthew | 21050bf497 | |
Miikka | 57ba9e410a | |
Dragory | 4051368cb6 | |
Dragory | 5a63fe5e34 | |
Dragory | 944c23cd9c | |
Dragory | adf772f2ff | |
Dragory | 56d7d737ba | |
Dragory | 1647fdb215 | |
Dragory | 3105bbebfe | |
baptiste0928 | 4544ac5db8 | |
Matthew | d206111806 | |
Matthew | 54edc4c863 | |
Matthew | 6ef5de1cdf | |
Matthew | 5f7fed7de4 | |
Matthew | bd6fc1cf6f | |
Matthew | fc865be319 | |
Matthew | 94cfb6a38c | |
Matthew | d421dda0ca | |
Matthew | 13e57f5d1e | |
Matthew | d3c54ea19e | |
Matthew | 8c131a9678 | |
Bsian | c84eaae0c0 | |
Bsian | 6b62582f44 |
|
@ -1,6 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
For instructions on how to update the bot, see **[✨ Updating the bot](docs/updating.md)**
|
For instructions on how to update the bot, see **[✨ Updating the bot](docs/updating.md)**
|
||||||
|
|
||||||
|
## v3.3.2
|
||||||
|
* Fix database warning when updating to v3.3.1 or higher
|
||||||
|
|
||||||
|
## v3.3.1
|
||||||
|
* Fix crash when a user joins or leaves a [stage channel](https://blog.discord.com/captivate-your-community-with-stage-channels-46bbb756e89b)
|
||||||
|
* Fix global moderator display role overrides (i.e. `!role` used outside of a thread) not working
|
||||||
|
|
||||||
## v3.3.0
|
## v3.3.0
|
||||||
|
|
||||||
**Breaking changes:**
|
**Breaking changes:**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017 Miikka Virtanen
|
Copyright (c) 2017–2021 Miikka (Dragory)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Required settings
|
|
||||||
# -----------------
|
|
||||||
token = REPLACE_WITH_TOKEN
|
|
||||||
mainServerId = REPLACE_WITH_MAIN_SERVER_ID
|
|
||||||
inboxServerId = REPLACE_WITH_INBOX_SERVER_ID
|
|
||||||
logChannelId = REPLACE_WITH_LOG_CHANNEL_ID
|
|
||||||
|
|
||||||
# Common settings
|
|
||||||
# ----------------------------------
|
|
||||||
prefix = !
|
|
||||||
inboxServerPermission = manageMessages
|
|
||||||
status = Message me for help!
|
|
||||||
responseMessage = Thank you for your message! Our mod team will reply to you here as soon as possible.
|
|
||||||
|
|
||||||
# Add new options below this line:
|
|
||||||
# ----------------------------------
|
|
|
@ -313,7 +313,7 @@ If enabled, the bot will react to messages sent to it with the emoji defined in
|
||||||
#### reactOnSeenEmoji
|
#### reactOnSeenEmoji
|
||||||
**Default:** `📨`
|
**Default:** `📨`
|
||||||
The emoji that the bot will react with when it sees a message. Requires `reactOnSeen` to be enabled.
|
The emoji that the bot will react with when it sees a message. Requires `reactOnSeen` to be enabled.
|
||||||
Must be pasted in the config file as the Emoji representation and not as a unicode codepoint.
|
Must be pasted in the config file as the Emoji representation and not as a unicode codepoint. Use `emojiName:emojiID` for custom emoji.
|
||||||
|
|
||||||
#### relaySmallAttachmentsAsAttachments
|
#### relaySmallAttachmentsAsAttachments
|
||||||
**Default:** `off`
|
**Default:** `off`
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "modmailbot",
|
"name": "modmailbot",
|
||||||
"version": "3.3.0",
|
"version": "3.3.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
|
@ -20,8 +20,9 @@
|
||||||
"url": "https://github.com/Dragory/modmailbot"
|
"url": "https://github.com/Dragory/modmailbot"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^0.19.2",
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
"eris": "https://github.com/Dragory/eris/archive/stickers-0.14.0.tar.gz",
|
"eris": "https://github.com/Dragory/eris/archive/0.14.0-stage-hotfix.tar.gz",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"helmet": "^4.1.1",
|
"helmet": "^4.1.1",
|
||||||
"humanize-duration": "^3.23.1",
|
"humanize-duration": "^3.23.1",
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "modmailbot",
|
||||||
|
"version": "3.3.2",
|
||||||
|
"description": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"watch": "supervisor -n exit -w src --inspect=0.0.0.0:9229 src/index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"lint": "eslint ./src ./db/migrations",
|
||||||
|
"lint-fix": "eslint --fix ./src ./db/migrations",
|
||||||
|
"generate-config-jsdoc": "node src/data/generateCfgJsdoc.js",
|
||||||
|
"generate-plugin-api-docs": "jsdoc2md -t docs/plugin-api-template.hbs src/pluginApi.js > docs/plugin-api.md",
|
||||||
|
"create-migration": "knex migrate:make",
|
||||||
|
"run-migrations": "knex migrate:latest"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Dragory/modmailbot"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"ajv": "^6.12.4",
|
||||||
|
"eris": "https://github.com/Dragory/eris/archive/0.14.0-stage-hotfix.tar.gz",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"helmet": "^4.1.1",
|
||||||
|
"humanize-duration": "^3.23.1",
|
||||||
|
"ini": "^1.3.6",
|
||||||
|
"json5": "^2.1.3",
|
||||||
|
"knex": "^0.21.5",
|
||||||
|
"knub-command-manager": "^6.1.0",
|
||||||
|
"mime": "^2.4.6",
|
||||||
|
"moment": "^2.27.0",
|
||||||
|
"mv": "^2.1.1",
|
||||||
|
"mysql2": "^2.1.0",
|
||||||
|
"pacote": "^11.1.11",
|
||||||
|
"public-ip": "^4.0.2",
|
||||||
|
"sqlite3": "^5.0.0",
|
||||||
|
"tmp": "^0.1.0",
|
||||||
|
"transliteration": "^2.1.11",
|
||||||
|
"uuid": "^8.3.0",
|
||||||
|
"yargs-parser": "^20.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^7.7.0",
|
||||||
|
"jsdoc-to-markdown": "^6.0.1",
|
||||||
|
"json-schema-to-jsdoc": "^1.1.0",
|
||||||
|
"supervisor": "https://github.com/petruisfan/node-supervisor/archive/fb89a695779770d3cd63b624ef4b1ab2908c105d.tar.gz"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0 <14.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,290 @@
|
||||||
|
/**
|
||||||
|
* !!! 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,
|
||||||
|
"errorChannelId": 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;
|
|
@ -0,0 +1,290 @@
|
||||||
|
/**
|
||||||
|
* !!! 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,
|
||||||
|
"errorChannelId": 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;
|
|
@ -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 configuration value: ${opt}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Configuration ok!");
|
||||||
|
|
||||||
|
module.exports = finalConfig;
|
|
@ -0,0 +1,290 @@
|
||||||
|
/**
|
||||||
|
* !!! 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,
|
||||||
|
"errorChannelId": 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;
|
|
@ -1,3 +1,4 @@
|
||||||
|
const axios = require("axios");
|
||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
const Eris = require("eris");
|
const Eris = require("eris");
|
||||||
|
|
||||||
|
@ -273,7 +274,7 @@ class Thread {
|
||||||
});
|
});
|
||||||
const threadMessage = await this._addThreadMessageToDB(rawThreadMessage.getSQLProps());
|
const threadMessage = await this._addThreadMessageToDB(rawThreadMessage.getSQLProps());
|
||||||
|
|
||||||
const dmContent = formatters.formatStaffReplyDM(threadMessage);
|
const dmContent = await formatters.formatStaffReplyDM(threadMessage);
|
||||||
const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage);
|
const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage);
|
||||||
|
|
||||||
// Because moderator replies have to be editable, we enforce them to fit within 1 message
|
// Because moderator replies have to be editable, we enforce them to fit within 1 message
|
||||||
|
@ -284,6 +285,7 @@ class Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the reply DM
|
// Send the reply DM
|
||||||
|
//const dmContent = await this._formatStaffReplyDM(moderator, text, isAnonymous);
|
||||||
let dmMessage;
|
let dmMessage;
|
||||||
try {
|
try {
|
||||||
dmMessage = await this._sendDMToUser(dmContent, files);
|
dmMessage = await this._sendDMToUser(dmContent, files);
|
||||||
|
|
|
@ -13,7 +13,9 @@ exports.up = async function(knex) {
|
||||||
const rows = await knex.table("old_moderator_role_overrides")
|
const rows = await knex.table("old_moderator_role_overrides")
|
||||||
.select();
|
.select();
|
||||||
|
|
||||||
await knex.table("moderator_role_overrides").insert(rows);
|
if (rows.length) {
|
||||||
|
await knex.table("moderator_role_overrides").insert(rows);
|
||||||
|
}
|
||||||
|
|
||||||
await knex.schema.dropTable("old_moderator_role_overrides");
|
await knex.schema.dropTable("old_moderator_role_overrides");
|
||||||
};
|
};
|
||||||
|
@ -32,10 +34,12 @@ exports.down = async function(knex) {
|
||||||
const rows = await knex.table("new_moderator_role_overrides")
|
const rows = await knex.table("new_moderator_role_overrides")
|
||||||
.select();
|
.select();
|
||||||
|
|
||||||
await knex.table("moderator_role_overrides").insert(rows.map(r => {
|
if (rows.length) {
|
||||||
delete r.id;
|
await knex.table("moderator_role_overrides").insert(rows.map(r => {
|
||||||
return r;
|
delete r.id;
|
||||||
}));
|
return r;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
await knex.schema.dropTable("new_moderator_role_overrides");
|
await knex.schema.dropTable("new_moderator_role_overrides");
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const Eris = require("eris");
|
const Eris = require("eris");
|
||||||
|
const axios = require ("axios");
|
||||||
const utils = require("./utils");
|
const utils = require("./utils");
|
||||||
const config = require("./cfg");
|
const config = require("./cfg");
|
||||||
const ThreadMessage = require("./data/ThreadMessage");
|
const ThreadMessage = require("./data/ThreadMessage");
|
||||||
|
@ -100,7 +101,7 @@ const bot = require("./bot");
|
||||||
* @type {MessageFormatters}
|
* @type {MessageFormatters}
|
||||||
*/
|
*/
|
||||||
const defaultFormatters = {
|
const defaultFormatters = {
|
||||||
formatStaffReplyDM(threadMessage) {
|
/*formatStaffReplyDM(threadMessage) {
|
||||||
const roleName = threadMessage.role_name || config.fallbackRoleName;
|
const roleName = threadMessage.role_name || config.fallbackRoleName;
|
||||||
const modInfo = threadMessage.is_anonymous
|
const modInfo = threadMessage.is_anonymous
|
||||||
? roleName
|
? roleName
|
||||||
|
@ -109,6 +110,19 @@ const defaultFormatters = {
|
||||||
return modInfo
|
return modInfo
|
||||||
? `**${modInfo}:** ${threadMessage.body}`
|
? `**${modInfo}:** ${threadMessage.body}`
|
||||||
: threadMessage.body;
|
: threadMessage.body;
|
||||||
|
},*/
|
||||||
|
|
||||||
|
async formatStaffReplyDM(threadMessage) {
|
||||||
|
let req = await axios.get("https://loc.sh/int/directory");
|
||||||
|
req = req.data;
|
||||||
|
const find = req.find(mem => mem.userID === threadMessage.user_id);
|
||||||
|
const roleName = threadMessage.role_name || config.fallbackRoleName;
|
||||||
|
// const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username);
|
||||||
|
const modInfo = threadMessage.is_anonymous
|
||||||
|
? (roleName || "Staff")
|
||||||
|
: (roleName ? `__**${threadMessage.user_name}**${find.isManager ? ' [k]' : ''}__\n*${find.title ? `${find.title} / ${find.dept}` : find.dept}*` : roleName);
|
||||||
|
|
||||||
|
return `${modInfo}\n_ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ____ __ __ ___\n\n${threadMessage.body}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
formatStaffReplyThreadMessage(threadMessage) {
|
formatStaffReplyThreadMessage(threadMessage) {
|
||||||
|
@ -313,7 +327,7 @@ const formatters = { ...defaultFormatters };
|
||||||
/**
|
/**
|
||||||
* @typedef {object} FormattersExport
|
* @typedef {object} FormattersExport
|
||||||
* @property {MessageFormatters} formatters Read only
|
* @property {MessageFormatters} formatters Read only
|
||||||
* @property {function(FormatStaffReplyDM): void} setStaffReplyDMFormatter
|
* @property {function(FormatStaffReplyDM): Promise<void>} setStaffReplyDMFormatter
|
||||||
* @property {function(FormatStaffReplyThreadMessage): void} setStaffReplyThreadMessageFormatter
|
* @property {function(FormatStaffReplyThreadMessage): void} setStaffReplyThreadMessageFormatter
|
||||||
* @property {function(FormatUserReplyThreadMessage): void} setUserReplyThreadMessageFormatter
|
* @property {function(FormatUserReplyThreadMessage): void} setUserReplyThreadMessageFormatter
|
||||||
* @property {function(FormatStaffReplyEditNotificationThreadMessage): void} setStaffReplyEditNotificationThreadMessageFormatter
|
* @property {function(FormatStaffReplyEditNotificationThreadMessage): void} setStaffReplyEditNotificationThreadMessageFormatter
|
||||||
|
|
|
@ -71,6 +71,11 @@ module.exports = {
|
||||||
console.log("");
|
console.log("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bot.on("error", err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
bot.connect();
|
bot.connect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = ({ commands }) => {
|
||||||
|
commands.addInboxThreadCommand('ping', '', async (msg) => {
|
||||||
|
msg.channel.createMessage('Pong!').then((m) => m.edit(`Pong!\nResponse: \`${m.createdAt - msg.createdAt}ms\`\nGateway: \`${msg.channel.guild.shard.latency}ms\``));
|
||||||
|
});
|
||||||
|
};
|
|
@ -33,34 +33,12 @@ module.exports = ({ bot, knex, config, commands }) => {
|
||||||
|
|
||||||
if (msg.author.bot) return;
|
if (msg.author.bot) return;
|
||||||
if (! msg.content) return;
|
if (! msg.content) return;
|
||||||
if (! msg.content.startsWith(config.snippetPrefix) && ! msg.content.startsWith(config.snippetPrefixAnon)) return;
|
if (! msg.content.startsWith(config.snippetPrefixAnon)) return;
|
||||||
|
|
||||||
let snippetPrefix, isAnonymous;
|
|
||||||
|
|
||||||
if (config.snippetPrefixAnon.length > config.snippetPrefix.length) {
|
|
||||||
// Anonymous prefix is longer -> check it first
|
|
||||||
if (msg.content.startsWith(config.snippetPrefixAnon)) {
|
|
||||||
snippetPrefix = config.snippetPrefixAnon;
|
|
||||||
isAnonymous = true;
|
|
||||||
} else {
|
|
||||||
snippetPrefix = config.snippetPrefix;
|
|
||||||
isAnonymous = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regular prefix is longer -> check it first
|
|
||||||
if (msg.content.startsWith(config.snippetPrefix)) {
|
|
||||||
snippetPrefix = config.snippetPrefix;
|
|
||||||
isAnonymous = false;
|
|
||||||
} else {
|
|
||||||
snippetPrefix = config.snippetPrefixAnon;
|
|
||||||
isAnonymous = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const thread = await threads.findByChannelId(msg.channel.id);
|
const thread = await threads.findByChannelId(msg.channel.id);
|
||||||
if (! thread) return;
|
if (! thread) return;
|
||||||
|
|
||||||
let [, trigger, rawArgs] = msg.content.slice(snippetPrefix.length).match(/(\S+)(?:\s+(.*))?/s);
|
let [, trigger, rawArgs] = msg.content.slice(config.snippetPrefixAnon.length).match(/(\S+)(?:\s+(.*))?/s);
|
||||||
trigger = trigger.toLowerCase();
|
trigger = trigger.toLowerCase();
|
||||||
|
|
||||||
const snippet = await snippets.get(trigger);
|
const snippet = await snippets.get(trigger);
|
||||||
|
@ -70,7 +48,7 @@ module.exports = ({ bot, knex, config, commands }) => {
|
||||||
args = args.map(arg => arg.value);
|
args = args.map(arg => arg.value);
|
||||||
const rendered = renderSnippet(snippet.body, args);
|
const rendered = renderSnippet(snippet.body, args);
|
||||||
|
|
||||||
const replied = await thread.replyToUser(msg.member, rendered, [], isAnonymous);
|
const replied = await thread.replyToUser(msg.member, rendered, [], false);
|
||||||
if (replied) msg.delete();
|
if (replied) msg.delete();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue