Last active
April 30, 2024 16:49
-
-
Save bosconian-dynamics/2ff366984df8259a191490f040a46740 to your computer and use it in GitHub Desktop.
Automated cracker for Hackmud Discord's AliceBot tier-1 lock simulations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) | |
}) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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