Skip to content

Instantly share code, notes, and snippets.

@bosconian-dynamics
Last active April 30, 2024 16:49
Show Gist options
  • Save bosconian-dynamics/2ff366984df8259a191490f040a46740 to your computer and use it in GitHub Desktop.
Save bosconian-dynamics/2ff366984df8259a191490f040a46740 to your computer and use it in GitHub Desktop.
Automated cracker for Hackmud Discord's AliceBot tier-1 lock simulations
class AliceBotCT1 {
constructor( httpHeaders ) {
this.alicebotMacro = "!alicebot.out_4l1c3b"
this.discordClient = new DiscordClient( httpHeaders.authorization, httpHeaders.cookie, httpHeaders.superProperties )
this.lockArgs = null
this.latency = {
process: {
start: null,
value: 0
},
dialogue: {
start: null,
value: 0
}
}
}
/**
* Start an AliceBot loc simulation of mode difficulty then crack it
*
* @param mode - an AliceBot difficulty string
*/
start( mode = "normal" ) {
this.lockArgs = new T1LockArgs()
this.latency.process.value = 0
this.latency.dialogue.value = 0
this.discordClient.start()
this.discordClient
.send( "!rotatelocks " + mode )
.then( message => this.getResponse( message, "Difficulty" ) )
.then( response => {
console.log( "Lock simulation initialized." )
console.log( "Clearing rate-limit timeout..." )
// Timeout to clear Discord rate-limiting
setTimeout(() => {
console.log( "Rate limit cleared. Cracking simulation..." )
this.latency.process.start = Date.now()
this.crackSimulation()
.then( msg => {
this.discordClient.stop()
let duration = ( this.latency.process.value + this.latency.dialogue.value ) / 1000
console.log( "Simulation cracked." )
console.log( "Lost " + (( this.latency.dialogue.value ) / 1000) + " seconds to communication latency" )
//console.log( "Finished: \"" + msg + )
//console.log( "Simulation cracked in " + duration + " (normalized) seconds\nLatency:" )
console.log( this.latency )
console.log( "Final arguments:" )
console.log( this.lockArgs.next() )
})
.catch( err => {
this.discordClient.stop()
console.log( "Error: ");
throw err
})
}, 2000 )
} )
}
/**
* Accumulates running totals of time spent waiting for a response and time spent processing
* responses.
*/
measureLatency( type ) {
let now = Date.now()
let offType = type === "dialogue" ? "process" : "dialogue"
this.latency[ type ].value += now - this.latency[ type ].start
this.latency[ offType ].start = now
}
/**
* Sends an argument object to the lock simulator and returns a Promise which resolves with the
* next viable lock simulator response.
*
* @param {object} args - lock arguments
* @returns {Promise} - A Promise that resolves with the response string
*/
callLoc( args ) {
return this.discordClient
.send( this.alicebotMacro + " " + JSON.stringify( args ) )
.then( this.getResponse.bind( this ) )
}
/**
* Returns a promise representing the next loc response. Resolves with the content of the message
* argument if it satisfies the conditions of a loc response - otherwise resolves with the
* content of next message which does.
*
* @param {object} message - a DiscordClient message object
* @returns {Promise} - A promise that resolves with the content of the first message that fits the format of a lock response
*/
getResponse( message, contains = null ) {
return new Promise( resolve => {
let response = this.processLocResponse( message, contains )
if( response )
return resolve( response )
this.discordClient.on( "message", message => {
let response = this.processLocResponse( message, contains )
if( response )
resolve( response )
}, 1 )
} )
}
/**
* Get the loc response from inner code elements - filter out irrelevent messages
*
* @param {object} message - a DiscordClient message object
* @returns {string} - the lock simulator's response
*/
processLocResponse( message, contains = null ) {
let code_el = message.$element.getElementsByTagName( "code" )
if( ! code_el.length )
return
let response = code_el[0].textContent
if( response.includes( "passion" ) )
return
if( contains && !response.includes( contains ) )
return
return response
}
/**
* Cracks the active AliceBot lock simulation by sending a full set of lock arguments
* then incrementing those which the response indicates might be incorrect. Continues
* the process through recursive promises until a "LOCK_ERROR" can no longer be found.
*
* @param {Set} [lastCrackedLocks=new Set()] The locks which have already been cracked
* @returns {Promise} - A Promise which resolves when no LOCK_ERRORs are found in the response
*/
crackSimulation( lastCrackedLocks = new Set() ) {
this.measureLatency( "process" ) // Accumulate processing latency
return this.callLoc( this.lockArgs.next() )
.then( response => {
this.measureLatency( "dialogue" ) // Accumulate Discord dialogue latency
// If a LOCK_ERROR can't be found, the simulation's solved
if( response.lastIndexOf( "LOCK_ERROR" ) < 0 )
return Promise.resolve( "No LOCK_ERROR in " + response )
// Determine which locks have already been cracked
let crackedLocks = new Set()
let unlockedRegex = /LOCK_UNLOCKED (\w*)/g
let unlockMatches = null
while( ( unlockMatches = unlockedRegex.exec( response ) ) )
crackedLocks.add( unlockMatches[1] )
// Determine which lock(s) the last iteration succeeded in cracking
lastCrackedLocks = new Set( [ ...crackedLocks ].filter( lock => !lastCrackedLocks.has( lock ) ) )
// Get the argument indicator from the error response
let lockError = (/\w* is not the correct ([^.]*)\./.exec( response ))[1]
/*let log = {
crackedLocks,
lastCrackedLocks,
response,
lockError,
reset: []
}*/
// Determine which lock argument iterators (if any) were incorrectly incremented in the
// last iteration and reset them to their first possible value
for( let lockType in AliceBotCT1.LOCK_TYPES ) {
if( !AliceBotCT1.LOCK_TYPES.hasOwnProperty( lockType ) )
continue
let lockTypeLocks = AliceBotCT1.LOCK_TYPES[ lockType ]
// Get the locks of this type that were cracked in the last iteration
let lastCrackedOfType = lockTypeLocks.filter( lock => lastCrackedLocks.has( lock ) )
// If no locks of this type were cracked in the last iteration, nothing need be reset
if( !lastCrackedOfType.length )
continue
//log.reset.push.apply( log.reset, lockTypeLocks.filter( lock => !crackedLocks.has( lock ) ) )
// Reset all lock argument iterators of this type for locks which have yet to be cracked
this.lockArgs.reset( lockTypeLocks.filter( lock => !crackedLocks.has( lock ) ) )
}
//log.advance = AliceBotCT1.LOCK_ERRORS[ lockError ].filter( lock => !crackedLocks.has( lock ) )
//console.log( log )
// Advance whatever arguments the error message might be referring to to their next possible
// value, ignoring those which are already cracked
this.lockArgs.next( AliceBotCT1.LOCK_ERRORS[ lockError ].filter( lock => !crackedLocks.has( lock ) ) )
// Run the next iteration
return this.crackSimulation( crackedLocks )
})
.catch( err => { throw err } )
}
}
// Lock names by "category" of primary argument value
AliceBotCT1.LOCK_TYPES = {
// EZ-type locks use an unlock command as their primary argument
EZ: [ "EZ_21", "EZ_35", "EZ_40" ],
// c00x-type locks have a color name as their primary argument
C00X: [ "c001", "c002", "c003" ]
}
// A mapping of error messages to corresponding T1LockArgs argument iterators
AliceBotCT1.LOCK_ERRORS = {
// An incorrect "unlock command" indicates an EZ-type lock
"unlock command": AliceBotCT1.LOCK_TYPES.EZ,
// An incorrect color indicates a c00x-type lock
"color": AliceBotCT1.LOCK_TYPES.C00X,
// An incorrect prime is always EZ_40's ez_prime argument
"prime": [ "ez_prime" ],
// An incorrect digit is always EZ_35's digit argument
"digit": [ "digit" ]
}
class DiscordClient {
constructor( authorizationHeader, cookieHeader, superPropertiesHeader, options = {} ) {
this.auth = authorizationHeader
this.superProps = superPropertiesHeader
this.cookie = cookieHeader
this.userAgent = options.userAgent || window.navigator.userAgent
this.dOMObserver = new MutationObserver( this.processDOMMutations.bind( this ) )
this.$messages = document.getElementsByClassName( "messages" )[0]
this.eventHandlers = {}
}
start() {
this.$messages = document.getElementsByClassName( "messages" )[0]
this.dOMObserver.observe( this.$messages, { childList: true, subtree: true } )
}
stop() {
this.dOMObserver.disconnect()
}
restart() {
this.stop()
this.start()
}
on( event, callback, times = -1 ) {
if( ! this.eventHandlers[ event ] )
this.eventHandlers[ event ] = []
this.eventHandlers[ event ].push( {
callback: callback,
count: times
} )
}
off( event, callback ) {
if( ! this.eventHandlers[ event ] )
return
let index
if( "number" === typeof callback && this.eventHandlers[ event ].length > callback )
index = callback
else
index = this.eventHandlers[ event ].findIndex( handler => handler.callback === callback )
if( index > -1 )
return this.eventHandlers[ event ].splice( index, 1 )
else
return false
}
emit( event, data ) {
if( ! this.eventHandlers[ event ] )
return
this.eventHandlers[ event ].forEach( ( handler, index ) => {
let removeHandler = handler.callback( data )
if( removeHandler || ( handler.count > 0 && --handler.count === 0 ) )
this.eventHandlers[ event ].splice( index, 1 )
})
}
processDOMMutations( mutations ) {
mutations.forEach( mutation => {
if( !mutation.addedNodes || !mutation.addedNodes.length )
return
mutation.addedNodes.forEach( $node => {
let $messageGroup = null
if( !$node.className || !$node.className.includes( "message" ) )
return
if( $node.className.includes( "message-group" ) ) {
if( $node.nextSibling )
return // Not the latest message group
$node = $node.getElementsByClassName( "message" )[0]
}
else if( $node.parentNode.parentNode.nextSibling )
return // Not the latest message group
if( $node.className.includes( "message-sending" ) )
return
this.processMessage( $node )
} )
} )
}
processMessage( $message ) {
let $firstMessage = $message.parentNode.getElementsByClassName( "first" )[0]
let $header = $firstMessage.getElementsByTagName( "h2" )[0]
let channelID = this.getChannelID()
this.emit( "message", {
user: $header.getElementsByClassName( "user-name" )[0].textContent,
time: $header.getElementsByClassName( "timestamp" )[0].textContent,
channelID: channelID,
content: $message.getElementsByClassName( "message-text" )[0].textContent,
$element: $message
} )
}
getChannelID( path ) {
if( ! path )
path = window.location.pathname
path = path.split( '/' )
return path[ path.length - 1 ]
}
getMessageEndpoint( channelID ) {
if( ! channelID )
channelID = this.getChannelID()
return "https://discordapp.com/api/v6/channels/" + channelID + "/messages"
}
/**
* Send a message to the specified channel (or the current channel by default). If sending to the
* current channel, returns a promise that resolves with the next message received.
*
* @param message
* @param channelID
*
* @returns Promise
*/
send( message, channelID ) {
let t = this
if( ! channelID )
channelID = this.getChannelID()
return fetch( new Request( this.getMessageEndpoint( channelID ), {
method: "POST",
headers: new Headers({
"accept": "*/*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US",
"authorization": t.auth,
"cookie": t.cookie,
"dnt": 1,
"origin": window.location.origin,
"referer": window.location.href,
"user-agent": t.userAgent,
"x-super-properties": t.superProps,
"content-type": "application/json"
}),
body: '{"content":"' + message.replace(/"/g, '\\"') + '","tts":false}'
}))
.catch( err => {
console.log( "Early request error:" )
console.log( err )
})
.then( response => {
if( response.ok ) {
this.emit( "send", {
channelID: channelID,
content: message
})
if( channelID === t.getChannelID() ) {
return new Promise( resolve => {
t.on( "message", resolve, 1 )
})
}
return Promise.resolve( response )
}
// If the message failed due to rate-limiting, try again after the timeout clears
if( 429 === response.status ) {
let timeout = response.headers.get( "retry-after" )
console.log( "Message was rate-limited. Retrying in " + timeout + "ms" )
return new Promise( resolve => {
setTimeout(
() => {
console.log( "Timeout cleared. Message re-sent." )
this.send( message, channelID ).then( resolve )
},
timeout
)
})
}
throw "Unhandled response code " + response.status
})
.catch( err => {
console.log( err )
})
}
}
class T1LockArgs {
constructor() {
this.argIterators = {}
this.args = {}
this.reset()
this.next( Object.getOwnPropertyNames( T1LockArgs.ITERATORS ) )
}
next( args ) {
if( args ) {
if( ! (args instanceof Array) )
args = [args]
for( let arg of args )
this.set( arg, this.argIterators[ arg ].next().value )
}
return this.args
}
reset( args ) {
if( !args )
args = Object.getOwnPropertyNames( T1LockArgs.ITERATORS )
else if( ! (args instanceof Array) )
args = [args]
for( let arg of args )
this.argIterators[ arg ] = T1LockArgs.ITERATORS[ arg ]()
//this.next( args )
}
set( arg, val ) {
if( "object" === typeof val ) {
for( let name in val ) {
if( ! val.hasOwnProperty( name ) )
continue
this.args[ name ] = val[ name ]
}
}
else
this.args[ arg ] = val
}
static *arrayGen( arr ) {
arr = arr.slice()
for( let val of arr )
yield val
}
static *colorAndLength() {
let colors = T1LockArgs.COLORS.slice()
for( let i = 0; i < colors.length; i++ ) {
yield {
c001: colors[ i ],
color_digit: colors[ i ].length
}
}
}
static *colorComplements() {
let colors = T1LockArgs.COLORS.slice()
for( let i = 0; i < colors.length; i++ ) {
yield {
c002: colors[ i ],
c002_complement: colors[ ( i + 4 ) % 8 ]
}
}
}
static *colorTriads() {
let colors = T1LockArgs.COLORS.slice()
for( let i = 0; i < colors.length; i++ ) {
yield {
c003: colors[ i ],
c003_triad_1: colors[ ( i + 3 ) % 8 ],
c003_triad_2: colors[ ( i + 5 ) % 8 ]
}
}
}
}
T1LockArgs.EZ_CMDS = [ "open", "release", "unlock" ]
T1LockArgs.COLORS = [ "purple", "blue", "cyan", "green", "lime", "yellow", "orange", "red" ]
T1LockArgs.DIGITS = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
T1LockArgs.PRIMES = [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 ]
T1LockArgs.ITERATORS = {
EZ_21: () => T1LockArgs.arrayGen( T1LockArgs.EZ_CMDS ),
EZ_35: () => T1LockArgs.arrayGen( T1LockArgs.EZ_CMDS ),
digit: () => T1LockArgs.arrayGen( T1LockArgs.DIGITS ),
EZ_40: () => T1LockArgs.arrayGen( T1LockArgs.EZ_CMDS ),
ez_prime: () => T1LockArgs.arrayGen( T1LockArgs.PRIMES ),
c001: () => T1LockArgs.colorAndLength(),
c002: () => T1LockArgs.colorComplements(),
c003: () => T1LockArgs.colorTriads()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment