import { Message , Guild , TextChannel , SnowflakeUtil } from 'discord.js' ; import { commandInterface } from '../../commands' ; import { permLevels } from '../../utils/permissions' ; import { Bot } from '../..' ; import { sendError } from '../../utils/messages' ; import { permToString , stringToChannel , stringToMember } from '../../utils/parsers' ; enum { number } enum messageTypes { text = 1 , file = 2 , image = 3 , url = 4 , invite = 5 , mention = 7 } enum substringLocation { beginning = 1 , anywhere = 2 , end = 3 } member member member member interface interface purgeCommandCriteria { type ? : messageTypes ; location ? : substringLocation ; value ? : string ; userID ? : string ; } var command = { name : 'purge' , path : '' , dm : false , permLevel : permLevels . mod , togglable : false , help : { shortDescription : 'Command deletes last x messages' , longDescription : 'Command deletes last x messages that match a certain criteria in the current channel.' , usages : [ '{command} [n messages]' , '{command} [user] [n messages]' , '{command} mentions [user] [n messages]' , '{command} endswith [content] [n messages]' , '{command} startswith [content] [n messages]' , '{command} contains [content] [n messages]' , '{command} has [img | text | file | link | invite] [n messages]' ] , examples : [ '{command} 30' , '{command} @jeff#1234 20' , '{command} mentions @jeff#1234 23' , '{command} endswith goodbye 500' , '{command} startswith hello there 20' , '{command} contains general kenobi 16' , '{command} has invite 3' , ] } , run : async ( message , args , permLevel , dm , requestTime ) => { try { if ( args . length == 0 ) { message . channel . send ( await Bot . commands . getHelpEmbed ( command , message . guild ) ) ; Bot . mStats . logMessageSend ( ) ; return false ; } let argsArray = args . split ( ' ' ) . filter ( x => x . length != 0 ) ; let maxMessages = 1000 ; if ( Bot . database . settingsDB . cache . commands [ command . name ] && Bot . database . settingsDB . cache . commands [ command . name ] . maxMessages ) { maxMessages = Bot . database . settingsDB . cache . commands [ command . name ] . maxMessages ; } else { console . warn ( `WARNINGS: PLEASE SET THE 'maxMessages' PROPERTY FOR THE PURGE COMMAND IN THE GLOBAL DOC` ) ; } let numberOfMessages = parseInt ( argsArray [ argsArray . length - 1 ] ) ; if ( ! Number . isInteger ( numberOfMessages ) ) { message . channel . send ( `Invalid argument for number of messages to delete` ) ; Bot . mStats . logMessageSend ( ) ; return false ; } if ( numberOfMessages > maxMessages ) { message . channel . send ( `You can only delete a maximum of ${ maxMessages } messages` ) ; Bot . mStats . logMessageSend ( ) ; return false ; } if ( numberOfMessages <= 0 ) { message . channel . send ( `The number of messages must be greater than 0` ) ; Bot . mStats . logMessageSend ( ) ; return false ; } if ( argsArray . length == 1 ) { executeBulkDeletion ( message , numberOfMessages , requestTime ) ; return true ; } else if ( argsArray . length == 2 ) { let user = await stringToMember ( message . guild , argsArray [ 0 ] , false , false , false ) ; if ( ! user ) { message . channel . send ( `Malformed command` ) ; Bot . mStats . logMessageSend ( ) ; return false ; } let criteria = { userID : user . id } ; executeBulkDeletion ( message , numberOfMessages , requestTime , criteria , 'user' ) ; return true ; } else { switch ( argsArray [ 0 ] ) { case 'mentions' : { let user = ( await stringToMember ( message . guild , argsArray [ 1 ] , false , false , false ) ) . id ; let criteria = { type : messageTypes . mention , value : user } ; executeBulkDeletion ( message , numberOfMessages , requestTime , criteria , argsArray [ 0 ] ) ; return true ; } case 'startswith' : { let startingString = argsArray . slice ( 1 , argsArray . length - 1 ) . join ( " " ) ; let criteria = { value : startingString , location : substringLocation . beginning } ; executeBulkDeletion ( message , numberOfMessages , requestTime , criteria , argsArray [ 0 ] ) ; return true ; } case 'endswith' : { let endingString = argsArray . slice ( 1 , argsArray . length - 1 ) . join ( " " ) ; let criteria = { value : endingString , location : substringLocation . end } ; executeBulkDeletion ( message , numberOfMessages , requestTime , criteria , argsArray [ 0 ] ) ; return true ; } case 'contains' : { let stringToFind = argsArray . slice ( 1 , argsArray . length - 1 ) . join ( " " ) ; let criteria = { value : stringToFind , location : substringLocation . anywhere } ; executeBulkDeletion ( message , numberOfMessages , requestTime , criteria , argsArray [ 0 ] ) ; return true ; } case 'has' : { let criteria = { } ; switch ( argsArray [ 1 ] ) { case 'text' : criteria . type = messageTypes . text ; break ; case 'file' : criteria . type = messageTypes . file ; break ; case 'img' : criteria . type = messageTypes . image ; break ; case 'invite' : criteria . type = messageTypes . invite ; break ; case 'link' : criteria . type = messageTypes . url ; break ; default : message . channel . send ( `Malformed command` ) ; Bot . mStats . logMessageSend ( ) ; return false ; } executeBulkDeletion ( message , numberOfMessages , requestTime , criteria , argsArray [ 0 ] + ` ` + argsArray [ 1 ] ) ; return true ; } default : break ; } } return true ; } catch ( e ) { sendError ( message . channel , e ) ; Bot . mStats . logError ( e , command . name ) ; return false ; } } } ; async function executeBulkDeletion ( message , numberOfMessages , requestTime , criteria ? , subCommand ? ) { await message . delete ( ) ; if ( ! ( message . channel instanceof TextChannel ) ) return ; let found = await DeleteLastXMessages ( numberOfMessages , message . channel , message . id , criteria ) ; Bot . mStats . logResponseTime ( command . name , requestTime ) ; if ( ! found ) { message . channel . send ( `I couldn't find any messages based on your criteria/range` ) ; Bot . mStats . logMessageSend ( ) ; } Bot . mStats . logCommandUsage ( command . name , subCommand ) ; } param { purgeCommandCriteria } criteria param { number } numberOfMessages param { TextChannel } channel param { string } requestSnowflake returns { boolean } async function DeleteLastXMessages ( numberOfMessages , channel , requestSnowflake , criteria ? ) { let found = false ; let lastSnowflake = requestSnowflake ; while ( numberOfMessages != 0 ) { let nMessages = numberOfMessages % 100 ? numberOfMessages % 100 : 100 ; if ( ! criteria ) { numberOfMessages -= ( await channel . bulkDelete ( nMessages , true ) ) . size ; found = true ; } else { let result = await bulkDeleteByCriteria ( channel , nMessages , criteria , lastSnowflake ) ; if ( ! result . found ) break ; found = true ; lastSnowflake = result . lastSnowflake ; numberOfMessages -= result . nMessages ; } } return found ; } param { TextChannel } channel param { number } nMessages param { purgeCommandCriteria } criteria param { string } beforeSnowflake returns async function bulkDeleteByCriteria ( channel , nMessages , criteria , beforeSnowflake ? ) { nMessages = nMessages > 100 ? 100 : nMessages ; beforeSnowflake = beforeSnowflake || undefined ; let returnedMessages = await channel . fetchMessages ( { limit : nMessages , before : beforeSnowflake } ) ; let messages = [ ] ; for ( const message of returnedMessages ) { if ( valid ( message [ 1 ] , criteria ) ) messages . push ( message [ 1 ] ) ; } let found = false ; if ( messages . length != 0 ) { if ( ( await channel . bulkDelete ( messages , true ) ) . size > 0 ) found = true ; } return { found : found , nMessages : messages . length , lastSnowflake : returnedMessages . last ( ) ? returnedMessages . last ( ) . id : undefined } ; } param { Message } message param { purgeCommandCriteria } criteria returns { boolean } function valid ( message , criteria ) { if ( ! message . deletable ) return false ; if ( criteria . userID ) { if ( message . author . id != criteria . userID ) return false ; } if ( criteria . value && criteria . location ) { if ( criteria . location == substringLocation . beginning ) { if ( ! message . content . startsWith ( criteria . value ) ) return false ; } else if ( criteria . location == substringLocation . anywhere ) { if ( ! message . content . includes ( criteria . value ) ) return false ; } else if ( criteria . location == substringLocation . end ) { if ( ! message . content . endsWith ( criteria . value ) ) return false ; } } if ( criteria . type ) { if ( criteria . type == messageTypes . text ) { if ( message . content == "" ) return false ; } if ( criteria . type == messageTypes . file ) { if ( message . attachments . size == 0 ) return false ; } if ( criteria . type == messageTypes . image ) { let containsImage = false ; for ( const messageAttachment of message . attachments ) { if ( messageAttachment [ 1 ] . width ) containsImage = true ; } if ( ! containsImage ) return false ; } if ( criteria . type == messageTypes . url ) { let regexp = /^(?:(?:https?|ftps?):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/ ; if ( ! regexp . test ( message . content ) ) return false ; } if ( criteria . type == messageTypes . invite ) { if ( ! ( message . content . includes ( 'discord.gg/' ) || message . content . includes ( 'discordapp.com/invite/' ) ) ) return false ; } if ( criteria . value && criteria . type == messageTypes . mention ) { let mentions = false ; for ( const messageMention of message . mentions . users ) { if ( messageMention [ 1 ] . id == criteria . value ) mentions = true ; } if ( ! mentions ) return false ; } } return true ; } export default command ;