Apply code style from .eslintrc

cshd
Dragory 2020-08-13 00:08:37 +03:00
parent b628ac1bfa
commit 86a060410f
No known key found for this signature in database
GPG Key ID: 5F387BA66DF8AAC1
34 changed files with 606 additions and 606 deletions

View File

@ -1,5 +1,5 @@
const Eris = require('eris');
const config = require('./cfg');
const Eris = require("eris");
const config = require("./cfg");
const bot = new Eris.Client(config.token, {
getAllUsers: true,

View File

@ -1,29 +1,29 @@
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');
const schema = require('./data/cfg.schema.json');
const fs = require("fs");
const path = require("path");
const Ajv = require("ajv");
const schema = require("./data/cfg.schema.json");
/** @type {ModmailConfig} */
let config = {};
// Config files to search for, in priority order
const configFiles = [
'config.ini',
'config.json',
'config.json5',
'config.js',
"config.ini",
"config.json",
"config.json5",
"config.js",
// Possible config files when file extensions are hidden
'config.ini.ini',
'config.ini.txt',
'config.json.json',
'config.json.txt',
"config.ini.ini",
"config.ini.txt",
"config.json.json",
"config.json.txt",
];
let foundConfigFile;
for (const configFile of configFiles) {
try {
fs.accessSync(__dirname + '/../' + configFile);
fs.accessSync(__dirname + "/../" + configFile);
foundConfigFile = configFile;
break;
} catch (e) {}
@ -33,14 +33,14 @@ for (const configFile of configFiles) {
if (foundConfigFile) {
console.log(`Loading configuration from ${foundConfigFile}...`);
try {
if (foundConfigFile.endsWith('.js')) {
if (foundConfigFile.endsWith(".js")) {
config = require(`../${foundConfigFile}`);
} else {
const raw = fs.readFileSync(__dirname + '/../' + foundConfigFile, {encoding: "utf8"});
if (foundConfigFile.endsWith('.ini') || foundConfigFile.endsWith('.ini.txt')) {
config = require('ini').decode(raw);
const raw = fs.readFileSync(__dirname + "/../" + foundConfigFile, {encoding: "utf8"});
if (foundConfigFile.endsWith(".ini") || foundConfigFile.endsWith(".ini.txt")) {
config = require("ini").decode(raw);
} else {
config = require('json5').parse(raw);
config = require("json5").parse(raw);
}
}
} catch (e) {
@ -49,11 +49,11 @@ if (foundConfigFile) {
}
// Set dynamic default values which can't be set in the schema directly
config.dbDir = path.join(__dirname, '..', 'db');
config.logDir = path.join(__dirname, '..', 'logs'); // Only used for migrating data from older Modmail versions
config.dbDir = path.join(__dirname, "..", "db");
config.logDir = path.join(__dirname, "..", "logs"); // Only used for migrating data from older Modmail versions
// Load config values from environment variables
const envKeyPrefix = 'MM_';
const envKeyPrefix = "MM_";
let loadedEnvValues = 0;
for (const [key, value] of Object.entries(process.env)) {
@ -64,10 +64,10 @@ for (const [key, value] of Object.entries(process.env)) {
const configKey = key.slice(envKeyPrefix.length)
.toLowerCase()
.replace(/([a-z])_([a-z])/g, (m, m1, m2) => `${m1}${m2.toUpperCase()}`)
.replace('__', '.');
.replace("__", ".");
config[configKey] = value.includes('||')
? value.split('||')
config[configKey] = value.includes("||")
? value.split("||")
: value;
loadedEnvValues++;
@ -80,15 +80,15 @@ if (process.env.PORT && ! process.env.MM_PORT) {
}
if (loadedEnvValues > 0) {
console.log(`Loaded ${loadedEnvValues} ${loadedEnvValues === 1 ? 'value' : 'values'} from environment variables`);
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(config)) {
if (! key.includes('.')) continue;
if (! key.includes(".")) continue;
const keys = key.split('.');
const keys = key.split(".");
let cursor = config;
for (let i = 0; i < keys.length; i++) {
if (i === keys.length - 1) {
@ -102,20 +102,20 @@ for (const [key, value] of Object.entries(config)) {
delete config[key];
}
if (! config['knex']) {
if (! config["knex"]) {
config.knex = {
client: 'sqlite',
client: "sqlite",
connection: {
filename: path.join(config.dbDir, 'data.sqlite')
filename: path.join(config.dbDir, "data.sqlite")
},
useNullAsDefault: true
};
}
// Make sure migration settings are always present in knex config
Object.assign(config['knex'], {
Object.assign(config["knex"], {
migrations: {
directory: path.join(config.dbDir, 'migrations')
directory: path.join(config.dbDir, "migrations")
}
});
@ -141,7 +141,7 @@ if (config.newThreadCategoryId) {
// Delete empty string options (i.e. "option=" without a value in config.ini)
for (const [key, value] of Object.entries(config)) {
if (value === '') {
if (value === "") {
delete config[key];
}
}
@ -152,7 +152,7 @@ const ajv = new Ajv({ useDefaults: true, coerceTypes: "array" });
// https://github.com/ajv-validator/ajv/issues/141#issuecomment-270692820
const truthyValues = ["1", "true", "on"];
const falsyValues = ["0", "false", "off"];
ajv.addKeyword('coerceBoolean', {
ajv.addKeyword("coerceBoolean", {
compile(value) {
return (data, dataPath, parentData, parentKey) => {
if (! value) {
@ -183,7 +183,7 @@ ajv.addKeyword('coerceBoolean', {
},
});
ajv.addKeyword('multilineString', {
ajv.addKeyword("multilineString", {
compile(value) {
return (data, dataPath, parentData, parentKey) => {
if (! value) {
@ -208,12 +208,12 @@ const validate = ajv.compile(schema);
const configIsValid = validate(config);
if (! configIsValid) {
console.error('Issues with configuration options:');
console.error("Issues with configuration options:");
for (const error of validate.errors) {
console.error(`The "${error.dataPath.slice(1)}" option ${error.message}`);
}
console.error('');
console.error('Please restart the bot after fixing the issues mentioned above.');
console.error("");
console.error("Please restart the bot after fixing the issues mentioned above.");
process.exit(1);
}

View File

@ -1,9 +1,9 @@
const { CommandManager, defaultParameterTypes, TypeConversionError, IParameter, ICommandConfig } = require('knub-command-manager');
const Eris = require('eris');
const config = require('./cfg');
const utils = require('./utils');
const threads = require('./data/threads');
const Thread = require('./data/Thread');
const { CommandManager, defaultParameterTypes, TypeConversionError, IParameter, ICommandConfig } = require("knub-command-manager");
const Eris = require("eris");
const config = require("./cfg");
const utils = require("./utils");
const threads = require("./data/threads");
const Thread = require("./data/Thread");
module.exports = {
createCommandManager(bot) {
@ -27,7 +27,7 @@ module.exports = {
const handlers = {};
const aliasMap = new Map();
bot.on('messageCreate', async msg => {
bot.on("messageCreate", async msg => {
if (msg.author.bot) return;
if (msg.author.id === bot.user.id) return;
if (! msg.content) return;

View File

@ -1,16 +1,16 @@
const moment = require('moment');
const Eris = require('eris');
const moment = require("moment");
const Eris = require("eris");
const bot = require('../bot');
const knex = require('../knex');
const utils = require('../utils');
const config = require('../cfg');
const attachments = require('./attachments');
const { formatters } = require('../formatters');
const bot = require("../bot");
const knex = require("../knex");
const utils = require("../utils");
const config = require("../cfg");
const attachments = require("./attachments");
const { formatters } = require("../formatters");
const ThreadMessage = require('./ThreadMessage');
const ThreadMessage = require("./ThreadMessage");
const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require('./constants');
const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require("./constants");
/**
* @property {String} id
@ -41,12 +41,12 @@ class Thread {
// Try to open a DM channel with the user
const dmChannel = await this.getDMChannel();
if (! dmChannel) {
throw new Error('Could not open DMs with the user. They may have blocked the bot or set their privacy settings higher.');
throw new Error("Could not open DMs with the user. They may have blocked the bot or set their privacy settings higher.");
}
let firstMessage;
if (typeof content === 'string') {
if (typeof content === "string") {
// Content is a string, chunk it and send it as individual messages.
// Files (attachments) are only sent with the last message.
const chunks = utils.chunk(content, 2000);
@ -79,7 +79,7 @@ class Thread {
try {
let firstMessage;
if (typeof content === 'string') {
if (typeof content === "string") {
// Content is a string, chunk it and send it as individual messages.
// Files (attachments) are only sent with the last message.
const chunks = utils.chunk(content, 2000);
@ -120,16 +120,16 @@ class Thread {
}
const dmChannel = await this.getDMChannel();
const insertedIds = await knex('thread_messages').insert({
const insertedIds = await knex("thread_messages").insert({
thread_id: this.id,
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'),
created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss"),
is_anonymous: 0,
dm_channel_id: dmChannel.id,
...data
});
const threadMessage = await knex('thread_messages')
.where('id', insertedIds[0])
const threadMessage = await knex("thread_messages")
.where("id", insertedIds[0])
.select();
return new ThreadMessage(threadMessage[0]);
@ -142,8 +142,8 @@ class Thread {
* @private
*/
async _updateThreadMessage(id, data) {
await knex('thread_messages')
.where('id', id)
await knex("thread_messages")
.where("id", id)
.update(data);
}
@ -153,8 +153,8 @@ class Thread {
* @private
*/
async _deleteThreadMessage(id) {
await knex('thread_messages')
.where('id', id)
await knex("thread_messages")
.where("id", id)
.delete();
}
@ -163,8 +163,8 @@ class Thread {
* @private
*/
_lastMessageNumberInThreadSQL() {
return knex('thread_messages AS tm_msg_num_ref')
.select(knex.raw('MAX(tm_msg_num_ref.message_number)'))
return knex("thread_messages AS tm_msg_num_ref")
.select(knex.raw("MAX(tm_msg_num_ref.message_number)"))
.whereRaw(`tm_msg_num_ref.thread_id = '${this.id}'`)
.toSQL()
.sql;
@ -212,7 +212,7 @@ class Thread {
message_type: THREAD_MESSAGE_TYPE.TO_USER,
user_id: moderator.id,
user_name: fullModeratorName,
body: '',
body: "",
is_anonymous: (isAnonymous ? 1 : 0),
dm_message_id: dmMessage.id
});
@ -227,7 +227,7 @@ class Thread {
// Interrupt scheduled closing, if in progress
if (this.scheduled_close_at) {
await this.cancelScheduledClose();
await this.postSystemMessage(`Cancelling scheduled closing of this thread due to new reply`);
await this.postSystemMessage("Cancelling scheduled closing of this thread due to new reply");
}
return true;
@ -306,11 +306,11 @@ class Thread {
async postSystemMessage(content, file = null, opts = {}) {
const msg = await this._postToThreadChannel(content, file);
if (msg && opts.saveToLog !== false) {
const finalLogBody = opts.logBody || msg.content || '<empty message>';
const finalLogBody = opts.logBody || msg.content || "<empty message>";
await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM,
user_id: null,
user_name: '',
user_name: "",
body: finalLogBody,
is_anonymous: 0,
inbox_message_id: msg.id,
@ -329,11 +329,11 @@ class Thread {
async sendSystemMessageToUser(content, file = null, opts = {}) {
const msg = await this._sendDMToUser(content, file);
if (opts.saveToLog !== false) {
const finalLogBody = opts.logBody || msg.content || '<empty message>';
const finalLogBody = opts.logBody || msg.content || "<empty message>";
await this._addThreadMessageToDB({
message_type: THREAD_MESSAGE_TYPE.SYSTEM_TO_USER,
user_id: null,
user_name: '',
user_name: "",
body: finalLogBody,
is_anonymous: 0,
dm_message_id: msg.id,
@ -381,9 +381,9 @@ class Thread {
* @returns {Promise<void>}
*/
async updateChatMessageInLogs(msg) {
await knex('thread_messages')
.where('thread_id', this.id)
.where('dm_message_id', msg.id)
await knex("thread_messages")
.where("thread_id", this.id)
.where("dm_message_id", msg.id)
.update({
body: msg.content
});
@ -394,9 +394,9 @@ class Thread {
* @returns {Promise<void>}
*/
async deleteChatMessageFromLogs(messageId) {
await knex('thread_messages')
.where('thread_id', this.id)
.where('dm_message_id', messageId)
await knex("thread_messages")
.where("thread_id", this.id)
.where("dm_message_id", messageId)
.delete();
}
@ -404,10 +404,10 @@ class Thread {
* @returns {Promise<ThreadMessage[]>}
*/
async getThreadMessages() {
const threadMessages = await knex('thread_messages')
.where('thread_id', this.id)
.orderBy('created_at', 'ASC')
.orderBy('id', 'ASC')
const threadMessages = await knex("thread_messages")
.where("thread_id", this.id)
.orderBy("created_at", "ASC")
.orderBy("id", "ASC")
.select();
return threadMessages.map(row => new ThreadMessage(row));
@ -418,9 +418,9 @@ class Thread {
* @returns {Promise<ThreadMessage>}
*/
async findThreadMessageByMessageNumber(messageNumber) {
const data = await knex('thread_messages')
.where('thread_id', this.id)
.where('message_number', messageNumber)
const data = await knex("thread_messages")
.where("thread_id", this.id)
.where("message_number", messageNumber)
.select();
return data ? new ThreadMessage(data) : null;
@ -434,15 +434,15 @@ class Thread {
console.log(`Closing thread ${this.id}`);
if (silent) {
await this.postSystemMessage('Closing thread silently...');
await this.postSystemMessage("Closing thread silently...");
} else {
await this.postSystemMessage('Closing thread...');
await this.postSystemMessage("Closing thread...");
}
}
// Update DB status
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
status: THREAD_STATUS.CLOSED
});
@ -451,7 +451,7 @@ class Thread {
const channel = bot.getChannel(this.channel_id);
if (channel) {
console.log(`Deleting channel ${this.channel_id}`);
await channel.delete('Thread closed');
await channel.delete("Thread closed");
}
}
@ -462,8 +462,8 @@ class Thread {
* @returns {Promise<void>}
*/
async scheduleClose(time, user, silent) {
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
scheduled_close_at: time,
scheduled_close_id: user.id,
@ -476,8 +476,8 @@ class Thread {
* @returns {Promise<void>}
*/
async cancelScheduledClose() {
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
scheduled_close_at: null,
scheduled_close_id: null,
@ -490,8 +490,8 @@ class Thread {
* @returns {Promise<void>}
*/
async suspend() {
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
status: THREAD_STATUS.SUSPENDED,
scheduled_suspend_at: null,
@ -504,8 +504,8 @@ class Thread {
* @returns {Promise<void>}
*/
async unsuspend() {
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
status: THREAD_STATUS.OPEN
});
@ -517,8 +517,8 @@ class Thread {
* @returns {Promise<void>}
*/
async scheduleSuspend(time, user) {
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
scheduled_suspend_at: time,
scheduled_suspend_id: user.id,
@ -530,8 +530,8 @@ class Thread {
* @returns {Promise<void>}
*/
async cancelScheduledSuspend() {
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
scheduled_suspend_at: null,
scheduled_suspend_id: null,
@ -544,8 +544,8 @@ class Thread {
* @returns {Promise<void>}
*/
async setAlert(userId) {
await knex('threads')
.where('id', this.id)
await knex("threads")
.where("id", this.id)
.update({
alert_id: userId
});

View File

@ -1,13 +1,13 @@
const Eris = require('eris');
const fs = require('fs');
const https = require('https');
const {promisify} = require('util');
const tmp = require('tmp');
const config = require('../cfg');
const utils = require('../utils');
const mv = promisify(require('mv'));
const Eris = require("eris");
const fs = require("fs");
const https = require("https");
const {promisify} = require("util");
const tmp = require("tmp");
const config = require("../cfg");
const utils = require("../utils");
const mv = promisify(require("mv"));
const getUtils = () => require('../utils');
const getUtils = () => require("../utils");
const access = promisify(fs.access);
const readFile = promisify(fs.readFile);
@ -20,7 +20,7 @@ const attachmentStorageTypes = {};
function getErrorResult(msg = null) {
return {
url: `Attachment could not be saved${msg ? ': ' + msg : ''}`,
url: `Attachment could not be saved${msg ? ": " + msg : ""}`,
failed: true
};
}
@ -61,8 +61,8 @@ async function saveLocalAttachment(attachment) {
function downloadAttachment(attachment, tries = 0) {
return new Promise((resolve, reject) => {
if (tries > 3) {
console.error('Attachment download failed after 3 tries:', attachment);
reject('Attachment download failed after 3 tries');
console.error("Attachment download failed after 3 tries:", attachment);
reject("Attachment download failed after 3 tries");
return;
}
@ -71,16 +71,16 @@ function downloadAttachment(attachment, tries = 0) {
https.get(attachment.url, (res) => {
res.pipe(writeStream);
writeStream.on('finish', () => {
writeStream.on("finish", () => {
writeStream.end();
resolve({
path: filepath,
cleanup: cleanupCallback
});
});
}).on('error', (err) => {
}).on("error", (err) => {
fs.unlink(filepath);
console.error('Error downloading attachment, retrying');
console.error("Error downloading attachment, retrying");
resolve(downloadAttachment(attachment, tries++));
});
});
@ -103,7 +103,7 @@ function getLocalAttachmentPath(attachmentId) {
* @returns {Promise<String>}
*/
function getLocalAttachmentUrl(attachmentId, desiredName = null) {
if (desiredName == null) desiredName = 'file.bin';
if (desiredName == null) desiredName = "file.bin";
return getUtils().getSelfUrl(`attachments/${attachmentId}/${desiredName}`);
}
@ -113,19 +113,19 @@ function getLocalAttachmentUrl(attachmentId, desiredName = null) {
*/
async function saveDiscordAttachment(attachment) {
if (attachment.size > 1024 * 1024 * 8) {
return getErrorResult('attachment too large (max 8MB)');
return getErrorResult("attachment too large (max 8MB)");
}
const attachmentChannelId = config.attachmentStorageChannelId;
const inboxGuild = utils.getInboxGuild();
if (! inboxGuild.channels.has(attachmentChannelId)) {
throw new Error('Attachment storage channel not found!');
throw new Error("Attachment storage channel not found!");
}
const attachmentChannel = inboxGuild.channels.get(attachmentChannelId);
if (! (attachmentChannel instanceof Eris.TextChannel)) {
throw new Error('Attachment storage channel must be a text channel!');
throw new Error("Attachment storage channel must be a text channel!");
}
const file = await attachmentToDiscordFileObject(attachment);

View File

@ -1,13 +1,13 @@
const moment = require('moment');
const knex = require('../knex');
const moment = require("moment");
const knex = require("../knex");
/**
* @param {String} userId
* @returns {Promise<{ isBlocked: boolean, expiresAt: string }>}
*/
async function getBlockStatus(userId) {
const row = await knex('blocked_users')
.where('user_id', userId)
const row = await knex("blocked_users")
.where("user_id", userId)
.first();
return {
@ -32,15 +32,15 @@ async function isBlocked(userId) {
* @param {String} blockedBy
* @returns {Promise}
*/
async function block(userId, userName = '', blockedBy = null, expiresAt = null) {
async function block(userId, userName = "", blockedBy = null, expiresAt = null) {
if (await isBlocked(userId)) return;
return knex('blocked_users')
return knex("blocked_users")
.insert({
user_id: userId,
user_name: userName,
blocked_by: blockedBy,
blocked_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'),
blocked_at: moment.utc().format("YYYY-MM-DD HH:mm:ss"),
expires_at: expiresAt
});
}
@ -51,8 +51,8 @@ async function block(userId, userName = '', blockedBy = null, expiresAt = null)
* @returns {Promise}
*/
async function unblock(userId) {
return knex('blocked_users')
.where('user_id', userId)
return knex("blocked_users")
.where("user_id", userId)
.delete();
}
@ -63,8 +63,8 @@ async function unblock(userId) {
* @returns {Promise<void>}
*/
async function updateExpiryTime(userId, expiresAt) {
return knex('blocked_users')
.where('user_id', userId)
return knex("blocked_users")
.where("user_id", userId)
.update({
expires_at: expiresAt
});
@ -74,11 +74,11 @@ async function updateExpiryTime(userId, expiresAt) {
* @returns {String[]}
*/
async function getExpiredBlocks() {
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
const now = moment.utc().format("YYYY-MM-DD HH:mm:ss");
const blocks = await knex('blocked_users')
.whereNotNull('expires_at')
.where('expires_at', '<=', now)
const blocks = await knex("blocked_users")
.whereNotNull("expires_at")
.where("expires_at", "<=", now)
.select();
return blocks.map(block => block.user_id);

View File

@ -27,41 +27,41 @@ module.exports = {
},
ACCIDENTAL_THREAD_MESSAGES: [
'ok',
'okay',
'thanks',
'ty',
'k',
'kk',
'thank you',
'thanx',
'thnx',
'thx',
'tnx',
'ok thank you',
'ok thanks',
'ok ty',
'ok thanx',
'ok thnx',
'ok thx',
'ok no problem',
'ok np',
'okay thank you',
'okay thanks',
'okay ty',
'okay thanx',
'okay thnx',
'okay thx',
'okay no problem',
'okay np',
'okey thank you',
'okey thanks',
'okey ty',
'okey thanx',
'okey thnx',
'okey thx',
'okey no problem',
'okey np',
'cheers'
"ok",
"okay",
"thanks",
"ty",
"k",
"kk",
"thank you",
"thanx",
"thnx",
"thx",
"tnx",
"ok thank you",
"ok thanks",
"ok ty",
"ok thanx",
"ok thnx",
"ok thx",
"ok no problem",
"ok np",
"okay thank you",
"okay thanks",
"okay ty",
"okay thanx",
"okay thnx",
"okay thx",
"okay no problem",
"okay np",
"okey thank you",
"okey thanks",
"okey ty",
"okey thanx",
"okey thnx",
"okey thx",
"okey no problem",
"okey np",
"cheers"
],
};

View File

@ -1,8 +1,8 @@
const path = require('path');
const fs = require('fs');
const toJsdoc = require('json-schema-to-jsdoc');
const schema = require('./cfg.schema.json');
const target = path.join(__dirname, 'cfg.jsdoc.js');
const path = require("path");
const fs = require("fs");
const toJsdoc = require("json-schema-to-jsdoc");
const schema = require("./cfg.schema.json");
const target = path.join(__dirname, "cfg.jsdoc.js");
// Fix up some custom types for the JSDoc conversion
const schemaCopy = JSON.parse(JSON.stringify(schema));
@ -20,4 +20,4 @@ for (const propertyDef of Object.values(schemaCopy.properties)) {
}
const result = toJsdoc(schemaCopy);
fs.writeFileSync(target, result, { encoding: 'utf8' });
fs.writeFileSync(target, result, { encoding: "utf8" });

View File

@ -1,14 +1,14 @@
const moment = require('moment');
const knex = require('../knex');
const Snippet = require('./Snippet');
const moment = require("moment");
const knex = require("../knex");
const Snippet = require("./Snippet");
/**
* @param {String} trigger
* @returns {Promise<Snippet>}
*/
async function getSnippet(trigger) {
const snippet = await knex('snippets')
.where('trigger', trigger)
const snippet = await knex("snippets")
.where("trigger", trigger)
.first();
return (snippet ? new Snippet(snippet) : null);
@ -22,11 +22,11 @@ async function getSnippet(trigger) {
async function addSnippet(trigger, body, createdBy = 0) {
if (await getSnippet(trigger)) return;
return knex('snippets').insert({
return knex("snippets").insert({
trigger,
body,
created_by: createdBy,
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss')
created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss")
});
}
@ -35,8 +35,8 @@ async function addSnippet(trigger, body, createdBy = 0) {
* @returns {Promise<void>}
*/
async function deleteSnippet(trigger) {
return knex('snippets')
.where('trigger', trigger)
return knex("snippets")
.where("trigger", trigger)
.delete();
}
@ -44,7 +44,7 @@ async function deleteSnippet(trigger) {
* @returns {Promise<Snippet[]>}
*/
async function getAllSnippets() {
const snippets = await knex('snippets')
const snippets = await knex("snippets")
.select();
return snippets.map(s => new Snippet(s));

View File

@ -1,19 +1,19 @@
const {User, Member} = require('eris');
const {User, Member} = require("eris");
const transliterate = require('transliteration');
const moment = require('moment');
const uuid = require('uuid');
const humanizeDuration = require('humanize-duration');
const transliterate = require("transliteration");
const moment = require("moment");
const uuid = require("uuid");
const humanizeDuration = require("humanize-duration");
const bot = require('../bot');
const knex = require('../knex');
const config = require('../cfg');
const utils = require('../utils');
const updates = require('./updates');
const bot = require("../bot");
const knex = require("../knex");
const config = require("../cfg");
const utils = require("../utils");
const updates = require("./updates");
const Thread = require('./Thread');
const Thread = require("./Thread");
const {callBeforeNewThreadHooks} = require("../hooks/beforeNewThread");
const {THREAD_STATUS, DISOCRD_CHANNEL_TYPES} = require('./constants');
const {THREAD_STATUS, DISOCRD_CHANNEL_TYPES} = require("./constants");
const MINUTES = 60 * 1000;
const HOURS = 60 * MINUTES;
@ -23,8 +23,8 @@ const HOURS = 60 * MINUTES;
* @returns {Promise<Thread>}
*/
async function findById(id) {
const thread = await knex('threads')
.where('id', id)
const thread = await knex("threads")
.where("id", id)
.first();
return (thread ? new Thread(thread) : null);
@ -35,9 +35,9 @@ async function findById(id) {
* @returns {Promise<Thread>}
*/
async function findOpenThreadByUserId(userId) {
const thread = await knex('threads')
.where('user_id', userId)
.where('status', THREAD_STATUS.OPEN)
const thread = await knex("threads")
.where("user_id", userId)
.where("status", THREAD_STATUS.OPEN)
.first();
return (thread ? new Thread(thread) : null);
@ -70,7 +70,7 @@ async function createNewThreadForUser(user, opts = {}) {
const existingThread = await findOpenThreadByUserId(user.id);
if (existingThread) {
throw new Error('Attempted to create a new thread for a user with an existing open thread!');
throw new Error("Attempted to create a new thread for a user with an existing open thread!");
}
// If set in config, check that the user's account is old enough (time since they registered on Discord)
@ -132,7 +132,7 @@ async function createNewThreadForUser(user, opts = {}) {
// Use the user's name+discrim for the thread channel's name
// Channel names are particularly picky about what characters they allow, so we gotta do some clean-up
let cleanName = transliterate.slugify(user.username);
if (cleanName === '') cleanName = 'unknown';
if (cleanName === "") cleanName = "unknown";
cleanName = cleanName.slice(0, 95); // Make sure the discrim fits
const channelName = `${cleanName}-${user.discriminator}`;
@ -161,7 +161,7 @@ async function createNewThreadForUser(user, opts = {}) {
let createdChannel;
try {
createdChannel = await utils.getInboxGuild().createChannel(channelName, DISOCRD_CHANNEL_TYPES.GUILD_TEXT, {
reason: 'New Modmail thread',
reason: "New Modmail thread",
parentID: newThreadCategoryId,
});
} catch (err) {
@ -175,7 +175,7 @@ async function createNewThreadForUser(user, opts = {}) {
user_id: user.id,
user_name: `${user.username}#${user.discriminator}`,
channel_id: createdChannel.id,
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss')
created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss")
});
const newThread = await findById(newThreadId);
@ -216,7 +216,7 @@ async function createNewThreadForUser(user, opts = {}) {
infoHeaderItems.push(`ID **${user.id}**`);
}
let infoHeader = infoHeaderItems.join(', ');
let infoHeader = infoHeaderItems.join(", ");
// Guild member info
for (const [guildId, guildData] of userGuildData.entries()) {
@ -235,10 +235,10 @@ async function createNewThreadForUser(user, opts = {}) {
if (config.rolesInThreadHeader && guildData.member.roles.length) {
const roles = guildData.member.roles.map(roleId => guildData.guild.roles.get(roleId)).filter(Boolean);
headerItems.push(`ROLES **${roles.map(r => r.name).join(', ')}**`);
headerItems.push(`ROLES **${roles.map(r => r.name).join(", ")}**`);
}
const headerStr = headerItems.join(', ');
const headerStr = headerItems.join(", ");
if (mainGuilds.length === 1) {
infoHeader += `\n${headerStr}`;
@ -253,7 +253,7 @@ async function createNewThreadForUser(user, opts = {}) {
infoHeader += `\n\nThis user has **${userLogCount}** previous modmail threads. Use \`${config.prefix}logs\` to see them.`;
}
infoHeader += '\n────────────────';
infoHeader += "\n────────────────";
await newThread.postSystemMessage(infoHeader);
@ -280,10 +280,10 @@ async function createNewThreadForUser(user, opts = {}) {
*/
async function createThreadInDB(data) {
const threadId = uuid.v4();
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
const now = moment.utc().format("YYYY-MM-DD HH:mm:ss");
const finalData = Object.assign({created_at: now, is_legacy: 0}, data, {id: threadId});
await knex('threads').insert(finalData);
await knex("threads").insert(finalData);
return threadId;
}
@ -293,8 +293,8 @@ async function createThreadInDB(data) {
* @returns {Promise<Thread>}
*/
async function findByChannelId(channelId) {
const thread = await knex('threads')
.where('channel_id', channelId)
const thread = await knex("threads")
.where("channel_id", channelId)
.first();
return (thread ? new Thread(thread) : null);
@ -305,9 +305,9 @@ async function findByChannelId(channelId) {
* @returns {Promise<Thread>}
*/
async function findOpenThreadByChannelId(channelId) {
const thread = await knex('threads')
.where('channel_id', channelId)
.where('status', THREAD_STATUS.OPEN)
const thread = await knex("threads")
.where("channel_id", channelId)
.where("status", THREAD_STATUS.OPEN)
.first();
return (thread ? new Thread(thread) : null);
@ -318,9 +318,9 @@ async function findOpenThreadByChannelId(channelId) {
* @returns {Promise<Thread>}
*/
async function findSuspendedThreadByChannelId(channelId) {
const thread = await knex('threads')
.where('channel_id', channelId)
.where('status', THREAD_STATUS.SUSPENDED)
const thread = await knex("threads")
.where("channel_id", channelId)
.where("status", THREAD_STATUS.SUSPENDED)
.first();
return (thread ? new Thread(thread) : null);
@ -331,9 +331,9 @@ async function findSuspendedThreadByChannelId(channelId) {
* @returns {Promise<Thread[]>}
*/
async function getClosedThreadsByUserId(userId) {
const threads = await knex('threads')
.where('status', THREAD_STATUS.CLOSED)
.where('user_id', userId)
const threads = await knex("threads")
.where("status", THREAD_STATUS.CLOSED)
.where("user_id", userId)
.select();
return threads.map(thread => new Thread(thread));
@ -344,10 +344,10 @@ async function getClosedThreadsByUserId(userId) {
* @returns {Promise<number>}
*/
async function getClosedThreadCountByUserId(userId) {
const row = await knex('threads')
.where('status', THREAD_STATUS.CLOSED)
.where('user_id', userId)
.first(knex.raw('COUNT(id) AS thread_count'));
const row = await knex("threads")
.where("status", THREAD_STATUS.CLOSED)
.where("user_id", userId)
.first(knex.raw("COUNT(id) AS thread_count"));
return parseInt(row.thread_count, 10);
}
@ -365,24 +365,24 @@ async function findOrCreateThreadForUser(user, opts = {}) {
}
async function getThreadsThatShouldBeClosed() {
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
const threads = await knex('threads')
.where('status', THREAD_STATUS.OPEN)
.whereNotNull('scheduled_close_at')
.where('scheduled_close_at', '<=', now)
.whereNotNull('scheduled_close_at')
const now = moment.utc().format("YYYY-MM-DD HH:mm:ss");
const threads = await knex("threads")
.where("status", THREAD_STATUS.OPEN)
.whereNotNull("scheduled_close_at")
.where("scheduled_close_at", "<=", now)
.whereNotNull("scheduled_close_at")
.select();
return threads.map(thread => new Thread(thread));
}
async function getThreadsThatShouldBeSuspended() {
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
const threads = await knex('threads')
.where('status', THREAD_STATUS.OPEN)
.whereNotNull('scheduled_suspend_at')
.where('scheduled_suspend_at', '<=', now)
.whereNotNull('scheduled_suspend_at')
const now = moment.utc().format("YYYY-MM-DD HH:mm:ss");
const threads = await knex("threads")
.where("status", THREAD_STATUS.OPEN)
.whereNotNull("scheduled_suspend_at")
.where("scheduled_suspend_at", "<=", now)
.whereNotNull("scheduled_suspend_at")
.select();
return threads.map(thread => new Thread(thread));

View File

@ -1,16 +1,16 @@
const url = require('url');
const https = require('https');
const moment = require('moment');
const knex = require('../knex');
const config = require('../cfg');
const url = require("url");
const https = require("https");
const moment = require("moment");
const knex = require("../knex");
const config = require("../cfg");
const UPDATE_CHECK_FREQUENCY = 12; // In hours
let updateCheckPromise = null;
async function initUpdatesTable() {
const row = await knex('updates').first();
const row = await knex("updates").first();
if (! row) {
await knex('updates').insert({
await knex("updates").insert({
available_version: null,
last_checked: null,
});
@ -24,48 +24,48 @@ async function initUpdatesTable() {
*/
async function refreshVersions() {
await initUpdatesTable();
const { last_checked } = await knex('updates').first();
const { last_checked } = await knex("updates").first();
// Only refresh available version if it's been more than UPDATE_CHECK_FREQUENCY since our last check
if (last_checked != null && last_checked > moment.utc().subtract(UPDATE_CHECK_FREQUENCY, 'hours').format('YYYY-MM-DD HH:mm:ss')) return;
if (last_checked != null && last_checked > moment.utc().subtract(UPDATE_CHECK_FREQUENCY, "hours").format("YYYY-MM-DD HH:mm:ss")) return;
const packageJson = require('../../package.json');
const packageJson = require("../../package.json");
const repositoryUrl = packageJson.repository && packageJson.repository.url;
if (! repositoryUrl) return;
const parsedUrl = url.parse(repositoryUrl);
if (parsedUrl.hostname !== 'github.com') return;
if (parsedUrl.hostname !== "github.com") return;
const [, owner, repo] = parsedUrl.pathname.split('/');
const [, owner, repo] = parsedUrl.pathname.split("/");
if (! owner || ! repo) return;
https.get(
{
hostname: 'api.github.com',
hostname: "api.github.com",
path: `/repos/${owner}/${repo}/tags`,
headers: {
'User-Agent': `Modmail Bot (https://github.com/${owner}/${repo}) (${packageJson.version})`
"User-Agent": `Modmail Bot (https://github.com/${owner}/${repo}) (${packageJson.version})`
}
},
async res => {
if (res.statusCode !== 200) {
await knex('updates').update({
last_checked: moment.utc().format('YYYY-MM-DD HH:mm:ss')
await knex("updates").update({
last_checked: moment.utc().format("YYYY-MM-DD HH:mm:ss")
});
console.warn(`[WARN] Got status code ${res.statusCode} when checking for available updates`);
return;
}
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', async () => {
let data = "";
res.on("data", chunk => data += chunk);
res.on("end", async () => {
const parsed = JSON.parse(data);
if (! Array.isArray(parsed) || parsed.length === 0) return;
const latestVersion = parsed[0].name;
await knex('updates').update({
await knex("updates").update({
available_version: latestVersion,
last_checked: moment.utc().format('YYYY-MM-DD HH:mm:ss')
last_checked: moment.utc().format("YYYY-MM-DD HH:mm:ss")
});
});
}
@ -78,11 +78,11 @@ async function refreshVersions() {
* @returns {Number} 1 if version a is larger than b, -1 is version a is smaller than b, 0 if they are equal
*/
function compareVersions(a, b) {
const aParts = a.split('.');
const bParts = b.split('.');
const aParts = a.split(".");
const bParts = b.split(".");
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
let aPart = parseInt((aParts[i] || '0').match(/\d+/)[0] || '0', 10);
let bPart = parseInt((bParts[i] || '0').match(/\d+/)[0] || '0', 10);
let aPart = parseInt((aParts[i] || "0").match(/\d+/)[0] || "0", 10);
let bPart = parseInt((bParts[i] || "0").match(/\d+/)[0] || "0", 10);
if (aPart > bPart) return 1;
if (aPart < bPart) return -1;
}
@ -92,9 +92,9 @@ function compareVersions(a, b) {
async function getAvailableUpdate() {
await initUpdatesTable();
const packageJson = require('../../package.json');
const packageJson = require("../../package.json");
const currentVersion = packageJson.version;
const { available_version: availableVersion } = await knex('updates').first();
const { available_version: availableVersion } = await knex("updates").first();
if (availableVersion == null) return null;
if (currentVersion == null) return availableVersion;

View File

@ -1,7 +1,7 @@
const Eris = require('eris');
const utils = require('./utils');
const config = require('./cfg');
const ThreadMessage = require('./data/ThreadMessage');
const Eris = require("eris");
const utils = require("./utils");
const config = require("./cfg");
const ThreadMessage = require("./data/ThreadMessage");
/**
* Function to format the DM that is sent to the user when a staff member replies to them via !reply
@ -116,7 +116,7 @@ const defaultFormatters = {
const mainRole = utils.getMainRole(moderator);
const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username);
const modInfo = opts.isAnonymous
? (mainRole ? mainRole.name : 'Moderator')
? (mainRole ? mainRole.name : "Moderator")
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
return `**${modInfo}:** ${text}`;
@ -126,7 +126,7 @@ const defaultFormatters = {
const mainRole = utils.getMainRole(moderator);
const modName = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username);
const modInfo = opts.isAnonymous
? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : 'Moderator'}`
? `(Anonymous) (${modName}) ${mainRole ? mainRole.name : "Moderator"}`
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
let result = `**${modInfo}:** ${text}`;
@ -147,13 +147,13 @@ const defaultFormatters = {
// Mirroring the DM formatting here...
const modInfo = opts.isAnonymous
? (mainRole ? mainRole.name : 'Moderator')
? (mainRole ? mainRole.name : "Moderator")
: (mainRole ? `(${mainRole.name}) ${modName}` : modName);
let result = `**${modInfo}:** ${text}`;
if (opts.attachmentLinks && opts.attachmentLinks.length) {
result += '\n';
result += "\n";
for (const link of opts.attachmentLinks) {
result += `\n**Attachment:** ${link}`;
}
@ -165,8 +165,8 @@ const defaultFormatters = {
},
formatUserReplyThreadMessage(user, msg, opts = {}) {
const content = (msg.content.trim() === '' && msg.embeds.length)
? '<message contains embeds>'
const content = (msg.content.trim() === "" && msg.embeds.length)
? "<message contains embeds>"
: msg.content;
let result = `**${user.username}#${user.discriminator}:** ${content}`;
@ -178,7 +178,7 @@ const defaultFormatters = {
}
if (config.threadTimestamps) {
const formattedTimestamp = utils.getTimestamp(msg.timestamp, 'x');
const formattedTimestamp = utils.getTimestamp(msg.timestamp, "x");
result = `[${formattedTimestamp}] ${result}`;
}
@ -186,8 +186,8 @@ const defaultFormatters = {
},
formatUserReplyLogMessage(user, msg, opts = {}) {
const content = (msg.content.trim() === '' && msg.embeds.length)
? '<message contains embeds>'
const content = (msg.content.trim() === "" && msg.embeds.length)
? "<message contains embeds>"
: msg.content;
let result = content;

View File

@ -1,4 +1,4 @@
const Eris = require('eris');
const Eris = require("eris");
/**
* @callback BeforeNewThreadHook_SetCategoryId

View File

@ -1,25 +1,25 @@
// Verify NodeJS version
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10);
const nodeMajorVersion = parseInt(process.versions.node.split(".")[0], 10);
if (nodeMajorVersion < 11) {
console.error('Unsupported NodeJS version! Please install Node.js 11, 12, 13, or 14.');
console.error("Unsupported NodeJS version! Please install Node.js 11, 12, 13, or 14.");
process.exit(1);
}
// Verify node modules have been installed
const fs = require('fs');
const path = require('path');
const fs = require("fs");
const path = require("path");
try {
fs.accessSync(path.join(__dirname, '..', 'node_modules'));
fs.accessSync(path.join(__dirname, "..", "node_modules"));
} catch (e) {
console.error('Please run "npm ci" before starting the bot');
console.error("Please run \"npm ci\" before starting the bot");
process.exit(1);
}
// Error handling
process.on('uncaughtException', err => {
process.on("uncaughtException", err => {
// Unknown message types (nitro boosting messages at the time) should be safe to ignore
if (err && err.message && err.message.startsWith('Unhandled MESSAGE_CREATE type')) {
if (err && err.message && err.message.startsWith("Unhandled MESSAGE_CREATE type")) {
return;
}
@ -28,27 +28,27 @@ process.on('uncaughtException', err => {
process.exit(1);
});
let testedPackage = '';
let testedPackage = "";
try {
const packageJson = require('../package.json');
const packageJson = require("../package.json");
const modules = Object.keys(packageJson.dependencies);
modules.forEach(mod => {
testedPackage = mod;
fs.accessSync(path.join(__dirname, '..', 'node_modules', mod))
fs.accessSync(path.join(__dirname, "..", "node_modules", mod))
});
} catch (e) {
console.error(`Please run "npm ci" again! Package "${testedPackage}" is missing.`);
process.exit(1);
}
const config = require('./cfg');
const utils = require('./utils');
const main = require('./main');
const knex = require('./knex');
const legacyMigrator = require('./legacy/legacyMigrator');
const config = require("./cfg");
const utils = require("./utils");
const main = require("./main");
const knex = require("./knex");
const legacyMigrator = require("./legacy/legacyMigrator");
// Force crash on unhandled rejections (use something like forever/pm2 to restart)
process.on('unhandledRejection', err => {
process.on("unhandledRejection", err => {
if (err instanceof utils.BotError || (err && err.code)) {
// We ignore stack traces for BotErrors (the message has enough info) and network errors from Eris (their stack traces are unreadably long)
console.error(`Error: ${err.message}`);
@ -63,34 +63,34 @@ process.on('unhandledRejection', err => {
// Make sure the database is up to date
const [completed, newMigrations] = await knex.migrate.list();
if (newMigrations.length > 0) {
console.log('Updating database. This can take a while. Don\'t close the bot!');
console.log("Updating database. This can take a while. Don't close the bot!");
await knex.migrate.latest();
console.log('Done!');
console.log("Done!");
}
// Migrate legacy data if we need to
if (await legacyMigrator.shouldMigrate()) {
console.log('=== MIGRATING LEGACY DATA ===');
console.log('Do not close the bot!');
console.log('');
console.log("=== MIGRATING LEGACY DATA ===");
console.log("Do not close the bot!");
console.log("");
await legacyMigrator.migrate();
const relativeDbDir = (path.isAbsolute(config.dbDir) ? config.dbDir : path.resolve(process.cwd(), config.dbDir));
const relativeLogDir = (path.isAbsolute(config.logDir) ? config.logDir : path.resolve(process.cwd(), config.logDir));
console.log('');
console.log('=== LEGACY DATA MIGRATION FINISHED ===');
console.log('');
console.log('IMPORTANT: After the bot starts, please verify that all logs, threads, blocked users, and snippets are still working correctly.');
console.log('Once you\'ve done that, the following files/directories are no longer needed. I would recommend keeping a backup of them, however.');
console.log('');
console.log('FILE: ' + path.resolve(relativeDbDir, 'threads.json'));
console.log('FILE: ' + path.resolve(relativeDbDir, 'blocked.json'));
console.log('FILE: ' + path.resolve(relativeDbDir, 'snippets.json'));
console.log('DIRECTORY: ' + relativeLogDir);
console.log('');
console.log('Starting the bot...');
console.log("");
console.log("=== LEGACY DATA MIGRATION FINISHED ===");
console.log("");
console.log("IMPORTANT: After the bot starts, please verify that all logs, threads, blocked users, and snippets are still working correctly.");
console.log("Once you've done that, the following files/directories are no longer needed. I would recommend keeping a backup of them, however.");
console.log("");
console.log("FILE: " + path.resolve(relativeDbDir, "threads.json"));
console.log("FILE: " + path.resolve(relativeDbDir, "blocked.json"));
console.log("FILE: " + path.resolve(relativeDbDir, "snippets.json"));
console.log("DIRECTORY: " + relativeLogDir);
console.log("");
console.log("Starting the bot...");
}
// Start the bot

View File

@ -1,2 +1,2 @@
const config = require('./cfg');
module.exports = require('knex')(config.knex);
const config = require("./cfg");
module.exports = require("knex")(config.knex);

View File

@ -1,6 +1,6 @@
const fs = require('fs');
const path = require('path');
const config = require('../cfg');
const fs = require("fs");
const path = require("path");
const config = require("../cfg");
const dbDir = config.dbDir;
@ -15,7 +15,7 @@ class JSONDB {
this.useCloneByDefault = useCloneByDefault;
this.load = new Promise(resolve => {
fs.readFile(path, {encoding: 'utf8'}, (err, data) => {
fs.readFile(path, {encoding: "utf8"}, (err, data) => {
if (err) return resolve(def);
let unserialized;
@ -39,7 +39,7 @@ class JSONDB {
save(newData) {
const serialized = JSON.stringify(newData);
this.load = new Promise((resolve, reject) => {
fs.writeFile(this.path, serialized, {encoding: 'utf8'}, () => {
fs.writeFile(this.path, serialized, {encoding: "utf8"}, () => {
resolve(newData);
});
});

View File

@ -1,15 +1,15 @@
const fs = require('fs');
const path = require('path');
const promisify = require('util').promisify;
const moment = require('moment');
const Eris = require('eris');
const fs = require("fs");
const path = require("path");
const promisify = require("util").promisify;
const moment = require("moment");
const Eris = require("eris");
const knex = require('../knex');
const config = require('../cfg');
const jsonDb = require('./jsonDb');
const threads = require('../data/threads');
const knex = require("../knex");
const config = require("../cfg");
const jsonDb = require("./jsonDb");
const threads = require("../data/threads");
const {THREAD_STATUS, THREAD_MESSAGE_TYPE} = require('../data/constants');
const {THREAD_STATUS, THREAD_MESSAGE_TYPE} = require("../data/constants");
const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
@ -17,43 +17,43 @@ const access = promisify(fs.access);
const writeFile = promisify(fs.writeFile);
async function migrate() {
console.log('Migrating open threads...');
console.log("Migrating open threads...");
await migrateOpenThreads();
console.log('Migrating logs...');
console.log("Migrating logs...");
await migrateLogs();
console.log('Migrating blocked users...');
console.log("Migrating blocked users...");
await migrateBlockedUsers();
console.log('Migrating snippets...');
console.log("Migrating snippets...");
await migrateSnippets();
await writeFile(path.join(config.dbDir, '.migrated_legacy'), '');
await writeFile(path.join(config.dbDir, ".migrated_legacy"), "");
}
async function shouldMigrate() {
// If there is a file marking a finished migration, assume we don't need to migrate
const migrationFile = path.join(config.dbDir, '.migrated_legacy');
const migrationFile = path.join(config.dbDir, ".migrated_legacy");
try {
await access(migrationFile);
return false;
} catch (e) {}
// If there are any old threads, we need to migrate
const oldThreads = await jsonDb.get('threads', []);
const oldThreads = await jsonDb.get("threads", []);
if (oldThreads.length) {
return true;
}
// If there are any old blocked users, we need to migrate
const blockedUsers = await jsonDb.get('blocked', []);
const blockedUsers = await jsonDb.get("blocked", []);
if (blockedUsers.length) {
return true;
}
// If there are any old snippets, we need to migrate
const snippets = await jsonDb.get('snippets', {});
const snippets = await jsonDb.get("snippets", {});
if (Object.keys(snippets).length) {
return true;
}
@ -71,12 +71,12 @@ async function migrateOpenThreads() {
const bot = new Eris.Client(config.token);
const toReturn = new Promise(resolve => {
bot.on('ready', async () => {
const oldThreads = await jsonDb.get('threads', []);
bot.on("ready", async () => {
const oldThreads = await jsonDb.get("threads", []);
const promises = oldThreads.map(async oldThread => {
const existingOpenThread = await knex('threads')
.where('channel_id', oldThread.channelId)
const existingOpenThread = await knex("threads")
.where("channel_id", oldThread.channelId)
.first();
if (existingOpenThread) return;
@ -86,9 +86,9 @@ async function migrateOpenThreads() {
const threadMessages = await oldChannel.getMessages(1000);
const log = threadMessages.reverse().map(msg => {
const date = moment.utc(msg.timestamp, 'x').format('YYYY-MM-DD HH:mm:ss');
const date = moment.utc(msg.timestamp, "x").format("YYYY-MM-DD HH:mm:ss");
return `[${date}] ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`;
}).join('\n') + '\n';
}).join("\n") + "\n";
const newThread = {
status: THREAD_STATUS.OPEN,
@ -100,14 +100,14 @@ async function migrateOpenThreads() {
const threadId = await threads.createThreadInDB(newThread);
await knex('thread_messages').insert({
await knex("thread_messages").insert({
thread_id: threadId,
message_type: THREAD_MESSAGE_TYPE.LEGACY,
user_id: oldThread.userId,
user_name: '',
user_name: "",
body: log,
is_anonymous: 0,
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss')
created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss")
});
});
@ -128,38 +128,38 @@ async function migrateLogs() {
for (let i = 0; i < logFiles.length; i++) {
const logFile = logFiles[i];
if (! logFile.endsWith('.txt')) continue;
if (! logFile.endsWith(".txt")) continue;
const [rawDate, userId, threadId] = logFile.slice(0, -4).split('__');
const date = `${rawDate.slice(0, 10)} ${rawDate.slice(11).replace('-', ':')}`;
const [rawDate, userId, threadId] = logFile.slice(0, -4).split("__");
const date = `${rawDate.slice(0, 10)} ${rawDate.slice(11).replace("-", ":")}`;
const fullPath = path.join(logDir, logFile);
const contents = await readFile(fullPath, {encoding: 'utf8'});
const contents = await readFile(fullPath, {encoding: "utf8"});
const newThread = {
id: threadId,
status: THREAD_STATUS.CLOSED,
user_id: userId,
user_name: '',
user_name: "",
channel_id: null,
is_legacy: 1,
created_at: date
};
await knex.transaction(async trx => {
const existingThread = await trx('threads')
.where('id', newThread.id)
const existingThread = await trx("threads")
.where("id", newThread.id)
.first();
if (existingThread) return;
await trx('threads').insert(newThread);
await trx("threads").insert(newThread);
await trx('thread_messages').insert({
await trx("thread_messages").insert({
thread_id: newThread.id,
message_type: THREAD_MESSAGE_TYPE.LEGACY,
user_id: userId,
user_name: '',
user_name: "",
body: contents,
is_anonymous: 0,
created_at: date
@ -174,19 +174,19 @@ async function migrateLogs() {
}
async function migrateBlockedUsers() {
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
const blockedUsers = await jsonDb.get('blocked', []);
const now = moment.utc().format("YYYY-MM-DD HH:mm:ss");
const blockedUsers = await jsonDb.get("blocked", []);
for (const userId of blockedUsers) {
const existingBlockedUser = await knex('blocked_users')
.where('user_id', userId)
const existingBlockedUser = await knex("blocked_users")
.where("user_id", userId)
.first();
if (existingBlockedUser) return;
await knex('blocked_users').insert({
await knex("blocked_users").insert({
user_id: userId,
user_name: '',
user_name: "",
blocked_by: null,
blocked_at: now
});
@ -194,17 +194,17 @@ async function migrateBlockedUsers() {
}
async function migrateSnippets() {
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
const snippets = await jsonDb.get('snippets', {});
const now = moment.utc().format("YYYY-MM-DD HH:mm:ss");
const snippets = await jsonDb.get("snippets", {});
const promises = Object.entries(snippets).map(async ([trigger, data]) => {
const existingSnippet = await knex('snippets')
.where('trigger', trigger)
const existingSnippet = await knex("snippets")
.where("trigger", trigger)
.first();
if (existingSnippet) return;
return knex('snippets').insert({
return knex("snippets").insert({
trigger,
body: data.text,
is_anonymous: data.isAnonymous ? 1 : 0,

View File

@ -1,58 +1,58 @@
const Eris = require('eris');
const path = require('path');
const Eris = require("eris");
const path = require("path");
const config = require('./cfg');
const bot = require('./bot');
const knex = require('./knex');
const {messageQueue} = require('./queue');
const utils = require('./utils');
const { createCommandManager } = require('./commands');
const { getPluginAPI, loadPlugin } = require('./plugins');
const { callBeforeNewThreadHooks } = require('./hooks/beforeNewThread');
const config = require("./cfg");
const bot = require("./bot");
const knex = require("./knex");
const {messageQueue} = require("./queue");
const utils = require("./utils");
const { createCommandManager } = require("./commands");
const { getPluginAPI, loadPlugin } = require("./plugins");
const { callBeforeNewThreadHooks } = require("./hooks/beforeNewThread");
const blocked = require('./data/blocked');
const threads = require('./data/threads');
const updates = require('./data/updates');
const blocked = require("./data/blocked");
const threads = require("./data/threads");
const updates = require("./data/updates");
const reply = require('./modules/reply');
const close = require('./modules/close');
const snippets = require('./modules/snippets');
const logs = require('./modules/logs');
const move = require('./modules/move');
const block = require('./modules/block');
const suspend = require('./modules/suspend');
const webserver = require('./modules/webserver');
const greeting = require('./modules/greeting');
const typingProxy = require('./modules/typingProxy');
const version = require('./modules/version');
const newthread = require('./modules/newthread');
const idModule = require('./modules/id');
const alert = require('./modules/alert');
const reply = require("./modules/reply");
const close = require("./modules/close");
const snippets = require("./modules/snippets");
const logs = require("./modules/logs");
const move = require("./modules/move");
const block = require("./modules/block");
const suspend = require("./modules/suspend");
const webserver = require("./modules/webserver");
const greeting = require("./modules/greeting");
const typingProxy = require("./modules/typingProxy");
const version = require("./modules/version");
const newthread = require("./modules/newthread");
const idModule = require("./modules/id");
const alert = require("./modules/alert");
const {ACCIDENTAL_THREAD_MESSAGES} = require('./data/constants');
const {ACCIDENTAL_THREAD_MESSAGES} = require("./data/constants");
module.exports = {
async start() {
console.log('Connecting to Discord...');
console.log("Connecting to Discord...");
bot.once('ready', async () => {
console.log('Connected! Waiting for guilds to become available...');
bot.once("ready", async () => {
console.log("Connected! Waiting for guilds to become available...");
await Promise.all([
...config.mainGuildId.map(id => waitForGuild(id)),
waitForGuild(config.mailGuildId)
]);
console.log('Initializing...');
console.log("Initializing...");
initStatus();
initBaseMessageHandlers();
console.log('Loading plugins...');
console.log("Loading plugins...");
const pluginResult = await initPlugins();
console.log(`Loaded ${pluginResult.loadedCount} plugins (${pluginResult.builtInCount} built-in plugins, ${pluginResult.externalCount} external plugins)`);
console.log('');
console.log('Done! Now listening to DMs.');
console.log('');
console.log("");
console.log("Done! Now listening to DMs.");
console.log("");
});
bot.connect();
@ -65,7 +65,7 @@ function waitForGuild(guildId) {
}
return new Promise(resolve => {
bot.on('guildAvailable', guild => {
bot.on("guildAvailable", guild => {
if (guild.id === guildId) {
resolve();
}
@ -89,7 +89,7 @@ function initBaseMessageHandlers() {
* 1) If alwaysReply is enabled, reply to the user
* 2) If alwaysReply is disabled, save that message as a chat message in the thread
*/
bot.on('messageCreate', async msg => {
bot.on("messageCreate", async msg => {
if (! utils.messageIsOnInboxServer(msg)) return;
if (msg.author.bot) return;
@ -116,7 +116,7 @@ function initBaseMessageHandlers() {
* 1) Find the open modmail thread for this user, or create a new one
* 2) Post the message as a user reply in the thread
*/
bot.on('messageCreate', async msg => {
bot.on("messageCreate", async msg => {
if (! (msg.channel instanceof Eris.PrivateChannel)) return;
if (msg.author.bot) return;
if (msg.type !== 0) return; // Ignore pins etc.
@ -133,7 +133,7 @@ function initBaseMessageHandlers() {
if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return;
thread = await threads.createNewThreadForUser(msg.author, {
source: 'dm',
source: "dm",
});
}
@ -148,13 +148,13 @@ function initBaseMessageHandlers() {
* 1) If that message was in DMs, and we have a thread open with that user, post the edit as a system message in the thread
* 2) If that message was moderator chatter in the thread, update the corresponding chat message in the DB
*/
bot.on('messageUpdate', async (msg, oldMessage) => {
bot.on("messageUpdate", async (msg, oldMessage) => {
if (! msg || ! msg.author) return;
if (msg.author.bot) return;
if (await blocked.isBlocked(msg.author.id)) return;
// Old message content doesn't persist between bot restarts
const oldContent = oldMessage && oldMessage.content || '*Unavailable due to bot restart*';
const oldContent = oldMessage && oldMessage.content || "*Unavailable due to bot restart*";
const newContent = msg.content;
// Ignore edit events with changes only in embeds etc.
@ -181,7 +181,7 @@ function initBaseMessageHandlers() {
/**
* When a staff message is deleted in a modmail thread, delete it from the database as well
*/
bot.on('messageDelete', async msg => {
bot.on("messageDelete", async msg => {
if (! msg.author) return;
if (msg.author.bot) return;
if (! utils.messageIsOnInboxServer(msg)) return;
@ -196,7 +196,7 @@ function initBaseMessageHandlers() {
/**
* When the bot is mentioned on the main server, ping staff in the log channel about it
*/
bot.on('messageCreate', async msg => {
bot.on("messageCreate", async msg => {
if (! utils.messageIsOnMainServer(msg)) return;
if (! msg.mentions.some(user => user.id === bot.user.id)) return;
if (msg.author.bot) return;
@ -215,7 +215,7 @@ function initBaseMessageHandlers() {
let content;
const mainGuilds = utils.getMainGuilds();
const staffMention = (config.pingOnBotMention ? utils.getInboxMention() : '');
const staffMention = (config.pingOnBotMention ? utils.getInboxMention() : "");
if (mainGuilds.length === 1) {
content = `${staffMention}Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}(${msg.author.id})**: "${msg.cleanContent}"\n\n<https:\/\/discordapp.com\/channels\/${msg.channel.guild.id}\/${msg.channel.id}\/${msg.id}>`;

View File

@ -1,8 +1,8 @@
module.exports = ({ bot, knex, config, commands }) => {
commands.addInboxThreadCommand('alert', '[opt:string]', async (msg, args, thread) => {
if (args.opt && args.opt.startsWith('c')) {
commands.addInboxThreadCommand("alert", "[opt:string]", async (msg, args, thread) => {
if (args.opt && args.opt.startsWith("c")) {
await thread.setAlert(null);
await thread.postSystemMessage(`Cancelled new message alert`);
await thread.postSystemMessage("Cancelled new message alert");
} else {
await thread.setAlert(msg.author.id);
await thread.postSystemMessage(`Pinging ${msg.author.username}#${msg.author.discriminator} when this thread gets a new reply`);

View File

@ -1,5 +1,5 @@
const humanizeDuration = require('humanize-duration');
const moment = require('moment');
const humanizeDuration = require("humanize-duration");
const moment = require("moment");
const blocked = require("../data/blocked");
const utils = require("../utils");
@ -23,7 +23,7 @@ module.exports = ({ bot, knex, config, commands }) => {
setTimeout(expiredBlockLoop, 2000);
}
bot.on('ready', expiredBlockLoop);
bot.on("ready", expiredBlockLoop);
const blockCmd = async (msg, args, thread) => {
const userIdToBlock = args.userId || (thread && thread.user_id);
@ -31,16 +31,16 @@ module.exports = ({ bot, knex, config, commands }) => {
const isBlocked = await blocked.isBlocked(userIdToBlock);
if (isBlocked) {
msg.channel.createMessage('User is already blocked');
msg.channel.createMessage("User is already blocked");
return;
}
const expiresAt = args.blockTime
? moment.utc().add(args.blockTime, 'ms').format('YYYY-MM-DD HH:mm:ss')
? moment.utc().add(args.blockTime, "ms").format("YYYY-MM-DD HH:mm:ss")
: null;
const user = bot.users.get(userIdToBlock);
await blocked.block(userIdToBlock, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id, expiresAt);
await blocked.block(userIdToBlock, (user ? `${user.username}#${user.discriminator}` : ""), msg.author.id, expiresAt);
if (expiresAt) {
const humanized = humanizeDuration(args.blockTime, { largest: 2, round: true });
@ -50,8 +50,8 @@ module.exports = ({ bot, knex, config, commands }) => {
}
};
commands.addInboxServerCommand('block', '<userId:userId> [blockTime:delay]', blockCmd);
commands.addInboxServerCommand('block', '[blockTime:delay]', blockCmd);
commands.addInboxServerCommand("block", "<userId:userId> [blockTime:delay]", blockCmd);
commands.addInboxServerCommand("block", "[blockTime:delay]", blockCmd);
const unblockCmd = async (msg, args, thread) => {
const userIdToUnblock = args.userId || (thread && thread.user_id);
@ -59,12 +59,12 @@ module.exports = ({ bot, knex, config, commands }) => {
const isBlocked = await blocked.isBlocked(userIdToUnblock);
if (! isBlocked) {
msg.channel.createMessage('User is not blocked');
msg.channel.createMessage("User is not blocked");
return;
}
const unblockAt = args.unblockDelay
? moment.utc().add(args.unblockDelay, 'ms').format('YYYY-MM-DD HH:mm:ss')
? moment.utc().add(args.unblockDelay, "ms").format("YYYY-MM-DD HH:mm:ss")
: null;
const user = bot.users.get(userIdToUnblock);
@ -78,10 +78,10 @@ module.exports = ({ bot, knex, config, commands }) => {
}
};
commands.addInboxServerCommand('unblock', '<userId:userId> [unblockDelay:delay]', unblockCmd);
commands.addInboxServerCommand('unblock', '[unblockDelay:delay]', unblockCmd);
commands.addInboxServerCommand("unblock", "<userId:userId> [unblockDelay:delay]", unblockCmd);
commands.addInboxServerCommand("unblock", "[unblockDelay:delay]", unblockCmd);
commands.addInboxServerCommand('is_blocked', '[userId:userId]',async (msg, args, thread) => {
commands.addInboxServerCommand("is_blocked", "[userId:userId]",async (msg, args, thread) => {
const userIdToCheck = args.userId || (thread && thread.user_id);
if (! userIdToCheck) return;

View File

@ -1,10 +1,10 @@
const moment = require('moment');
const Eris = require('eris');
const config = require('../cfg');
const utils = require('../utils');
const threads = require('../data/threads');
const blocked = require('../data/blocked');
const {messageQueue} = require('../queue');
const moment = require("moment");
const Eris = require("eris");
const config = require("../cfg");
const utils = require("../utils");
const threads = require("../data/threads");
const blocked = require("../data/blocked");
const {messageQueue} = require("../queue");
module.exports = ({ bot, knex, config, commands }) => {
// Check for threads that are scheduled to be closed and close them
@ -39,7 +39,7 @@ module.exports = ({ bot, knex, config, commands }) => {
scheduledCloseLoop();
// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
commands.addGlobalCommand('close', '[opts...]', async (msg, args) => {
commands.addGlobalCommand("close", "[opts...]", async (msg, args) => {
let thread, closedBy;
let hasCloseMessage = !! config.closeMessage;
@ -56,11 +56,11 @@ module.exports = ({ bot, knex, config, commands }) => {
// We need to add this operation to the message queue so we don't get a race condition
// between showing the close command in the thread and closing the thread
await messageQueue.add(async () => {
thread.postSystemMessage('Thread closed by user, closing...');
thread.postSystemMessage("Thread closed by user, closing...");
await thread.close(true);
});
closedBy = 'the user';
closedBy = "the user";
} else {
// A staff member is closing the thread
if (! utils.messageIsOnInboxServer(msg)) return;
@ -70,18 +70,18 @@ module.exports = ({ bot, knex, config, commands }) => {
if (! thread) return;
if (args.opts && args.opts.length) {
if (args.opts.includes('cancel') || args.opts.includes('c')) {
if (args.opts.includes("cancel") || args.opts.includes("c")) {
// Cancel timed close
if (thread.scheduled_close_at) {
await thread.cancelScheduledClose();
thread.postSystemMessage(`Cancelled scheduled closing`);
thread.postSystemMessage("Cancelled scheduled closing");
}
return;
}
// Silent close (= no close message)
if (args.opts.includes('silent') || args.opts.includes('s')) {
if (args.opts.includes("silent") || args.opts.includes("s")) {
silentClose = true;
}
@ -90,12 +90,12 @@ module.exports = ({ bot, knex, config, commands }) => {
if (delayStringArg) {
const delay = utils.convertDelayStringToMS(delayStringArg);
if (delay === 0 || delay === null) {
thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`);
thread.postSystemMessage("Invalid delay specified. Format: \"1h30m\"");
return;
}
const closeAt = moment.utc().add(delay, 'ms');
await thread.scheduleClose(closeAt.format('YYYY-MM-DD HH:mm:ss'), msg.author, silentClose ? 1 : 0);
const closeAt = moment.utc().add(delay, "ms");
await thread.scheduleClose(closeAt.format("YYYY-MM-DD HH:mm:ss"), msg.author, silentClose ? 1 : 0);
let response;
if (silentClose) {
@ -129,7 +129,7 @@ module.exports = ({ bot, knex, config, commands }) => {
});
// Auto-close threads if their channel is deleted
bot.on('channelDelete', async (channel) => {
bot.on("channelDelete", async (channel) => {
if (! (channel instanceof Eris.TextChannel)) return;
if (channel.guild.id !== utils.getInboxGuild().id) return;

View File

@ -1,12 +1,12 @@
const path = require('path');
const fs = require('fs');
const config = require('../cfg');
const utils = require('../utils');
const path = require("path");
const fs = require("fs");
const config = require("../cfg");
const utils = require("../utils");
module.exports = ({ bot }) => {
if (! config.enableGreeting) return;
bot.on('guildMemberAdd', (guild, member) => {
bot.on("guildMemberAdd", (guild, member) => {
const guildGreeting = config.guildGreetings[guild.id];
if (! guildGreeting || (! guildGreeting.message && ! guildGreeting.attachment)) return;
@ -14,7 +14,7 @@ module.exports = ({ bot }) => {
bot.getDMChannel(member.id).then(channel => {
if (! channel) return;
channel.createMessage(message || '', file)
channel.createMessage(message || "", file)
.catch(e => {
if (e.code === 50007) return;
throw e;

View File

@ -1,9 +1,9 @@
module.exports = ({ bot, knex, config, commands }) => {
commands.addInboxThreadCommand('id', [], async (msg, args, thread) => {
commands.addInboxThreadCommand("id", [], async (msg, args, thread) => {
thread.postSystemMessage(thread.user_id);
});
commands.addInboxThreadCommand('dm_channel_id', [], async (msg, args, thread) => {
commands.addInboxThreadCommand("dm_channel_id", [], async (msg, args, thread) => {
const dmChannel = await thread.getDMChannel();
thread.postSystemMessage(dmChannel.id);
});

View File

@ -1,5 +1,5 @@
const threads = require("../data/threads");
const moment = require('moment');
const moment = require("moment");
const utils = require("../utils");
const LOG_LINES_PER_PAGE = 10;
@ -30,7 +30,7 @@ module.exports = ({ bot, knex, config, commands }) => {
const threadLines = await Promise.all(userThreads.map(async thread => {
const logUrl = await thread.getLogUrl();
const formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]');
const formattedDate = moment.utc(thread.created_at).format("MMM Do [at] HH:mm [UTC]");
return `\`${formattedDate}\`: <${logUrl}>`;
}));
@ -38,26 +38,26 @@ module.exports = ({ bot, knex, config, commands }) => {
? `**Log files for <@${userId}>** (page **${page}/${maxPage}**, showing logs **${start + 1}-${end}/${totalUserThreads}**):`
: `**Log files for <@${userId}>:**`;
message += `\n${threadLines.join('\n')}`;
message += `\n${threadLines.join("\n")}`;
if (isPaginated) {
message += `\nTo view more, add a page number to the end of the command`;
message += "\nTo view more, add a page number to the end of the command";
}
// Send the list of logs in chunks of 15 lines per message
const lines = message.split('\n');
const lines = message.split("\n");
const chunks = utils.chunk(lines, 15);
let root = Promise.resolve();
chunks.forEach(lines => {
root = root.then(() => msg.channel.createMessage(lines.join('\n')));
root = root.then(() => msg.channel.createMessage(lines.join("\n")));
});
};
commands.addInboxServerCommand('logs', '<userId:userId> [page:number]', logsCmd);
commands.addInboxServerCommand('logs', '[page:number]', logsCmd);
commands.addInboxServerCommand("logs", "<userId:userId> [page:number]", logsCmd);
commands.addInboxServerCommand("logs", "[page:number]", logsCmd);
commands.addInboxServerCommand('loglink', [], async (msg, args, thread) => {
commands.addInboxServerCommand("loglink", [], async (msg, args, thread) => {
if (! thread) {
thread = await threads.findSuspendedThreadByChannelId(msg.channel.id);
if (! thread) return;
@ -65,21 +65,21 @@ module.exports = ({ bot, knex, config, commands }) => {
const logUrl = await thread.getLogUrl();
const query = [];
if (args.verbose) query.push('verbose=1');
if (args.simple) query.push('simple=1');
let qs = query.length ? `?${query.join('&')}` : '';
if (args.verbose) query.push("verbose=1");
if (args.simple) query.push("simple=1");
let qs = query.length ? `?${query.join("&")}` : "";
thread.postSystemMessage(`Log URL: ${logUrl}${qs}`);
}, {
options: [
{
name: 'verbose',
shortcut: 'v',
name: "verbose",
shortcut: "v",
isSwitch: true,
},
{
name: 'simple',
shortcut: 's',
name: "simple",
shortcut: "s",
isSwitch: true,
},
],

View File

@ -1,12 +1,12 @@
const config = require('../cfg');
const Eris = require('eris');
const config = require("../cfg");
const Eris = require("eris");
const transliterate = require("transliteration");
const erisEndpoints = require('eris/lib/rest/Endpoints');
const erisEndpoints = require("eris/lib/rest/Endpoints");
module.exports = ({ bot, knex, config, commands }) => {
if (! config.allowMove) return;
commands.addInboxThreadCommand('move', '<category:string$>', async (msg, args, thread) => {
commands.addInboxThreadCommand("move", "<category:string$>", async (msg, args, thread) => {
const searchStr = args.category;
const normalizedSearchStr = transliterate.slugify(searchStr);
@ -41,7 +41,7 @@ module.exports = ({ bot, knex, config, commands }) => {
});
if (containsRankings[0][1] === 0) {
thread.postSystemMessage('No matching category');
thread.postSystemMessage("No matching category");
return;
}

View File

@ -2,10 +2,10 @@ const utils = require("../utils");
const threads = require("../data/threads");
module.exports = ({ bot, knex, config, commands }) => {
commands.addInboxServerCommand('newthread', '<userId:userId>', async (msg, args, thread) => {
commands.addInboxServerCommand("newthread", "<userId:userId>", async (msg, args, thread) => {
const user = bot.users.get(args.userId);
if (! user) {
utils.postSystemMessageWithFallback(msg.channel, thread, 'User not found!');
utils.postSystemMessageWithFallback(msg.channel, thread, "User not found!");
return;
}
@ -18,7 +18,7 @@ module.exports = ({ bot, knex, config, commands }) => {
const createdThread = await threads.createNewThreadForUser(user, {
quiet: true,
ignoreRequirements: true,
source: 'command',
source: "command",
});
createdThread.postSystemMessage(`Thread was opened by ${msg.author.username}#${msg.author.discriminator}`);

View File

@ -1,71 +1,71 @@
const attachments = require("../data/attachments");
const utils = require('../utils');
const config = require('../cfg');
const Thread = require('../data/Thread');
const utils = require("../utils");
const config = require("../cfg");
const Thread = require("../data/Thread");
module.exports = ({ bot, knex, config, commands }) => {
// Mods can reply to modmail threads using !r or !reply
// These messages get relayed back to the DM thread between the bot and the user
commands.addInboxThreadCommand('reply', '[text$]', async (msg, args, thread) => {
commands.addInboxThreadCommand("reply", "[text$]", async (msg, args, thread) => {
if (! args.text && msg.attachments.length === 0) {
utils.postError(msg.channel, 'Text or attachment required');
utils.postError(msg.channel, "Text or attachment required");
return;
}
const replied = await thread.replyToUser(msg.member, args.text || '', msg.attachments, false);
const replied = await thread.replyToUser(msg.member, args.text || "", msg.attachments, false);
if (replied) msg.delete();
}, {
aliases: ['r']
aliases: ["r"]
});
// Anonymous replies only show the role, not the username
commands.addInboxThreadCommand('anonreply', '[text$]', async (msg, args, thread) => {
commands.addInboxThreadCommand("anonreply", "[text$]", async (msg, args, thread) => {
if (! args.text && msg.attachments.length === 0) {
utils.postError(msg.channel, 'Text or attachment required');
utils.postError(msg.channel, "Text or attachment required");
return;
}
const replied = await thread.replyToUser(msg.member, args.text || '', msg.attachments, true);
const replied = await thread.replyToUser(msg.member, args.text || "", msg.attachments, true);
if (replied) msg.delete();
}, {
aliases: ['ar']
aliases: ["ar"]
});
if (config.allowStaffEdit) {
commands.addInboxThreadCommand('edit', '<messageNumber:number> <text:string$>', async (msg, args, thread) => {
commands.addInboxThreadCommand("edit", "<messageNumber:number> <text:string$>", async (msg, args, thread) => {
const threadMessage = await thread.findThreadMessageByMessageNumber(args.messageNumber);
if (! threadMessage) {
utils.postError(msg.channel, 'Unknown message number');
utils.postError(msg.channel, "Unknown message number");
return;
}
if (threadMessage.user_id !== msg.author.id) {
utils.postError(msg.channel, 'You can only edit your own replies');
utils.postError(msg.channel, "You can only edit your own replies");
return;
}
await thread.editStaffReply(msg.member, threadMessage, args.text)
}, {
aliases: ['e']
aliases: ["e"]
});
}
if (config.allowStaffDelete) {
commands.addInboxThreadCommand('delete', '<messageNumber:number>', async (msg, args, thread) => {
commands.addInboxThreadCommand("delete", "<messageNumber:number>", async (msg, args, thread) => {
const threadMessage = await thread.findThreadMessageByMessageNumber(args.messageNumber);
if (! threadMessage) {
utils.postError(msg.channel, 'Unknown message number');
utils.postError(msg.channel, "Unknown message number");
return;
}
if (threadMessage.user_id !== msg.author.id) {
utils.postError(msg.channel, 'You can only delete your own replies');
utils.postError(msg.channel, "You can only delete your own replies");
return;
}
await thread.deleteStaffReply(msg.member, threadMessage);
}, {
aliases: ['d']
aliases: ["d"]
});
}
};

View File

@ -1,11 +1,11 @@
const threads = require('../data/threads');
const snippets = require('../data/snippets');
const config = require('../cfg');
const utils = require('../utils');
const { parseArguments } = require('knub-command-manager');
const threads = require("../data/threads");
const snippets = require("../data/snippets");
const config = require("../cfg");
const utils = require("../utils");
const { parseArguments } = require("knub-command-manager");
const whitespaceRegex = /\s/;
const quoteChars = ["'", '"'];
const quoteChars = ["'", "\""];
module.exports = ({ bot, knex, config, commands }) => {
/**
@ -21,13 +21,13 @@ module.exports = ({ bot, knex, config, commands }) => {
const index = parseInt(match.slice(1, -1), 10) - 1;
return (args[index] != null ? args[index] : match);
})
.replace(/\\{/g, '{');
.replace(/\\{/g, "{");
}
/**
* When a staff member uses a snippet (snippet prefix + trigger word), find the snippet and post it as a reply in the thread
*/
bot.on('messageCreate', async msg => {
bot.on("messageCreate", async msg => {
if (! utils.messageIsOnInboxServer(msg)) return;
if (! utils.isStaff(msg.member)) return;
@ -75,7 +75,7 @@ module.exports = ({ bot, knex, config, commands }) => {
});
// Show or add a snippet
commands.addInboxServerCommand('snippet', '<trigger> [text$]', async (msg, args, thread) => {
commands.addInboxServerCommand("snippet", "<trigger> [text$]", async (msg, args, thread) => {
const snippet = await snippets.get(args.trigger);
if (snippet) {
@ -97,10 +97,10 @@ module.exports = ({ bot, knex, config, commands }) => {
}
}
}, {
aliases: ['s']
aliases: ["s"]
});
commands.addInboxServerCommand('delete_snippet', '<trigger>', async (msg, args, thread) => {
commands.addInboxServerCommand("delete_snippet", "<trigger>", async (msg, args, thread) => {
const snippet = await snippets.get(args.trigger);
if (! snippet) {
utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`);
@ -110,10 +110,10 @@ module.exports = ({ bot, knex, config, commands }) => {
await snippets.del(args.trigger);
utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" deleted!`);
}, {
aliases: ['ds']
aliases: ["ds"]
});
commands.addInboxServerCommand('edit_snippet', '<trigger> [text$]', async (msg, args, thread) => {
commands.addInboxServerCommand("edit_snippet", "<trigger> [text$]", async (msg, args, thread) => {
const snippet = await snippets.get(args.trigger);
if (! snippet) {
utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" doesn't exist!`);
@ -125,14 +125,14 @@ module.exports = ({ bot, knex, config, commands }) => {
utils.postSystemMessageWithFallback(msg.channel, thread, `Snippet "${args.trigger}" edited!`);
}, {
aliases: ['es']
aliases: ["es"]
});
commands.addInboxServerCommand('snippets', [], async (msg, args, thread) => {
commands.addInboxServerCommand("snippets", [], async (msg, args, thread) => {
const allSnippets = await snippets.all();
const triggers = allSnippets.map(s => s.trigger);
triggers.sort();
utils.postSystemMessageWithFallback(msg.channel, thread, `Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(', ')}`);
utils.postSystemMessageWithFallback(msg.channel, thread, `Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(", ")}`);
});
};

View File

@ -1,9 +1,9 @@
const moment = require('moment');
const moment = require("moment");
const threads = require("../data/threads");
const utils = require('../utils');
const config = require('../cfg');
const utils = require("../utils");
const config = require("../cfg");
const {THREAD_STATUS} = require('../data/constants');
const {THREAD_STATUS} = require("../data/constants");
module.exports = ({ bot, knex, config, commands }) => {
// Check for threads that are scheduled to be suspended and suspend them
@ -29,20 +29,20 @@ module.exports = ({ bot, knex, config, commands }) => {
scheduledSuspendLoop();
commands.addInboxThreadCommand('suspend cancel', [], async (msg, args, thread) => {
commands.addInboxThreadCommand("suspend cancel", [], async (msg, args, thread) => {
// Cancel timed suspend
if (thread.scheduled_suspend_at) {
await thread.cancelScheduledSuspend();
thread.postSystemMessage(`Cancelled scheduled suspension`);
thread.postSystemMessage("Cancelled scheduled suspension");
} else {
thread.postSystemMessage(`Thread is not scheduled to be suspended`);
thread.postSystemMessage("Thread is not scheduled to be suspended");
}
});
commands.addInboxThreadCommand('suspend', '[delay:delay]', async (msg, args, thread) => {
commands.addInboxThreadCommand("suspend", "[delay:delay]", async (msg, args, thread) => {
if (args.delay) {
const suspendAt = moment.utc().add(args.delay, 'ms');
await thread.scheduleSuspend(suspendAt.format('YYYY-MM-DD HH:mm:ss'), msg.author);
const suspendAt = moment.utc().add(args.delay, "ms");
await thread.scheduleSuspend(suspendAt.format("YYYY-MM-DD HH:mm:ss"), msg.author);
thread.postSystemMessage(`Thread will be suspended in ${utils.humanizeDelay(args.delay)}. Use \`${config.prefix}suspend cancel\` to cancel.`);
@ -50,18 +50,18 @@ module.exports = ({ bot, knex, config, commands }) => {
}
await thread.suspend();
thread.postSystemMessage(`**Thread suspended!** This thread will act as closed until unsuspended with \`!unsuspend\``);
thread.postSystemMessage("**Thread suspended!** This thread will act as closed until unsuspended with `!unsuspend`");
});
commands.addInboxServerCommand('unsuspend', [], async (msg, args, thread) => {
commands.addInboxServerCommand("unsuspend", [], async (msg, args, thread) => {
if (thread) {
thread.postSystemMessage(`Thread is not suspended`);
thread.postSystemMessage("Thread is not suspended");
return;
}
thread = await threads.findSuspendedThreadByChannelId(msg.channel.id);
if (! thread) {
msg.channel.createMessage(`Not in a thread`);
msg.channel.createMessage("Not in a thread");
return;
}
@ -72,6 +72,6 @@ module.exports = ({ bot, knex, config, commands }) => {
}
await thread.unsuspend();
thread.postSystemMessage(`**Thread unsuspended!**`);
thread.postSystemMessage("**Thread unsuspended!**");
});
};

View File

@ -1,4 +1,4 @@
const config = require('../cfg');
const config = require("../cfg");
const threads = require("../data/threads");
const Eris = require("eris");

View File

@ -1,18 +1,18 @@
const path = require('path');
const fs = require('fs');
const {promisify} = require('util');
const path = require("path");
const fs = require("fs");
const {promisify} = require("util");
const utils = require("../utils");
const updates = require('../data/updates');
const config = require('../cfg');
const updates = require("../data/updates");
const config = require("../cfg");
const access = promisify(fs.access);
const readFile = promisify(fs.readFile);
const GIT_DIR = path.join(__dirname, '..', '..', '.git');
const GIT_DIR = path.join(__dirname, "..", "..", ".git");
module.exports = ({ bot, knex, config, commands }) => {
commands.addInboxServerCommand('version', [], async (msg, args, thread) => {
const packageJson = require('../../package.json');
commands.addInboxServerCommand("version", [], async (msg, args, thread) => {
const packageJson = require("../../package.json");
const packageVersion = packageJson.version;
let response = `Modmail v${packageVersion}`;
@ -27,12 +27,12 @@ module.exports = ({ bot, knex, config, commands }) => {
if (isGit) {
let commitHash;
const HEAD = await readFile(path.join(GIT_DIR, 'HEAD'), {encoding: 'utf8'});
const HEAD = await readFile(path.join(GIT_DIR, "HEAD"), {encoding: "utf8"});
if (HEAD.startsWith('ref:')) {
if (HEAD.startsWith("ref:")) {
// Branch
const ref = HEAD.match(/^ref: (.*)$/m)[1];
commitHash = (await readFile(path.join(GIT_DIR, ref), {encoding: 'utf8'})).trim();
commitHash = (await readFile(path.join(GIT_DIR, ref), {encoding: "utf8"})).trim();
} else {
// Detached head
commitHash = HEAD.trim();

View File

@ -1,18 +1,18 @@
const http = require('http');
const mime = require('mime');
const url = require('url');
const fs = require('fs');
const qs = require('querystring');
const moment = require('moment');
const config = require('../cfg');
const threads = require('../data/threads');
const attachments = require('../data/attachments');
const http = require("http");
const mime = require("mime");
const url = require("url");
const fs = require("fs");
const qs = require("querystring");
const moment = require("moment");
const config = require("../cfg");
const threads = require("../data/threads");
const attachments = require("../data/attachments");
const {THREAD_MESSAGE_TYPE} = require('../data/constants');
const {THREAD_MESSAGE_TYPE} = require("../data/constants");
function notfound(res) {
res.statusCode = 404;
res.end('Page Not Found');
res.end("Page Not Found");
}
async function serveLogs(req, res, pathParts, query) {
@ -41,7 +41,7 @@ async function serveLogs(req, res, pathParts, query) {
return message.body;
}
let line = `[${moment.utc(message.created_at).format('YYYY-MM-DD HH:mm:ss')}]`;
let line = `[${moment.utc(message.created_at).format("YYYY-MM-DD HH:mm:ss")}]`;
if (query.verbose) {
if (message.dm_channel_id) {
@ -72,12 +72,12 @@ async function serveLogs(req, res, pathParts, query) {
return line;
});
const openedAt = moment(thread.created_at).format('YYYY-MM-DD HH:mm:ss');
const openedAt = moment(thread.created_at).format("YYYY-MM-DD HH:mm:ss");
const header = `# Modmail thread with ${thread.user_name} (${thread.user_id}) started at ${openedAt}. All times are in UTC+0.`;
const fullResponse = header + '\n\n' + lines.join('\n');
const fullResponse = header + "\n\n" + lines.join("\n");
res.setHeader('Content-Type', 'text/plain; charset=UTF-8');
res.setHeader("Content-Type", "text/plain; charset=UTF-8");
res.end(fullResponse);
}
@ -92,11 +92,11 @@ function serveAttachments(req, res, pathParts) {
fs.access(attachmentPath, (err) => {
if (err) return notfound(res);
const filenameParts = desiredFilename.split('.');
const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : 'bin');
const filenameParts = desiredFilename.split(".");
const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : "bin");
const fileMime = mime.getType(ext);
res.setHeader('Content-Type', fileMime);
res.setHeader("Content-Type", fileMime);
const read = fs.createReadStream(attachmentPath);
read.pipe(res);
@ -106,20 +106,20 @@ function serveAttachments(req, res, pathParts) {
module.exports = () => {
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(`http://${req.url}`);
const pathParts = parsedUrl.pathname.split('/').filter(v => v !== '');
const pathParts = parsedUrl.pathname.split("/").filter(v => v !== "");
const query = qs.parse(parsedUrl.query);
if (parsedUrl.pathname.startsWith('/logs/')) {
if (parsedUrl.pathname.startsWith("/logs/")) {
serveLogs(req, res, pathParts, query);
} else if (parsedUrl.pathname.startsWith('/attachments/')) {
} else if (parsedUrl.pathname.startsWith("/attachments/")) {
serveAttachments(req, res, pathParts, query);
} else {
notfound(res);
}
});
server.on('error', err => {
console.log('[WARN] Web server error:', err.message);
server.on("error", err => {
console.log("[WARN] Web server error:", err.message);
});
server.listen(config.port);

View File

@ -1,6 +1,6 @@
const attachments = require('./data/attachments');
const { beforeNewThread } = require('./hooks/beforeNewThread');
const formats = require('./formatters');
const attachments = require("./data/attachments");
const { beforeNewThread } = require("./hooks/beforeNewThread");
const formats = require("./formatters");
module.exports = {
getPluginAPI({ bot, knex, config, commands }) {

View File

@ -1,9 +1,9 @@
const Eris = require('eris');
const bot = require('./bot');
const moment = require('moment');
const humanizeDuration = require('humanize-duration');
const publicIp = require('public-ip');
const config = require('./cfg');
const Eris = require("eris");
const bot = require("./bot");
const moment = require("moment");
const humanizeDuration = require("humanize-duration");
const publicIp = require("public-ip");
const config = require("./cfg");
class BotError extends Error {}
@ -18,7 +18,7 @@ let logChannel = null;
*/
function getInboxGuild() {
if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.mailGuildId);
if (! inboxGuild) throw new BotError('The bot is not on the modmail (inbox) server!');
if (! inboxGuild) throw new BotError("The bot is not on the modmail (inbox) server!");
return inboxGuild;
}
@ -32,9 +32,9 @@ function getMainGuilds() {
if (mainGuilds.length !== config.mainGuildId.length) {
if (config.mainGuildId.length === 1) {
console.warn(`[WARN] The bot hasn't joined the main guild!`);
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!`);
console.warn("[WARN] The bot hasn't joined one or more main guilds!");
}
}
@ -50,11 +50,11 @@ function getLogChannel() {
const logChannel = inboxGuild.channels.get(config.logChannelId);
if (! logChannel) {
throw new BotError('Log channel (logChannelId) not found!');
throw new BotError("Log channel (logChannelId) not found!");
}
if (! (logChannel instanceof Eris.TextChannel)) {
throw new BotError('Make sure the logChannelId option is set to a text channel!');
throw new BotError("Make sure the logChannelId option is set to a text channel!");
}
return logChannel;
@ -155,7 +155,7 @@ function getUserMention(str) {
* @returns {String}
*/
function getTimestamp(...momentArgs) {
return moment.utc(...momentArgs).format('HH:mm');
return moment.utc(...momentArgs).format("HH:mm");
}
/**
@ -164,7 +164,7 @@ function getTimestamp(...momentArgs) {
* @returns {String}
*/
function disableLinkPreviews(str) {
return str.replace(/(^|[^<])(https?:\/\/\S+)/ig, '$1<$2>');
return str.replace(/(^|[^<])(https?:\/\/\S+)/ig, "$1<$2>");
}
/**
@ -172,7 +172,7 @@ function disableLinkPreviews(str) {
* @param {String} path
* @returns {Promise<String>}
*/
async function getSelfUrl(path = '') {
async function getSelfUrl(path = "") {
if (config.url) {
return `${config.url}/${path}`;
} else {
@ -216,9 +216,9 @@ function chunk(items, chunkSize) {
*/
function trimAll(str) {
return str
.split('\n')
.split("\n")
.map(str => str.trim())
.join('\n');
.join("\n");
}
const delayStringRegex = /^([0-9]+)(?:([dhms])[a-z]*)?/i;
@ -234,17 +234,17 @@ function convertDelayStringToMS(str) {
str = str.trim();
while (str !== '' && (match = str.match(delayStringRegex)) !== null) {
if (match[2] === 'd') ms += match[1] * 1000 * 60 * 60 * 24;
else if (match[2] === 'h') ms += match[1] * 1000 * 60 * 60;
else if (match[2] === 's') ms += match[1] * 1000;
else if (match[2] === 'm' || ! match[2]) ms += match[1] * 1000 * 60;
while (str !== "" && (match = str.match(delayStringRegex)) !== null) {
if (match[2] === "d") ms += match[1] * 1000 * 60 * 60 * 24;
else if (match[2] === "h") ms += match[1] * 1000 * 60 * 60;
else if (match[2] === "s") ms += match[1] * 1000;
else if (match[2] === "m" || ! match[2]) ms += match[1] * 1000 * 60;
str = str.slice(match[0].length);
}
// Invalid delay string
if (str !== '') {
if (str !== "") {
return null;
}
@ -256,11 +256,11 @@ function getInboxMention() {
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 if (role === "here") mentions.push("@here");
else if (role === "everyone") mentions.push("@everyone");
else mentions.push(`<@&${role}>`);
}
return mentions.join(' ') + ' ';
return mentions.join(" ") + " ";
}
function postSystemMessageWithFallback(channel, thread, text) {
@ -286,7 +286,7 @@ function setDataModelProps(target, props) {
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');
target[prop] = moment.utc(props[prop]).format("YYYY-MM-DD HH:mm:ss");
}
} else {
target[prop] = props[prop];
@ -299,11 +299,11 @@ function isSnowflake(str) {
return str && snowflakeRegex.test(str);
}
const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: ' and '}, opts));
const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: " and "}, opts));
const markdownCharsRegex = /([\\_*|`~])/g;
function escapeMarkdown(str) {
return str.replace(markdownCharsRegex, '\\$1');
return str.replace(markdownCharsRegex, "\\$1");
}
function disableCodeBlocks(str) {
@ -314,7 +314,7 @@ function disableCodeBlocks(str) {
*
*/
function readMultilineConfigValue(str) {
return Array.isArray(str) ? str.join('\n') : str;
return Array.isArray(str) ? str.join("\n") : str;
}
module.exports = {