2016-12-05 17:45:33 -05:00
const fs = require ( 'fs' ) ;
2016-12-05 19:29:55 -05:00
const http = require ( 'http' ) ;
const url = require ( 'url' ) ;
const crypto = require ( 'crypto' ) ;
const publicIp = require ( 'public-ip' ) ;
2016-12-05 17:25:20 -05:00
const Eris = require ( 'eris' ) ;
const moment = require ( 'moment' ) ;
const Queue = require ( './queue' ) ;
const config = require ( './config' ) ;
2016-12-05 19:29:55 -05:00
const logServerPort = config . port || 8890 ;
2016-12-05 17:25:20 -05:00
const bot = new Eris . CommandClient ( config . token , { } , {
prefix : config . prefix || '!' ,
ignoreSelf : true ,
ignoreBots : true ,
defaultHelpCommand : false ,
} ) ;
let modMailGuild ;
const modMailChannels = { } ;
const messageQueue = new Queue ( ) ;
2016-12-05 17:45:33 -05:00
const blockFile = ` ${ _ _dirname } /blocked.json ` ;
let blocked = [ ] ;
2016-12-05 19:29:55 -05:00
const logDir = ` ${ _ _dirname } /logs ` ;
const logFileFormatRegex = /^([0-9\-]+?)__([0-9]+?)__([0-9a-f]+?)\.txt$/ ;
2016-12-07 18:46:03 -05:00
const userMentionRegex = /^<@\!?([0-9]+?)>$/ ;
2016-12-05 17:45:33 -05:00
try {
const blockedJSON = fs . readFileSync ( blockFile , { encoding : 'utf8' } ) ;
blocked = JSON . parse ( blockedJSON ) ;
} catch ( e ) {
fs . writeFileSync ( blockFile , '[]' ) ;
}
function saveBlocked ( ) {
fs . writeFileSync ( blockFile , JSON . stringify ( blocked , null , 4 ) ) ;
}
2016-12-05 19:31:42 -05:00
/ *
* MODMAIL LOG UTILITY FUNCTIONS
* /
2016-12-05 19:29:55 -05:00
function getLogFileInfo ( logfile ) {
const match = logfile . match ( logFileFormatRegex ) ;
if ( ! match ) return null ;
return {
date : match [ 1 ] ,
userId : match [ 2 ] ,
token : match [ 3 ] ,
} ;
}
function getLogFilePath ( logfile ) {
return ` ${ logDir } / ${ logfile } ` ;
}
function getLogFileUrl ( logfile ) {
const info = getLogFileInfo ( logfile ) ;
return publicIp . v4 ( ) . then ( ip => {
return ` http:// ${ ip } : ${ logServerPort } /logs/ ${ info . token } ` ;
} ) ;
}
function getRandomLogFile ( userId ) {
return new Promise ( resolve => {
crypto . randomBytes ( 16 , ( err , buf ) => {
const token = buf . toString ( 'hex' ) ;
const date = moment . utc ( ) . format ( 'YYYY-MM-DD-HH-mm-ss' ) ;
resolve ( ` ${ date } __ ${ userId } __ ${ token } .txt ` ) ;
} ) ;
} ) ;
}
function findLogFile ( token ) {
return new Promise ( resolve => {
fs . readdir ( logDir , ( err , files ) => {
for ( const file of files ) {
if ( file . endsWith ( ` __ ${ token } .txt ` ) ) {
resolve ( file ) ;
return ;
}
}
resolve ( null ) ;
} ) ;
} ) ;
}
function findLogFilesByUserId ( userId ) {
return new Promise ( resolve => {
fs . readdir ( logDir , ( err , files ) => {
const logfiles = files . filter ( file => {
const info = getLogFileInfo ( file ) ;
if ( ! info ) return false ;
return info . userId === userId ;
} ) ;
resolve ( logfiles ) ;
} ) ;
} ) ;
}
2016-12-05 19:31:42 -05:00
/ *
* MAIN FUNCTIONALITY
* /
2016-12-05 17:25:20 -05:00
bot . on ( 'ready' , ( ) => {
modMailGuild = bot . guilds . find ( g => g . id === config . mailGuildId ) ;
if ( ! modMailGuild ) {
console . error ( 'You need to set and invite me to the mod mail guild first!' ) ;
process . exit ( 0 ) ;
}
2016-12-05 17:45:33 -05:00
2016-12-06 09:33:53 -05:00
bot . editStatus ( null , { name : config . status || 'Message me for help' } ) ;
2016-12-05 17:25:20 -05:00
} ) ;
function getModmailChannelInfo ( channel ) {
if ( ! channel . topic ) return null ;
const match = channel . topic . match ( /^MODMAIL\|([0-9]+)\|(.*)$/ ) ;
if ( ! match ) return null ;
return {
userId : match [ 1 ] ,
name : match [ 2 ] ,
} ;
}
function getModmailChannel ( user ) {
if ( modMailChannels [ user . id ] ) {
// Cached
const channel = modMailGuild . channels . get ( modMailChannels [ user . id ] ) ;
if ( channel ) {
return Promise . resolve ( channel ) ;
} else {
// If the cache value was invalid, remove it
delete modMailChannels [ user . id ] ;
}
}
// Try to find a matching channel
let candidate = modMailGuild . channels . find ( c => {
const info = getModmailChannelInfo ( c ) ;
return info && info . userId === user . id ;
} ) ;
if ( candidate ) {
return Promise . resolve ( candidate ) ;
} else {
// If one is not found, create and cache it
return modMailGuild . createChannel ( ` ${ user . username } - ${ user . discriminator } ` )
. then ( channel => {
// This is behind a timeout because Discord was telling me the channel didn't exist after creation even though it clearly did
// ¯\_(ツ)_/¯
return new Promise ( resolve => {
const topic = ` MODMAIL| ${ user . id } | ${ user . username } # ${ user . discriminator } ` ;
setTimeout ( ( ) => resolve ( channel . edit ( { topic : topic } ) ) , 200 ) ;
} ) ;
} )
. then ( channel => {
modMailChannels [ user . id ] = channel . id ;
channel . _wasCreated = true ;
return channel ;
} ) ;
}
}
2016-12-07 18:46:03 -05:00
function formatAttachment ( attachment ) {
let filesize = attachment . size || 0 ;
filesize /= 1024 ;
return ` **Attachment:** ${ attachment . filename } ( ${ filesize . toFixed ( 1 ) } KB) \n ${ attachment . url } `
}
2016-12-05 17:25:20 -05:00
bot . on ( 'messageCreate' , ( msg ) => {
if ( ! ( msg . channel instanceof Eris . PrivateChannel ) ) return ;
if ( msg . author . id === bot . user . id ) return ;
2016-12-05 17:45:33 -05:00
if ( blocked . indexOf ( msg . author . id ) !== - 1 ) return ;
2016-12-05 17:25:20 -05:00
// This needs to be queued as otherwise, if a user sent a bunch of messages initially and the createChannel endpoint is delayed, we might get duplicate channels
messageQueue . add ( ( ) => {
return getModmailChannel ( msg . author ) . then ( channel => {
2016-12-07 18:46:03 -05:00
let content = msg . content ;
msg . attachments . forEach ( attachment => {
content += ` \n \n ${ formatAttachment ( attachment ) } ` ;
} ) ;
channel . createMessage ( ` « ** ${ msg . author . username } # ${ msg . author . discriminator } :** ${ content } ` ) ;
2016-12-05 17:25:20 -05:00
if ( channel . _wasCreated ) {
2016-12-07 18:46:03 -05:00
let creationNotificationMessage = ` New modmail thread: ${ channel . mention } ` ;
if ( config . pingCreationNotification ) creationNotificationMessage = ` @here ${ config . pingCreationNotification } ` ;
2016-12-05 17:51:32 -05:00
bot . createMessage ( modMailGuild . id , {
2016-12-07 18:46:03 -05:00
content : creationNotificationMessage ,
2016-12-05 17:51:32 -05:00
disableEveryone : false ,
} ) ;
2016-12-05 17:25:20 -05:00
msg . channel . createMessage ( "Thank you for your message! Our mod team will reply to you here as soon as possible." ) ;
}
} ) ;
} ) ;
} ) ;
bot . registerCommand ( 'reply' , ( msg , args ) => {
if ( msg . channel . guild . id !== modMailGuild . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
const channelInfo = getModmailChannelInfo ( msg . channel ) ;
if ( ! channelInfo ) return ;
bot . getDMChannel ( channelInfo . userId ) . then ( channel => {
2016-12-07 18:46:03 -05:00
let argMsg = args . join ( ' ' ) . trim ( ) ;
let content = ` ** ${ msg . author . username } :** ${ argMsg } ` ;
if ( msg . attachments . length > 0 && argMsg !== '' ) content += '\n\n' ;
content += msg . attachments . map ( attachment => {
return ` ${ attachment . url } ` ;
} ) . join ( '\n' ) ;
2016-12-05 17:25:20 -05:00
2016-12-07 18:46:03 -05:00
channel . createMessage ( content ) ;
msg . channel . createMessage ( ` » ${ content } ` ) ;
// Delete the !r message if there are no attachments
// When there are attachments, we need to keep the original message or the attachments get deleted as well
if ( msg . attachments . length === 0 ) msg . delete ( ) ;
2016-12-05 17:25:20 -05:00
} ) ;
} ) ;
bot . registerCommandAlias ( 'r' , 'reply' ) ;
bot . registerCommand ( 'close' , ( msg , args ) => {
if ( msg . channel . guild . id !== modMailGuild . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
const channelInfo = getModmailChannelInfo ( msg . channel ) ;
if ( ! channelInfo ) return ;
msg . channel . createMessage ( 'Saving logs and closing channel...' ) ;
msg . channel . getMessages ( 10000 ) . then ( messages => {
const log = messages . reverse ( ) . map ( message => {
const date = moment . utc ( message . timestamp , 'x' ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
return ` [ ${ date } ] ${ message . author . username } # ${ message . author . discriminator } : ${ message . content } ` ;
} ) . join ( '\n' ) + '\n' ;
2016-12-05 19:29:55 -05:00
getRandomLogFile ( channelInfo . userId ) . then ( logfile => {
fs . writeFile ( getLogFilePath ( logfile ) , log , { encoding : 'utf8' } , err => {
getLogFileUrl ( logfile ) . then ( logurl => {
bot . createMessage ( modMailGuild . id , ` Log of modmail thread with ${ channelInfo . name } : \n < ${ logurl } > ` ) ;
2016-12-05 17:25:20 -05:00
2016-12-05 19:29:55 -05:00
delete modMailChannels [ channelInfo . userId ] ;
msg . channel . delete ( ) ;
} ) ;
} ) ;
} )
2016-12-05 17:25:20 -05:00
} ) ;
} ) ;
2016-12-05 17:45:33 -05:00
bot . registerCommand ( 'block' , ( msg , args ) => {
if ( msg . channel . guild . id !== modMailGuild . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
if ( args . length !== 1 ) return ;
let userId ;
if ( args [ 0 ] . match ( /^[0-9]+$/ ) ) {
userId = args [ 0 ] ;
} else {
2016-12-07 18:46:03 -05:00
let mentionMatch = args [ 0 ] . match ( userMentionRegex ) ;
2016-12-05 17:45:33 -05:00
if ( mentionMatch ) userId = mentionMatch [ 1 ] ;
}
if ( ! userId ) return ;
2016-12-07 18:46:03 -05:00
blocked . push ( userId ) ;
2016-12-05 17:45:33 -05:00
saveBlocked ( ) ;
msg . channel . createMessage ( ` Blocked <@ ${ userId } > (id ${ userId } ) from modmail ` ) ;
} ) ;
bot . registerCommand ( 'unblock' , ( msg , args ) => {
if ( msg . channel . guild . id !== modMailGuild . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
if ( args . length !== 1 ) return ;
let userId ;
if ( args [ 0 ] . match ( /^[0-9]+$/ ) ) {
userId = args [ 0 ] ;
} else {
2016-12-07 18:46:03 -05:00
let mentionMatch = args [ 0 ] . match ( userMentionRegex ) ;
2016-12-05 17:45:33 -05:00
if ( mentionMatch ) userId = mentionMatch [ 1 ] ;
}
if ( ! userId ) return ;
2016-12-07 18:46:03 -05:00
blocked . splice ( blocked . indexOf ( userId ) , 1 ) ;
2016-12-05 17:45:33 -05:00
saveBlocked ( ) ;
msg . channel . createMessage ( ` Unblocked <@ ${ userId } > (id ${ userId } ) from modmail ` ) ;
} ) ;
2016-12-05 19:29:55 -05:00
bot . registerCommand ( 'logs' , ( msg , args ) => {
if ( msg . channel . guild . id !== modMailGuild . id ) return ;
if ( ! msg . member . permission . has ( 'manageRoles' ) ) return ;
if ( args . length !== 1 ) return ;
let userId ;
if ( args [ 0 ] . match ( /^[0-9]+$/ ) ) {
userId = args [ 0 ] ;
} else {
2016-12-07 18:46:03 -05:00
let mentionMatch = args [ 0 ] . match ( userMentionRegex ) ;
2016-12-05 19:29:55 -05:00
if ( mentionMatch ) userId = mentionMatch [ 1 ] ;
}
if ( ! userId ) return ;
findLogFilesByUserId ( userId ) . then ( logfiles => {
let message = ` **Log files for <@ ${ userId } >:** \n ` ;
const urlPromises = logfiles . map ( logfile => {
const info = getLogFileInfo ( logfile ) ;
return getLogFileUrl ( logfile ) . then ( url => {
info . url = url ;
return info ;
} ) ;
} ) ;
Promise . all ( urlPromises ) . then ( infos => {
infos . sort ( ( a , b ) => {
if ( a . date > b . date ) return 1 ;
if ( a . date < b . date ) return - 1 ;
return 0 ;
} ) ;
message += infos . map ( info => {
const formattedDate = moment . utc ( info . date , 'YYYY-MM-DD-HH-mm-ss' ) . format ( 'MMM Mo [at] HH:mm [UTC]' ) ;
return ` ${ formattedDate } : < ${ info . url } > ` ;
} ) . join ( '\n' ) ;
msg . channel . createMessage ( message ) ;
} ) ;
} ) ;
} ) ;
2016-12-05 17:25:20 -05:00
bot . connect ( ) ;
2016-12-05 19:29:55 -05:00
2016-12-05 19:31:42 -05:00
/ *
* MODMAIL LOG SERVER
* /
2016-12-05 19:29:55 -05:00
const server = http . createServer ( ( req , res ) => {
const parsedUrl = url . parse ( ` http:// ${ req . url } ` ) ;
if ( ! parsedUrl . path . startsWith ( '/logs/' ) ) return ;
const pathParts = parsedUrl . path . split ( '/' ) . filter ( v => v !== '' ) ;
const token = pathParts [ pathParts . length - 1 ] ;
if ( token . match ( /^[0-9a-f]+$/ ) === null ) return res . end ( ) ;
findLogFile ( token ) . then ( logfile => {
if ( logfile === null ) return res . end ( ) ;
fs . readFile ( getLogFilePath ( logfile ) , { encoding : 'utf8' } , ( err , data ) => {
res . setHeader ( 'Content-Type' , 'text/plain' ) ;
res . end ( data ) ;
} ) ;
} ) ;
} ) ;
server . listen ( logServerPort ) ;