2017-02-09 23:36:47 -05:00
|
|
|
const Eris = require('eris');
|
2017-09-19 13:23:55 -04:00
|
|
|
const bot = require('./bot');
|
2017-02-09 21:56:36 -05:00
|
|
|
const moment = require('moment');
|
2019-03-06 14:37:36 -05:00
|
|
|
const humanizeDuration = require('humanize-duration');
|
2017-02-09 21:56:36 -05:00
|
|
|
const publicIp = require('public-ip');
|
2017-09-19 13:23:55 -04:00
|
|
|
const config = require('./config');
|
|
|
|
|
|
|
|
class BotError extends Error {}
|
|
|
|
|
2018-04-21 08:38:21 -04:00
|
|
|
const userMentionRegex = /^<@!?([0-9]+?)>$/;
|
2017-02-09 21:56:36 -05:00
|
|
|
|
2017-09-19 10:38:37 -04:00
|
|
|
let inboxGuild = null;
|
2018-04-21 08:38:21 -04:00
|
|
|
let mainGuilds = [];
|
2017-09-19 10:38:37 -04:00
|
|
|
let logChannel = null;
|
2017-02-14 17:57:41 -05:00
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
/**
|
|
|
|
* @returns {Eris~Guild}
|
|
|
|
*/
|
2017-09-19 13:23:55 -04:00
|
|
|
function getInboxGuild() {
|
2017-09-19 10:38:37 -04:00
|
|
|
if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.mailGuildId);
|
2017-09-19 13:23:55 -04:00
|
|
|
if (! inboxGuild) throw new BotError('The bot is not on the modmail (inbox) server!');
|
2017-09-19 10:38:37 -04:00
|
|
|
return inboxGuild;
|
2017-02-09 21:56:36 -05:00
|
|
|
}
|
|
|
|
|
2018-02-11 14:54:30 -05:00
|
|
|
/**
|
2018-04-21 08:38:21 -04:00
|
|
|
* @returns {Eris~Guild[]}
|
2018-02-11 14:54:30 -05:00
|
|
|
*/
|
2018-04-21 08:38:21 -04:00
|
|
|
function getMainGuilds() {
|
|
|
|
if (mainGuilds.length === 0) {
|
|
|
|
mainGuilds = bot.guilds.filter(g => config.mainGuildId.includes(g.id));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mainGuilds.length !== config.mainGuildId.length) {
|
|
|
|
if (config.mainGuildId.length === 1) {
|
|
|
|
console.warn(`[WARN] The bot hasn't joined the main guild!`);
|
|
|
|
} else {
|
|
|
|
console.warn(`[WARN] The bot hasn't joined one or more main guilds!`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return mainGuilds;
|
2017-02-14 17:57:41 -05:00
|
|
|
}
|
|
|
|
|
2017-09-19 13:23:55 -04:00
|
|
|
/**
|
|
|
|
* Returns the designated log channel, or the default channel if none is set
|
2018-02-11 14:54:30 -05:00
|
|
|
* @returns {Eris~TextChannel}
|
2017-09-19 13:23:55 -04:00
|
|
|
*/
|
|
|
|
function getLogChannel() {
|
|
|
|
const inboxGuild = getInboxGuild();
|
2019-12-05 14:04:45 -05:00
|
|
|
const logChannel = inboxGuild.channels.get(config.logChannelId);
|
2017-09-19 10:38:37 -04:00
|
|
|
|
|
|
|
if (! logChannel) {
|
2017-09-19 13:23:55 -04:00
|
|
|
throw new BotError('Log channel not found!');
|
2017-09-19 10:38:37 -04:00
|
|
|
}
|
|
|
|
|
2019-12-05 14:04:45 -05:00
|
|
|
if (! (logChannel instanceof Eris.TextChannel)) {
|
|
|
|
throw new BotError('Make sure log channel is set to a text channel!');
|
|
|
|
}
|
|
|
|
|
2017-09-19 10:38:37 -04:00
|
|
|
return logChannel;
|
|
|
|
}
|
|
|
|
|
2018-02-18 14:21:03 -05:00
|
|
|
function postLog(...args) {
|
|
|
|
getLogChannel().createMessage(...args);
|
|
|
|
}
|
|
|
|
|
2019-06-16 15:27:30 -04:00
|
|
|
function postError(channel, str, opts = {}) {
|
|
|
|
return channel.createMessage({
|
|
|
|
...opts,
|
|
|
|
content: `⚠ ${str}`
|
2017-09-19 13:23:55 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the given member has permission to use modmail commands
|
|
|
|
* @param member
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
function isStaff(member) {
|
2019-04-15 09:43:22 -04:00
|
|
|
if (! member) return false;
|
2018-09-20 15:27:59 -04:00
|
|
|
if (config.inboxServerPermission.length === 0) return true;
|
|
|
|
|
|
|
|
return config.inboxServerPermission.some(perm => {
|
|
|
|
if (isSnowflake(perm)) {
|
|
|
|
// If perm is a snowflake, check it against the member's user id and roles
|
|
|
|
if (member.id === perm) return true;
|
|
|
|
if (member.roles.includes(perm)) return true;
|
|
|
|
} else {
|
|
|
|
// Otherwise assume perm is the name of a permission
|
|
|
|
return member.permission.has(perm);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
});
|
2017-09-19 13:23:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the given message is on the inbox server
|
|
|
|
* @param msg
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
function messageIsOnInboxServer(msg) {
|
|
|
|
if (! msg.channel.guild) return false;
|
|
|
|
if (msg.channel.guild.id !== getInboxGuild().id) return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether the given message is on the main server
|
|
|
|
* @param msg
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
function messageIsOnMainServer(msg) {
|
|
|
|
if (! msg.channel.guild) return false;
|
2018-04-21 08:38:21 -04:00
|
|
|
|
|
|
|
return getMainGuilds()
|
|
|
|
.some(g => msg.channel.guild.id === g.id);
|
2017-09-19 13:23:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param attachment
|
|
|
|
* @returns {Promise<string>}
|
|
|
|
*/
|
2019-03-06 16:31:24 -05:00
|
|
|
async function formatAttachment(attachment, attachmentUrl) {
|
2017-09-19 13:23:55 -04:00
|
|
|
let filesize = attachment.size || 0;
|
|
|
|
filesize /= 1024;
|
|
|
|
|
|
|
|
return `**Attachment:** ${attachment.filename} (${filesize.toFixed(1)}KB)\n${attachmentUrl}`;
|
|
|
|
}
|
2017-02-09 21:56:36 -05:00
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
|
|
|
* Returns the user ID of the user mentioned in str, if any
|
|
|
|
* @param {String} str
|
|
|
|
* @returns {String|null}
|
|
|
|
*/
|
2017-02-09 21:56:36 -05:00
|
|
|
function getUserMention(str) {
|
2019-06-09 13:25:21 -04:00
|
|
|
if (! str) return null;
|
|
|
|
|
2017-02-09 21:56:36 -05:00
|
|
|
str = str.trim();
|
|
|
|
|
2019-06-09 13:25:21 -04:00
|
|
|
if (isSnowflake(str)) {
|
2017-02-09 21:56:36 -05:00
|
|
|
// User ID
|
|
|
|
return str;
|
|
|
|
} else {
|
|
|
|
let mentionMatch = str.match(userMentionRegex);
|
|
|
|
if (mentionMatch) return mentionMatch[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
|
|
|
* Returns the current timestamp in an easily readable form
|
|
|
|
* @returns {String}
|
|
|
|
*/
|
2018-02-18 16:29:24 -05:00
|
|
|
function getTimestamp(...momentArgs) {
|
|
|
|
return moment.utc(...momentArgs).format('HH:mm');
|
2017-02-09 21:56:36 -05:00
|
|
|
}
|
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
|
|
|
* Disables link previews in the given string by wrapping links in < >
|
|
|
|
* @param {String} str
|
|
|
|
* @returns {String}
|
|
|
|
*/
|
2017-02-09 21:56:36 -05:00
|
|
|
function disableLinkPreviews(str) {
|
|
|
|
return str.replace(/(^|[^<])(https?:\/\/\S+)/ig, '$1<$2>');
|
|
|
|
}
|
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
|
|
|
* Returns a URL to the bot's web server
|
|
|
|
* @param {String} path
|
2017-12-31 19:16:05 -05:00
|
|
|
* @returns {Promise<String>}
|
2017-02-09 23:36:47 -05:00
|
|
|
*/
|
2017-12-31 19:16:05 -05:00
|
|
|
async function getSelfUrl(path = '') {
|
2017-02-09 21:56:36 -05:00
|
|
|
if (config.url) {
|
2017-12-31 19:16:05 -05:00
|
|
|
return `${config.url}/${path}`;
|
2017-02-09 21:56:36 -05:00
|
|
|
} else {
|
2017-02-09 23:36:47 -05:00
|
|
|
const port = config.port || 8890;
|
2017-12-31 19:16:05 -05:00
|
|
|
const ip = await publicIp.v4();
|
|
|
|
return `http://${ip}:${port}/${path}`;
|
2017-02-09 21:56:36 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-09 23:36:47 -05:00
|
|
|
/**
|
|
|
|
* Returns the highest hoisted role of the given member
|
2018-02-11 14:54:30 -05:00
|
|
|
* @param {Eris~Member} member
|
|
|
|
* @returns {Eris~Role}
|
2017-02-09 23:36:47 -05:00
|
|
|
*/
|
|
|
|
function getMainRole(member) {
|
|
|
|
const roles = member.roles.map(id => member.guild.roles.get(id));
|
|
|
|
roles.sort((a, b) => a.position > b.position ? -1 : 1);
|
|
|
|
return roles.find(r => r.hoist);
|
|
|
|
}
|
|
|
|
|
2017-07-23 20:54:03 -04:00
|
|
|
/**
|
|
|
|
* Splits array items into chunks of the specified size
|
2018-05-03 13:33:19 -04:00
|
|
|
* @param {Array|String} items
|
2017-07-23 20:54:03 -04:00
|
|
|
* @param {Number} chunkSize
|
|
|
|
* @returns {Array}
|
|
|
|
*/
|
|
|
|
function chunk(items, chunkSize) {
|
|
|
|
const result = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
|
|
result.push(items.slice(i, i + chunkSize));
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-03-11 15:32:14 -04:00
|
|
|
/**
|
|
|
|
* Trims every line in the string
|
|
|
|
* @param {String} str
|
|
|
|
* @returns {String}
|
|
|
|
*/
|
2018-02-18 14:21:03 -05:00
|
|
|
function trimAll(str) {
|
|
|
|
return str
|
|
|
|
.split('\n')
|
|
|
|
.map(str => str.trim())
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
|
2019-03-06 14:15:09 -05:00
|
|
|
const delayStringRegex = /^([0-9]+)(?:([dhms])[a-z]*)?/i;
|
|
|
|
|
2018-03-11 15:32:14 -04:00
|
|
|
/**
|
|
|
|
* Turns a "delay string" such as "1h30m" to milliseconds
|
|
|
|
* @param {String} str
|
2019-06-09 13:04:40 -04:00
|
|
|
* @returns {Number|null}
|
2018-03-11 15:32:14 -04:00
|
|
|
*/
|
|
|
|
function convertDelayStringToMS(str) {
|
|
|
|
let match;
|
|
|
|
let ms = 0;
|
|
|
|
|
2018-03-13 01:24:01 -04:00
|
|
|
str = str.trim();
|
|
|
|
|
2019-03-06 14:15:09 -05:00
|
|
|
while (str !== '' && (match = str.match(delayStringRegex)) !== null) {
|
2018-03-13 01:24:01 -04:00
|
|
|
if (match[2] === 'd') ms += match[1] * 1000 * 60 * 60 * 24;
|
|
|
|
else if (match[2] === 'h') ms += match[1] * 1000 * 60 * 60;
|
2018-04-21 09:44:03 -04:00
|
|
|
else if (match[2] === 's') ms += match[1] * 1000;
|
|
|
|
else if (match[2] === 'm' || ! match[2]) ms += match[1] * 1000 * 60;
|
2018-03-13 01:24:01 -04:00
|
|
|
|
|
|
|
str = str.slice(match[0].length);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Invalid delay string
|
|
|
|
if (str !== '') {
|
|
|
|
return null;
|
2018-03-11 15:32:14 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return ms;
|
|
|
|
}
|
|
|
|
|
2018-03-11 15:55:47 -04:00
|
|
|
function getInboxMention() {
|
2019-06-09 12:24:00 -04:00
|
|
|
const mentionRoles = Array.isArray(config.mentionRole) ? config.mentionRole : [config.mentionRole];
|
|
|
|
const mentions = [];
|
|
|
|
for (const role of mentionRoles) {
|
|
|
|
if (role == null) continue;
|
|
|
|
else if (role === 'here') mentions.push('@here');
|
|
|
|
else if (role === 'everyone') mentions.push('@everyone');
|
|
|
|
else mentions.push(`<@&${role}>`);
|
|
|
|
}
|
|
|
|
return mentions.join(' ') + ' ';
|
2018-03-11 15:55:47 -04:00
|
|
|
}
|
|
|
|
|
2018-03-11 17:17:14 -04:00
|
|
|
function postSystemMessageWithFallback(channel, thread, text) {
|
|
|
|
if (thread) {
|
|
|
|
thread.postSystemMessage(text);
|
|
|
|
} else {
|
|
|
|
channel.createMessage(text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-13 00:23:32 -04:00
|
|
|
/**
|
|
|
|
* A normalized way to set props in data models, fixing some inconsistencies between different DB drivers in knex
|
|
|
|
* @param {Object} target
|
|
|
|
* @param {Object} props
|
|
|
|
*/
|
|
|
|
function setDataModelProps(target, props) {
|
|
|
|
for (const prop in props) {
|
|
|
|
if (! props.hasOwnProperty(prop)) continue;
|
|
|
|
// DATETIME fields are always returned as Date objects in MySQL/MariaDB
|
|
|
|
if (props[prop] instanceof Date) {
|
|
|
|
// ...even when NULL, in which case the date's set to unix epoch
|
|
|
|
if (props[prop].getUTCFullYear() === 1970) {
|
|
|
|
target[prop] = null;
|
|
|
|
} else {
|
|
|
|
// Set the value as a string in the same format it's returned in SQLite
|
|
|
|
target[prop] = moment.utc(props[prop]).format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
target[prop] = props[prop];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-20 15:27:59 -04:00
|
|
|
const snowflakeRegex = /^[0-9]{17,}$/;
|
|
|
|
function isSnowflake(str) {
|
2019-06-09 13:25:21 -04:00
|
|
|
return str && snowflakeRegex.test(str);
|
2018-09-20 15:27:59 -04:00
|
|
|
}
|
|
|
|
|
2019-03-06 14:37:36 -05:00
|
|
|
const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: ' and '}, opts));
|
|
|
|
|
2019-03-27 23:03:47 -04:00
|
|
|
const markdownCharsRegex = /([\\_*|`~])/g;
|
|
|
|
function escapeMarkdown(str) {
|
|
|
|
return str.replace(markdownCharsRegex, '\\$1');
|
|
|
|
}
|
|
|
|
|
2019-06-16 15:27:30 -04:00
|
|
|
function disableCodeBlocks(str) {
|
|
|
|
return str.replace(/`/g, "`\u200b");
|
|
|
|
}
|
|
|
|
|
2017-02-09 21:56:36 -05:00
|
|
|
module.exports = {
|
2017-09-19 13:23:55 -04:00
|
|
|
BotError,
|
|
|
|
|
2017-09-19 10:38:37 -04:00
|
|
|
getInboxGuild,
|
2018-04-21 08:38:21 -04:00
|
|
|
getMainGuilds,
|
2017-09-19 10:38:37 -04:00
|
|
|
getLogChannel,
|
2017-09-19 13:23:55 -04:00
|
|
|
postError,
|
2018-02-18 14:21:03 -05:00
|
|
|
postLog,
|
2017-09-19 13:23:55 -04:00
|
|
|
|
|
|
|
isStaff,
|
|
|
|
messageIsOnInboxServer,
|
|
|
|
messageIsOnMainServer,
|
|
|
|
|
|
|
|
formatAttachment,
|
|
|
|
|
2017-02-09 21:56:36 -05:00
|
|
|
getUserMention,
|
|
|
|
getTimestamp,
|
|
|
|
disableLinkPreviews,
|
|
|
|
getSelfUrl,
|
2017-02-09 23:36:47 -05:00
|
|
|
getMainRole,
|
2019-03-06 14:15:09 -05:00
|
|
|
delayStringRegex,
|
2018-03-11 15:32:14 -04:00
|
|
|
convertDelayStringToMS,
|
2018-03-11 15:55:47 -04:00
|
|
|
getInboxMention,
|
2018-03-11 17:17:14 -04:00
|
|
|
postSystemMessageWithFallback,
|
2018-02-18 14:21:03 -05:00
|
|
|
|
2017-07-23 20:54:03 -04:00
|
|
|
chunk,
|
2018-02-18 14:21:03 -05:00
|
|
|
trimAll,
|
2018-03-13 00:23:32 -04:00
|
|
|
|
|
|
|
setDataModelProps,
|
2018-09-20 15:27:59 -04:00
|
|
|
|
|
|
|
isSnowflake,
|
2019-03-06 14:37:36 -05:00
|
|
|
|
|
|
|
humanizeDelay,
|
2019-03-27 23:03:47 -04:00
|
|
|
|
|
|
|
escapeMarkdown,
|
2019-06-16 15:27:30 -04:00
|
|
|
disableCodeBlocks,
|
2017-02-09 21:56:36 -05:00
|
|
|
};
|