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' ) ;
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 ;
2018-02-18 14:07:26 -05:00
if ( ! utils . isStaff ( msg . member ) ) return ;
2017-12-31 19:16:05 -05:00
if ( msg . author . bot ) return ;
if ( msg . content . startsWith ( config . prefix ) || msg . content . startsWith ( config . snippetPrefix ) ) return ;
const thread = await threads . findByChannelId ( msg . channel . id ) ;
if ( ! thread ) return ;
2017-12-24 15:04:08 -05:00
2017-12-31 19:16:05 -05:00
if ( config . alwaysReply ) {
// AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
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 }
` ));
}
}
async function closeLoop ( ) {
try {
await applyScheduledCloses ( ) ;
} catch ( e ) {
console . error ( e ) ;
}
setTimeout ( closeLoop , 2000 ) ;
}
closeLoop ( ) ;
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 ) {
thread . postNonLogMessage ( ` Invalid delay specified. Format: "1h30m" ` ) ;
return ;
}
const closeAt = moment . utc ( ) . add ( delay , 'ms' ) ;
await thread . scheduleClose ( closeAt . format ( 'YYYY-MM-DD HH:mm:ss' ) , msg . author ) ;
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
} ) ;
2017-12-31 19:16:05 -05:00
addInboxServerCommand ( 'block' , ( msg , args , thread ) => {
2017-12-24 15:04:08 -05:00
async function block ( userId ) {
2018-02-24 06:02:54 -05:00
const user = bot . users . get ( userId ) ;
await blocked . block ( userId , ( user ? ` ${ user . username } # ${ user . discriminator } ` : '' ) , msg . author . id ) ;
2017-12-24 15:04:08 -05:00
msg . channel . createMessage ( ` Blocked <@ ${ userId } > (id ${ userId } ) from modmail ` ) ;
}
if ( args . length > 0 ) {
// User mention/id as argument
const userId = utils . getUserMention ( args . join ( ' ' ) ) ;
if ( ! userId ) return ;
block ( userId ) ;
} else if ( thread ) {
// Calling !block without args in a modmail thread blocks the user of that thread
2018-02-11 14:54:30 -05:00
block ( thread . user _id ) ;
2017-12-24 15:04:08 -05:00
}
} ) ;
2017-12-31 19:16:05 -05:00
addInboxServerCommand ( 'unblock' , ( msg , args , thread ) => {
2017-12-24 15:04:08 -05:00
async function unblock ( userId ) {
await blocked . unblock ( userId ) ;
msg . channel . createMessage ( ` Unblocked <@ ${ userId } > (id ${ userId } ) from modmail ` ) ;
}
if ( args . length > 0 ) {
// User mention/id as argument
const userId = utils . getUserMention ( args . join ( ' ' ) ) ;
if ( ! userId ) return ;
unblock ( userId ) ;
} else if ( thread ) {
// Calling !unblock without args in a modmail thread unblocks the user of that thread
2018-02-11 14:54:30 -05:00
unblock ( thread . user _id ) ;
2017-12-24 15:04:08 -05:00
}
} ) ;
2017-12-31 19:16:05 -05:00
addInboxServerCommand ( 'logs' , ( msg , args , thread ) => {
2017-12-24 15:04:08 -05:00
async function getLogs ( userId ) {
2017-12-31 19:16:05 -05:00
const userThreads = await threads . getClosedThreadsByUserId ( userId ) ;
2018-02-18 14:09:52 -05:00
// Descending by date
userThreads . sort ( ( a , b ) => {
if ( a . created _at > b . created _at ) return - 1 ;
if ( a . created _at < b . created _at ) return 1 ;
return 0 ;
} ) ;
2018-02-18 12:43:32 -05:00
2017-12-31 19:16:05 -05:00
const threadLines = await Promise . all ( userThreads . map ( async thread => {
const logUrl = await thread . getLogUrl ( ) ;
const formattedDate = moment . utc ( thread . created _at ) . format ( 'MMM Do [at] HH:mm [UTC]' ) ;
return ` \` ${ formattedDate } \` : < ${ logUrl } > ` ;
} ) ) ;
2017-12-24 15:04:08 -05:00
2017-12-31 19:16:05 -05:00
const message = ` **Log files for <@ ${ userId } >:** \n ${ threadLines . join ( '\n' ) } ` ;
2017-12-24 15:04:08 -05:00
// Send the list of logs in chunks of 15 lines per message
const lines = message . split ( '\n' ) ;
const chunks = utils . chunk ( lines , 15 ) ;
let root = Promise . resolve ( ) ;
chunks . forEach ( lines => {
root = root . then ( ( ) => msg . channel . createMessage ( lines . join ( '\n' ) ) ) ;
} ) ;
}
if ( args . length > 0 ) {
// User mention/id as argument
const userId = utils . getUserMention ( args . join ( ' ' ) ) ;
if ( ! userId ) return ;
getLogs ( userId ) ;
} else if ( thread ) {
// Calling !logs without args in a modmail thread returns the logs of the user of that thread
2018-02-11 14:54:30 -05:00
getLogs ( thread . user _id ) ;
2017-12-24 15:04:08 -05:00
}
} ) ;
2018-02-18 17:23:29 -05:00
addInboxServerCommand ( 'move' , async ( msg , args , thread ) => {
if ( ! config . allowMove ) return ;
if ( ! thread ) return ;
const searchStr = args [ 0 ] ;
if ( ! searchStr || searchStr . trim ( ) === '' ) return ;
const normalizedSearchStr = transliterate . slugify ( searchStr ) ;
const categories = msg . channel . guild . channels . filter ( c => {
// Filter to categories that are not the thread's current parent category
return ( c instanceof Eris . CategoryChannel ) && ( c . id !== msg . channel . parentID ) ;
} ) ;
if ( categories . length === 0 ) return ;
// See if any category name contains a part of the search string
const containsRankings = categories . map ( cat => {
const normalizedCatName = transliterate . slugify ( cat . name ) ;
let i ;
for ( i = 1 ; i < normalizedSearchStr . length ; i ++ ) {
if ( ! normalizedCatName . includes ( normalizedSearchStr . slice ( 0 , i ) ) ) {
i -- ;
break ;
}
}
return [ cat , i ] ;
} ) ;
// Sort by best match
containsRankings . sort ( ( a , b ) => {
return a [ 1 ] > b [ 1 ] ? - 1 : 1 ;
} ) ;
if ( containsRankings [ 0 ] [ 1 ] === 0 ) {
thread . postNonLogMessage ( 'No matching category' ) ;
return ;
}
const targetCategory = containsRankings [ 0 ] [ 0 ] ;
await bot . editChannel ( thread . channel _id , {
parentID : targetCategory . id
} ) ;
thread . postSystemMessage ( ` Thread moved to ${ targetCategory . name . toUpperCase ( ) } ` ) ;
} ) ;
2018-02-18 17:45:56 -05:00
addInboxServerCommand ( 'loglink' , async ( msg , args , thread ) => {
if ( ! thread ) return ;
const logUrl = await thread . getLogUrl ( ) ;
thread . postNonLogMessage ( ` Log URL: ${ logUrl } ` ) ;
} ) ;
2018-03-11 16:27:52 -04:00
addInboxServerCommand ( 'suspend' , async ( msg , args , thread ) => {
if ( ! thread ) return ;
await thread . suspend ( ) ;
thread . postSystemMessage ( ` **Thread suspended!** This thread will act as closed until unsuspended with \` !unsuspend \` ` ) ;
} ) ;
addInboxServerCommand ( 'unsuspend' , async ( msg , args ) => {
const thread = await threads . findSuspendedThreadByChannelId ( msg . channel . id ) ;
if ( ! thread ) return ;
const otherOpenThread = await threads . findOpenThreadByUserId ( thread . user _id ) ;
if ( otherOpenThread ) {
thread . postSystemMessage ( ` Cannot unsuspend; there is another open thread with this user: <# ${ otherOpenThread . channel _id } > ` ) ;
return ;
}
await thread . unsuspend ( ) ;
thread . postSystemMessage ( ` **Thread unsuspended!** ` ) ;
} ) ;
2018-02-11 14:54:30 -05:00
module . exports = {
async start ( ) {
// Load plugins
console . log ( 'Loading plugins...' ) ;
await snippets ( bot ) ;
await greeting ( bot ) ;
await webserver ( bot ) ;
2017-12-24 15:04:08 -05:00
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-02-11 14:54:30 -05:00
console . log ( 'Done! Now listening to DMs.' ) ;
2017-12-24 15:04:08 -05:00
}
} ;