Continue rewrite. Modularize greeting, snippet, and web server functionality.
parent
bb6d8e5dbf
commit
ad7aa66c99
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2.0.0
|
||||||
|
* Rewrote large parts of the code to be more modular and maintainable. There may be some new bugs because of this - please report them through GitHub issues if you encounter any!
|
||||||
|
* Threads, logs, and snippets are now stored in an SQLite database. The bot will migrate old data on the first run.
|
||||||
|
* Fixed system messages like pins in DMs being relayed to the thread
|
||||||
|
* Fixed channels sometimes being created without a category
|
||||||
|
|
||||||
## Sep 22, 2017
|
## Sep 22, 2017
|
||||||
* Added `newThreadCategoryId` option. This option can be set to a category ID to place all new threads in that category.
|
* Added `newThreadCategoryId` option. This option can be set to a category ID to place all new threads in that category.
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ exports.up = async function(knex, Promise) {
|
||||||
table.string('id', 36).notNullable().primary();
|
table.string('id', 36).notNullable().primary();
|
||||||
table.integer('status').unsigned().notNullable().index();
|
table.integer('status').unsigned().notNullable().index();
|
||||||
table.integer('is_legacy').unsigned().notNullable();
|
table.integer('is_legacy').unsigned().notNullable();
|
||||||
table.bigInteger('user_id').unsigned().notNullable().index();
|
table.string('user_id', 20).notNullable().index();
|
||||||
table.string('user_name', 128).notNullable();
|
table.string('user_name', 128).notNullable();
|
||||||
table.bigInteger('channel_id').unsigned().nullable().unique();
|
table.string('channel_id', 20).nullable().unique();
|
||||||
table.dateTime('created_at').notNullable().index();
|
table.dateTime('created_at').notNullable().index();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,18 +13,18 @@ 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().nullable();
|
table.string('user_id', 20).nullable();
|
||||||
table.string('user_name', 128).notNullable();
|
table.string('user_name', 128).notNullable();
|
||||||
table.text('body').notNullable();
|
table.text('body').notNullable();
|
||||||
table.integer('is_anonymous').unsigned().notNullable();
|
table.integer('is_anonymous').unsigned().notNullable();
|
||||||
table.bigInteger('original_message_id').unsigned().nullable().unique();
|
table.string('original_message_id', 20).nullable().unique();
|
||||||
table.dateTime('created_at').notNullable().index();
|
table.dateTime('created_at').notNullable().index();
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.createTableIfNotExists('blocked_users', table => {
|
await knex.schema.createTableIfNotExists('blocked_users', table => {
|
||||||
table.bigInteger('user_id').unsigned().primary().notNullable();
|
table.string('user_id', 20).primary().notNullable();
|
||||||
table.string('user_name', 128).notNullable();
|
table.string('user_name', 128).notNullable();
|
||||||
table.bigInteger('blocked_by').unsigned().nullable();
|
table.string('blocked_by', 20).nullable();
|
||||||
table.dateTime('blocked_at').notNullable();
|
table.dateTime('blocked_at').notNullable();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ exports.up = async function(knex, Promise) {
|
||||||
table.string('trigger', 32).primary().notNullable();
|
table.string('trigger', 32).primary().notNullable();
|
||||||
table.text('body').notNullable();
|
table.text('body').notNullable();
|
||||||
table.integer('is_anonymous').unsigned().notNullable();
|
table.integer('is_anonymous').unsigned().notNullable();
|
||||||
table.bigInteger('created_by').unsigned().nullable();
|
table.string('created_by', 20).nullable();
|
||||||
table.dateTime('created_at').notNullable();
|
table.dateTime('created_at').notNullable();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -413,13 +413,13 @@
|
||||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||||
},
|
},
|
||||||
"eris": {
|
"eris": {
|
||||||
"version": "0.7.2",
|
"version": "0.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/eris/-/eris-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/eris/-/eris-0.8.4.tgz",
|
||||||
"integrity": "sha512-YcgXHH81tk9/nbnwZZ47cQVtaAySjIJi/JJFt0lbIMTHhHa77zo682nLIJrBbU5P8u9LQUbWoqiFA/NabR3qww==",
|
"integrity": "sha512-mhQlh5iamo3Ls8xk1xJsu9rHgiW7wkR79e76Nuhrwu1fswnmC7WA/KypGpI51G5h9BFPMxSeFYXy+tyladhJtQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"opusscript": "0.0.3",
|
"opusscript": "0.0.4",
|
||||||
"tweetnacl": "1.0.0",
|
"tweetnacl": "1.0.0",
|
||||||
"ws": "3.2.0"
|
"ws": "3.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error-ex": {
|
"error-ex": {
|
||||||
|
@ -1668,9 +1668,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"opusscript": {
|
"opusscript": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.4.tgz",
|
||||||
"integrity": "sha1-zkZxf8jW+QHFGR5pSFyImgNqhnQ=",
|
"integrity": "sha512-bEPZFE2lhUJYQD5yfTFO4RhbRZ937x6hRwBC1YoGacT35bwDVwKFP1+amU8NYfZL/v4EU7ZTU3INTqzYAnuP7Q==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"os-homedir": {
|
"os-homedir": {
|
||||||
|
@ -3006,9 +3006,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ultron": {
|
"ultron": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
||||||
"integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ="
|
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
|
||||||
},
|
},
|
||||||
"unc-path-regex": {
|
"unc-path-regex": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
|
@ -3120,13 +3120,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "3.2.0",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
|
||||||
"integrity": "sha512-hTS3mkXm/j85jTQOIcwVz3yK3up9xHgPtgEhDBOH3G18LDOZmSAG1omJeXejLKJakx+okv8vS1sopgs7rw0kVw==",
|
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"async-limiter": "1.0.0",
|
"async-limiter": "1.0.0",
|
||||||
"safe-buffer": "5.1.1",
|
"safe-buffer": "5.1.1",
|
||||||
"ultron": "1.1.0"
|
"ultron": "1.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "modmailbot",
|
"name": "modmailbot",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --trace-warnings src/index.js",
|
"start": "node src/index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"lint": "./node_modules/.bin/eslint ./src"
|
"lint": "./node_modules/.bin/eslint ./src"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eris": "^0.7.2",
|
"eris": "^0.8.4",
|
||||||
"humanize-duration": "^3.10.0",
|
"humanize-duration": "^3.10.0",
|
||||||
"knex": "^0.14.2",
|
"knex": "^0.14.2",
|
||||||
"mime": "^1.3.4",
|
"mime": "^1.3.4",
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
const bot = require('../bot');
|
const bot = require('../bot');
|
||||||
const knex = require('../knex');
|
const knex = require('../knex');
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
|
const config = require('../config');
|
||||||
const attachments = require('./attachments');
|
const attachments = require('./attachments');
|
||||||
|
|
||||||
|
const ThreadMessage = require('./ThreadMessage');
|
||||||
|
|
||||||
const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require('./constants');
|
const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require('./constants');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,17 +17,16 @@ const {THREAD_MESSAGE_TYPE, THREAD_STATUS} = require('./constants');
|
||||||
* @property {String} user_name
|
* @property {String} user_name
|
||||||
* @property {String} channel_id
|
* @property {String} channel_id
|
||||||
* @property {String} created_at
|
* @property {String} created_at
|
||||||
* @property {Boolean} _wasCreated
|
|
||||||
*/
|
*/
|
||||||
class Thread {
|
class Thread {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
Object.assign(this, {_wasCreated: false}, props);
|
Object.assign(this, props);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Eris.Member} moderator
|
* @param {Eris~Member} moderator
|
||||||
* @param {String} text
|
* @param {String} text
|
||||||
* @param {Eris.Attachment[]} replyAttachments
|
* @param {Eris~Attachment[]} replyAttachments
|
||||||
* @param {Boolean} isAnonymous
|
* @param {Boolean} isAnonymous
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
|
@ -81,6 +85,7 @@ class Thread {
|
||||||
user_id: moderator.id,
|
user_id: moderator.id,
|
||||||
user_name: logModUsername,
|
user_name: logModUsername,
|
||||||
body: logContent,
|
body: logContent,
|
||||||
|
is_anonymous: (isAnonymous ? 1 : 0),
|
||||||
original_message_id: originalMessage.id
|
original_message_id: originalMessage.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -92,7 +97,12 @@ class Thread {
|
||||||
async receiveUserReply(msg) {
|
async receiveUserReply(msg) {
|
||||||
const timestamp = utils.getTimestamp();
|
const timestamp = utils.getTimestamp();
|
||||||
|
|
||||||
let threadContent = `[${timestamp}] « **${msg.author.username}#${msg.author.discriminator}:** ${msg.content}`;
|
let content = msg.content;
|
||||||
|
if (msg.content.trim() === '' && msg.embeds.length) {
|
||||||
|
content = '<message contains embeds>';
|
||||||
|
}
|
||||||
|
|
||||||
|
let threadContent = `[${timestamp}] « **${msg.author.username}#${msg.author.discriminator}:** ${content}`;
|
||||||
let logContent = msg.content;
|
let logContent = msg.content;
|
||||||
let finalThreadContent;
|
let finalThreadContent;
|
||||||
let attachmentSavePromise;
|
let attachmentSavePromise;
|
||||||
|
@ -113,6 +123,7 @@ class Thread {
|
||||||
user_id: this.user_id,
|
user_id: this.user_id,
|
||||||
user_name: `${msg.author.username}#${msg.author.discriminator}`,
|
user_name: `${msg.author.username}#${msg.author.discriminator}`,
|
||||||
body: logContent,
|
body: logContent,
|
||||||
|
is_anonymous: 0,
|
||||||
original_message_id: msg.id
|
original_message_id: msg.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -128,8 +139,7 @@ class Thread {
|
||||||
* @returns {Promise<Eris.Message>}
|
* @returns {Promise<Eris.Message>}
|
||||||
*/
|
*/
|
||||||
async postToThreadChannel(text, file = null) {
|
async postToThreadChannel(text, file = null) {
|
||||||
const channel = bot.getChannel(this.channel_id);
|
return bot.createMessage(this.channel_id, text, file);
|
||||||
return channel.createMessage(text, file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,10 +153,58 @@ class Thread {
|
||||||
user_id: null,
|
user_id: null,
|
||||||
user_name: '',
|
user_name: '',
|
||||||
body: text,
|
body: text,
|
||||||
|
is_anonymous: 0,
|
||||||
original_message_id: msg.id
|
original_message_id: msg.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} text
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async postNonLogMessage(text) {
|
||||||
|
await this.postToThreadChannel(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Eris.Message} msg
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async saveChatMessage(msg) {
|
||||||
|
return this.addThreadMessageToDB({
|
||||||
|
message_type: THREAD_MESSAGE_TYPE.CHAT,
|
||||||
|
user_id: msg.author.id,
|
||||||
|
user_name: `${msg.author.username}#${msg.author.discriminator}`,
|
||||||
|
body: msg.content,
|
||||||
|
is_anonymous: 0,
|
||||||
|
original_message_id: msg.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Eris.Message} msg
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async updateChatMessage(msg) {
|
||||||
|
await knex('thread_messages')
|
||||||
|
.where('thread_id', this.id)
|
||||||
|
.where('original_message_id', msg.id)
|
||||||
|
.update({
|
||||||
|
content: msg.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} messageId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async deleteChatMessage(messageId) {
|
||||||
|
await knex('thread_messages')
|
||||||
|
.where('thread_id', this.id)
|
||||||
|
.where('original_message_id', messageId)
|
||||||
|
.delete();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
|
@ -155,14 +213,29 @@ class Thread {
|
||||||
await knex('thread_messages').insert({
|
await knex('thread_messages').insert({
|
||||||
thread_id: this.id,
|
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,
|
||||||
...data
|
...data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<ThreadMessage[]>}
|
||||||
|
*/
|
||||||
|
async getThreadMessages() {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async close() {
|
async close() {
|
||||||
|
console.log(`Closing thread ${this.id}`);
|
||||||
await this.postToThreadChannel('Closing thread...');
|
await this.postToThreadChannel('Closing thread...');
|
||||||
|
|
||||||
// Update DB status
|
// Update DB status
|
||||||
|
@ -173,9 +246,10 @@ class Thread {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete channel
|
// Delete channel
|
||||||
|
console.log(`Deleting channel ${this.channel_id}`);
|
||||||
const channel = bot.getChannel(this.channel_id);
|
const channel = bot.getChannel(this.channel_id);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
channel.delete('Thread closed');
|
await channel.delete('Thread closed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* @property {Number} id
|
||||||
|
* @property {String} thread_id
|
||||||
|
* @property {Number} message_type
|
||||||
|
* @property {String} user_id
|
||||||
|
* @property {String} user_name
|
||||||
|
* @property {String} body
|
||||||
|
* @property {Number} is_anonymous
|
||||||
|
* @property {Number} original_message_id
|
||||||
|
* @property {String} created_at
|
||||||
|
*/
|
||||||
|
class ThreadMessage {
|
||||||
|
constructor(props) {
|
||||||
|
Object.assign(this, props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ThreadMessage;
|
|
@ -5,7 +5,7 @@ const config = require('../config');
|
||||||
|
|
||||||
const getUtils = () => require('../utils');
|
const getUtils = () => require('../utils');
|
||||||
|
|
||||||
const attachmentDir = config.attachmentDir || `${__dirname}/../attachments`;
|
const attachmentDir = config.attachmentDir || `${__dirname}/../../attachments`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the filesystem path for the given attachment id
|
* Returns the filesystem path for the given attachment id
|
||||||
|
@ -36,7 +36,7 @@ function saveAttachment(attachment, tries = 0) {
|
||||||
https.get(attachment.url, (res) => {
|
https.get(attachment.url, (res) => {
|
||||||
res.pipe(writeStream);
|
res.pipe(writeStream);
|
||||||
writeStream.on('finish', () => {
|
writeStream.on('finish', () => {
|
||||||
writeStream.closeByChannelId()
|
writeStream.end();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}).on('error', (err) => {
|
}).on('error', (err) => {
|
||||||
|
|
|
@ -12,6 +12,14 @@ const utils = require('../utils');
|
||||||
const Thread = require('./Thread');
|
const Thread = require('./Thread');
|
||||||
const {THREAD_STATUS} = require('./constants');
|
const {THREAD_STATUS} = require('./constants');
|
||||||
|
|
||||||
|
async function findById(id) {
|
||||||
|
const thread = await knex('threads')
|
||||||
|
.where('id', id)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return (thread ? new Thread(thread) : null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} userId
|
* @param {String} userId
|
||||||
* @returns {Promise<Thread>}
|
* @returns {Promise<Thread>}
|
||||||
|
@ -20,7 +28,7 @@ async function findOpenThreadByUserId(userId) {
|
||||||
const thread = await knex('threads')
|
const thread = await knex('threads')
|
||||||
.where('user_id', userId)
|
.where('user_id', userId)
|
||||||
.where('status', THREAD_STATUS.OPEN)
|
.where('status', THREAD_STATUS.OPEN)
|
||||||
.select();
|
.first();
|
||||||
|
|
||||||
return (thread ? new Thread(thread) : null);
|
return (thread ? new Thread(thread) : null);
|
||||||
}
|
}
|
||||||
|
@ -50,11 +58,7 @@ async function createNewThreadForUser(user) {
|
||||||
// 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 utils.getInboxGuild().createChannel(channelName);
|
createdChannel = await utils.getInboxGuild().createChannel(channelName, null, 'New ModMail thread', config.newThreadCategoryId);
|
||||||
if (config.newThreadCategoryId) {
|
|
||||||
// If a category id for new threads is specified, move the newly created channel there
|
|
||||||
bot.editChannel(createdChannel.id, {parentID: config.newThreadCategoryId});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`);
|
console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`);
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -71,6 +75,10 @@ async function createNewThreadForUser(user) {
|
||||||
|
|
||||||
const newThread = await findById(newThreadId);
|
const newThread = await findById(newThreadId);
|
||||||
|
|
||||||
|
// Post the log link to the beginning (but don't save it in thread messages)
|
||||||
|
const logUrl = await newThread.getLogUrl();
|
||||||
|
await newThread.postNonLogMessage(`Log URL: <${logUrl}>`);
|
||||||
|
|
||||||
// Post some info to the beginning of the new thread
|
// Post some info to the beginning of the new thread
|
||||||
const mainGuild = utils.getMainGuild();
|
const mainGuild = utils.getMainGuild();
|
||||||
const member = (mainGuild ? mainGuild.members.get(user.id) : null);
|
const member = (mainGuild ? mainGuild.members.get(user.id) : null);
|
||||||
|
@ -134,6 +142,19 @@ async function findByChannelId(channelId) {
|
||||||
return (thread ? new Thread(thread) : null);
|
return (thread ? new Thread(thread) : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} channelId
|
||||||
|
* @returns {Promise<Thread>}
|
||||||
|
*/
|
||||||
|
async function findOpenThreadByChannelId(channelId) {
|
||||||
|
const thread = await knex('threads')
|
||||||
|
.where('channel_id', channelId)
|
||||||
|
.where('status', THREAD_STATUS.OPEN)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return (thread ? new Thread(thread) : null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} userId
|
* @param {String} userId
|
||||||
* @returns {Promise<Thread[]>}
|
* @returns {Promise<Thread[]>}
|
||||||
|
@ -160,9 +181,20 @@ async function getClosedThreadCountByUserId(userId) {
|
||||||
return parseInt(row.thread_count, 10);
|
return parseInt(row.thread_count, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findOrCreateThreadForUser(user) {
|
||||||
|
const existingThread = await findOpenThreadByUserId(user.id);
|
||||||
|
if (existingThread) return existingThread;
|
||||||
|
|
||||||
|
return createNewThreadForUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
findById,
|
||||||
findOpenThreadByUserId,
|
findOpenThreadByUserId,
|
||||||
findByChannelId,
|
findByChannelId,
|
||||||
|
findOpenThreadByChannelId,
|
||||||
createNewThreadForUser,
|
createNewThreadForUser,
|
||||||
getClosedThreadsByUserId,
|
getClosedThreadsByUserId,
|
||||||
|
findOrCreateThreadForUser,
|
||||||
|
createThreadInDB
|
||||||
};
|
};
|
||||||
|
|
17
src/index.js
17
src/index.js
|
@ -1,4 +1,21 @@
|
||||||
|
// Verify NodeJS version
|
||||||
|
const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10);
|
||||||
|
if (nodeMajorVersion < 8) {
|
||||||
|
console.error('Unsupported NodeJS version! Please install NodeJS 8 or newer.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify node modules have been installed
|
||||||
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.accessSync(path.join(__dirname, '..', 'node_modules'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Please run "npm install" before trying to start the bot.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const main = require('./main');
|
const main = require('./main');
|
||||||
|
|
|
@ -9,6 +9,8 @@ const config = require('../config');
|
||||||
const jsonDb = require('./jsonDb');
|
const jsonDb = require('./jsonDb');
|
||||||
const threads = require('../data/threads');
|
const threads = require('../data/threads');
|
||||||
|
|
||||||
|
const {THREAD_STATUS, THREAD_MESSAGE_TYPE} = require('../data/constants');
|
||||||
|
|
||||||
const readDir = promisify(fs.readdir);
|
const readDir = promisify(fs.readdir);
|
||||||
const readFile = promisify(fs.readFile);
|
const readFile = promisify(fs.readFile);
|
||||||
const access = promisify(fs.access);
|
const access = promisify(fs.access);
|
||||||
|
@ -75,7 +77,7 @@ async function migrateOpenThreads() {
|
||||||
if (existingOpenThread) return;
|
if (existingOpenThread) return;
|
||||||
|
|
||||||
const newThread = {
|
const newThread = {
|
||||||
status: threads.THREAD_STATUS.OPEN,
|
status: THREAD_STATUS.OPEN,
|
||||||
user_id: oldThread.userId,
|
user_id: oldThread.userId,
|
||||||
user_name: oldThread.username,
|
user_name: oldThread.username,
|
||||||
channel_id: oldThread.channelId,
|
channel_id: oldThread.channelId,
|
||||||
|
@ -103,7 +105,7 @@ async function migrateLogs() {
|
||||||
|
|
||||||
const newThread = {
|
const newThread = {
|
||||||
id: threadId,
|
id: threadId,
|
||||||
status: threads.THREAD_STATUS.CLOSED,
|
status: THREAD_STATUS.CLOSED,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
user_name: '',
|
user_name: '',
|
||||||
channel_id: null,
|
channel_id: null,
|
||||||
|
@ -122,10 +124,11 @@ async function migrateLogs() {
|
||||||
|
|
||||||
await trx('thread_messages').insert({
|
await trx('thread_messages').insert({
|
||||||
thread_id: newThread.id,
|
thread_id: newThread.id,
|
||||||
message_type: threads.THREAD_MESSAGE_TYPE.LEGACY,
|
message_type: THREAD_MESSAGE_TYPE.LEGACY,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
user_name: '',
|
user_name: '',
|
||||||
body: contents,
|
body: contents,
|
||||||
|
is_anonymous: 0,
|
||||||
created_at: date
|
created_at: date
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
287
src/main.js
287
src/main.js
|
@ -5,48 +5,28 @@ const config = require('./config');
|
||||||
const bot = require('./bot');
|
const bot = require('./bot');
|
||||||
const Queue = require('./queue');
|
const Queue = require('./queue');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
const threadUtils = require('./threadUtils');
|
||||||
const blocked = require('./data/blocked');
|
const blocked = require('./data/blocked');
|
||||||
const threads = require('./data/threads');
|
const threads = require('./data/threads');
|
||||||
const snippets = require('./data/snippets');
|
|
||||||
const webserver = require('./webserver');
|
const snippets = require('./plugins/snippets');
|
||||||
const greeting = require('./greeting');
|
const webserver = require('./plugins/webserver');
|
||||||
const Thread = require('./data/Thread');
|
const greeting = require('./plugins/greeting');
|
||||||
|
|
||||||
const messageQueue = new Queue();
|
const messageQueue = new Queue();
|
||||||
|
|
||||||
/**
|
const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);
|
||||||
* @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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle moderator messages in thread channels
|
/**
|
||||||
|
* When a moderator posts in a modmail thread...
|
||||||
|
* 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 (! utils.messageIsOnInboxServer(msg)) return;
|
||||||
if (! utils.isStaff(msg)) return;
|
if (! utils.isStaff(msg)) return;
|
||||||
|
@ -62,17 +42,81 @@ bot.on('messageCreate', async msg => {
|
||||||
msg.delete();
|
msg.delete();
|
||||||
} else {
|
} else {
|
||||||
// Otherwise just save the messages as "chat" in the logs
|
// Otherwise just save the messages as "chat" in the logs
|
||||||
thread.addThreadMessageToDB({
|
thread.saveChatMessage(msg);
|
||||||
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
|
/**
|
||||||
|
* When we get a private message...
|
||||||
|
* 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 => {
|
||||||
|
if (! (msg.channel instanceof Eris.PrivateChannel)) return;
|
||||||
|
if (msg.author.bot) return;
|
||||||
|
if (msg.type !== 0) return; // Ignore pins etc.
|
||||||
|
|
||||||
|
if (await blocked.isBlocked(msg.author.id)) return;
|
||||||
|
|
||||||
|
// Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
|
||||||
|
messageQueue.add(async () => {
|
||||||
|
const thread = await threads.findOrCreateThreadForUser(msg.author);
|
||||||
|
await thread.receiveUserReply(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a message is edited...
|
||||||
|
* 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) => {
|
||||||
|
if (msg.author.bot) return;
|
||||||
|
if (await blocked.isBlocked(msg.author.id)) return;
|
||||||
|
|
||||||
|
let oldContent = oldMessage.content;
|
||||||
|
const newContent = msg.content;
|
||||||
|
|
||||||
|
// Old message content doesn't persist between bot restarts
|
||||||
|
if (oldContent == null) oldContent = '*Unavailable due to bot restart*';
|
||||||
|
|
||||||
|
// Ignore bogus edit events with no changes
|
||||||
|
if (newContent.trim() === oldContent.trim()) return;
|
||||||
|
|
||||||
|
// 1) Edit in DMs
|
||||||
|
if (msg.channel instanceof Eris.PrivateChannel) {
|
||||||
|
const thread = await threads.findOpenThreadByUserId(msg.author.id);
|
||||||
|
const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);
|
||||||
|
|
||||||
|
thread.postSystemMessage(editMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Edit in the thread
|
||||||
|
else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) {
|
||||||
|
const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
|
||||||
|
if (! thread) return;
|
||||||
|
|
||||||
|
thread.updateChatMessage(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a staff message is deleted in a modmail thread, delete it from the database as well
|
||||||
|
*/
|
||||||
|
bot.on('messageDelete', async msg => {
|
||||||
|
if (msg.author.bot) return;
|
||||||
|
if (! utils.messageIsOnInboxServer(msg)) return;
|
||||||
|
if (! utils.isStaff(msg.member)) return;
|
||||||
|
|
||||||
|
const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
|
||||||
|
if (! thread) return;
|
||||||
|
|
||||||
|
thread.deleteChatMessage(msg.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (! 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;
|
||||||
|
@ -89,62 +133,25 @@ bot.on('messageCreate', async msg => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// When we get a private message, forward the contents to the corresponding modmail thread
|
|
||||||
bot.on('messageCreate', async msg => {
|
|
||||||
if (! (msg.channel instanceof Eris.PrivateChannel)) return;
|
|
||||||
if (msg.author.id === bot.user.id) return;
|
|
||||||
|
|
||||||
if (await blocked.isBlocked(msg.author.id)) return;
|
|
||||||
|
|
||||||
// Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
|
|
||||||
messageQueue.add(async () => {
|
|
||||||
let thread = await threads.findOpenThreadByUserId(msg.author.id);
|
|
||||||
if (! thread) {
|
|
||||||
thread = await threads.createNewThreadForUser(msg.author, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
thread.receiveUserReply(msg);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Edits in DMs
|
|
||||||
bot.on('messageUpdate', async (msg, oldMessage) => {
|
|
||||||
if (! (msg.channel instanceof Eris.PrivateChannel)) return;
|
|
||||||
if (msg.author.id === bot.user.id) return;
|
|
||||||
|
|
||||||
if (await blocked.isBlocked(msg.author.id)) return;
|
|
||||||
|
|
||||||
let oldContent = oldMessage.content;
|
|
||||||
const newContent = msg.content;
|
|
||||||
|
|
||||||
// Old message content doesn't persist between bot restarts
|
|
||||||
if (oldContent == null) oldContent = '*Unavailable due to bot restart*';
|
|
||||||
|
|
||||||
// Ignore bogus edit events with no changes
|
|
||||||
if (newContent.trim() === oldContent.trim()) return;
|
|
||||||
|
|
||||||
const thread = await threads.createNewThreadForUser(msg.author);
|
|
||||||
if (! thread) return;
|
|
||||||
|
|
||||||
const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);
|
|
||||||
bot.createMessage(thread.channelId, editMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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
|
||||||
addInboxServerCommand('reply', (msg, args, thread) => {
|
addInboxServerCommand('reply', async (msg, args, thread) => {
|
||||||
if (! thread) return;
|
if (! thread) return;
|
||||||
|
|
||||||
const text = args.join(' ').trim();
|
const text = args.join(' ').trim();
|
||||||
thread.replyToUser(msg.member, text, msg.attachments, false);
|
await thread.replyToUser(msg.member, text, msg.attachments, false);
|
||||||
|
msg.delete();
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
||||||
addInboxServerCommand('anonreply', (msg, args, thread) => {
|
addInboxServerCommand('anonreply', async (msg, args, thread) => {
|
||||||
if (! thread) return;
|
if (! thread) return;
|
||||||
|
|
||||||
const text = args.join(' ').trim();
|
const text = args.join(' ').trim();
|
||||||
thread.replyToUser(msg.member, text, msg.attachments, true);
|
await thread.replyToUser(msg.member, text, msg.attachments, true);
|
||||||
|
msg.delete();
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.registerCommandAlias('ar', 'anonreply');
|
bot.registerCommandAlias('ar', 'anonreply');
|
||||||
|
@ -168,7 +175,7 @@ addInboxServerCommand('block', (msg, args, thread) => {
|
||||||
block(userId);
|
block(userId);
|
||||||
} else if (thread) {
|
} else if (thread) {
|
||||||
// Calling !block without args in a modmail thread blocks the user of that thread
|
// Calling !block without args in a modmail thread blocks the user of that thread
|
||||||
block(thread.userId);
|
block(thread.user_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -185,7 +192,7 @@ addInboxServerCommand('unblock', (msg, args, thread) => {
|
||||||
unblock(userId);
|
unblock(userId);
|
||||||
} else if (thread) {
|
} else if (thread) {
|
||||||
// Calling !unblock without args in a modmail thread unblocks the user of that thread
|
// Calling !unblock without args in a modmail thread unblocks the user of that thread
|
||||||
unblock(thread.userId);
|
unblock(thread.user_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -217,105 +224,21 @@ addInboxServerCommand('logs', (msg, args, thread) => {
|
||||||
getLogs(userId);
|
getLogs(userId);
|
||||||
} else if (thread) {
|
} else if (thread) {
|
||||||
// Calling !logs without args in a modmail thread returns the logs of the user of that thread
|
// Calling !logs without args in a modmail thread returns the logs of the user of that thread
|
||||||
getLogs(thread.userId);
|
getLogs(thread.user_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Snippets
|
|
||||||
bot.on('messageCreate', async msg => {
|
|
||||||
if (! utils.messageIsOnInboxServer(msg)) return;
|
|
||||||
if (! utils.isStaff(msg.member)) return;
|
|
||||||
|
|
||||||
if (msg.author.bot) return;
|
|
||||||
if (! msg.content) return;
|
|
||||||
if (! msg.content.startsWith(config.snippetPrefix)) return;
|
|
||||||
|
|
||||||
const shortcut = msg.content.replace(config.snippetPrefix, '').toLowerCase();
|
|
||||||
const snippet = await snippets.get(shortcut);
|
|
||||||
if (! snippet) return;
|
|
||||||
|
|
||||||
reply(msg, snippet.text, snippet.isAnonymous);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show or add a snippet
|
|
||||||
addInboxServerCommand('snippet', async (msg, args) => {
|
|
||||||
const shortcut = args[0];
|
|
||||||
if (! shortcut) return
|
|
||||||
|
|
||||||
const text = args.slice(1).join(' ').trim();
|
|
||||||
const snippet = await snippets.get(shortcut);
|
|
||||||
|
|
||||||
if (snippet) {
|
|
||||||
if (text) {
|
|
||||||
// If the snippet exists and we're trying to create a new one, inform the user the snippet already exists
|
|
||||||
msg.channel.createMessage(`Snippet "${shortcut}" already exists! You can edit or delete it with ${prefix}edit_snippet and ${prefix}delete_snippet respectively.`);
|
|
||||||
} else {
|
|
||||||
// If the snippet exists and we're NOT trying to create a new one, show info about the existing snippet
|
|
||||||
msg.channel.createMessage(`\`${config.snippetPrefix}${shortcut}\` replies ${snippet.isAnonymous ? 'anonymously ' : ''}with:\n${snippet.text}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (text) {
|
|
||||||
// If the snippet doesn't exist and the user wants to create it, create it
|
|
||||||
await snippets.add(shortcut, text, false);
|
|
||||||
msg.channel.createMessage(`Snippet "${shortcut}" created!`);
|
|
||||||
} else {
|
|
||||||
// If the snippet doesn't exist and the user isn't trying to create it, inform them how to create it
|
|
||||||
msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist! You can create it with \`${prefix}snippet ${shortcut} text\``);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.registerCommandAlias('s', 'snippet');
|
|
||||||
|
|
||||||
addInboxServerCommand('delete_snippet', async (msg, args) => {
|
|
||||||
const shortcut = args[0];
|
|
||||||
if (! shortcut) return;
|
|
||||||
|
|
||||||
const snippet = await snippets.get(shortcut);
|
|
||||||
if (! snippet) {
|
|
||||||
msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await snippets.del(shortcut);
|
|
||||||
msg.channel.createMessage(`Snippet "${shortcut}" deleted!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.registerCommandAlias('ds', 'delete_snippet');
|
|
||||||
|
|
||||||
addInboxServerCommand('edit_snippet', async (msg, args) => {
|
|
||||||
const shortcut = args[0];
|
|
||||||
if (! shortcut) return;
|
|
||||||
|
|
||||||
const text = args.slice(1).join(' ').trim();
|
|
||||||
if (! text) return;
|
|
||||||
|
|
||||||
const snippet = await snippets.get(shortcut);
|
|
||||||
if (! snippet) {
|
|
||||||
msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await snippets.del(shortcut);
|
|
||||||
await snippets.add(shortcut, text, snippet.isAnonymous);
|
|
||||||
|
|
||||||
msg.channel.createMessage(`Snippet "${shortcut}" edited!`);
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.registerCommandAlias('es', 'edit_snippet');
|
|
||||||
|
|
||||||
addInboxServerCommand('snippets', async msg => {
|
|
||||||
const allSnippets = await snippets.all();
|
|
||||||
const shortcuts = Object.keys(allSnippets);
|
|
||||||
shortcuts.sort();
|
|
||||||
|
|
||||||
msg.channel.createMessage(`Available snippets (prefix ${config.snippetPrefix}):\n${shortcuts.join(', ')}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
start() {
|
async start() {
|
||||||
bot.connect();
|
// Load plugins
|
||||||
// webserver.run();
|
console.log('Loading plugins...');
|
||||||
greeting.init(bot);
|
await snippets(bot);
|
||||||
|
await greeting(bot);
|
||||||
|
await webserver(bot);
|
||||||
|
|
||||||
|
console.log('Connecting to Discord...');
|
||||||
|
await bot.connect();
|
||||||
|
|
||||||
|
console.log('Done! Now listening to DMs.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const config = require('./config');
|
const config = require('../config');
|
||||||
|
|
||||||
const greetingGuildId = config.mainGuildId || config.greetingGuildId;
|
module.exports = bot => {
|
||||||
|
|
||||||
function init(bot) {
|
|
||||||
if (! config.enableGreeting) return;
|
if (! config.enableGreeting) return;
|
||||||
|
|
||||||
|
const greetingGuildId = config.mainGuildId || config.greetingGuildId;
|
||||||
|
|
||||||
bot.on('guildMemberAdd', (guild, member) => {
|
bot.on('guildMemberAdd', (guild, member) => {
|
||||||
if (guild.id !== greetingGuildId) return;
|
if (guild.id !== greetingGuildId) return;
|
||||||
|
|
||||||
|
@ -24,11 +24,7 @@ function init(bot) {
|
||||||
sendGreeting(file);
|
sendGreeting(file);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
sendGreeting();
|
sendGreeting();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
};
|
};
|
|
@ -0,0 +1,106 @@
|
||||||
|
const threads = require('../data/threads');
|
||||||
|
const snippets = require('../data/snippets');
|
||||||
|
const config = require('../config');
|
||||||
|
const utils = require('../utils');
|
||||||
|
const threadUtils = require('../threadUtils');
|
||||||
|
|
||||||
|
module.exports = bot => {
|
||||||
|
const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 => {
|
||||||
|
if (! utils.messageIsOnInboxServer(msg)) return;
|
||||||
|
if (! utils.isStaff(msg.member)) return;
|
||||||
|
|
||||||
|
if (msg.author.bot) return;
|
||||||
|
if (! msg.content) return;
|
||||||
|
if (! msg.content.startsWith(config.snippetPrefix)) return;
|
||||||
|
|
||||||
|
const thread = await threads.findByChannelId(msg.channel.id);
|
||||||
|
if (! thread) return;
|
||||||
|
|
||||||
|
const trigger = msg.content.replace(config.snippetPrefix, '').toLowerCase();
|
||||||
|
const snippet = await snippets.get(trigger);
|
||||||
|
if (! snippet) return;
|
||||||
|
|
||||||
|
await thread.replyToUser(msg.member, snippet.body, [], !! snippet.is_anonymous);
|
||||||
|
msg.delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show or add a snippet
|
||||||
|
addInboxServerCommand('snippet', async (msg, args) => {
|
||||||
|
const trigger = args[0];
|
||||||
|
if (! trigger) return
|
||||||
|
|
||||||
|
const text = args.slice(1).join(' ').trim();
|
||||||
|
const snippet = await snippets.get(trigger);
|
||||||
|
|
||||||
|
if (snippet) {
|
||||||
|
if (text) {
|
||||||
|
// If the snippet exists and we're trying to create a new one, inform the user the snippet already exists
|
||||||
|
msg.channel.createMessage(`Snippet "${trigger}" already exists! You can edit or delete it with ${config.prefix}edit_snippet and ${config.prefix}delete_snippet respectively.`);
|
||||||
|
} else {
|
||||||
|
// If the snippet exists and we're NOT trying to create a new one, show info about the existing snippet
|
||||||
|
msg.channel.createMessage(`\`${config.snippetPrefix}${trigger}\` replies ${snippet.is_anonymous ? 'anonymously ' : ''}with:\n${snippet.body}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (text) {
|
||||||
|
// If the snippet doesn't exist and the user wants to create it, create it
|
||||||
|
await snippets.add(trigger, text, false);
|
||||||
|
msg.channel.createMessage(`Snippet "${trigger}" created!`);
|
||||||
|
} else {
|
||||||
|
// If the snippet doesn't exist and the user isn't trying to create it, inform them how to create it
|
||||||
|
msg.channel.createMessage(`Snippet "${trigger}" doesn't exist! You can create it with \`${config.prefix}snippet ${trigger} text\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.registerCommandAlias('s', 'snippet');
|
||||||
|
|
||||||
|
addInboxServerCommand('delete_snippet', async (msg, args) => {
|
||||||
|
const trigger = args[0];
|
||||||
|
if (! trigger) return;
|
||||||
|
|
||||||
|
const snippet = await snippets.get(trigger);
|
||||||
|
if (! snippet) {
|
||||||
|
msg.channel.createMessage(`Snippet "${trigger}" doesn't exist!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await snippets.del(trigger);
|
||||||
|
msg.channel.createMessage(`Snippet "${trigger}" deleted!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.registerCommandAlias('ds', 'delete_snippet');
|
||||||
|
|
||||||
|
addInboxServerCommand('edit_snippet', async (msg, args) => {
|
||||||
|
const trigger = args[0];
|
||||||
|
if (! trigger) return;
|
||||||
|
|
||||||
|
const text = args.slice(1).join(' ').trim();
|
||||||
|
if (! text) return;
|
||||||
|
|
||||||
|
const snippet = await snippets.get(trigger);
|
||||||
|
if (! snippet) {
|
||||||
|
msg.channel.createMessage(`Snippet "${trigger}" doesn't exist!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await snippets.del(trigger);
|
||||||
|
await snippets.add(trigger, text, snippet.isAnonymous);
|
||||||
|
|
||||||
|
msg.channel.createMessage(`Snippet "${trigger}" edited!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.registerCommandAlias('es', 'edit_snippet');
|
||||||
|
|
||||||
|
addInboxServerCommand('snippets', async msg => {
|
||||||
|
const allSnippets = await snippets.all();
|
||||||
|
const triggers = allSnippets.map(s => s.trigger);
|
||||||
|
triggers.sort();
|
||||||
|
|
||||||
|
msg.channel.createMessage(`Available snippets (prefix ${config.snippetPrefix}):\n${triggers.join(', ')}`);
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,88 @@
|
||||||
|
const http = require('http');
|
||||||
|
const mime = require('mime');
|
||||||
|
const url = require('url');
|
||||||
|
const fs = require('fs');
|
||||||
|
const moment = require('moment');
|
||||||
|
const config = require('../config');
|
||||||
|
const threads = require('../data/threads');
|
||||||
|
const attachments = require('../data/attachments');
|
||||||
|
|
||||||
|
const {THREAD_MESSAGE_TYPE} = require('../data/constants');
|
||||||
|
|
||||||
|
function notfound(res) {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('Page Not Found');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveLogs(res, pathParts) {
|
||||||
|
const threadId = pathParts[pathParts.length - 1];
|
||||||
|
if (threadId.match(/^[0-9a-f\-]+$/) === null) return notfound(res);
|
||||||
|
|
||||||
|
const thread = await threads.findById(threadId);
|
||||||
|
if (! thread) return notfound(res);
|
||||||
|
|
||||||
|
const threadMessages = await thread.getThreadMessages();
|
||||||
|
const lines = threadMessages.map(message => {
|
||||||
|
// Legacy messages are the entire log in one message, so just serve them as they are
|
||||||
|
if (message.message_type === THREAD_MESSAGE_TYPE.LEGACY) {
|
||||||
|
return message.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = `[${moment(message.created_at).format('YYYY-MM-DD HH:mm:ss')}] `;
|
||||||
|
|
||||||
|
if (message.message_type === THREAD_MESSAGE_TYPE.SYSTEM) {
|
||||||
|
// System messages don't need the username
|
||||||
|
line += message.body;
|
||||||
|
} else if (message.message_type === THREAD_MESSAGE_TYPE.FROM_USER) {
|
||||||
|
line += `[FROM USER] ${message.user_name}: ${message.body}`;
|
||||||
|
} else if (message.message_type === THREAD_MESSAGE_TYPE.TO_USER) {
|
||||||
|
line += `[TO USER] ${message.user_name}: ${message.body}`;
|
||||||
|
} else {
|
||||||
|
line += `${message.user_name}: ${message.body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=UTF-8');
|
||||||
|
res.end(lines.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveAttachments(res, pathParts) {
|
||||||
|
const desiredFilename = pathParts[pathParts.length - 1];
|
||||||
|
const id = pathParts[pathParts.length - 2];
|
||||||
|
|
||||||
|
if (id.match(/^[0-9]+$/) === null) return notfound(res);
|
||||||
|
if (desiredFilename.match(/^[0-9a-z\._-]+$/i) === null) return notfound(res);
|
||||||
|
|
||||||
|
const attachmentPath = attachments.getPath(id);
|
||||||
|
fs.access(attachmentPath, (err) => {
|
||||||
|
if (err) return notfound(res);
|
||||||
|
|
||||||
|
const filenameParts = desiredFilename.split('.');
|
||||||
|
const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : 'bin');
|
||||||
|
const fileMime = mime.lookup(ext);
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', fileMime);
|
||||||
|
|
||||||
|
const read = fs.createReadStream(attachmentPath);
|
||||||
|
read.pipe(res);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const parsedUrl = url.parse(`http://${req.url}`);
|
||||||
|
const pathParts = parsedUrl.path.split('/').filter(v => v !== '');
|
||||||
|
|
||||||
|
if (parsedUrl.path.startsWith('/logs/')) {
|
||||||
|
serveLogs(res, pathParts);
|
||||||
|
} else if (parsedUrl.path.startsWith('/attachments/')) {
|
||||||
|
serveAttachments(res, pathParts);
|
||||||
|
} else {
|
||||||
|
notfound(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(config.port);
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
const threads = require('./data/threads');
|
||||||
|
const utils = require("./utils");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {Eris~CommandClient} bot
|
||||||
|
* @param {String} cmd
|
||||||
|
* @param {Function} commandHandler
|
||||||
|
* @param {Eris~CommandOptions} opts
|
||||||
|
*/
|
||||||
|
function addInboxServerCommand(bot, cmd, commandHandler, opts) {
|
||||||
|
bot.registerCommand(cmd, async (msg, args) => {
|
||||||
|
if (! utils.messageIsOnInboxServer(msg)) return;
|
||||||
|
if (! utils.isStaff(msg.member)) return;
|
||||||
|
|
||||||
|
const thread = await threads.findByChannelId(msg.channel.id);
|
||||||
|
commandHandler(msg, args, thread);
|
||||||
|
}, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addInboxServerCommand
|
||||||
|
};
|
12
src/utils.js
12
src/utils.js
|
@ -13,12 +13,18 @@ let inboxGuild = null;
|
||||||
let mainGuild = null;
|
let mainGuild = null;
|
||||||
let logChannel = null;
|
let logChannel = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Eris~Guild}
|
||||||
|
*/
|
||||||
function getInboxGuild() {
|
function getInboxGuild() {
|
||||||
if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.mailGuildId);
|
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;
|
return inboxGuild;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Eris~Guild}
|
||||||
|
*/
|
||||||
function getMainGuild() {
|
function getMainGuild() {
|
||||||
if (! mainGuild) mainGuild = bot.guilds.find(g => g.id === config.mainGuildId);
|
if (! mainGuild) mainGuild = bot.guilds.find(g => g.id === config.mainGuildId);
|
||||||
if (! mainGuild) console.warn('[WARN] The bot is not on the main server! If this is intentional, you can ignore this warning.');
|
if (! mainGuild) console.warn('[WARN] The bot is not on the main server! If this is intentional, you can ignore this warning.');
|
||||||
|
@ -28,7 +34,7 @@ function getMainGuild() {
|
||||||
/**
|
/**
|
||||||
* Returns the designated log channel, or the default channel if none is set
|
* Returns the designated log channel, or the default channel if none is set
|
||||||
* @param bot
|
* @param bot
|
||||||
* @returns {object}
|
* @returns {Eris~TextChannel}
|
||||||
*/
|
*/
|
||||||
function getLogChannel() {
|
function getLogChannel() {
|
||||||
const inboxGuild = getInboxGuild();
|
const inboxGuild = getInboxGuild();
|
||||||
|
@ -151,8 +157,8 @@ async function getSelfUrl(path = '') {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the highest hoisted role of the given member
|
* Returns the highest hoisted role of the given member
|
||||||
* @param {Eris.Member} member
|
* @param {Eris~Member} member
|
||||||
* @returns {Eris.Role}
|
* @returns {Eris~Role}
|
||||||
*/
|
*/
|
||||||
function getMainRole(member) {
|
function getMainRole(member) {
|
||||||
const roles = member.roles.map(id => member.guild.roles.get(id));
|
const roles = member.roles.map(id => member.guild.roles.get(id));
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
const http = require('http');
|
|
||||||
const mime = require('mime');
|
|
||||||
const url = require('url');
|
|
||||||
const fs = require('fs');
|
|
||||||
const config = require('./config');
|
|
||||||
const attachments = require('./data/attachments');
|
|
||||||
|
|
||||||
const port = config.port || 8890;
|
|
||||||
|
|
||||||
function serveLogs(res, pathParts) {
|
|
||||||
const token = pathParts[pathParts.length - 1];
|
|
||||||
if (token.match(/^[0-9a-f]+$/) === null) return res.end();
|
|
||||||
|
|
||||||
logs.findLogFile(token).then(logFilename => {
|
|
||||||
if (logFilename === null) return res.end();
|
|
||||||
|
|
||||||
fs.readFile(logs.getLogFilePath(logFilename), {encoding: 'utf8'}, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('Log not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/plain; charset=UTF-8');
|
|
||||||
res.end(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function serveAttachments(res, pathParts) {
|
|
||||||
const desiredFilename = pathParts[pathParts.length - 1];
|
|
||||||
const id = pathParts[pathParts.length - 2];
|
|
||||||
|
|
||||||
if (id.match(/^[0-9]+$/) === null) return res.end();
|
|
||||||
if (desiredFilename.match(/^[0-9a-z\._-]+$/i) === null) return res.end();
|
|
||||||
|
|
||||||
const attachmentPath = attachments.getPath(id);
|
|
||||||
fs.access(attachmentPath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('Attachment not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filenameParts = desiredFilename.split('.');
|
|
||||||
const ext = (filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : 'bin');
|
|
||||||
const fileMime = mime.lookup(ext);
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', fileMime);
|
|
||||||
|
|
||||||
const read = fs.createReadStream(attachmentPath);
|
|
||||||
read.pipe(res);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
const server = http.createServer((req, res) => {
|
|
||||||
const parsedUrl = url.parse(`http://${req.url}`);
|
|
||||||
const pathParts = parsedUrl.path.split('/').filter(v => v !== '');
|
|
||||||
|
|
||||||
if (parsedUrl.path.startsWith('/logs/')) serveLogs(res, pathParts);
|
|
||||||
if (parsedUrl.path.startsWith('/attachments/')) serveAttachments(res, pathParts);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
run,
|
|
||||||
};
|
|
Loading…
Reference in New Issue