Refactor large parts of the bot
parent
8e388f93bb
commit
bb6d8e5dbf
|
@ -3,7 +3,10 @@
|
||||||
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 8,
|
"ecmaVersion": 8,
|
||||||
"sourceType": "module"
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"experimentalObjectRestSpread": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
@ -13,10 +13,11 @@ exports.up = async function(knex, Promise) {
|
||||||
table.increments('id');
|
table.increments('id');
|
||||||
table.string('thread_id', 36).notNullable().index().references('id').inTable('threads').onDelete('CASCADE');
|
table.string('thread_id', 36).notNullable().index().references('id').inTable('threads').onDelete('CASCADE');
|
||||||
table.integer('message_type').unsigned().notNullable();
|
table.integer('message_type').unsigned().notNullable();
|
||||||
table.bigInteger('user_id').unsigned().notNullable();
|
table.bigInteger('user_id').unsigned().nullable();
|
||||||
table.string('user_name', 128).notNullable();
|
table.string('user_name', 128).notNullable();
|
||||||
table.text('body').notNullable();
|
table.text('body').notNullable();
|
||||||
table.bigInteger('original_message_id').unsigned().nullable();
|
table.integer('is_anonymous').unsigned().notNullable();
|
||||||
|
table.bigInteger('original_message_id').unsigned().nullable().unique();
|
||||||
table.dateTime('created_at').notNullable().index();
|
table.dateTime('created_at').notNullable().index();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,8 @@ const defaultConfig = {
|
||||||
"logDir": path.join(__dirname, '..', 'logs'),
|
"logDir": path.join(__dirname, '..', 'logs'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const required = ['token', 'mailGuildId', 'mainGuildId', 'logChannelId'];
|
||||||
|
|
||||||
const finalConfig = Object.assign({}, defaultConfig);
|
const finalConfig = Object.assign({}, defaultConfig);
|
||||||
|
|
||||||
for (const [prop, value] of Object.entries(userConfig)) {
|
for (const [prop, value] of Object.entries(userConfig)) {
|
||||||
|
@ -67,7 +69,6 @@ Object.assign(finalConfig['knex'], {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const required = ['token', 'mailGuildId', 'mainGuildId', 'logChannelId'];
|
|
||||||
for (const opt of required) {
|
for (const opt of required) {
|
||||||
if (! finalConfig[opt]) {
|
if (! finalConfig[opt]) {
|
||||||
console.error(`Missing required config.json value: ${opt}`);
|
console.error(`Missing required config.json value: ${opt}`);
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* @property {String} trigger
|
||||||
|
* @property {String} body
|
||||||
|
* @property {Number} is_anonymous
|
||||||
|
* @property {String} created_by
|
||||||
|
* @property {String} created_at
|
||||||
|
*/
|
||||||
|
class Snippet {
|
||||||
|
constructor(props) {
|
||||||
|
Object.assign(this, props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Snippet;
|
|
@ -0,0 +1,190 @@
|
||||||
|
const bot = require('../bot');
|
||||||
|
const knex = require('../knex');
|
||||||
|
const utils = require('../utils');
|
||||||
|
const attachments = require('./attachments');
|
||||||
|
|
||||||
|
const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require('./constants');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property {String} id
|
||||||
|
* @property {Number} status
|
||||||
|
* @property {String} user_id
|
||||||
|
* @property {String} user_name
|
||||||
|
* @property {String} channel_id
|
||||||
|
* @property {String} created_at
|
||||||
|
* @property {Boolean} _wasCreated
|
||||||
|
*/
|
||||||
|
class Thread {
|
||||||
|
constructor(props) {
|
||||||
|
Object.assign(this, {_wasCreated: false}, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Eris.Member} moderator
|
||||||
|
* @param {String} text
|
||||||
|
* @param {Eris.Attachment[]} replyAttachments
|
||||||
|
* @param {Boolean} isAnonymous
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async replyToUser(moderator, text, replyAttachments = [], isAnonymous = false) {
|
||||||
|
// Try to open a DM channel with the user
|
||||||
|
const dmChannel = await bot.getDMChannel(this.user_id);
|
||||||
|
if (! dmChannel) {
|
||||||
|
const channel = bot.getChannel(this.channel_id);
|
||||||
|
if (channel) {
|
||||||
|
channel.createMessage('Could not send reply: couldn\'t open DM channel with user');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Username to reply with
|
||||||
|
let modUsername, logModUsername;
|
||||||
|
const mainRole = utils.getMainRole(moderator);
|
||||||
|
|
||||||
|
if (isAnonymous) {
|
||||||
|
modUsername = (mainRole ? mainRole.name : 'Moderator');
|
||||||
|
logModUsername = `(Anonymous) (${moderator.user.username}) ${mainRole ? mainRole.name : 'Moderator'}`;
|
||||||
|
} else {
|
||||||
|
const name = (config.useNicknames ? moderator.nick || moderator.user.username : moderator.user.username);
|
||||||
|
modUsername = (mainRole ? `(${mainRole.name}) ${name}` : name);
|
||||||
|
logModUsername = modUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the reply message
|
||||||
|
let dmContent = `**${modUsername}:** ${text}`;
|
||||||
|
let threadContent = `**${logModUsername}:** ${text}`;
|
||||||
|
let logContent = text;
|
||||||
|
|
||||||
|
let attachmentFile = null;
|
||||||
|
let attachmentUrl = null;
|
||||||
|
|
||||||
|
// Prepare attachments, if any
|
||||||
|
if (replyAttachments.length > 0) {
|
||||||
|
fs.readFile(attachments.getPath(replyAttachments[0].id), async (err, data) => {
|
||||||
|
attachmentFile = {file: data, name: replyAttachments[0].filename};
|
||||||
|
attachmentUrl = await attachments.getUrl(replyAttachments[0].id, replyAttachments[0].filename);
|
||||||
|
|
||||||
|
threadContent += `\n\n**Attachment:** ${attachmentUrl}`;
|
||||||
|
logContent += `\n\n**Attachment:** ${attachmentUrl}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the reply DM
|
||||||
|
dmChannel.createMessage(dmContent, attachmentFile);
|
||||||
|
|
||||||
|
// Send the reply to the modmail thread
|
||||||
|
const originalMessage = await this.postToThreadChannel(threadContent);
|
||||||
|
|
||||||
|
// Add the message to the database
|
||||||
|
await this.addThreadMessageToDB({
|
||||||
|
message_type: THREAD_MESSAGE_TYPE.TO_USER,
|
||||||
|
user_id: moderator.id,
|
||||||
|
user_name: logModUsername,
|
||||||
|
body: logContent,
|
||||||
|
original_message_id: originalMessage.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Eris.Message} msg
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async receiveUserReply(msg) {
|
||||||
|
const timestamp = utils.getTimestamp();
|
||||||
|
|
||||||
|
let threadContent = `[${timestamp}] « **${msg.author.username}#${msg.author.discriminator}:** ${msg.content}`;
|
||||||
|
let logContent = msg.content;
|
||||||
|
let finalThreadContent;
|
||||||
|
let attachmentSavePromise;
|
||||||
|
|
||||||
|
if (msg.attachments.length) {
|
||||||
|
attachmentSavePromise = attachments.saveAttachmentsInMessage(msg);
|
||||||
|
const formattedAttachments = await Promise.all(msg.attachments.map(utils.formatAttachment));
|
||||||
|
const attachmentMsg = `\n\n` + formattedAttachments.reduce((str, formatted) => str + `\n\n${formatted}`);
|
||||||
|
|
||||||
|
finalThreadContent = threadContent + attachmentMsg;
|
||||||
|
threadContent += '\n\n*Attachments pending...*';
|
||||||
|
logContent += attachmentMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdMessage = await this.postToThreadChannel(threadContent);
|
||||||
|
await this.addThreadMessageToDB({
|
||||||
|
message_type: THREAD_MESSAGE_TYPE.FROM_USER,
|
||||||
|
user_id: this.user_id,
|
||||||
|
user_name: `${msg.author.username}#${msg.author.discriminator}`,
|
||||||
|
body: logContent,
|
||||||
|
original_message_id: msg.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (msg.attachments.length) {
|
||||||
|
await attachmentSavePromise;
|
||||||
|
await createdMessage.edit(finalThreadContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} text
|
||||||
|
* @param {Eris.MessageFile} file
|
||||||
|
* @returns {Promise<Eris.Message>}
|
||||||
|
*/
|
||||||
|
async postToThreadChannel(text, file = null) {
|
||||||
|
const channel = bot.getChannel(this.channel_id);
|
||||||
|
return channel.createMessage(text, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} text
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async postSystemMessage(text) {
|
||||||
|
const msg = await this.postToThreadChannel(text);
|
||||||
|
await this.addThreadMessageToDB({
|
||||||
|
message_type: THREAD_MESSAGE_TYPE.SYSTEM,
|
||||||
|
user_id: null,
|
||||||
|
user_name: '',
|
||||||
|
body: text,
|
||||||
|
original_message_id: msg.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} data
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async addThreadMessageToDB(data) {
|
||||||
|
await knex('thread_messages').insert({
|
||||||
|
thread_id: this.id,
|
||||||
|
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
...data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async close() {
|
||||||
|
await this.postToThreadChannel('Closing thread...');
|
||||||
|
|
||||||
|
// Update DB status
|
||||||
|
await knex('threads')
|
||||||
|
.where('id', this.id)
|
||||||
|
.update({
|
||||||
|
status: THREAD_STATUS.CLOSED
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete channel
|
||||||
|
const channel = bot.getChannel(this.channel_id);
|
||||||
|
if (channel) {
|
||||||
|
channel.delete('Thread closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<String>}
|
||||||
|
*/
|
||||||
|
getLogUrl() {
|
||||||
|
return utils.getSelfUrl(`logs/${this.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Thread;
|
|
@ -11,15 +11,17 @@ async function isBlocked(userId) {
|
||||||
.where('user_id', userId)
|
.where('user_id', userId)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
return !!row;
|
return !! row;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocks the given userId
|
* Blocks the given userId
|
||||||
* @param {String} userId
|
* @param {String} userId
|
||||||
|
* @param {String} userName
|
||||||
|
* @param {String} blockedBy
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async function block(userId, userName = '', blockedBy = 0) {
|
async function block(userId, userName = '', blockedBy = null) {
|
||||||
if (await isBlocked(userId)) return;
|
if (await isBlocked(userId)) return;
|
||||||
|
|
||||||
return knex('blocked_users')
|
return knex('blocked_users')
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
module.exports = {
|
||||||
|
THREAD_STATUS: {
|
||||||
|
OPEN: 1,
|
||||||
|
CLOSED: 2
|
||||||
|
},
|
||||||
|
|
||||||
|
THREAD_MESSAGE_TYPE: {
|
||||||
|
SYSTEM: 1,
|
||||||
|
CHAT: 2,
|
||||||
|
FROM_USER: 3,
|
||||||
|
TO_USER: 4,
|
||||||
|
LEGACY: 5
|
||||||
|
},
|
||||||
|
|
||||||
|
ACCIDENTAL_THREAD_MESSAGES: [
|
||||||
|
'ok',
|
||||||
|
'okay',
|
||||||
|
'thanks',
|
||||||
|
'ty',
|
||||||
|
'k',
|
||||||
|
'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'
|
||||||
|
],
|
||||||
|
};
|
|
@ -1,18 +1,6 @@
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const knex = require('../knex');
|
const knex = require('../knex');
|
||||||
|
const Snippet = require('./Snippet');
|
||||||
/**
|
|
||||||
* @property {String} trigger
|
|
||||||
* @property {String} body
|
|
||||||
* @property {Number} is_anonymous
|
|
||||||
* @property {String} created_by
|
|
||||||
* @property {String} created_at
|
|
||||||
*/
|
|
||||||
class Snippet {
|
|
||||||
constructor(props) {
|
|
||||||
Object.assign(this, props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} trigger
|
* @param {String} trigger
|
||||||
|
@ -46,7 +34,7 @@ async function addSnippet(trigger, body, isAnonymous = false, createdBy = 0) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} trigger
|
* @param {String} trigger
|
||||||
* @returns {Promise}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function deleteSnippet(trigger) {
|
async function deleteSnippet(trigger) {
|
||||||
return knex('snippets')
|
return knex('snippets')
|
||||||
|
@ -54,6 +42,9 @@ async function deleteSnippet(trigger) {
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<Snippet[]>}
|
||||||
|
*/
|
||||||
async function getAllSnippets() {
|
async function getAllSnippets() {
|
||||||
const snippets = await knex('snippets')
|
const snippets = await knex('snippets')
|
||||||
.select();
|
.select();
|
||||||
|
|
|
@ -2,115 +2,39 @@ const Eris = require('eris');
|
||||||
const transliterate = require('transliteration');
|
const transliterate = require('transliteration');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const uuid = require('uuid');
|
const uuid = require('uuid');
|
||||||
|
const humanizeDuration = require('humanize-duration');
|
||||||
|
|
||||||
const bot = require('../bot');
|
const bot = require('../bot');
|
||||||
const knex = require('../knex');
|
const knex = require('../knex');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
const utils = require('../utils');
|
||||||
|
|
||||||
const getUtils = () => require('../utils');
|
const Thread = require('./Thread');
|
||||||
|
const {THREAD_STATUS} = require('./constants');
|
||||||
// If the following messages would be used to start a thread, ignore it instead
|
|
||||||
// This is to prevent accidental threads from e.g. irrelevant replies after the thread was already closed
|
|
||||||
// or replies to the greeting message
|
|
||||||
const accidentalThreadMessages = [
|
|
||||||
'ok',
|
|
||||||
'okay',
|
|
||||||
'thanks',
|
|
||||||
'ty',
|
|
||||||
'k',
|
|
||||||
'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'
|
|
||||||
];
|
|
||||||
|
|
||||||
const THREAD_STATUS = {
|
|
||||||
OPEN: 1,
|
|
||||||
CLOSED: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
const THREAD_MESSAGE_TYPE = {
|
|
||||||
SYSTEM: 1,
|
|
||||||
CHAT: 2,
|
|
||||||
FROM_USER: 3,
|
|
||||||
TO_USER: 4,
|
|
||||||
LEGACY: 5
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property {Number} id
|
* @param {String} userId
|
||||||
* @property {Number} status
|
|
||||||
* @property {String} user_id
|
|
||||||
* @property {String} user_name
|
|
||||||
* @property {String} channel_id
|
|
||||||
* @property {String} created_at
|
|
||||||
* @property {Boolean} _wasCreated
|
|
||||||
*/
|
|
||||||
class Thread {
|
|
||||||
constructor(props) {
|
|
||||||
Object.assign(this, {_wasCreated: false}, props);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns information about the modmail thread channel for the given user. We can't return channel objects
|
|
||||||
* directly since they're not always available immediately after creation.
|
|
||||||
* @param {Eris.User} user
|
|
||||||
* @param {Boolean} allowCreate
|
|
||||||
* @returns {Promise<Thread>}
|
* @returns {Promise<Thread>}
|
||||||
*/
|
*/
|
||||||
async function getOpenThreadForUser(user, allowCreate = true, originalMessage = null) {
|
async function findOpenThreadByUserId(userId) {
|
||||||
// Attempt to find an open thread for this user
|
|
||||||
const thread = await knex('threads')
|
const thread = await knex('threads')
|
||||||
.where('user_id', user.id)
|
.where('user_id', userId)
|
||||||
.where('status', THREAD_STATUS.OPEN)
|
.where('status', THREAD_STATUS.OPEN)
|
||||||
.select();
|
.select();
|
||||||
|
|
||||||
if (thread) {
|
return (thread ? new Thread(thread) : null);
|
||||||
return new Thread(thread);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If no open thread was found, and we're not allowed to create one, just return null
|
/**
|
||||||
if (! allowCreate) {
|
* Creates a new modmail thread for the specified user
|
||||||
return null;
|
* @param {Eris.User} user
|
||||||
}
|
* @returns {Promise<Thread>}
|
||||||
|
* @throws {Error}
|
||||||
// No open thread was found, and we *are* allowed to create a new one, so let's do that
|
*/
|
||||||
|
async function createNewThreadForUser(user) {
|
||||||
// If the message's content matches any of the values in accidentalThreadMessages,
|
const existingThread = await findOpenThreadByUserId(user.id);
|
||||||
// and config.ignoreAccidentalThreads is enabled, ignore this thread
|
if (existingThread) {
|
||||||
if (config.ignoreAccidentalThreads && originalMessage && originalMessage.cleanContent) {
|
throw new Error('Attempted to create a new thread for a user with an existing open thread!');
|
||||||
const cleaned = originalMessage.cleanContent.replace(/[^a-z\s]/gi, '').toLowerCase().trim();
|
|
||||||
if (accidentalThreadMessages.includes(cleaned)) {
|
|
||||||
console.log('[NOTE] Skipping thread creation for message:', originalMessage.cleanContent);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the user's name+discrim for the thread channel's name
|
// Use the user's name+discrim for the thread channel's name
|
||||||
|
@ -126,7 +50,7 @@ async function getOpenThreadForUser(user, allowCreate = true, originalMessage =
|
||||||
// Attempt to create the inbox channel for this thread
|
// Attempt to create the inbox channel for this thread
|
||||||
let createdChannel;
|
let createdChannel;
|
||||||
try {
|
try {
|
||||||
createdChannel = await getUtils().getInboxGuild().createChannel(channelName);
|
createdChannel = await utils.getInboxGuild().createChannel(channelName);
|
||||||
if (config.newThreadCategoryId) {
|
if (config.newThreadCategoryId) {
|
||||||
// If a category id for new threads is specified, move the newly created channel there
|
// If a category id for new threads is specified, move the newly created channel there
|
||||||
bot.editChannel(createdChannel.id, {parentID: config.newThreadCategoryId});
|
bot.editChannel(createdChannel.id, {parentID: config.newThreadCategoryId});
|
||||||
|
@ -137,7 +61,7 @@ async function getOpenThreadForUser(user, allowCreate = true, originalMessage =
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the new thread in the database
|
// Save the new thread in the database
|
||||||
const newThreadId = await create({
|
const newThreadId = await createThreadInDB({
|
||||||
status: THREAD_STATUS.OPEN,
|
status: THREAD_STATUS.OPEN,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
user_name: `${user.username}#${user.discriminator}`,
|
user_name: `${user.username}#${user.discriminator}`,
|
||||||
|
@ -145,10 +69,28 @@ async function getOpenThreadForUser(user, allowCreate = true, originalMessage =
|
||||||
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss')
|
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss')
|
||||||
});
|
});
|
||||||
|
|
||||||
const newThreadObj = new Thread(newThread);
|
const newThread = await findById(newThreadId);
|
||||||
newThreadObj._wasCreated = true;
|
|
||||||
|
|
||||||
return newThreadObj;
|
// Post some info to the beginning of the new thread
|
||||||
|
const mainGuild = utils.getMainGuild();
|
||||||
|
const member = (mainGuild ? mainGuild.members.get(user.id) : null);
|
||||||
|
if (! member) console.log(`[INFO] Member ${user.id} not found in main guild ${config.mainGuildId}`);
|
||||||
|
|
||||||
|
let mainGuildNickname = null;
|
||||||
|
if (member && member.nick) mainGuildNickname = member.nick;
|
||||||
|
else if (member && member.user) mainGuildNickname = member.user.username;
|
||||||
|
else if (member == null) mainGuildNickname = 'NOT ON SERVER';
|
||||||
|
|
||||||
|
if (mainGuildNickname == null) mainGuildNickname = 'UNKNOWN';
|
||||||
|
|
||||||
|
const userLogCount = await getClosedThreadCountByUserId(user.id);
|
||||||
|
const accountAge = humanizeDuration(Date.now() - user.createdAt, {largest: 2});
|
||||||
|
const infoHeader = `ACCOUNT AGE **${accountAge}**, ID **${user.id}**, NICKNAME **${mainGuildNickname}**, LOGS **${userLogCount}**\n-------------------------------`;
|
||||||
|
|
||||||
|
await newThread.postSystemMessage(infoHeader);
|
||||||
|
|
||||||
|
// Return the thread
|
||||||
|
return newThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -156,32 +98,35 @@ async function getOpenThreadForUser(user, allowCreate = true, originalMessage =
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise<String>} The ID of the created thread
|
* @returns {Promise<String>} The ID of the created thread
|
||||||
*/
|
*/
|
||||||
async function create(data) {
|
async function createThreadInDB(data) {
|
||||||
const threadId = uuid.v4();
|
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});
|
const finalData = Object.assign({created_at: now, is_legacy: 0}, data, {id: threadId});
|
||||||
|
|
||||||
await knex('threads').insert(newThread);
|
await knex('threads').insert(finalData);
|
||||||
|
|
||||||
return threadId;
|
return threadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addThreadMessage(threadId, messageType, user, body) {
|
/**
|
||||||
return knex('thread_messages').insert({
|
* @param {String} id
|
||||||
thread_id: threadId,
|
* @returns {Promise<Thread>}
|
||||||
message_type: messageType,
|
*/
|
||||||
user_id: (user ? user.id : 0),
|
async function findById(id) {
|
||||||
user_name: (user ? `${user.username}#${user.discriminator}` : ''),
|
const row = await knex('threads')
|
||||||
body,
|
.where('id', id)
|
||||||
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss')
|
.first();
|
||||||
});
|
|
||||||
|
if (! row) return null;
|
||||||
|
|
||||||
|
return new Thread(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} channelId
|
* @param {String} channelId
|
||||||
* @returns {Promise<Thread>}
|
* @returns {Promise<Thread>}
|
||||||
*/
|
*/
|
||||||
async function getByChannelId(channelId) {
|
async function findByChannelId(channelId) {
|
||||||
const thread = await knex('threads')
|
const thread = await knex('threads')
|
||||||
.where('channel_id', channelId)
|
.where('channel_id', channelId)
|
||||||
.first();
|
.first();
|
||||||
|
@ -190,24 +135,34 @@ async function getByChannelId(channelId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the modmail thread for the given channel id
|
* @param {String} userId
|
||||||
* @param {String} channelId
|
* @returns {Promise<Thread[]>}
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
async function closeByChannelId(channelId) {
|
async function getClosedThreadsByUserId(userId) {
|
||||||
await knex('threads')
|
const threads = await knex('threads')
|
||||||
.where('channel_id', channelId)
|
.where('status', THREAD_STATUS.CLOSED)
|
||||||
.update({
|
.where('user_id', userId)
|
||||||
status: THREAD_STATUS.CLOSED
|
.select();
|
||||||
});
|
|
||||||
|
return threads.map(thread => new Thread(thread));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} 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'));
|
||||||
|
|
||||||
|
return parseInt(row.thread_count, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getOpenThreadForUser,
|
findOpenThreadByUserId,
|
||||||
getByChannelId,
|
findByChannelId,
|
||||||
closeByChannelId,
|
createNewThreadForUser,
|
||||||
create,
|
getClosedThreadsByUserId,
|
||||||
|
|
||||||
THREAD_STATUS,
|
|
||||||
THREAD_MESSAGE_TYPE,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,7 +36,7 @@ process.on('unhandledRejection', err => {
|
||||||
console.log('=== LEGACY DATA MIGRATION FINISHED ===');
|
console.log('=== LEGACY DATA MIGRATION FINISHED ===');
|
||||||
console.log('');
|
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('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, feel free to delete the following legacy files/directories:`);
|
console.log('Once you\'ve done that, feel free to delete the following legacy files/directories:');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('FILE: ' + path.resolve(relativeDbDir, 'threads.json'));
|
console.log('FILE: ' + path.resolve(relativeDbDir, 'threads.json'));
|
||||||
console.log('FILE: ' + path.resolve(relativeDbDir, 'blocked.json'));
|
console.log('FILE: ' + path.resolve(relativeDbDir, 'blocked.json'));
|
||||||
|
@ -47,5 +47,5 @@ process.on('unhandledRejection', err => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the bot
|
// Start the bot
|
||||||
// main.start();
|
main.start();
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -82,7 +82,7 @@ async function migrateOpenThreads() {
|
||||||
is_legacy: 1
|
is_legacy: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
return threads.create(newThread);
|
return threads.createThreadInDB(newThread);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
|
|
267
src/main.js
267
src/main.js
|
@ -1,7 +1,5 @@
|
||||||
const fs = require('fs');
|
|
||||||
const Eris = require('eris');
|
const Eris = require('eris');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const humanizeDuration = require('humanize-duration');
|
|
||||||
|
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const bot = require('./bot');
|
const bot = require('./bot');
|
||||||
|
@ -9,32 +7,72 @@ const Queue = require('./queue');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const blocked = require('./data/blocked');
|
const blocked = require('./data/blocked');
|
||||||
const threads = require('./data/threads');
|
const threads = require('./data/threads');
|
||||||
const attachments = require('./data/attachments');
|
|
||||||
const snippets = require('./data/snippets');
|
const snippets = require('./data/snippets');
|
||||||
const webserver = require('./webserver');
|
const webserver = require('./webserver');
|
||||||
const greeting = require('./greeting');
|
const greeting = require('./greeting');
|
||||||
|
const Thread = require('./data/Thread');
|
||||||
|
|
||||||
const messageQueue = new Queue();
|
const messageQueue = new Queue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback CommandHandlerCB
|
||||||
|
* @interface
|
||||||
|
* @param {Eris~Message} msg
|
||||||
|
* @param {Array} args
|
||||||
|
* @param {Thread} thread
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a command that can only be triggered on the inbox server.
|
||||||
|
* Command handlers added with this function also get the thread the message was posted in as a third argument, if any.
|
||||||
|
* @param {String} cmd
|
||||||
|
* @param {CommandHandlerCB} commandHandler
|
||||||
|
* @param {Eris~CommandOptions} opts
|
||||||
|
*/
|
||||||
|
function addInboxServerCommand(cmd, commandHandler, opts) {
|
||||||
|
bot.registerCommand(cmd, async (msg, args) => {
|
||||||
|
if (! messageIsOnInboxServer(msg)) return;
|
||||||
|
if (! isStaff(msg.member)) return;
|
||||||
|
|
||||||
|
const thread = await threads.findByChannelId(msg.channel.id);
|
||||||
|
commandHandler(msg, args, thread);
|
||||||
|
}, opts);
|
||||||
|
}
|
||||||
|
|
||||||
// Once the bot has connected, set the status/"playing" message
|
// Once the bot has connected, set the status/"playing" message
|
||||||
bot.on('ready', () => {
|
bot.on('ready', () => {
|
||||||
bot.editStatus(null, {name: config.status});
|
bot.editStatus(null, {name: config.status});
|
||||||
console.log('Bot started, listening to DMs');
|
console.log('Bot started, listening to DMs');
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the alwaysReply option is set to true, send all messages in modmail threads as replies, unless they start with a command prefix
|
// Handle moderator messages in thread channels
|
||||||
if (config.alwaysReply) {
|
bot.on('messageCreate', async msg => {
|
||||||
bot.on('messageCreate', msg => {
|
if (! utils.messageIsOnInboxServer(msg)) return;
|
||||||
if (! utils.messageIsOnInboxServer(msg)) return;
|
if (! utils.isStaff(msg)) return;
|
||||||
if (! utils.isStaff(msg)) return;
|
if (msg.author.bot) return;
|
||||||
if (msg.author.bot) return;
|
if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return;
|
||||||
if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return;
|
|
||||||
|
|
||||||
reply(msg, msg.content.trim(), config.alwaysReplyAnon || false);
|
const thread = await threads.findByChannelId(msg.channel.id);
|
||||||
});
|
if (! thread) return;
|
||||||
}
|
|
||||||
|
|
||||||
// "Bot was mentioned in #general-discussion"
|
if (config.alwaysReply) {
|
||||||
|
// AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
|
||||||
|
await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false);
|
||||||
|
msg.delete();
|
||||||
|
} else {
|
||||||
|
// Otherwise just save the messages as "chat" in the logs
|
||||||
|
thread.addThreadMessageToDB({
|
||||||
|
message_type: threads.THREAD_MESSAGE_TYPE.CHAT,
|
||||||
|
user_id: msg.author.id,
|
||||||
|
user_name: `${msg.author.username}#${msg.author.discriminator}`,
|
||||||
|
body: msg.content,
|
||||||
|
original_message_id: msg.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the bot is mentioned on the main server, post a log message about it
|
||||||
bot.on('messageCreate', async msg => {
|
bot.on('messageCreate', async msg => {
|
||||||
if (! utils.messageIsOnMainServer(msg)) return;
|
if (! utils.messageIsOnMainServer(msg)) return;
|
||||||
if (! msg.mentions.some(user => user.id === bot.user.id)) return;
|
if (! msg.mentions.some(user => user.id === bot.user.id)) return;
|
||||||
|
@ -58,79 +96,14 @@ bot.on('messageCreate', async msg => {
|
||||||
|
|
||||||
if (await blocked.isBlocked(msg.author.id)) return;
|
if (await blocked.isBlocked(msg.author.id)) return;
|
||||||
|
|
||||||
// Download and save copies of attachments in the background
|
|
||||||
const attachmentSavePromise = attachments.saveAttachmentsInMessage(msg);
|
|
||||||
|
|
||||||
let threadCreationFailed = false;
|
|
||||||
|
|
||||||
// Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
|
// Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
|
||||||
messageQueue.add(async () => {
|
messageQueue.add(async () => {
|
||||||
let thread;
|
let thread = await threads.findOpenThreadByUserId(msg.author.id);
|
||||||
|
|
||||||
// Find the corresponding modmail thread
|
|
||||||
try {
|
|
||||||
thread = await threads.getOpenThreadForUser(msg.author, true, msg);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
utils.postError(`
|
|
||||||
Modmail thread for ${msg.author.username}#${msg.author.discriminator} (${msg.author.id}) could not be created:
|
|
||||||
\`\`\`${e.message}\`\`\`
|
|
||||||
|
|
||||||
Here's what their message contained:
|
|
||||||
\`\`\`${msg.cleanContent}\`\`\``);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! thread) {
|
if (! thread) {
|
||||||
// If there's no thread returned, this message was probably ignored (e.g. due to a common word)
|
thread = await threads.createNewThreadForUser(msg.author, msg);
|
||||||
// TODO: Move that logic here instead?
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thread._wasCreated) {
|
thread.receiveUserReply(msg);
|
||||||
const mainGuild = utils.getMainGuild();
|
|
||||||
const member = (mainGuild ? mainGuild.members.get(msg.author.id) : null);
|
|
||||||
if (! member) console.log(`[INFO] Member ${msg.author.id} not found in main guild ${config.mainGuildId}`);
|
|
||||||
|
|
||||||
let mainGuildNickname = null;
|
|
||||||
if (member && member.nick) mainGuildNickname = member.nick;
|
|
||||||
else if (member && member.user) mainGuildNickname = member.user.username;
|
|
||||||
else if (member == null) mainGuildNickname = 'NOT ON SERVER';
|
|
||||||
|
|
||||||
if (mainGuildNickname == null) mainGuildNickname = 'UNKNOWN';
|
|
||||||
|
|
||||||
const userLogs = await logs.getLogsByUserId(msg.author.id);
|
|
||||||
const accountAge = humanizeDuration(Date.now() - msg.author.createdAt, {largest: 2});
|
|
||||||
const infoHeader = `ACCOUNT AGE **${accountAge}**, ID **${msg.author.id}**, NICKNAME **${mainGuildNickname}**, LOGS **${userLogs.length}**\n-------------------------------`;
|
|
||||||
|
|
||||||
await bot.createMessage(thread.channelId, infoHeader);
|
|
||||||
|
|
||||||
// Ping mods of the new thread
|
|
||||||
await bot.createMessage(thread.channelId, {
|
|
||||||
content: `@here New modmail thread (${msg.author.username}#${msg.author.discriminator})`,
|
|
||||||
disableEveryone: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send an automatic reply to the user informing them of the successfully created modmail thread
|
|
||||||
msg.channel.createMessage(config.responseMessage).catch(err => {
|
|
||||||
utils.postError(`There is an issue sending messages to ${msg.author.username}#${msg.author.discriminator} (${msg.author.id}); consider messaging manually`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = utils.getTimestamp();
|
|
||||||
const attachmentsPendingStr = '\n\n*Attachments pending...*';
|
|
||||||
|
|
||||||
let content = msg.content;
|
|
||||||
if (msg.attachments.length > 0) content += attachmentsPendingStr;
|
|
||||||
|
|
||||||
const createdMsg = await bot.createMessage(thread.channelId, `[${timestamp}] « **${msg.author.username}#${msg.author.discriminator}:** ${content}`);
|
|
||||||
|
|
||||||
if (msg.attachments.length > 0) {
|
|
||||||
await attachmentSavePromise;
|
|
||||||
const formattedAttachments = await Promise.all(msg.attachments.map(utils.formatAttachment));
|
|
||||||
const attachmentMsg = `\n\n` + formattedAttachments.reduce((str, formatted) => str + `\n\n${formatted}`);
|
|
||||||
createdMsg.edit(createdMsg.content.replace(attachmentsPendingStr, attachmentMsg));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -150,122 +123,39 @@ bot.on('messageUpdate', async (msg, oldMessage) => {
|
||||||
// Ignore bogus edit events with no changes
|
// Ignore bogus edit events with no changes
|
||||||
if (newContent.trim() === oldContent.trim()) return;
|
if (newContent.trim() === oldContent.trim()) return;
|
||||||
|
|
||||||
const thread = await threads.getOpenThreadForUser(msg.author);
|
const thread = await threads.createNewThreadForUser(msg.author);
|
||||||
if (! thread) return;
|
if (! thread) return;
|
||||||
|
|
||||||
const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);
|
const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);
|
||||||
bot.createMessage(thread.channelId, editMessage);
|
bot.createMessage(thread.channelId, editMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a reply to the modmail thread where `msg` was posted.
|
|
||||||
* @param {Eris.Message} msg
|
|
||||||
* @param {string} text
|
|
||||||
* @param {bool} anonymous
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function reply(msg, text, anonymous = false) {
|
|
||||||
const thread = await threads.getByChannelId(msg.channel.id);
|
|
||||||
if (! thread) return;
|
|
||||||
|
|
||||||
await attachments.saveAttachmentsInMessage(msg);
|
|
||||||
|
|
||||||
const dmChannel = await bot.getDMChannel(thread.userId);
|
|
||||||
|
|
||||||
let modUsername, logModUsername;
|
|
||||||
const mainRole = utils.getMainRole(msg.member);
|
|
||||||
|
|
||||||
if (anonymous) {
|
|
||||||
modUsername = (mainRole ? mainRole.name : 'Moderator');
|
|
||||||
logModUsername = `(Anonymous) (${msg.author.username}) ${mainRole ? mainRole.name : 'Moderator'}`;
|
|
||||||
} else {
|
|
||||||
const name = (config.useNicknames ? msg.member.nick || msg.author.username : msg.author.username);
|
|
||||||
modUsername = (mainRole ? `(${mainRole.name}) ${name}` : name);
|
|
||||||
logModUsername = modUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = `**${modUsername}:** ${text}`;
|
|
||||||
let logContent = `**${logModUsername}:** ${text}`;
|
|
||||||
|
|
||||||
async function sendMessage(file, attachmentUrl) {
|
|
||||||
try {
|
|
||||||
await dmChannel.createMessage(content, file);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.resp && e.resp.statusCode === 403) {
|
|
||||||
msg.channel.createMessage(`Could not send reply; the user has likely left the server or blocked the bot`);
|
|
||||||
} else if (e.resp) {
|
|
||||||
msg.channel.createMessage(`Could not send reply; error code ${e.resp.statusCode}`);
|
|
||||||
} else {
|
|
||||||
msg.channel.createMessage(`Could not send reply: ${e.toString()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachmentUrl) {
|
|
||||||
content += `\n\n**Attachment:** ${attachmentUrl}`;
|
|
||||||
logContent += `\n\n**Attachment:** ${attachmentUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the message in the modmail thread as well
|
|
||||||
msg.channel.createMessage(`[${utils.getTimestamp()}] » ${logContent}`);
|
|
||||||
msg.delete();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (msg.attachments.length > 0) {
|
|
||||||
// If the reply has an attachment, relay it as is
|
|
||||||
fs.readFile(attachments.getPath(msg.attachments[0].id), async (err, data) => {
|
|
||||||
const file = {file: data, name: msg.attachments[0].filename};
|
|
||||||
|
|
||||||
const attachmentUrl = await attachments.getUrl(msg.attachments[0].id, msg.attachments[0].filename);
|
|
||||||
sendMessage(file, attachmentUrl);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Otherwise just send the message regularly
|
|
||||||
sendMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mods can reply to modmail threads using !r or !reply
|
// 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
|
// These messages get relayed back to the DM thread between the bot and the user
|
||||||
utils.addInboxCommand('reply', (msg, args) => {
|
addInboxServerCommand('reply', (msg, args, thread) => {
|
||||||
|
if (! thread) return;
|
||||||
const text = args.join(' ').trim();
|
const text = args.join(' ').trim();
|
||||||
reply(msg, text, false);
|
thread.replyToUser(msg.member, text, msg.attachments, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.registerCommandAlias('r', 'reply');
|
bot.registerCommandAlias('r', 'reply');
|
||||||
|
|
||||||
// Anonymous replies only show the role, not the username
|
// Anonymous replies only show the role, not the username
|
||||||
utils.addInboxCommand('anonreply', (msg, args) => {
|
addInboxServerCommand('anonreply', (msg, args, thread) => {
|
||||||
|
if (! thread) return;
|
||||||
const text = args.join(' ').trim();
|
const text = args.join(' ').trim();
|
||||||
reply(msg, text, true);
|
thread.replyToUser(msg.member, text, msg.attachments, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.registerCommandAlias('ar', 'anonreply');
|
bot.registerCommandAlias('ar', 'anonreply');
|
||||||
|
|
||||||
// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
|
// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
|
||||||
utils.addInboxCommand('close', async (msg, args, thread) => {
|
addInboxServerCommand('close', async (msg, args, thread) => {
|
||||||
if (! thread) return;
|
if (! thread) return;
|
||||||
|
thread.close();
|
||||||
await msg.channel.createMessage('Saving logs and closing channel...');
|
|
||||||
|
|
||||||
const logMessages = await msg.channel.getMessages(10000);
|
|
||||||
const log = logMessages.reverse().map(msg => {
|
|
||||||
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';
|
|
||||||
|
|
||||||
const logFilename = await logs.getNewLogFile(thread.userId);
|
|
||||||
await logs.saveLogFile(logFilename, log);
|
|
||||||
|
|
||||||
const logUrl = await logs.getLogFileUrl(logFilename);
|
|
||||||
const closeMessage = `Modmail thread with ${thread.username} (${thread.userId}) was closed by ${msg.author.username}
|
|
||||||
Logs: <${logUrl}>`;
|
|
||||||
|
|
||||||
bot.createMessage(utils.getLogChannel(bot).id, closeMessage);
|
|
||||||
await threads.closeByChannelId(thread.channelId);
|
|
||||||
msg.channel.delete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.addInboxCommand('block', (msg, args, thread) => {
|
addInboxServerCommand('block', (msg, args, thread) => {
|
||||||
async function block(userId) {
|
async function block(userId) {
|
||||||
await blocked.block(userId);
|
await blocked.block(userId);
|
||||||
msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
|
msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
|
||||||
|
@ -282,7 +172,7 @@ utils.addInboxCommand('block', (msg, args, thread) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.addInboxCommand('unblock', (msg, args, thread) => {
|
addInboxServerCommand('unblock', (msg, args, thread) => {
|
||||||
async function unblock(userId) {
|
async function unblock(userId) {
|
||||||
await blocked.unblock(userId);
|
await blocked.unblock(userId);
|
||||||
msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
|
msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
|
||||||
|
@ -299,15 +189,16 @@ utils.addInboxCommand('unblock', (msg, args, thread) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.addInboxCommand('logs', (msg, args, thread) => {
|
addInboxServerCommand('logs', (msg, args, thread) => {
|
||||||
async function getLogs(userId) {
|
async function getLogs(userId) {
|
||||||
const infos = await logs.getLogsWithUrlByUserId(userId);
|
const userThreads = await threads.getClosedThreadsByUserId(userId);
|
||||||
let message = `**Log files for <@${userId}>:**\n`;
|
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]');
|
||||||
|
return `\`${formattedDate}\`: <${logUrl}>`;
|
||||||
|
}));
|
||||||
|
|
||||||
message += infos.map(info => {
|
const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`;
|
||||||
const formattedDate = moment.utc(info.date, 'YYYY-MM-DD HH:mm:ss').format('MMM Do [at] HH:mm [UTC]');
|
|
||||||
return `\`${formattedDate}\`: <${info.url}>`;
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
// Send the list of logs in chunks of 15 lines per message
|
// Send the list of logs in chunks of 15 lines per message
|
||||||
const lines = message.split('\n');
|
const lines = message.split('\n');
|
||||||
|
@ -347,7 +238,7 @@ bot.on('messageCreate', async msg => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show or add a snippet
|
// Show or add a snippet
|
||||||
utils.addInboxCommand('snippet', async (msg, args) => {
|
addInboxServerCommand('snippet', async (msg, args) => {
|
||||||
const shortcut = args[0];
|
const shortcut = args[0];
|
||||||
if (! shortcut) return
|
if (! shortcut) return
|
||||||
|
|
||||||
|
@ -376,7 +267,7 @@ utils.addInboxCommand('snippet', async (msg, args) => {
|
||||||
|
|
||||||
bot.registerCommandAlias('s', 'snippet');
|
bot.registerCommandAlias('s', 'snippet');
|
||||||
|
|
||||||
utils.addInboxCommand('delete_snippet', async (msg, args) => {
|
addInboxServerCommand('delete_snippet', async (msg, args) => {
|
||||||
const shortcut = args[0];
|
const shortcut = args[0];
|
||||||
if (! shortcut) return;
|
if (! shortcut) return;
|
||||||
|
|
||||||
|
@ -392,7 +283,7 @@ utils.addInboxCommand('delete_snippet', async (msg, args) => {
|
||||||
|
|
||||||
bot.registerCommandAlias('ds', 'delete_snippet');
|
bot.registerCommandAlias('ds', 'delete_snippet');
|
||||||
|
|
||||||
utils.addInboxCommand('edit_snippet', async (msg, args) => {
|
addInboxServerCommand('edit_snippet', async (msg, args) => {
|
||||||
const shortcut = args[0];
|
const shortcut = args[0];
|
||||||
if (! shortcut) return;
|
if (! shortcut) return;
|
||||||
|
|
||||||
|
@ -413,7 +304,7 @@ utils.addInboxCommand('edit_snippet', async (msg, args) => {
|
||||||
|
|
||||||
bot.registerCommandAlias('es', 'edit_snippet');
|
bot.registerCommandAlias('es', 'edit_snippet');
|
||||||
|
|
||||||
utils.addInboxCommand('snippets', async msg => {
|
addInboxServerCommand('snippets', async msg => {
|
||||||
const allSnippets = await snippets.all();
|
const allSnippets = await snippets.all();
|
||||||
const shortcuts = Object.keys(allSnippets);
|
const shortcuts = Object.keys(allSnippets);
|
||||||
shortcuts.sort();
|
shortcuts.sort();
|
||||||
|
@ -424,7 +315,7 @@ utils.addInboxCommand('snippets', async msg => {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
start() {
|
start() {
|
||||||
bot.connect();
|
bot.connect();
|
||||||
webserver.run();
|
// webserver.run();
|
||||||
greeting.init(bot);
|
greeting.init(bot);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
30
src/utils.js
30
src/utils.js
|
@ -2,7 +2,6 @@ const Eris = require('eris');
|
||||||
const bot = require('./bot');
|
const bot = require('./bot');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const publicIp = require('public-ip');
|
const publicIp = require('public-ip');
|
||||||
const threads = require('./data/threads');
|
|
||||||
const attachments = require('./data/attachments');
|
const attachments = require('./data/attachments');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
|
||||||
|
@ -86,23 +85,6 @@ function messageIsOnMainServer(msg) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a command that can only be triggered on the inbox server.
|
|
||||||
* Command handlers added with this function also get the thread the message was posted in as a third argument, if any.
|
|
||||||
* @param cmd
|
|
||||||
* @param fn
|
|
||||||
* @param opts
|
|
||||||
*/
|
|
||||||
function addInboxCommand(cmd, fn, opts) {
|
|
||||||
bot.registerCommand(cmd, async (msg, args) => {
|
|
||||||
if (! messageIsOnInboxServer(msg)) return;
|
|
||||||
if (! isStaff(msg.member)) return;
|
|
||||||
|
|
||||||
const thread = await threads.getByChannelId(msg.channel.id);
|
|
||||||
fn(msg, args, thread);
|
|
||||||
}, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param attachment
|
* @param attachment
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
|
@ -155,16 +137,15 @@ function disableLinkPreviews(str) {
|
||||||
/**
|
/**
|
||||||
* Returns a URL to the bot's web server
|
* Returns a URL to the bot's web server
|
||||||
* @param {String} path
|
* @param {String} path
|
||||||
* @returns {String}
|
* @returns {Promise<String>}
|
||||||
*/
|
*/
|
||||||
function getSelfUrl(path = '') {
|
async function getSelfUrl(path = '') {
|
||||||
if (config.url) {
|
if (config.url) {
|
||||||
return Promise.resolve(`${config.url}/${path}`);
|
return `${config.url}/${path}`;
|
||||||
} else {
|
} else {
|
||||||
const port = config.port || 8890;
|
const port = config.port || 8890;
|
||||||
return publicIp.v4().then(ip => {
|
const ip = await publicIp.v4();
|
||||||
return `http://${ip}:${port}/${path}`;
|
return `http://${ip}:${port}/${path}`;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +187,6 @@ module.exports = {
|
||||||
isStaff,
|
isStaff,
|
||||||
messageIsOnInboxServer,
|
messageIsOnInboxServer,
|
||||||
messageIsOnMainServer,
|
messageIsOnMainServer,
|
||||||
addInboxCommand,
|
|
||||||
|
|
||||||
formatAttachment,
|
formatAttachment,
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue