Temm / Anime-Loads AntiCaptcha

// ==UserScript==
// @name         Anime-Loads AntiCaptcha
// @namespace    https://github.com/leumasme
// @version      1.1.1
// @description  Solve the Captcha on anime-loads.org
// @author       Temm
// @match        http*://*.anime-loads.org/media/*
// @icon         https://www.google.com/s2/favicons?domain=anime-loads.org
// @grant        none
// @run-at       document-body
// @license      MIT
// @updateURL    https://openuserjs.org/meta/Temm/Anime-Loads_AntiCaptcha.meta.js
// @downloadURL  https://openuserjs.org/install/Temm/Anime-Loads_AntiCaptcha.user.js
// ==/UserScript==

function log(m) { console.log(`%c[AC] ${m}`, "border: 3px solid red; border-radius: 7px; padding: 3px") }

window.addEventListener("unhandledrejection", console.error)

// Hook JQuery captcha function
jQuery.fn.extend = new Proxy(jQuery.fn.extend, {
    apply: function (target, thisArg, argsList) {
        if (argsList[0].iC) {
            log("Captcha Hook deployed!")
            target.apply(thisArg, [{
                iC: fakeIc
            }])
            return;
        }

        return target.apply(thisArg, argsList);
    }
})

// on done will trigger event (bind) "success.iC"
// Result storage:
// .captcha-holder > input[name="captcha-idhf"] : value = 0
// .captcha-holder > input[name="captcha-hf"] : value = <correct hash>
function fakeIc() {
    let holder = this;
    console.trace();
    log("Starting Solve...")

    let txt = document.createElement("p");
    txt.innerText = "[AC] Solving Captcha...";
    holder.append(txt);

    // Get async context
    (async () => {
        let idhf = document.createElement("input");
        idhf.name = "captcha-idhf";
        idhf.hidden = true;
        idhf.value = "0";

        let hf = document.createElement("input");
        hf.name = "captcha-hf";
        hf.hidden = true;
        hf.value = await getSolvedCaptcha();
        holder.empty();
        holder.append(idhf)
        holder.append(hf)
        txt.innerText = "[AC] Getting Links...";
        holder.append(txt);
        console.log("Holder is ",holder)
        onSuccess()
    })();

    // silence .bind calls after iC; save success event for manual calling
    return {
        bind: function (evt, fun) {
            if (evt == "success.iC") onSuccess = fun;
            return this
        }
    }
}

let cached = null;
async function getSolvedCaptcha() {
    if (cached != null) {
        let c = cached;
        cached = null
        setTimeout(()=>generateSolvedCaptcha().then(r => {
            log("Restored Cache with " + r)
            cached = r
        }), 250);
        log("Solved from Cache! "+c)
        return c;
    } else {
        // return await generateSolvedCaptcha()
        await new Promise(r=>setTimeout(r, 250))
        return await getSolvedCaptcha();
    }
}

async function generateSolvedCaptcha(depth = 0) {
    let hashes = await createCaptcha();
    let pimgs = hashes.map(async (h) => {
        return await loadImage("https://www.anime-loads.org/files/captcha?cid=0&hash=" + h);
    })

    let imgs = await Promise.all(pimgs);

    let base = imgs[0];
    let diffs = [];
    for (let i = 1; i < 5; i++) {
        let diff = diffImages(base, imgs[i]);
        diffs.push(diff);
        // console.log(base, imgs[i])
        // console.log(hashes[0], hashes[i])
        // console.log(diff)
    }
    let sorted = [...diffs].sort((a, b) => b - a);

    // console.log(hashes, imgs, diffs, sorted)

    let correct;
    // If different set produces lower diff, base is correct 
    let checkDiff = diffImages(imgs[1], imgs[2]);
    if (sorted[sorted.length - 1] > checkDiff * 100) {
        console.log("First elem! Check was " + checkDiff + " ; regular was " + sorted[sorted.length - 1])
        correct = hashes[0];
    } else {
        correct = hashes[diffs.indexOf(sorted[0]) + 1];
    }

    log("Found Solution: " + correct)


    let serverCheck = await fetch("https://www.anime-loads.org/files/captcha", {
        method: "POST",
        body: "cID=0&pC=" + correct + "&rT=2",
        headers: {
            "X-Requested-With": "XMLHttpRequest",
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        }
    }).then(t => t.text())

    if (serverCheck != "1") {
        log("Failed - retrying ("+(depth+1)+")")
        if (depth > 3) {
            alert("Unable to solve in 3 tries :/\nAre you blocked?")
            throw new Error("Failed");
        } else return await generateSolvedCaptcha(depth + 1);
    }
    log("Success!")
    return correct;
}

generateSolvedCaptcha().then(r => {
    log("Created Cache with " + r)
    cached = r
});

/**
 * @returns {Promise<string[]>}
 */
function createCaptcha() {
    return fetch("https://www.anime-loads.org/files/captcha",
        {
            method: "POST",
            body: "cID=0&rT=1",
            headers: {
                "X-Requested-With": "XMLHttpRequest",
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
                "Accept": "application/json, text/javascript, */*; q=0.01"
            }
        }
    ).then(r => r.json())
}

/**
 * @param {HTMLImageElement} img 
 * @returns {number}
 */
function diffImages(ia, ib) {
    let ctxa = makeContext();
    console.log("[AC] Drawing and Hashing...")
    ctxa.drawImage(ia, 0, 0);
    let da = ctxa.getImageData(0, 0, ia.width, ia.width);

    var ctxb = makeContext();

    ctxb.drawImage(ib, 0, 0);
    let db = ctxb.getImageData(0, 0, ia.width, ia.width);

    let diff = 0;
    for (let x = 0; x < ia.width; x++) {
        for (let y = 0; y < ia.width; y++) {
            let ca = getColor(x, y, ia.width, da);
            let cb = getColor(x, y, ia.width, db);

            let oa = ca[3] / 255, ob = cb[3] / 255;
            let difr = Math.abs(ca[0] * oa - cb[0] * ob)
            let difg = Math.abs(ca[1] * oa - cb[1] * ob)
            let difb = Math.abs(ca[2] * oa - cb[2] * ob)
            let difa = Math.abs(ca[3] * oa - cb[3] * ob)
            diff += difr + difg + difb + difa
        }
    }
    console.log("[AC] Hashed!")
    return diff;
}

function makeContext() {
    // var ctx = new OffscreenCanvas(48, 48).getContext("2d");
    let c = document.createElement("canvas");
    c.width = 48; c.height = 48;
    c.style.display = "none";
    document.body.appendChild(c);
    return c.getContext("2d");
}
/**
 * @param {string} url 
 * @returns {HTMLImageElement}
 */
function loadImage(url) {
    return new Promise((resolve, reject) => {
        fetch(url, {
            //"mode": "no-cors"
        })
            .then((res) => res.blob())
            .then((blob) => {
                let reader = new FileReader();
                reader.onloadend = () => {
                    console.log("[AC]: Image Reader Finished")
                    let img = new Image();
                    img.onload = () => {
                        resolve(img);
                    }
                    img.src = reader.result;
                }
                // console.log(blob);
                reader.readAsDataURL(blob);
            })
    });
}

// RGBA
/**
 * 
 * @param {number} x 
 * @param {number} y 
 * @param {number} width 
 * @param {ImageData} img
 * @returns {[number, number, number, number]}
 */
function getColor(x, y, width, img) { // stackoverflow magic
    const red = y * (width * 4) + x * 4;
    let [r, g, b, a] = [red, red + 1, red + 2, red + 3];
    return [img.data[r], img.data[g], img.data[b], img.data[a]];
};