who doesn't love loopholes
Tags: api, javascript, github, node
Published: 2023-03-12 01:01:38

recently, Khan Academy has deprecated their ol' REST API an' transitioned to GraphQL. well, Khan Academy has fostered a large number of amateur devs on its platform who extensively used the API an' the transition to GraphQL meant that peepz on KA couldn't access the API directly anymore due to CORS. until, two minds, one brilliant an' the other not as much, invented an ingenious way to still access the API.

for those unfamiliar with KA, it's filled with all sorts of teenage devs. on the outside, they are hindered by the limitations of KA, stuck with a sandboxed iframe, long outdated syntax, an' only a few CDNs [jsDelivr, cloudflare, etc]. on the inside, they spend way too much time on a crappy educational site that hates 'em, but have created the most absurd workarounds one can think of an' this one might take the cake.

the idea started here when the brilliant mind, [WKoA](https://github.com/Reginald-Gillepsie), released this [repo](https://github.com/Reginald-Gillepsie/LiveKhanImports) which allowed a user to grab the current bitcoin price from a KA program. i'll explain how it works in greater detail in a sec.

i took this idea an' decided to use it with a node.js script an' a friend's library to grab the API data from KA an' store that instead of the bitcoin price. the process is below.

first, GitHub Actions runs our [YAML script](https://github.com/thelegendski/automated/blob/main/.github/workflows/primavera.yml) which is on a schedule. the YAML script imports [the library](https://github.com/bhavjitChauhan/khan-api) through npm then runs my node.js script.

the node.js script is a lil' complicated after WKoA decided to use XSS to hack the final result, >:(. in a nutshell, it retrieves the API data from KA an' writes the result to a new file that stores the data in an array.
```js
//global constants
const https = require('https')
const fs = require('fs')
const { Client } = require("@bhavjit/khan-api")
const client = new Client()

async function main() {
    //get the replies
    const replies = await client.getAllMessageReplies("ag5zfmtoYW4tYWNhZGVteXJBCxIIVXNlckRhdGEiHmthaWRfMTAzNzkwNDA4MTM5MTE4NzA4MDQ5ODUwNwwLEghGZWVkYmFjaxiAgJOjuLmJCww")
    //cache function variables/constants
    const contestants = [], blacklist = [], teams = ["ninjaz", "astronomers", "leviathans"], regex = {
        join: /\$\[\s*("|')[a-zA-Z0-9 ]+("|')\s*,\s*[0-9]\s*,\s*("|')[A-Za-z0-9 ]+("|')\s*\]\$/,
        username: /(;|function|=>|\/\/|"|'|`)/,
    }
    let comments = ''
    replies.forEach(reply => {
        //the XSS nonsense.
        let string
        try {
            if(!blacklist.includes(reply.author.kaid) && !regex.username.test(reply.author.nickname)){
                //this line is the most important
                if(regex.join.test(reply.text)) string = JSON.parse(reply.text.match(/\[.+\]/))
                else comments += `//WARNING! join failed: ${reply.author.nickname} | ${reply.author.kaid}\n`
            }
            else comments += `//WARNING! blacklisted user attempted to join: ${reply.author.nickname} | ${reply.author.kaid}\n`
        }
        catch (e) { 
            comments += `//ERROR! join failed: ${reply.author.nickname} | ${reply.author.kaid}\n`
            console.error(e) 
        }
        if (string) {
            if(teams.includes(string[2])){
                contestants.push(`{name: "${reply.author.nickname}", kaid: "${reply.author.kaid}", level: ${Math.min(Math.max(string[1], 0), 3)}, team: "${string[2]}"}`)
                console.log(`${reply.author.nickname} joined successfully.`)
            }
            else {
                comments += `//WARNING! invalid join code [team]: ${reply.author.nickname} | ${reply.author.kaid} | ${string[2]}\n`
            }
        }
    })
    //edit the file to match current data
    fs.writeFile('./primaveraContestants.js', `var contestants = [${contestants}]\n${comments}`, err => {
        if (err) console.error(err)
    })
    
//purge the script
https.get("https://purge.jsdelivr.net/gh/thelegendski/automated@master/primaveraContestants.js");
}
main()
```
that file is then imported on KA through jsDelivr an' voila!

well, GitHub Actions wouldn't cut it as there was a possibility that i'd have to pay for the minutes that the script ran on the servers, so we transitioned over to Google Apps Script (GAS). i ironically found the script to commit to GitHub from GAS from an ex-KA dev, an' WKoA found a slightly better version of the script [here](https://gist.github.com/mvl/356be3063026dce6c44d457227b8e167). the link to the complete script can be found [here](https://script.google.com/d/1EGW_mITASHWkumKBAbdEJ9tg0_tQX4Bc9gRFCVvWZauU-GCL_n5fMNlN/edit?usp=sharing).

currently, the only issue that remains is jsDelivr update times. the GAS script should purge the cached script since jsDelivr claims it was purged, but jsDelivr has smth against truthful results since it doesn't update. we're currently in contact with the jsDelivr support team to see if we can resolve this issue, :).

an' that's all! if KA won't let us access their API, we'll figure it out ourselves, >:).