2021-06-19 15:06:20 +02:00
const fs = require ( 'fs' ) ;
2021-06-20 13:12:01 +02:00
const ChannelHandler = require ( './ChannelHandler' ) ;
2021-06-19 15:06:20 +02:00
2021-06-18 15:41:57 +02:00
class Modmail {
2021-06-19 23:57:12 +02:00
// A lot of this can probably be simplified but I wrote all of this in 2 days and I cba to fix this atm
// TODO: Fix everything
2021-10-22 09:35:04 +02:00
constructor ( client ) {
2021-06-18 15:41:57 +02:00
this . client = client ;
2021-06-20 13:12:01 +02:00
this . cache = client . cache ;
2021-06-18 15:41:57 +02:00
this . mainServer = null ;
this . bansServer = null ;
2021-06-21 16:54:40 +02:00
this . logChannel = null ;
this . reminderChannel = null ;
2021-06-19 21:31:51 +02:00
2021-06-19 23:57:12 +02:00
const opts = client . _options ;
this . anonColor = opts . anonColor ;
this . reminderInterval = opts . modmailReminderInterval || 30 ;
this . _reminderChannel = opts . modmailReminderChannel || null ;
2021-06-21 16:54:40 +02:00
this . _logChannel = opts . logChannel || null ;
2021-06-20 13:12:01 +02:00
this . categories = opts . modmailCategory ;
2021-06-19 21:16:36 +02:00
2021-06-19 15:06:20 +02:00
this . updatedThreads = [ ] ;
2021-06-19 23:57:12 +02:00
this . queue = [ ] ;
2021-06-19 15:06:20 +02:00
this . spammers = { } ;
this . replies = { } ;
2021-06-18 15:41:57 +02:00
2021-06-19 23:57:12 +02:00
this . lastReminder = null ;
2021-12-22 10:30:50 +01:00
this . disabled = false ;
this . disabledReason = null ;
2021-06-19 23:57:12 +02:00
2021-06-20 13:12:01 +02:00
this . channels = new ChannelHandler ( this , opts ) ;
this . _ready = false ;
2021-06-18 15:41:57 +02:00
}
2021-10-22 09:35:04 +02:00
async init ( ) {
2021-06-18 15:41:57 +02:00
2021-06-19 15:06:20 +02:00
this . mainServer = this . client . mainServer ;
if ( ! this . mainServer ) throw new Error ( ` Missing main server ` ) ;
2021-06-18 15:41:57 +02:00
2021-06-19 15:06:20 +02:00
this . bansServer = this . client . bansServer ;
if ( ! this . bansServer ) this . client . logger . warn ( ` Missing bans server ` ) ;
2021-06-19 22:59:42 +02:00
if ( ! this . anonColor ) this . anonColor = this . mainServer . me . highestRoleColor ;
2021-06-19 15:06:20 +02:00
this . replies = this . loadReplies ( ) ;
2021-06-20 13:12:01 +02:00
this . queue = this . client . cache . queue ;
2021-06-19 23:57:12 +02:00
if ( this . _reminderChannel ) {
this . reminderChannel = this . client . channels . resolve ( this . _reminderChannel ) ;
this . reminder = setInterval ( this . sendReminder . bind ( this ) , this . reminderInterval * 60 * 1000 ) ;
2021-07-19 21:30:31 +02:00
this . lastReminder = await this . reminderChannel . messages . fetch ( this . cache . misc . lastReminder ) . catch ( ( ) => null ) ;
2021-06-20 01:21:49 +02:00
this . sendReminder ( ) ;
2021-06-19 23:57:12 +02:00
}
2021-06-19 15:06:20 +02:00
2021-06-21 16:54:40 +02:00
if ( this . _logChannel ) {
this . logChannel = this . client . channels . resolve ( this . _logChannel ) ;
}
2021-06-19 15:06:20 +02:00
let logStr = ` Started modmail handler for ${ this . mainServer . name } ` ;
if ( this . bansServer ) logStr += ` with ${ this . bansServer . name } for ban appeals ` ;
this . client . logger . info ( logStr ) ;
2021-10-22 09:35:04 +02:00
// this.client.logger.info(`Fetching messages from discord for modmail`);
2021-06-19 15:06:20 +02:00
// TODO: Fetch messages from discord in modmail channels
2021-12-22 10:30:50 +01:00
this . disabled = this . cache . misc . disabled || false ;
this . disabledReason = this . cache . misc . disabledReason || null ;
2021-06-20 13:12:01 +02:00
this . channels . init ( ) ;
this . _ready = true ;
2021-06-19 15:06:20 +02:00
}
2021-10-22 09:35:04 +02:00
async getMember ( user ) {
2021-06-19 15:06:20 +02:00
let result = this . mainServer . members . cache . get ( user ) ;
2021-10-22 09:35:04 +02:00
if ( ! result ) result = await this . mainServer . members . fetch ( user ) . catch ( ( ) => {
2021-06-19 15:06:20 +02:00
return null ;
} ) ;
if ( ! result && this . bansServer ) {
result = this . bansServer . members . cache . get ( user ) ;
2021-10-22 09:35:04 +02:00
if ( ! result ) result = await this . bansServer . members . fetch ( user ) . catch ( ( ) => {
2021-06-19 15:06:20 +02:00
return null ;
} ) ;
2021-07-19 17:44:28 +02:00
if ( result ) result . inAppealServer = true ;
2021-06-19 15:06:20 +02:00
}
return result ;
}
2021-10-22 09:35:04 +02:00
async getUser ( user ) {
2021-06-19 15:06:20 +02:00
let result = this . client . users . cache . get ( user ) ;
if ( ! result ) result = await this . client . users . fetch ( user ) . catch ( ( ) => {
return null ;
} ) ;
return result ;
2021-06-18 15:41:57 +02:00
}
2021-10-22 09:35:04 +02:00
async handleUser ( message ) {
2021-06-18 15:41:57 +02:00
2021-06-19 15:06:20 +02:00
const { author , content } = message ;
const member = await this . getMember ( author . id ) ;
if ( ! member ) return ; // No member object found in main or bans server?
const now = Math . floor ( Date . now ( ) / 1000 ) ;
2021-12-22 10:36:06 +01:00
const { cache } = this ;
2022-01-03 19:23:55 +01:00
// Anti spam -- never seen user
if ( ! this . spammers [ author . id ] ) this . spammers [ author . id ] = {
start : now , // when counting started
count : 1 , // # messages
timeout : false , // timed out?
warned : false // warned?
} ;
else if ( this . spammers [ author . id ] . timeout ) { // User was timed out, check if 5 minutes have passsed, if so, reset their timeout else ignore them
2021-10-22 09:35:04 +02:00
if ( now - this . spammers [ author . id ] . start > 5 * 60 ) this . spammers [ author . id ] = { start : now , count : 1 , timeout : false , warned : false } ;
else return ;
} else if ( this . spammers [ author . id ] . count > 5 && now - this . spammers [ author . id ] . start < 15 ) {
2022-01-03 19:23:55 +01:00
// Has sent more than 5 messages in less than 15 seconds at this point, time them out
2021-10-22 09:35:04 +02:00
this . spammers [ author . id ] . timeout = true ;
2022-01-03 19:23:55 +01:00
if ( ! this . spammers [ author . id ] . warned ) { // Let them know they've been timed out, toggle the warned property so it doesn't send the warning every time
2021-10-22 09:35:04 +02:00
this . spammers [ author . id ] . warned = true ;
await author . send ( ` I've blocked you for spamming, please try again in 5 minutes ` ) ;
if ( cache . _channels [ author . id ] ) await cache . _channels [ author . id ] . send ( ` I've blocked ${ author . tag } from DMing me as they were spamming. ` ) ;
2021-06-19 15:06:20 +02:00
}
2022-01-03 19:23:55 +01:00
} else if ( now - this . spammers [ author . id ] . start > 15 ) this . spammers [ author . id ] = { start : now , count : 1 , timeout : false , warned : false } ; // Enough time has passed, reset the object
2021-10-22 09:35:04 +02:00
else this . spammers [ author . id ] . count ++ ;
2021-06-19 15:06:20 +02:00
2021-12-22 10:30:50 +01:00
if ( this . disabled ) {
let reason = ` Modmail has been disabled for the time being ` ;
if ( this . disabledReason ) reason += ` for the following reason: \n \n ${ this . disabledReason } ` ;
else reason += ` . ` ;
return author . send ( reason ) ;
}
2022-01-03 19:23:55 +01:00
const lastActivity = this . cache . lastActivity [ author . id ] ;
if ( ! lastActivity || now - lastActivity > 30 * 60 ) { // No point in sending this for *every* message
await author . send ( ` Thank you for your message, we'll get back to you soon! ` ) ;
}
this . cache . lastActivity [ author . id ] = now ;
2021-06-20 13:12:01 +02:00
const pastModmail = await this . cache . loadModmailHistory ( author . id )
2021-06-19 15:06:20 +02:00
. catch ( ( err ) => {
this . client . logger . error ( ` Error during loading of past mail: \n ${ err . stack } ` ) ;
return { error : true } ;
} ) ;
if ( pastModmail . error ) return author . send ( ` Internal error, this has been logged. ` ) ;
2021-06-20 13:12:01 +02:00
const channel = await this . channels . load ( member , pastModmail )
2021-06-19 15:06:20 +02:00
. catch ( ( err ) => {
this . client . logger . error ( ` Error during channel handling: \n ${ err . stack } ` ) ;
return { error : true } ;
} ) ;
if ( channel . error ) return author . send ( ` Internal error, this has been logged. ` ) ;
if ( ! cache . _channels ) cache . _channels = { } ;
cache . _channels [ author . id ] = channel ;
const embed = {
footer : {
text : member . id
} ,
author : {
name : member . user . tag ,
// eslint-disable-next-line camelcase
icon _url : member . user . displayAvatarURL ( { dynamic : true } )
} ,
// eslint-disable-next-line no-nested-ternary
description : content && content . length ? content . length > 2000 ? ` ${ content . substring ( 0 , 2000 ) } ... \n \n **Content cut off** ` : content : ` **__MISSING CONTENT__** ` ,
color : member . highestRoleColor ,
fields : [ ] ,
timestamp : new Date ( )
} ;
2021-06-20 00:32:43 +02:00
const attachments = message . attachments . map ( ( att ) => att . url ) ;
if ( message . attachments . size ) {
embed . fields . push ( {
name : '__Attachments__' ,
value : attachments . join ( '\n' ) . substring ( 0 , 1000 )
} ) ;
}
pastModmail . push ( { attachments , author : author . id , content , timestamp : Date . now ( ) , isReply : false } ) ;
if ( ! this . updatedThreads . includes ( author . id ) ) this . updatedThreads . push ( author . id ) ;
2021-06-21 16:54:40 +02:00
if ( ! this . queue . includes ( author . id ) ) this . queue . push ( author . id ) ;
this . log ( { author , action : ` ${ author . tag } ( ${ author . id } ) sent new modmail ` , content } ) ;
2021-06-19 15:06:20 +02:00
await channel . send ( { embed } ) . catch ( ( err ) => {
this . client . logger . error ( ` channel.send errored: \n ${ err . stack } \n Content: " ${ content } " ` ) ;
} ) ;
}
2021-10-22 09:35:04 +02:00
async sendCannedResponse ( { message , responseName , anon } ) {
2021-06-19 15:06:20 +02:00
const content = this . getCanned ( responseName ) ;
if ( ! content ) return {
error : true ,
msg : ` No canned reply by the name \` ${ responseName } \` exists `
} ;
return this . sendResponse ( { message , content , anon } ) ;
}
2021-06-20 13:12:01 +02:00
// Send reply from channel
2021-10-22 09:35:04 +02:00
async sendResponse ( { message , content , anon } ) {
2021-06-19 15:06:20 +02:00
const { channel , member , author } = message ;
if ( ! this . categories . includes ( channel . parentID ) ) return {
error : true ,
msg : ` This command only works in modmail channels. `
} ;
2021-06-20 13:12:01 +02:00
// Resolve target user from cache
const chCache = this . cache . channels ;
2021-10-22 09:35:04 +02:00
const result = Object . entries ( chCache ) . find ( ( [ , val ] ) => {
2021-06-19 15:06:20 +02:00
return val === channel . id ;
} ) ;
if ( ! result ) return {
error : true ,
msg : ` This doesn't seem to be a valid modmail channel. Cache might be out of sync. **[MISSING TARGET]** `
} ;
2021-06-20 13:12:01 +02:00
// Ensure target exists, this should never run into issues
2021-10-22 09:35:04 +02:00
const [ userId ] = result ;
2021-06-21 16:54:40 +02:00
const targetMember = await this . getMember ( userId ) ;
if ( ! targetMember ) return {
2021-06-19 15:06:20 +02:00
error : true ,
msg : ` User seems to have left. \n Report this if the user is still present. `
} ;
2021-06-21 23:36:58 +02:00
this . log ( { author , action : ` ${ author . tag } replied to ${ targetMember . user . tag } ` , content , target : targetMember . user } ) ;
2021-06-19 15:06:20 +02:00
await message . delete ( ) . catch ( this . client . logger . warn . bind ( this . client . logger ) ) ;
2021-11-28 18:31:17 +01:00
return this . send ( { target : targetMember , staff : member , content , anon } ) . catch ( ( err ) => this . client . logger . error ( ` Error during Modmail.send: \n ${ err . stack } ` ) ) ;
2021-06-19 15:06:20 +02:00
}
2021-06-20 13:12:01 +02:00
// Send modmail with the modmail command
2021-10-22 09:35:04 +02:00
async sendModmail ( { message , content , anon , target } ) {
2021-06-19 15:06:20 +02:00
const targetMember = await this . getMember ( target . id ) ;
if ( ! targetMember ) return {
error : true ,
2021-06-21 16:54:40 +02:00
msg : ` Cannot find member. `
2021-06-19 15:06:20 +02:00
} ;
2021-06-21 16:54:40 +02:00
const { member : staff , author } = message ;
2021-07-19 17:44:28 +02:00
// Send to channel in server & target
2021-11-28 18:31:17 +01:00
const sent = await this . send ( { target : targetMember , staff , anon , content } ) . catch ( ( err ) => this . client . logger . error ( ` Error during Modmail.sendModmail: \n ${ err . stack } ` ) ) ;
2021-07-19 17:44:28 +02:00
if ( sent . error ) return sent ;
2021-06-21 16:54:40 +02:00
// Inline response
2021-11-28 18:31:17 +01:00
await message . channel . send ( 'Delivered.' ) . catch ( ( err ) => this . client . logger . error ( ` Error during Modmail.sendModmail: \n ${ err . stack } ` ) ) ;
2021-06-21 16:54:40 +02:00
this . log ( { author , action : ` ${ author . tag } sent a message to ${ targetMember . user . tag } ` , content , target : targetMember . user } ) ;
}
2021-10-22 09:35:04 +02:00
async send ( { target , staff , anon , content } ) {
2021-06-19 15:06:20 +02:00
const embed = {
author : {
2021-06-21 16:54:40 +02:00
name : anon ? ` ${ this . mainServer . name . toUpperCase ( ) } STAFF ` : staff . user . tag ,
2021-06-19 15:06:20 +02:00
// eslint-disable-next-line camelcase
2021-06-21 16:54:40 +02:00
icon _url : anon ? this . mainServer . iconURL ( { dynamic : true } ) : staff . user . displayAvatarURL ( { dynamic : true } )
2021-06-19 15:06:20 +02:00
} ,
description : content ,
2021-06-21 16:54:40 +02:00
color : anon ? this . anonColor : staff . highestRoleColor
2021-06-19 15:06:20 +02:00
} ;
2021-06-20 13:12:01 +02:00
// Dm the user
2021-06-19 15:06:20 +02:00
const sent = await target . send ( { embed } ) . catch ( ( err ) => {
this . client . logger . warn ( ` Error during DMing user: ${ err . message } ` ) ;
return {
error : true ,
msg : ` Failed to send message to target. `
} ;
} ) ;
if ( sent . error ) return sent ;
2021-06-21 16:54:40 +02:00
if ( anon ) embed . author = {
name : ` ${ staff . user . tag } (ANON) ` ,
// eslint-disable-next-line camelcase
icon _url : staff . user . displayAvatarURL ( { dynamic : true } )
} ;
2021-06-19 15:06:20 +02:00
2021-07-19 17:44:28 +02:00
return this . channels . send ( target , embed , { author : staff . id , content , timestamp : Date . now ( ) , isReply : true , anon } ) ;
2021-06-19 15:06:20 +02:00
2021-06-19 21:09:36 +02:00
}
2021-10-22 09:35:04 +02:00
async changeReadState ( message , args , state = 'read' ) {
2021-06-19 15:06:20 +02:00
2021-06-20 13:12:01 +02:00
const { author } = message ;
2021-06-19 15:06:20 +02:00
2021-06-20 13:12:01 +02:00
if ( ! this . categories . includes ( message . channel . parentID ) && ! args . length ) return {
2021-06-19 15:06:20 +02:00
error : true ,
2021-06-20 13:12:01 +02:00
msg : ` This command only works in modmail channels without arguments. `
2021-06-19 15:06:20 +02:00
} ;
2021-06-21 16:54:40 +02:00
let response = null ,
user = null ;
2021-06-20 16:43:05 +02:00
if ( args . length ) {
// Eventually support marking several threads read at the same time
2021-10-22 09:35:04 +02:00
const [ id ] = args ;
2021-06-21 16:54:40 +02:00
user = await this . client . resolveUser ( id , true ) ;
2021-06-20 16:43:05 +02:00
let channel = await this . client . resolveChannel ( id ) ;
if ( channel ) {
const chCache = this . cache . channels ;
2021-10-22 09:35:04 +02:00
const result = Object . entries ( chCache ) . find ( ( [ , val ] ) => {
2021-06-20 16:43:05 +02:00
return val === channel . id ;
} ) ;
if ( ! result ) return {
error : true ,
msg : ` That doesn't seem to be a valid modmail channel. Cache might be out of sync. **[MISSING TARGET]** `
} ;
user = await this . client . resolveUser ( result [ 0 ] ) ;
2021-09-02 13:38:55 +02:00
response = await this . channels . setReadState ( user . id , channel , author , state ) ;
2021-06-20 16:43:05 +02:00
} else if ( user ) {
const _ch = this . cache . channels [ user . id ] ;
if ( _ch ) channel = await this . client . resolveChannel ( _ch ) ;
2021-09-02 13:38:55 +02:00
response = await this . channels . setReadState ( user . id , channel , author , state ) ;
2021-06-20 16:43:05 +02:00
2021-06-21 16:54:40 +02:00
} else return ` Could not resolve ${ id } to a target. ` ;
2021-06-20 16:43:05 +02:00
}
2021-06-20 13:12:01 +02:00
2021-06-21 16:54:40 +02:00
if ( ! response ) {
const { channel } = message ;
const chCache = this . cache . channels ;
2021-10-22 09:35:04 +02:00
const result = Object . entries ( chCache ) . find ( ( [ , val ] ) => {
2021-06-21 16:54:40 +02:00
return val === channel . id ;
} ) ;
2021-06-19 20:05:32 +02:00
2021-06-21 16:54:40 +02:00
if ( ! result ) return {
error : true ,
msg : ` This doesn't seem to be a valid modmail channel. Cache might be out of sync. **[MISSING TARGET]** `
} ;
2021-06-19 20:05:32 +02:00
2021-10-22 09:35:04 +02:00
const [ userId ] = result ;
2021-06-21 16:54:40 +02:00
user = await this . getUser ( userId ) ;
2021-09-02 13:38:55 +02:00
response = await this . channels . setReadState ( userId , channel , author , state ) ;
2021-06-21 16:54:40 +02:00
}
2021-06-20 13:12:01 +02:00
if ( response . error ) return response ;
2021-09-02 13:38:55 +02:00
this . log ( { author , action : ` ${ author . tag } marked ${ user . tag } 's thread as ${ state } ` , target : user } ) ;
2021-06-20 13:12:01 +02:00
return 'Done' ;
2021-06-19 15:06:20 +02:00
}
2021-10-22 09:35:04 +02:00
async sendReminder ( ) {
2021-06-19 23:57:12 +02:00
2022-01-12 17:29:52 +01:00
await this . cache . verifyQueue ( ) ;
2021-06-19 23:57:12 +02:00
const channel = this . reminderChannel ;
const amount = this . queue . length ;
2021-06-21 23:53:00 +02:00
if ( ! amount ) {
2021-06-22 18:01:16 +02:00
if ( this . lastReminder ) {
await this . lastReminder . delete ( ) ;
this . lastReminder = null ;
}
2021-06-21 23:53:00 +02:00
return ;
}
2021-06-19 23:57:12 +02:00
2021-06-21 23:53:00 +02:00
const str = ` ${ amount } modmail in queue. ` ;
2021-06-20 00:33:45 +02:00
this . client . logger . debug ( ` Sending modmail reminder, #mm: ${ amount } ` ) ;
2021-06-19 23:57:12 +02:00
if ( this . lastReminder ) {
2021-11-28 18:31:17 +01:00
if ( channel . lastMessage ? . id === this . lastReminder ? . id ) return this . lastReminder . edit ( str ) ;
2021-06-19 23:57:12 +02:00
await this . lastReminder . delete ( ) ;
2021-06-20 01:20:50 +02:00
}
this . lastReminder = await channel . send ( str ) ;
2021-11-29 11:48:45 +01:00
this . cache . misc . lastReminder = this . lastReminder . id ;
2021-06-19 23:57:12 +02:00
}
2021-10-22 09:35:04 +02:00
async log ( { author , content , action , target } ) {
2021-06-21 16:54:40 +02:00
const embed = {
author : {
name : action ,
// eslint-disable-next-line camelcase
icon _url : author . displayAvatarURL ( { dynamic : true } )
} ,
description : content ? ` \` \` \` ${ content } \` \` \` ` : '' ,
color : this . mainServer . me . highestRoleColor
} ;
if ( target ) {
embed . footer = {
text : ` Staff: ${ author . id } | Target: ${ target . id } `
} ;
}
2021-11-28 18:31:17 +01:00
this . logChannel . send ( { embed } ) . catch ( ( err ) => this . client . logger . error ( ` Error during logging of modmail: \n ${ err . stack } ` ) ) ;
2021-06-21 16:54:40 +02:00
}
2021-10-22 09:35:04 +02:00
getCanned ( name ) {
2021-06-19 15:06:20 +02:00
return this . replies [ name . toLowerCase ( ) ] ;
}
2021-10-22 09:35:04 +02:00
loadReplies ( ) {
2021-06-19 15:06:20 +02:00
2021-06-19 20:05:32 +02:00
this . client . logger . info ( 'Loading canned replies' ) ;
2021-06-19 15:06:20 +02:00
if ( ! fs . existsSync ( './canned_replies.json' ) ) return { } ;
return JSON . parse ( fs . readFileSync ( './canned_replies.json' , { encoding : 'utf-8' } ) ) ;
2021-06-18 15:41:57 +02:00
}
2021-10-22 09:35:04 +02:00
saveReplies ( ) {
2021-06-19 20:05:32 +02:00
this . client . logger . info ( 'Saving canned replies' ) ;
fs . writeFileSync ( './canned_replies.json' , JSON . stringify ( this . replies ) ) ;
}
2021-12-22 10:30:50 +01:00
disable ( reason ) {
this . disabled = true ;
if ( reason ) this . disabledReason = reason ;
else this . disabledReason = null ;
this . cache . misc . disabled = true ;
this . cache . misc . disabledReason = this . disabledReason ;
this . cache . savePersistentCache ( ) ;
}
enable ( ) {
this . disabled = false ;
this . cache . misc . disabled = false ;
this . cache . savePersistentCache ( ) ;
}
2021-06-18 15:41:57 +02:00
}
module . exports = Modmail ;