2017-12-24 15:04:08 -05:00
const Eris = require ( 'eris' ) ;
const moment = require ( 'moment' ) ;
2018-02-18 17:23:29 -05:00
const transliterate = require ( 'transliteration' ) ;
2017-12-24 15:04:08 -05:00
const config = require ( './config' ) ;
const bot = require ( './bot' ) ;
const Queue = require ( './queue' ) ;
const utils = require ( './utils' ) ;
2018-02-11 14:54:30 -05:00
const threadUtils = require ( './threadUtils' ) ;
2017-12-24 15:04:08 -05:00
const blocked = require ( './data/blocked' ) ;
const threads = require ( './data/threads' ) ;
2018-02-11 14:54:30 -05:00
const snippets = require ( './plugins/snippets' ) ;
2018-03-11 17:17:14 -04:00
const logCommands = require ( './plugins/logCommands' ) ;
const moving = require ( './plugins/moving' ) ;
const blocking = require ( './plugins/blocking' ) ;
const suspending = require ( './plugins/suspending' ) ;
2018-02-11 14:54:30 -05:00
const webserver = require ( './plugins/webserver' ) ;
const greeting = require ( './plugins/greeting' ) ;
2018-02-14 01:53:34 -05:00
const attachments = require ( "./data/attachments" ) ;
2018-02-18 15:52:37 -05:00
const { ACCIDENTAL _THREAD _MESSAGES } = require ( './data/constants' ) ;
2017-12-31 19:16:05 -05:00
2018-02-11 14:54:30 -05:00
const messageQueue = new Queue ( ) ;
2017-12-31 19:16:05 -05:00
2018-02-11 14:54:30 -05:00
const addInboxServerCommand = ( ... args ) => threadUtils . addInboxServerCommand ( bot , ... args ) ;
2017-12-31 19:16:05 -05:00
2017-12-24 15:04:08 -05:00
// Once the bot has connected, set the status/"playing" message
bot . on ( 'ready' , ( ) => {
bot . editStatus ( null , { name : config . status } ) ;
} ) ;
2018-02-11 14:54:30 -05:00
/ * *
* 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
* /
2017-12-31 19:16:05 -05:00
bot . on ( 'messageCreate' , async msg => {
if ( ! utils . messageIsOnInboxServer ( msg ) ) return ;
if ( msg . author . bot ) return ;
const thread = await threads . findByChannelId ( msg . channel . id ) ;
if ( ! thread ) return ;
2017-12-24 15:04:08 -05:00
2018-03-11 17:17:14 -04:00
if ( msg . content . startsWith ( config . prefix ) || msg . content . startsWith ( config . snippetPrefix ) ) {
// Save commands as "command messages"
if ( msg . content . startsWith ( config . snippetPrefix ) ) return ; // Ignore snippets
thread . saveCommandMessage ( msg ) ;
} else if ( config . alwaysReply ) {
2017-12-31 19:16:05 -05:00
// AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
2018-03-11 17:17:14 -04:00
if ( ! utils . isStaff ( msg . member ) ) return ; // Only staff are allowed to reply
2018-02-14 01:53:34 -05:00
if ( msg . attachments . length ) await attachments . saveAttachmentsInMessage ( msg ) ;
2017-12-31 19:16:05 -05:00
await thread . replyToUser ( msg . member , msg . content . trim ( ) , msg . attachments , config . alwaysReplyAnon || false ) ;
msg . delete ( ) ;
} else {
// Otherwise just save the messages as "chat" in the logs
2018-02-11 14:54:30 -05:00
thread . saveChatMessage ( msg ) ;
2017-12-31 19:16:05 -05:00
}
} ) ;
2017-12-24 15:04:08 -05:00
2018-02-11 14:54:30 -05:00
/ * *
* 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
* /
2017-12-24 15:04:08 -05:00
bot . on ( 'messageCreate' , async msg => {
if ( ! ( msg . channel instanceof Eris . PrivateChannel ) ) return ;
2018-02-11 14:54:30 -05:00
if ( msg . author . bot ) return ;
if ( msg . type !== 0 ) return ; // Ignore pins etc.
2017-12-24 15:04:08 -05:00
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 ( ) => {
2018-02-18 15:52:37 -05:00
let thread = await threads . findOpenThreadByUserId ( msg . author . id ) ;
// New thread
if ( ! thread ) {
// Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc.
2018-02-20 05:57:34 -05:00
if ( config . ignoreAccidentalThreads && msg . content && ACCIDENTAL _THREAD _MESSAGES . includes ( msg . content . trim ( ) . toLowerCase ( ) ) ) return ;
2018-02-18 15:52:37 -05:00
thread = await threads . createNewThreadForUser ( msg . author ) ;
}
2018-02-11 14:54:30 -05:00
await thread . receiveUserReply ( msg ) ;
2017-12-24 15:04:08 -05:00
} ) ;
} ) ;
2018-02-11 14:54:30 -05:00
/ * *
* 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
* /
2017-12-24 15:04:08 -05:00
bot . on ( 'messageUpdate' , async ( msg , oldMessage ) => {
2018-02-18 19:03:53 -05:00
if ( ! msg || ! msg . author ) return ;
2018-02-11 14:54:30 -05:00
if ( msg . author . bot ) return ;
2017-12-24 15:04:08 -05:00
if ( await blocked . isBlocked ( msg . author . id ) ) return ;
// Old message content doesn't persist between bot restarts
2018-02-18 15:30:10 -05:00
const oldContent = oldMessage && oldMessage . content || '*Unavailable due to bot restart*' ;
const newContent = msg . content ;
2017-12-24 15:04:08 -05:00
// Ignore bogus edit events with no changes
if ( newContent . trim ( ) === oldContent . trim ( ) ) return ;
2018-02-11 14:54:30 -05:00
// 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 => {
2018-02-18 19:03:53 -05:00
if ( ! msg . author ) return ;
2018-02-11 14:54:30 -05:00
if ( msg . author . bot ) return ;
if ( ! utils . messageIsOnInboxServer ( msg ) ) return ;
if ( ! utils . isStaff ( msg . member ) ) return ;
const thread = await threads . findOpenThreadByChannelId ( msg . channel . id ) ;
2017-12-24 15:04:08 -05:00
if ( ! thread ) return ;
2018-02-11 14:54:30 -05:00
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 ;
// If the person who mentioned the modmail bot is also on the modmail server, ignore them
if ( utils . getInboxGuild ( ) . members . get ( msg . author . id ) ) return ;
// If the person who mentioned the bot is blocked, ignore them
if ( await blocked . isBlocked ( msg . author . id ) ) return ;
bot . createMessage ( utils . getLogChannel ( bot ) . id , {
2018-03-11 15:55:47 -04:00
content : ` ${ utils . getInboxMention ( ) } Bot mentioned in ${ msg . channel . mention } by ** ${ msg . author . username } # ${ msg . author . discriminator } **: " ${ msg . cleanContent } " ` ,
2018-02-11 14:54:30 -05:00
disableEveryone : false ,
} ) ;
2017-12-24 15:04:08 -05:00
} ) ;
2018-02-24 16:16:28 -05:00
// Typing proxy: forwarding typing events between the DM and the modmail thread
if ( config . typingProxy || config . typingProxyReverse ) {
bot . on ( "typingStart" , async ( channel , user ) => {
// config.typingProxy: forward user typing in a DM to the modmail thread
if ( config . typingProxy && ( channel instanceof Eris . PrivateChannel ) ) {
const thread = await threads . findOpenThreadByUserId ( user . id ) ;
if ( ! thread ) return ;
try {
await bot . sendChannelTyping ( thread . channel _id ) ;
} catch ( e ) { }
}
// config.typingProxyReverse: forward moderator typing in a thread to the DM
else if ( config . typingProxyReverse && ( channel instanceof Eris . GuildChannel ) && ! user . bot ) {
const thread = await threads . findByChannelId ( channel . id ) ;
if ( ! thread ) return ;
2018-03-11 15:32:14 -04:00
const dmChannel = await thread . getDMChannel ( ) ;
2018-02-24 16:16:28 -05:00
if ( ! dmChannel ) return ;
try {
await bot . sendChannelTyping ( dmChannel . id ) ;
} catch ( e ) { }
}
} ) ;
}
2018-03-11 15:32:14 -04:00
// Check for threads that are scheduled to be closed and close them
async function applyScheduledCloses ( ) {
const threadsToBeClosed = await threads . getThreadsThatShouldBeClosed ( ) ;
for ( const thread of threadsToBeClosed ) {
await thread . close ( ) ;
const logUrl = await thread . getLogUrl ( ) ;
utils . postLog ( utils . trimAll ( `
Modmail thread with $ { thread . user _name } ( $ { thread . user _id } ) was closed as scheduled by $ { thread . scheduled _close _name }
Logs : $ { logUrl }
` ));
}
}
2018-03-11 16:45:43 -04:00
async function scheduledCloseLoop ( ) {
2018-03-11 15:32:14 -04:00
try {
await applyScheduledCloses ( ) ;
} catch ( e ) {
console . error ( e ) ;
}
2018-03-11 16:45:43 -04:00
setTimeout ( scheduledCloseLoop , 2000 ) ;
2018-03-11 15:32:14 -04:00
}
2018-03-11 16:15:16 -04:00
// Auto-close threads if their channel is deleted
bot . on ( 'channelDelete' , async ( channel ) => {
if ( ! ( channel instanceof Eris . TextChannel ) ) return ;
if ( channel . guild . id !== utils . getInboxGuild ( ) . id ) return ;
const thread = await threads . findOpenThreadByChannelId ( channel . id ) ;
if ( ! thread ) return ;
console . log ( ` [INFO] Auto-closing thread with ${ thread . user _name } because the channel was deleted ` ) ;
await thread . close ( true ) ;
const logUrl = await thread . getLogUrl ( ) ;
utils . postLog ( utils . trimAll ( `
Modmail thread with $ { thread . user _name } ( $ { thread . user _id } ) was closed automatically because the channel was deleted
Logs : $ { logUrl }
` ));
} ) ;
2017-12-24 15:04:08 -05:00
// 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
2018-02-11 14:54:30 -05:00
addInboxServerCommand ( 'reply' , async ( msg , args , thread ) => {
2017-12-31 19:16:05 -05:00
if ( ! thread ) return ;
2018-02-11 14:54:30 -05:00
2017-12-24 15:04:08 -05:00
const text = args . join ( ' ' ) . trim ( ) ;
2018-02-14 01:53:34 -05:00
if ( msg . attachments . length ) await attachments . saveAttachmentsInMessage ( msg ) ;
2018-02-11 14:54:30 -05:00
await thread . replyToUser ( msg . member , text , msg . attachments , false ) ;
msg . delete ( ) ;
2017-12-24 15:04:08 -05:00
} ) ;
bot . registerCommandAlias ( 'r' , 'reply' ) ;
// Anonymous replies only show the role, not the username
2018-02-11 14:54:30 -05:00
addInboxServerCommand ( 'anonreply' , async ( msg , args , thread ) => {
2017-12-31 19:16:05 -05:00
if ( ! thread ) return ;
2018-02-11 14:54:30 -05:00
2017-12-24 15:04:08 -05:00
const text = args . join ( ' ' ) . trim ( ) ;
2018-02-14 01:53:34 -05:00
if ( msg . attachments . length ) await attachments . saveAttachmentsInMessage ( msg ) ;
2018-02-11 14:54:30 -05:00
await thread . replyToUser ( msg . member , text , msg . attachments , true ) ;
msg . delete ( ) ;
2017-12-24 15:04:08 -05:00
} ) ;
bot . registerCommandAlias ( 'ar' , 'anonreply' ) ;
// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
2017-12-31 19:16:05 -05:00
addInboxServerCommand ( 'close' , async ( msg , args , thread ) => {
2017-12-24 15:04:08 -05:00
if ( ! thread ) return ;
2018-03-11 15:32:14 -04:00
// Timed close
if ( args . length ) {
if ( args [ 0 ] === 'cancel' ) {
// Cancel timed close
if ( thread . scheduled _close _at ) {
await thread . cancelScheduledClose ( ) ;
thread . postSystemMessage ( ` Cancelled scheduled closing ` ) ;
}
return ;
}
// Set a timed close
const delay = utils . convertDelayStringToMS ( args . join ( ' ' ) ) ;
if ( delay === 0 ) {
2018-03-11 17:17:14 -04:00
thread . postSystemMessage ( ` Invalid delay specified. Format: "1h30m" ` ) ;
2018-03-11 15:32:14 -04:00
return ;
}
const closeAt = moment . utc ( ) . add ( delay , 'ms' ) ;
await thread . scheduleClose ( closeAt . format ( 'YYYY-MM-DD HH:mm:ss' ) , msg . author ) ;
thread . postSystemMessage ( ` Thread is scheduled to be closed ${ moment . duration ( delay ) . humanize ( true ) } by ${ msg . author . username } . Use \` ${ config . prefix } close cancel \` to cancel. ` ) ;
return ;
}
// Regular close
2018-02-18 14:21:03 -05:00
await thread . close ( ) ;
const logUrl = await thread . getLogUrl ( ) ;
utils . postLog ( utils . trimAll ( `
Modmail thread with $ { thread . user _name } ( $ { thread . user _id } ) was closed by $ { msg . author . username }
Logs : $ { logUrl }
` ));
2017-12-24 15:04:08 -05:00
} ) ;
2018-02-11 14:54:30 -05:00
module . exports = {
async start ( ) {
// Load plugins
console . log ( 'Loading plugins...' ) ;
2018-03-11 17:17:14 -04:00
await logCommands ( bot ) ;
await blocking ( bot ) ;
await moving ( bot ) ;
2018-02-11 14:54:30 -05:00
await snippets ( bot ) ;
2018-03-11 17:17:14 -04:00
await suspending ( bot ) ;
2018-02-11 14:54:30 -05:00
await greeting ( bot ) ;
await webserver ( bot ) ;
2017-12-24 15:04:08 -05:00
2018-03-11 16:45:43 -04:00
// Connect to Discord
2018-02-11 14:54:30 -05:00
console . log ( 'Connecting to Discord...' ) ;
await bot . connect ( ) ;
2017-12-24 15:04:08 -05:00
2018-03-11 16:45:43 -04:00
// Start scheduled close loop
scheduledCloseLoop ( ) ;
2018-02-11 14:54:30 -05:00
console . log ( 'Done! Now listening to DMs.' ) ;
2017-12-24 15:04:08 -05:00
}
} ;