Continue rewrite. Modularize greeting, snippet, and web server functionality.
parent
bb6d8e5dbf
commit
ad7aa66c99
|
@ -1,5 +1,11 @@
|
|||
# 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
|
||||
* 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.integer('status').unsigned().notNullable().index();
|
||||
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.bigInteger('channel_id').unsigned().nullable().unique();
|
||||
table.string('channel_id', 20).nullable().unique();
|
||||
table.dateTime('created_at').notNullable().index();
|
||||
});
|
||||
|
||||
|
@ -13,18 +13,18 @@ exports.up = async function(knex, Promise) {
|
|||
table.increments('id');
|
||||
table.string('thread_id', 36).notNullable().index().references('id').inTable('threads').onDelete('CASCADE');
|
||||
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.text('body').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();
|
||||
});
|
||||
|
||||
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.bigInteger('blocked_by').unsigned().nullable();
|
||||
table.string('blocked_by', 20).nullable();
|
||||
table.dateTime('blocked_at').notNullable();
|
||||
});
|
||||
|
||||
|
@ -32,7 +32,7 @@ exports.up = async function(knex, Promise) {
|
|||
table.string('trigger', 32).primary().notNullable();
|
||||
table.text('body').notNullable();
|
||||
table.integer('is_anonymous').unsigned().notNullable();
|
||||
table.bigInteger('created_by').unsigned().nullable();
|
||||
table.string('created_by', 20).nullable();
|
||||
table.dateTime('created_at').notNullable();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -413,13 +413,13 @@
|
|||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||
},
|
||||
"eris": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/eris/-/eris-0.7.2.tgz",
|
||||
"integrity": "sha512-YcgXHH81tk9/nbnwZZ47cQVtaAySjIJi/JJFt0lbIMTHhHa77zo682nLIJrBbU5P8u9LQUbWoqiFA/NabR3qww==",
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/eris/-/eris-0.8.4.tgz",
|
||||
"integrity": "sha512-mhQlh5iamo3Ls8xk1xJsu9rHgiW7wkR79e76Nuhrwu1fswnmC7WA/KypGpI51G5h9BFPMxSeFYXy+tyladhJtQ==",
|
||||
"requires": {
|
||||
"opusscript": "0.0.3",
|
||||
"opusscript": "0.0.4",
|
||||
"tweetnacl": "1.0.0",
|
||||
"ws": "3.2.0"
|
||||
"ws": "3.3.3"
|
||||
}
|
||||
},
|
||||
"error-ex": {
|
||||
|
@ -1668,9 +1668,9 @@
|
|||
}
|
||||
},
|
||||
"opusscript": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.3.tgz",
|
||||
"integrity": "sha1-zkZxf8jW+QHFGR5pSFyImgNqhnQ=",
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.4.tgz",
|
||||
"integrity": "sha512-bEPZFE2lhUJYQD5yfTFO4RhbRZ937x6hRwBC1YoGacT35bwDVwKFP1+amU8NYfZL/v4EU7ZTU3INTqzYAnuP7Q==",
|
||||
"optional": true
|
||||
},
|
||||
"os-homedir": {
|
||||
|
@ -3006,9 +3006,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"ultron": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz",
|
||||
"integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ="
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
||||
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
|
||||
},
|
||||
"unc-path-regex": {
|
||||
"version": "0.1.2",
|
||||
|
@ -3120,13 +3120,13 @@
|
|||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-3.2.0.tgz",
|
||||
"integrity": "sha512-hTS3mkXm/j85jTQOIcwVz3yK3up9xHgPtgEhDBOH3G18LDOZmSAG1omJeXejLKJakx+okv8vS1sopgs7rw0kVw==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
|
||||
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
|
||||
"requires": {
|
||||
"async-limiter": "1.0.0",
|
||||
"safe-buffer": "5.1.1",
|
||||
"ultron": "1.1.0"
|
||||
"ultron": "1.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"name": "modmailbot",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node --trace-warnings src/index.js",
|
||||
"start": "node src/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "./node_modules/.bin/eslint ./src"
|
||||
},
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"eris": "^0.7.2",
|
||||
"eris": "^0.8.4",
|
||||
"humanize-duration": "^3.10.0",
|
||||
"knex": "^0.14.2",
|
||||
"mime": "^1.3.4",
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
const moment = require('moment');
|
||||
|
||||
const bot = require('../bot');
|
||||
const knex = require('../knex');
|
||||
const utils = require('../utils');
|
||||
const config = require('../config');
|
||||
const attachments = require('./attachments');
|
||||
|
||||
const ThreadMessage = require('./ThreadMessage');
|
||||
|
||||
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} channel_id
|
||||
* @property {String} created_at
|
||||
* @property {Boolean} _wasCreated
|
||||
*/
|
||||
class Thread {
|
||||
constructor(props) {
|
||||
Object.assign(this, {_wasCreated: false}, props);
|
||||
Object.assign(this, props);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Eris.Member} moderator
|
||||
* @param {Eris~Member} moderator
|
||||
* @param {String} text
|
||||
* @param {Eris.Attachment[]} replyAttachments
|
||||
* @param {Eris~Attachment[]} replyAttachments
|
||||
* @param {Boolean} isAnonymous
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
@ -81,6 +85,7 @@ class Thread {
|
|||
user_id: moderator.id,
|
||||
user_name: logModUsername,
|
||||
body: logContent,
|
||||
is_anonymous: (isAnonymous ? 1 : 0),
|
||||
original_message_id: originalMessage.id
|
||||
});
|
||||
}
|
||||
|
@ -92,7 +97,12 @@ class Thread {
|
|||
async receiveUserReply(msg) {
|
||||
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 finalThreadContent;
|
||||
let attachmentSavePromise;
|
||||
|
@ -113,6 +123,7 @@ class Thread {
|
|||
user_id: this.user_id,
|
||||
user_name: `${msg.author.username}#${msg.author.discriminator}`,
|
||||
body: logContent,
|
||||
is_anonymous: 0,
|
||||
original_message_id: msg.id
|
||||
});
|
||||
|
||||
|
@ -128,8 +139,7 @@ class Thread {
|
|||
* @returns {Promise<Eris.Message>}
|
||||
*/
|
||||
async postToThreadChannel(text, file = null) {
|
||||
const channel = bot.getChannel(this.channel_id);
|
||||
return channel.createMessage(text, file);
|
||||
return bot.createMessage(this.channel_id, text, file);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,10 +153,58 @@ class Thread {
|
|||
user_id: null,
|
||||
user_name: '',
|
||||
body: text,
|
||||
is_anonymous: 0,
|
||||
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
|
||||
* @returns {Promise<void>}
|
||||
|
@ -155,14 +213,29 @@ class Thread {
|
|||
await knex('thread_messages').insert({
|
||||
thread_id: this.id,
|
||||
created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'),
|
||||
is_anonymous: 0,
|
||||
...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>}
|
||||
*/
|
||||
async close() {
|
||||
console.log(`Closing thread ${this.id}`);
|
||||
await this.postToThreadChannel('Closing thread...');
|
||||
|
||||
// Update DB status
|
||||
|
@ -173,9 +246,10 @@ class Thread {
|
|||
});
|
||||
|
||||
// Delete channel
|
||||
console.log(`Deleting channel ${this.channel_id}`);
|
||||
const channel = bot.getChannel(this.channel_id);
|
||||
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 attachmentDir = config.attachmentDir || `${__dirname}/../attachments`;
|
||||
const attachmentDir = config.attachmentDir || `${__dirname}/../../attachments`;
|
||||
|
||||
/**
|
||||
* Returns the filesystem path for the given attachment id
|
||||
|
@ -36,7 +36,7 @@ function saveAttachment(attachment, tries = 0) {
|
|||
https.get(attachment.url, (res) => {
|
||||
res.pipe(writeStream);
|
||||
writeStream.on('finish', () => {
|
||||
writeStream.closeByChannelId()
|
||||
writeStream.end();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
|
|
|
@ -12,6 +12,14 @@ const utils = require('../utils');
|
|||
const Thread = require('./Thread');
|
||||
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
|
||||
* @returns {Promise<Thread>}
|
||||
|
@ -20,7 +28,7 @@ async function findOpenThreadByUserId(userId) {
|
|||
const thread = await knex('threads')
|
||||
.where('user_id', userId)
|
||||
.where('status', THREAD_STATUS.OPEN)
|
||||
.select();
|
||||
.first();
|
||||
|
||||
return (thread ? new Thread(thread) : null);
|
||||
}
|
||||
|
@ -50,11 +58,7 @@ async function createNewThreadForUser(user) {
|
|||
// Attempt to create the inbox channel for this thread
|
||||
let createdChannel;
|
||||
try {
|
||||
createdChannel = await utils.getInboxGuild().createChannel(channelName);
|
||||
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});
|
||||
}
|
||||
createdChannel = await utils.getInboxGuild().createChannel(channelName, null, 'New ModMail thread', config.newThreadCategoryId);
|
||||
} catch (err) {
|
||||
console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`);
|
||||
throw err;
|
||||
|
@ -71,6 +75,10 @@ async function createNewThreadForUser(user) {
|
|||
|
||||
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
|
||||
const mainGuild = utils.getMainGuild();
|
||||
const member = (mainGuild ? mainGuild.members.get(user.id) : null);
|
||||
|
@ -134,6 +142,19 @@ async function findByChannelId(channelId) {
|
|||
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
|
||||
* @returns {Promise<Thread[]>}
|
||||
|
@ -160,9 +181,20 @@ async function getClosedThreadCountByUserId(userId) {
|
|||
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 = {
|
||||
findById,
|
||||
findOpenThreadByUserId,
|
||||
findByChannelId,
|
||||
findOpenThreadByChannelId,
|
||||
createNewThreadForUser,
|
||||
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');
|
||||
|
||||
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 utils = require('./utils');
|
||||
const main = require('./main');
|
||||
|
|
|
@ -9,6 +9,8 @@ const config = require('../config');
|
|||
const jsonDb = require('./jsonDb');
|
||||
const threads = require('../data/threads');
|
||||
|
||||
const {THREAD_STATUS, THREAD_MESSAGE_TYPE} = require('../data/constants');
|
||||
|
||||
const readDir = promisify(fs.readdir);
|
||||
const readFile = promisify(fs.readFile);
|
||||
const access = promisify(fs.access);
|
||||
|
@ -75,7 +77,7 @@ async function migrateOpenThreads() {
|
|||
if (existingOpenThread) return;
|
||||
|
||||
const newThread = {
|
||||
status: threads.THREAD_STATUS.OPEN,
|
||||
status: THREAD_STATUS.OPEN,
|
||||
user_id: oldThread.userId,
|
||||
user_name: oldThread.username,
|
||||
channel_id: oldThread.channelId,
|
||||
|
@ -103,7 +105,7 @@ async function migrateLogs() {
|
|||
|
||||
const newThread = {
|
||||
id: threadId,
|
||||
status: threads.THREAD_STATUS.CLOSED,
|
||||
status: THREAD_STATUS.CLOSED,
|
||||
user_id: userId,
|
||||
user_name: '',
|
||||
channel_id: null,
|
||||
|
@ -122,10 +124,11 @@ async function migrateLogs() {
|
|||
|
||||
await trx('thread_messages').insert({
|
||||
thread_id: newThread.id,
|
||||
message_type: threads.THREAD_MESSAGE_TYPE.LEGACY,
|
||||
message_type: THREAD_MESSAGE_TYPE.LEGACY,
|
||||
user_id: userId,
|
||||
user_name: '',
|
||||
body: contents,
|
||||
is_anonymous: 0,
|
||||
created_at: date
|
||||
});
|
||||
});
|
||||
|
|
287
src/main.js
287
src/main.js
|
@ -5,48 +5,28 @@ const config = require('./config');
|
|||
const bot = require('./bot');
|
||||
const Queue = require('./queue');
|
||||
const utils = require('./utils');
|
||||
const threadUtils = require('./threadUtils');
|
||||
const blocked = require('./data/blocked');
|
||||
const threads = require('./data/threads');
|
||||
const snippets = require('./data/snippets');
|
||||
const webserver = require('./webserver');
|
||||
const greeting = require('./greeting');
|
||||
const Thread = require('./data/Thread');
|
||||
|
||||
const snippets = require('./plugins/snippets');
|
||||
const webserver = require('./plugins/webserver');
|
||||
const greeting = require('./plugins/greeting');
|
||||
|
||||
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);
|
||||
}
|
||||
const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);
|
||||
|
||||
// Once the bot has connected, set the status/"playing" message
|
||||
bot.on('ready', () => {
|
||||
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 => {
|
||||
if (! utils.messageIsOnInboxServer(msg)) return;
|
||||
if (! utils.isStaff(msg)) return;
|
||||
|
@ -62,17 +42,81 @@ bot.on('messageCreate', async msg => {
|
|||
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
|
||||
});
|
||||
thread.saveChatMessage(msg);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 => {
|
||||
if (! utils.messageIsOnMainServer(msg)) 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
|
||||
// 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;
|
||||
|
||||
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');
|
||||
|
||||
// Anonymous replies only show the role, not the username
|
||||
addInboxServerCommand('anonreply', (msg, args, thread) => {
|
||||
addInboxServerCommand('anonreply', async (msg, args, thread) => {
|
||||
if (! thread) return;
|
||||
|
||||
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');
|
||||
|
@ -168,7 +175,7 @@ addInboxServerCommand('block', (msg, args, thread) => {
|
|||
block(userId);
|
||||
} else if (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);
|
||||
} else if (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);
|
||||
} else if (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 = {
|
||||
start() {
|
||||
bot.connect();
|
||||
// webserver.run();
|
||||
greeting.init(bot);
|
||||
async start() {
|
||||
// Load plugins
|
||||
console.log('Loading plugins...');
|
||||
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 fs = require('fs');
|
||||
const config = require('./config');
|
||||
const config = require('../config');
|
||||
|
||||
const greetingGuildId = config.mainGuildId || config.greetingGuildId;
|
||||
|
||||
function init(bot) {
|
||||
module.exports = bot => {
|
||||
if (! config.enableGreeting) return;
|
||||
|
||||
const greetingGuildId = config.mainGuildId || config.greetingGuildId;
|
||||
|
||||
bot.on('guildMemberAdd', (guild, member) => {
|
||||
if (guild.id !== greetingGuildId) return;
|
||||
|
||||
|
@ -27,8 +27,4 @@ function init(bot) {
|
|||
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 logChannel = null;
|
||||
|
||||
/**
|
||||
* @returns {Eris~Guild}
|
||||
*/
|
||||
function getInboxGuild() {
|
||||
if (! inboxGuild) inboxGuild = bot.guilds.find(g => g.id === config.mailGuildId);
|
||||
if (! inboxGuild) throw new BotError('The bot is not on the modmail (inbox) server!');
|
||||
return inboxGuild;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Eris~Guild}
|
||||
*/
|
||||
function getMainGuild() {
|
||||
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.');
|
||||
|
@ -28,7 +34,7 @@ function getMainGuild() {
|
|||
/**
|
||||
* Returns the designated log channel, or the default channel if none is set
|
||||
* @param bot
|
||||
* @returns {object}
|
||||
* @returns {Eris~TextChannel}
|
||||
*/
|
||||
function getLogChannel() {
|
||||
const inboxGuild = getInboxGuild();
|
||||
|
@ -151,8 +157,8 @@ async function getSelfUrl(path = '') {
|
|||
|
||||
/**
|
||||
* Returns the highest hoisted role of the given member
|
||||
* @param {Eris.Member} member
|
||||
* @returns {Eris.Role}
|
||||
* @param {Eris~Member} member
|
||||
* @returns {Eris~Role}
|
||||
*/
|
||||
function getMainRole(member) {
|
||||
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