2017-02-09 21:56:36 -05:00
const Eris = require ( 'eris' ) ;
2017-02-09 23:36:47 -05:00
const fs = require ( 'fs' ) ;
2017-02-09 21:56:36 -05:00
const moment = require ( 'moment' ) ;
2017-02-14 17:57:41 -05:00
const humanizeDuration = require ( 'humanize-duration' ) ;
2017-02-09 21:56:36 -05:00
const config = require ( '../config' ) ;
2017-02-09 23:36:47 -05:00
const Queue = require ( './queue' ) ;
2017-02-09 21:56:36 -05:00
const utils = require ( './utils' ) ;
const blocked = require ( './blocked' ) ;
const threads = require ( './threads' ) ;
const logs = require ( './logs' ) ;
const attachments = require ( './attachments' ) ;
const webserver = require ( './webserver' ) ;
2017-02-10 00:04:23 -05:00
const greeting = require ( './greeting' ) ;
2017-02-09 21:56:36 -05:00
const bot = new Eris . CommandClient ( config . token , { } , {
prefix : config . prefix || '!' ,
ignoreSelf : true ,
ignoreBots : true ,
defaultHelpCommand : false ,
} ) ;
2017-02-14 17:57:41 -05:00
const restBot = new Eris . Client ( ` Bot ${ config . token } ` , {
restMode : true ,
} ) ;
2017-02-09 21:56:36 -05:00
const messageQueue = new Queue ( ) ;
bot . on ( 'ready' , ( ) => {
bot . editStatus ( null , { name : config . status || 'Message me for help' } ) ;
console . log ( 'Bot started, listening to DMs' ) ;
} ) ;
2017-02-14 17:57:41 -05:00
restBot . on ( 'ready' , ( ) => {
console . log ( 'Rest client ready' ) ;
} ) ;
2017-02-09 23:36:47 -05:00
function formatAttachment ( attachment ) {
let filesize = attachment . size || 0 ;
filesize /= 1024 ;
return attachments . getUrl ( attachment . id , attachment . filename ) . then ( attachmentUrl => {
return ` **Attachment:** ${ attachment . filename } ( ${ filesize . toFixed ( 1 ) } KB) \n ${ attachmentUrl } ` ;
} ) ;
}
2017-05-01 05:14:28 -04:00
// "Forward all messages not starting in prefix"
if ( config . alwaysReply ) {
bot . on ( 'messageCreate' , msg => {
if ( ! msg . channel . guild ) return ;
if ( msg . channel . guild . id !== utils . getModmailGuild ( bot ) . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
if ( msg . author . bot ) return ;
if ( msg . content [ 0 ] == bot . commandOptions . prefix ) return ;
reply ( msg , msg . content . trim ( ) , config . alwaysReplyAnon || false ) ;
} ) ;
}
2017-05-01 03:56:49 -04:00
2017-02-09 21:56:36 -05:00
// "Bot was mentioned in #general-discussion"
bot . on ( 'messageCreate' , msg => {
if ( msg . author . id === bot . user . id ) return ;
if ( msg . mentions . some ( user => user . id === bot . user . id ) ) {
2017-02-10 00:04:23 -05:00
// If the person who mentioned the modmail bot is on the modmail server, don't ping about it
if ( utils . getModmailGuild ( bot ) . members . get ( msg . author . id ) ) return ;
2017-02-09 21:56:36 -05:00
blocked . isBlocked ( msg . author . id ) . then ( isBlocked => {
if ( isBlocked ) return ;
bot . createMessage ( utils . getModmailGuild ( bot ) . id , {
content : ` @here Bot mentioned in ${ msg . channel . mention } by ** ${ msg . author . username } # ${ msg . author . discriminator } **: " ${ msg . cleanContent } " ` ,
disableEveryone : false ,
} ) ;
} ) ;
}
} ) ;
// When we get a private message, forward the contents to the corresponding modmail thread
bot . on ( 'messageCreate' , ( msg ) => {
if ( ! ( msg . channel instanceof Eris . PrivateChannel ) ) return ;
if ( msg . author . id === bot . user . id ) return ;
blocked . isBlocked ( msg . author . id ) . then ( isBlocked => {
if ( isBlocked ) return ;
// Download and save copies of attachments in the background
2017-05-17 22:20:19 -04:00
const attachmentSavePromise = attachments . saveAttachmentsInMessage ( msg ) ;
2017-02-09 21:56:36 -05:00
2017-02-09 23:36:47 -05:00
let thread , userLogs ;
2017-02-09 21:56:36 -05:00
2017-05-17 22:20:19 -04:00
// Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
2017-02-09 21:56:36 -05:00
messageQueue . add ( ( ) => {
2017-02-09 23:36:47 -05:00
return threads . getForUser ( bot , msg . author )
2017-02-09 21:56:36 -05:00
. then ( userThread => {
thread = userThread ;
return logs . getLogsByUserId ( msg . author . id ) ;
} )
2017-02-09 23:36:47 -05:00
. then ( foundUserLogs => {
userLogs = foundUserLogs ;
2017-02-09 21:56:36 -05:00
} )
2017-05-17 22:20:19 -04:00
. then ( ( ) => {
let content = msg . content ;
2017-02-09 21:56:36 -05:00
// If the thread does not exist and could not be created, send a warning about this to all mods so they can DM the user directly instead
if ( ! thread ) {
let warningMessage = `
2017-02-14 17:57:41 -05:00
@ here Error creating modmail thread for $ { msg . author . username } # $ { msg . author . discriminator } ( $ { msg . author . id } ) !
Here ' s what their message contained :
\ ` \` \` ${ content } \` \` \`
` .trim();
2017-02-09 21:56:36 -05:00
bot . createMessage ( utils . getModmailGuild ( bot ) . id , {
content : ` @here Error creating modmail thread for ${ msg . author . username } # ${ msg . author . discriminator } ( ${ msg . author . id } )! ` ,
disableEveryone : false ,
} ) ;
return ;
}
2017-02-14 17:57:41 -05:00
let threadInitDonePromise = Promise . resolve ( ) ;
2017-02-09 21:56:36 -05:00
// If the thread was just created, do some extra stuff
if ( thread . _wasCreated ) {
2017-02-14 17:57:41 -05:00
const mainGuild = utils . getMainGuild ( restBot ) ;
const memberPromise = ( mainGuild ? mainGuild . getRESTMember ( msg . author . id ) : Promise . resolve ( ) ) ;
threadInitDonePromise = memberPromise
2017-02-14 18:26:17 -05:00
. catch ( err => {
2017-02-14 18:04:57 -05:00
console . log ( ` Member ${ msg . author . id } not found in main guild ${ config . mainGuildId } ` ) ;
2017-02-14 18:10:44 -05:00
console . error ( String ( err ) ) ;
2017-02-14 18:04:57 -05:00
} )
2017-02-14 17:57:41 -05:00
. then ( member => {
2017-02-14 18:14:26 -05:00
let mainGuildNickname = null ;
if ( member && member . nick ) mainGuildNickname = member . nick ;
else if ( member && member . user ) mainGuildNickname = member . user . username ;
2017-02-14 18:26:17 -05:00
else if ( member == null ) mainGuildNickname = 'NOT ON SERVER' ;
2017-02-14 18:14:26 -05:00
if ( mainGuildNickname == null ) mainGuildNickname = 'UNKNOWN' ;
2017-02-14 17:57:41 -05:00
const accountAge = humanizeDuration ( Date . now ( ) - msg . author . createdAt , { largest : 2 } ) ;
const infoHeader = ` ACCOUNT AGE ** ${ accountAge } **, ID ** ${ msg . author . id } **, NICKNAME ** ${ mainGuildNickname } **, LOGS ** ${ userLogs . length } ** \n ------------------------------- ` ;
2017-05-17 22:20:19 -04:00
return bot . createMessage ( thread . channelId , infoHeader ) ;
} )
. then ( ( ) => {
// Ping mods of the new thread
bot . createMessage ( thread . channelId , {
content : ` @here New modmail thread ( ${ msg . author . username } # ${ msg . author . discriminator } ) ` ,
disableEveryone : false ,
} ) ;
2017-02-14 17:57:41 -05:00
} ) ;
2017-02-09 21:56:36 -05:00
// Send an automatic reply to the user informing them of the successfully created modmail thread
2017-05-01 05:14:28 -04:00
msg . channel . createMessage ( config . responseMessage || "Thank you for your message! Our mod team will reply to you here as soon as possible." ) . then ( null , ( err ) => {
2017-02-09 23:36:47 -05:00
bot . createMessage ( utils . getModmailGuild ( bot ) . id , {
2017-02-09 21:56:36 -05:00
content : ` There is an issue sending messages to ${ msg . author . username } # ${ msg . author . discriminator } (id ${ msg . author . id } ); consider messaging manually `
} ) ;
} ) ;
}
2017-05-17 22:20:19 -04:00
const attachmentsPendingStr = '\n\n*Attachments pending...*' ;
if ( msg . attachments . length > 0 ) content += attachmentsPendingStr ;
2017-02-14 17:57:41 -05:00
threadInitDonePromise . then ( ( ) => {
const timestamp = utils . getTimestamp ( ) ;
2017-05-17 22:20:19 -04:00
bot . createMessage ( thread . channelId , ` [ ${ timestamp } ] « ** ${ msg . author . username } # ${ msg . author . discriminator } :** ${ content } ` ) . then ( createdMsg => {
if ( msg . attachments . length === 0 ) return ;
// Once attachments have been saved, add links to them to the message
attachmentSavePromise . then ( ( ) => {
const attachmentFormatPromises = msg . attachments . map ( formatAttachment ) ;
Promise . all ( attachmentFormatPromises ) . then ( formattedAttachments => {
let attachmentMsg = '' ;
formattedAttachments . forEach ( str => {
attachmentMsg += ` \n \n ${ str } ` ;
} ) ;
createdMsg . edit ( createdMsg . content . replace ( attachmentsPendingStr , attachmentMsg ) ) ;
} ) ;
} ) ;
} ) ;
2017-02-14 17:57:41 -05:00
} ) ;
2017-02-09 23:36:47 -05:00
} ) ;
2017-02-09 21:56:36 -05:00
} ) ;
} ) ;
} ) ;
// Edits in DMs
bot . on ( 'messageUpdate' , ( msg , oldMessage ) => {
if ( ! ( msg . channel instanceof Eris . PrivateChannel ) ) return ;
if ( msg . author . id === bot . user . id ) return ;
blocked . isBlocked ( msg . author . id ) . then ( isBlocked => {
if ( isBlocked ) return ;
let oldContent = oldMessage . content ;
const newContent = msg . content ;
if ( oldContent == null ) oldContent = '*Unavailable due to bot restart*' ;
2017-05-17 22:20:19 -04:00
// Ignore bogus edit events with no changes
if ( newContent . trim ( ) === oldContent . trim ( ) ) return ;
2017-02-09 21:56:36 -05:00
threads . getForUser ( bot , msg . author ) . then ( thread => {
if ( ! thread ) return ;
const editMessage = utils . disableLinkPreviews ( ` **The user edited their message:** \n **Before:** ${ oldContent } \n **After:** ${ newContent } ` ) ;
bot . createMessage ( thread . channelId , editMessage ) ;
} ) ;
} ) ;
} ) ;
2017-02-10 00:04:23 -05:00
function reply ( msg , text , anonymous = false ) {
2017-02-09 21:56:36 -05:00
threads . getByChannelId ( msg . channel . id ) . then ( thread => {
if ( ! thread ) return ;
2017-02-09 23:36:47 -05:00
attachments . saveAttachmentsInMessage ( msg ) . then ( ( ) => {
2017-02-09 21:56:36 -05:00
bot . getDMChannel ( thread . userId ) . then ( dmChannel => {
2017-02-10 00:39:17 -05:00
let modUsername , logModUsername ;
2017-02-09 23:36:47 -05:00
const mainRole = utils . getMainRole ( msg . member ) ;
2017-02-09 21:56:36 -05:00
2017-02-10 00:04:23 -05:00
if ( anonymous ) {
modUsername = ( mainRole ? mainRole . name : 'Moderator' ) ;
2017-02-10 00:39:17 -05:00
logModUsername = ` (Anonymous) ( ${ msg . author . username } ) ${ mainRole ? mainRole . name : 'Moderator' } ` ;
2017-02-10 00:04:23 -05:00
} else {
modUsername = ( mainRole ? ` ( ${ mainRole . name } ) ${ msg . author . username } ` : msg . author . username ) ;
2017-02-10 00:39:17 -05:00
logModUsername = modUsername ;
2017-02-10 00:04:23 -05:00
}
let content = ` ** ${ modUsername } :** ${ text } ` ;
2017-02-10 00:39:17 -05:00
let logContent = ` ** ${ logModUsername } :** ${ text } ` ;
2017-02-09 21:56:36 -05:00
function sendMessage ( file , attachmentUrl ) {
dmChannel . createMessage ( content , file ) . then ( ( ) => {
2017-02-10 00:39:17 -05:00
if ( attachmentUrl ) {
content += ` \n \n **Attachment:** ${ attachmentUrl } ` ;
logContent += ` \n \n **Attachment:** ${ attachmentUrl } ` ;
}
2017-02-09 21:56:36 -05:00
2017-02-10 00:39:17 -05:00
// Show the message in the modmail thread as well
2017-02-09 23:36:47 -05:00
const timestamp = utils . getTimestamp ( ) ;
2017-02-10 00:39:17 -05:00
msg . channel . createMessage ( ` [ ${ timestamp } ] » ${ logContent } ` ) ;
2017-02-09 21:56:36 -05:00
} , ( err ) => {
if ( err . resp && err . resp . statusCode === 403 ) {
msg . channel . createMessage ( ` Could not send reply; the user has likely blocked the bot ` ) ;
} else if ( err . resp ) {
msg . channel . createMessage ( ` Could not send reply; error code ${ err . resp . statusCode } ` ) ;
} else {
msg . channel . createMessage ( ` Could not send reply: ${ err . toString ( ) } ` ) ;
}
} ) ;
msg . delete ( ) ;
} ;
// If the reply has an attachment, relay it as is
if ( msg . attachments . length > 0 ) {
2017-02-09 23:36:47 -05:00
fs . readFile ( attachments . getPath ( msg . attachments [ 0 ] . id ) , ( err , data ) => {
2017-02-09 21:56:36 -05:00
const file = { file : data , name : msg . attachments [ 0 ] . filename } ;
2017-02-09 23:36:47 -05:00
attachments . getUrl ( msg . attachments [ 0 ] . id , msg . attachments [ 0 ] . filename ) . then ( attachmentUrl => {
2017-02-09 21:56:36 -05:00
sendMessage ( file , attachmentUrl ) ;
} ) ;
} ) ;
} else {
sendMessage ( ) ;
}
} ) ;
} ) ;
} ) ;
2017-02-10 00:04:23 -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
bot . registerCommand ( 'reply' , ( msg , args ) => {
if ( ! msg . channel . guild ) return ;
if ( msg . channel . guild . id !== utils . getModmailGuild ( bot ) . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
const text = args . join ( ' ' ) . trim ( ) ;
reply ( msg , text , false ) ;
2017-02-09 21:56:36 -05:00
} ) ;
bot . registerCommandAlias ( 'r' , 'reply' ) ;
2017-02-10 00:04:23 -05:00
// Anonymous replies only show the role, not the username
bot . registerCommand ( 'anonreply' , ( msg , args ) => {
if ( ! msg . channel . guild ) return ;
if ( msg . channel . guild . id !== utils . getModmailGuild ( bot ) . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
const text = args . join ( ' ' ) . trim ( ) ;
reply ( msg , text , true ) ;
} ) ;
bot . registerCommandAlias ( 'ar' , 'anonreply' ) ;
2017-02-09 21:56:36 -05:00
bot . registerCommand ( 'close' , ( msg , args ) => {
if ( ! msg . channel . guild ) return ;
2017-02-09 23:36:47 -05:00
if ( msg . channel . guild . id !== utils . getModmailGuild ( bot ) . id ) return ;
2017-02-09 21:56:36 -05:00
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
threads . getByChannelId ( msg . channel . id ) . then ( thread => {
if ( ! thread ) return ;
msg . channel . createMessage ( 'Saving logs and closing channel...' ) ;
msg . channel . getMessages ( 10000 ) . then ( messages => {
const log = messages . reverse ( ) . map ( msg => {
const date = moment . utc ( msg . timestamp , 'x' ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
return ` [ ${ date } ] ${ msg . author . username } # ${ msg . author . discriminator } : ${ msg . content } ` ;
} ) . join ( '\n' ) + '\n' ;
logs . getNewLogFile ( thread . userId ) . then ( logFilename => {
logs . saveLogFile ( logFilename , log )
2017-02-09 23:36:47 -05:00
. then ( ( ) => logs . getLogFileUrl ( logFilename ) )
2017-02-09 21:56:36 -05:00
. then ( url => {
2017-05-17 22:20:19 -04:00
const closeMessage = ` Modmail thread with ${ thread . username } ( ${ thread . userId } ) was closed by ${ msg . author . username }
2017-02-09 21:56:36 -05:00
Logs : < $ { url } > ` ;
bot . createMessage ( utils . getModmailGuild ( bot ) . id , closeMessage ) ;
threads . close ( thread . channelId ) . then ( ( ) => msg . channel . delete ( ) ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
bot . registerCommand ( 'block' , ( msg , args ) => {
if ( ! msg . channel . guild ) return ;
if ( msg . channel . guild . id !== utils . getModmailGuild ( bot ) . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
function block ( userId ) {
blocked . block ( userId ) . then ( ( ) => {
msg . channel . createMessage ( ` Blocked <@ ${ userId } > (id ${ userId } ) from modmail ` ) ;
} ) ;
}
if ( args . length > 0 ) {
const userId = utils . getUserMention ( args . join ( ' ' ) ) ;
if ( ! userId ) return ;
block ( userId ) ;
} else {
// Calling !block without args in a modmail thread blocks the user of that thread
threads . getByChannelId ( msg . channel . id ) . then ( thread => {
if ( ! thread ) return ;
2017-02-09 23:36:47 -05:00
block ( thread . userId ) ;
2017-02-09 21:56:36 -05:00
} ) ;
}
} ) ;
bot . registerCommand ( 'unblock' , ( msg , args ) => {
if ( ! msg . channel . guild ) return ;
if ( msg . channel . guild . id !== utils . getModmailGuild ( bot ) . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
function unblock ( userId ) {
blocked . unblock ( userId ) . then ( ( ) => {
msg . channel . createMessage ( ` Unblocked <@ ${ userId } > (id ${ userId } ) from modmail ` ) ;
} ) ;
}
if ( args . length > 0 ) {
const userId = utils . getUserMention ( args . join ( ' ' ) ) ;
if ( ! userId ) return ;
unblock ( userId ) ;
} else {
// Calling !unblock without args in a modmail thread unblocks the user of that thread
threads . getByChannelId ( msg . channel . id ) . then ( thread => {
if ( ! thread ) return ;
2017-02-09 23:36:47 -05:00
unblock ( thread . userId ) ;
2017-02-09 21:56:36 -05:00
} ) ;
}
} ) ;
bot . registerCommand ( 'logs' , ( msg , args ) => {
if ( ! msg . channel . guild ) return ;
if ( msg . channel . guild . id !== utils . getModmailGuild ( bot ) . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
function getLogs ( userId ) {
2017-02-09 23:36:47 -05:00
logs . getLogsWithUrlByUserId ( userId ) . then ( infos => {
2017-02-09 21:56:36 -05:00
let message = ` **Log files for <@ ${ userId } >:** \n ` ;
message += infos . map ( info => {
const formattedDate = moment . utc ( info . date , 'YYYY-MM-DD HH:mm:ss' ) . format ( 'MMM Do [at] HH:mm [UTC]' ) ;
return ` \` ${ formattedDate } \` : < ${ info . url } > ` ;
} ) . join ( '\n' ) ;
msg . channel . createMessage ( message ) ;
} ) ;
}
if ( args . length > 0 ) {
const userId = utils . getUserMention ( args . join ( ' ' ) ) ;
if ( ! userId ) return ;
getLogs ( userId ) ;
} else {
// Calling !logs without args in a modmail thread returns the logs of the user of that thread
threads . getByChannelId ( msg . channel . id ) . then ( thread => {
if ( ! thread ) return ;
2017-02-09 23:36:47 -05:00
getLogs ( thread . userId ) ;
2017-02-09 21:56:36 -05:00
} ) ;
}
} ) ;
bot . connect ( ) ;
2017-02-14 17:57:41 -05:00
restBot . connect ( ) ;
2017-02-09 21:56:36 -05:00
webserver . run ( ) ;
2017-02-10 00:04:23 -05:00
greeting . enable ( bot ) ;
2017-02-14 17:57:41 -05:00