ExamDL

Export your exam submissions

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         ExamDL
// @namespace    http://tampermonkey.net/
// @version      2025-12-04
// @description  Export your exam submissions
// @author       PsychedelicPalimpsest
// @match        https://osu.instructure.com/courses/*/quizzes/*
// @match        https://osu.instructure.com/courses/*/modules
// @icon         https://www.google.com/s2/favicons?sz=64&domain=instructure.com
// @require      https://unpkg.com/[email protected]/worker.js
// @grant        none
// @license      MIT
// ==/UserScript==
 
 
(async function() {
    'use strict';


    function downloadJson(name, data) {
        let blob = new Blob([JSON.stringify(data)], {
            type: "application/json",
        });

        let a = document.createElement("a");
        a.href = URL.createObjectURL(blob);
        a.download = name;
        a.click();
    }

    async function getSubmissions(course_id, quiz_id) {
        let req = await fetch(`https://osu.instructure.com/api/v1/courses/${course_id}/quizzes/${quiz_id}/submissions/`);
        let jso = await req.json();
        if (req.status != 200) {
            alert("Error: " + JSON.stringify(jso));
            throw "Shit";
        }
        return jso.quiz_submissions;
    }
    async function getSubmission(submission_id) {
        let req = await fetch(`https://osu.instructure.com/api/v1/quiz_submissions/${submission_id}/questions?include=quiz_question`)
        let jso = await req.json();
        if (req.status != 200) {
            alert("Error: " + JSON.stringify(jso));
            throw "Shit";
        }
        return jso;

    }



    async function getLastSubmission(course_id, quiz_id) {
        let subs = await getSubmissions(course_id, quiz_id);

        if (subs.length == 0) {
            alert("You must first submit the quiz!");
            throw 'Shit';
        }
        return await getSubmission(subs[subs.length - 1].id);

    }

    function onExportLastSubmission(course_id, quiz_id) {
        getLastSubmission(course_id, quiz_id).then(r => {
            downloadJson("submission.json", r);
        });
    }

    // https://osu.instructure.com/courses/195866/quizzes/1312570/history?version=1&headless=1
    async function getSubmissionHtml(course_id, quiz_id, version) {
        let req = await fetch(`https://osu.instructure.com/courses/${course_id}/quizzes/${quiz_id}/history?version=${version}&headless=1`);
        let dp = new DOMParser()

        return dp.parseFromString(await req.text(), "text/html")
    }


    async function attemptExport(course_id, quiz_id, noError) {
        let subs = await getSubmissions(course_id, quiz_id);

        if (subs.length == 0) {
            if (noError) return null;
            alert("You must first submit the quiz!");
            throw 'Shit';
        }


        let [sub_json, sub_html] = await Promise.all([
            getSubmission(subs[subs.length - 1].id),
            getSubmissionHtml(course_id, quiz_id, subs.length)
        ]);
        return sub_json.quiz_questions.map((question) => {
            let question_html = sub_html.querySelector(`div#question_${question.id} > div.text > div.answers`);

            switch (question.question_type) {

                case "multiple_choice_question":
                case "multiple_answers_question":
                case "true_false_question":
                    if (!question_html) break;
                    question.answers = question.answers.map((answer) => {
                        answer.is_correct = !!question_html.querySelector(`div.correct_answer#answer_${answer.id}`);
                        return answer;
                    });



                    break;



                default:
                    break;
            }
            return question;


        });



    }

    function onAttemptExport(course_id, quiz_id) {
        attemptExport(course_id, quiz_id).then((answers) => JSON.stringify(downloadJson(ENV.QUIZ.title + ".json", answers), null, 2));
    }

    function quizzes_page() {
        // Path of '/courses/COURSE_ID/quizzes/QUIZ_ID'

        let split = location.pathname.split("/quizzes/");

        let course_id = 1 * split[0].split("/")[split[0].split("/").length - 1];
        let quiz_id = 1 * split[1].split("/")[0];

        let header = document.querySelector("#quiz_title");
        let ref = header.querySelector("button.ally-add-tooltip");

        let btn = document.createElement("button");
        btn.classList.add("bux-button--small")
        btn.textContent = "Export last submission data";
        btn.onclick = onExportLastSubmission.bind(this, course_id, quiz_id);

        header.insertBefore(btn, ref)


        btn = document.createElement("button");
        btn.classList.add("bux-button--small")
        btn.textContent = "Attempt export with answers";
        btn.onclick = onAttemptExport.bind(this, course_id, quiz_id);


        header.insertBefore(btn, ref)
    }

    let downloadElem;

    function addToDownloadElem(text) {
        let p = document.createElement('span');
        p.textContent = text;
        downloadElem.appendChild(p);
        downloadElem.appendChild(document.createElement('br'));

        downloadElem.scrollTo(0, downloadElem.scrollHeight);
    }

    async function* getAllQuizzes(course_id) {
        const PER_PAGE = 10;

        for (let page = 1;; page++) {
            let req = await fetch(`https://osu.instructure.com/api/v1/courses/${course_id}/quizzes?per_page=${PER_PAGE}&page=${page}`);
            let jso = await req.json();
            if (req.status != 200) {
                alert("Error: " + JSON.stringify(jso));
                throw "Shit";
            }
            if (jso.length == 0) break;

            for (let item of jso) {
                yield item;
            }

        }
    }


    async function exportAll(course_id) {
        let files = [];

        for await (let quiz of getAllQuizzes(course_id)) {
            addToDownloadElem(`Starting ${quiz.title}`);

            let answer = await attemptExport(course_id, quiz.id, true);
            if (answer != null)
                files.push({
                    name: quiz.title + ".json",
                    input: JSON.stringify(answer, null, 2)
                });
        }




        let zipBlob = await downloadZip(files).blob();
        let a = document.createElement("a");
        a.href = URL.createObjectURL(zipBlob);
        a.download = "quizes.zip";
        a.click();
    }

    function onExportAll(course_id) {

        downloadElem.classList.add("active");
        downloadElem.textContent = "";


        addToDownloadElem("Starting quiz export!");

        exportAll(course_id).then(_ => {
            downloadElem.classList.remove("active");
        })

    }



    function modules_page() {
        document.head.innerHTML += `
        <style>
    .foldMenu{
        position: absolute;
        width: 100%;
        top: 0px;
        height: 0%;

         z-index: 1000;

        background-color: grey;
        color: white;

        overflow-x: hidden;
        overflow-y: scroll;

        transition: height 0.3s
    }
    .active.foldMenu{
        height: 40%;
        border: double;
    }
        </style>`;



        downloadElem = document.createElement("div");
        downloadElem.classList.add("foldMenu");
        downloadElem.setAttribute("tabindex", "-1"); // Don't mess with tab key
        document.body.insertBefore(downloadElem, document.body.children[0])

        let btn = document.createElement("button");
        btn.textContent = "Export all quizzes";
        btn.classList.add('btn');
        btn.onclick = onExportAll.bind(this, 1 * location.pathname.match(/\/courses\/(\d+)\/modules/)[1]);


        document.querySelector(".header-bar-right > div").appendChild(btn);
    }


    if (location.href.includes("/quizzes/")) quizzes_page();
    if (location.href.includes("/modules")) modules_page();




})();