TronClass Copilot

Your best copilot for TronClass

Per 06-05-2024. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name          TronClass Copilot
// @namespace     Anong0u0
// @version       0.0.8
// @description   Your best copilot for TronClass
// @author        Anong0u0
// @match         https://eclass.yuntech.edu.tw/*
// @match         https://elearning.aeust.edu.tw/*
// @match         https://elearn.ntuspecs.ntu.edu.tw/*
// @match         https://elearn2.fju.edu.tw/*
// @match         https://iclass.tku.edu.tw/*
// @match         https://tronclass.ntou.edu.tw/*
// @match         https://tronclass.hk.edu.tw/*
// @match         https://tronclass.kh.usc.edu.tw/*
// @match         https://tronclass.usc.edu.tw/*
// @match         https://tronclass.cyut.edu.tw/*
// @match         https://tronclass.ypu.edu.tw/*
// @match         https://tccas.thu.edu.tw/*
// @match         https://ilearn.thu.edu.tw:8080/*
// @match         https://tronclass.cjcu.edu.tw/*
// @match         https://tronclass.asia.edu.tw/*
// @match         https://tronclass.mdu.edu.tw/*
// @match         https://ulearn.nfu.edu.tw/*
// @match         https://tc.nutc.edu.tw/*
// @match         https://tronclass.au.edu.tw/*
// @match         https://tronclass.cgust.edu.tw/*
// @match         https://tronclass.ocu.edu.tw/*
// @match         https://tc.stu.edu.tw/*
// @match         https://tronclass.ctust.edu.tw/*
// @match         https://tronclass.pu.edu.tw/*
// @match         https://nou.tronclass.com.tw/*
// @match         https://tronclass.must.edu.tw/*
// @match         https://tronclass.scu.edu.tw/*
// @match         https://ilearn.ttu.edu.tw/*
// @match         https://iclass.hwu.edu.tw/*
// @icon          https://tronclass.com.tw/static/assets/images/favicon-b420ac72.ico
// @grant         GM_xmlhttpRequest
// @run-at        document-start
// @license       Beerware
// ==/UserScript==

const _parse = JSON.parse
const parseSet = {allow_download: true, allow_forward_seeking: true, pause_when_leaving_window: false}
JSON.parse = (text, reviver) => _parse(text, (k, v) => {
    if(reviver) v = reviver(k, v)
    return parseSet[k] || v
})

const _addEventListener = Window.prototype.addEventListener;
Window.prototype.addEventListener = (eventName, fn, options) => {
    if (eventName === "blur" && String(fn).match(/pause/)) return;
    _addEventListener(eventName, fn, options);
};

const delay = (ms = 0) => {return new Promise((r)=>{setTimeout(r, ms)})}

const waitElementLoad = (elementSelector, selectCount = 1, tryTimes = 1, interval = 0) =>
{
    return new Promise(async (resolve, reject)=>
    {
        let t = 1, result;
        while(true)
        {
            if(selectCount != 1) {if((result = document.querySelectorAll(elementSelector)).length >= selectCount) break;}
            else {if(result = document.querySelector(elementSelector)) break;}

            if(tryTimes>0 && ++t>tryTimes) return reject(new Error("Wait Timeout"));
            await delay(interval);
        }
        resolve(result);
    })
}

const requests = ({method, url, type = "", data = null, headers = {}}) => {
    return new Promise(async (resolve, reject) => {
        GM_xmlhttpRequest({
            method: method,
            url: url,
            headers: headers,
            responseType: type,
            data: data,
            onload: resolve,
            onerror: reject,
            onabort: reject
        });
    });
};

Node.prototype.catch = function ()
{
    const a = document.createElement("a")
    a.target = "_blank"
    this.insertAdjacentElement("beforebegin", a)
    a.appendChild(this)
    return a
}

const css = document.createElement("style")
css.innerHTML = `
.title,
.forum-category-title,
.group-set > span
{color:var(--primary-text-color)}
`

const embedLink = async () =>
{
    const courseID = location.href.match(/(?<=course\/)\d+/)
    document.body.appendChild(css)
    if (location.href.match(/learning-activity\/full-screen/))
    {
        const dict = {};
        (await requests({method:"get", url:`/api/courses/${courseID}/activities`,type:"json"}))
                    .response.activities.forEach((e)=>{dict[e.title] = e.type=="questionnaire"?`questionnaire/${e.id}`:e.id});
        (await requests({method:"get", url:`/api/courses/${courseID}/exams`,type:"json"}))
                    .response.exams.forEach((e)=>{dict[e.title] = `exam/${e.id}`});
        document.querySelectorAll(".activity a[ng-click]").forEach((e) =>
        {
            if(!(e.textContent.trim() in dict)) return;
            e.addEventListener("click", (e) => e.stopImmediatePropagation(), true)
            e.href = `/course/${courseID}/learning-activity/full-screen#/${dict[e.textContent.trim()]}`
        })
    }
    else if (location.href.match(/homework/))
    {
        const ids = (await requests({method:"get", url:`/api/courses/${courseID}/homework-activities?page_size=20`,type:"json"}))
                    .response.homework_activities.map((e)=>e.id).reverse()
        document.querySelectorAll(".list-item").forEach((e, idx)=>
        {
            e.querySelectorAll("[ng-click]").forEach((e)=>e.addEventListener("click", (e) => e.stopImmediatePropagation(), true))
            e.catch().href = `/course/${courseID}/learning-activity#/${ids[idx]}`
        })
        // TODO: 交作業
    }
    else if (location.href.match(/(?<!learning-activity#\/)exam/))
    {
        const ids = (await requests({method:"get", url:`/api/courses/${courseID}/exam-list?page_size=20`,type:"json"}))
                    .response.exams.map((e)=>e.id).reverse()
        document.querySelectorAll(".sub-title").forEach((e, idx)=>
        {
            e.querySelector("[ng-click]").addEventListener("click", (e) => e.stopImmediatePropagation(), true)
            e.catch().href = `/course/${courseID}/learning-activity#/exam/${ids[idx]}`
        })
    }
    else if(location.href.match(/forum(?!#\/topic-category)/))
    {
        const ids = (await requests({method:"get", url:`/api/courses/${courseID}/topic-categories?page_size=20`,type:"json"}))
                    .response.topic_categories.map((e)=>e.id).reverse()
        ids.unshift(ids.pop())
        document.querySelectorAll(".list-item").forEach((e, idx)=>
        {
            e.addEventListener("click", (e) => e.stopImmediatePropagation(), true);
            e.catch().href = `/course/${courseID}/forum#/topic-category/${ids[idx]}`
        })
    }
    else if(location.href.match(/forum#\/topic-category/))
    {
        const page = document.querySelector(".pager-button.active").innerText,
              count = document.querySelector(".last-page").innerText,
              ids = (await requests({method:"get", url:`/api/forum/categories/${location.href.match(/(?<=category\/)\d+/)}?page=${page}`,type:"json"}))
                    .response.result.topics.map((e)=>e.id),
              idList = ids.join(",")
        document.querySelectorAll(".list-item a[ng-click]").forEach((e, idx)=>
        {
            e.addEventListener("click", (e) => e.stopImmediatePropagation(), true);
            e.href = `/course/${courseID}/forum#/topics/${ids[idx]}?topicIds=${idList}&pageIndex=${page}&pageCount=${count}&predicate=lastUpdatedDate&reverse`
        })
    }
    else if (location.href.match(/\/user\/index/))
    {
        const li = (await requests({method:"get", url:`/api/todos`,type:"json"}))
                    .response.todo_list.sort((a,b)=>new Date(a.end_time)-new Date(b.end_time)).map((e)=>{return {cid:e.course_id, aid:e.id, type:e.type}})
        document.querySelectorAll(".todo-list > a").forEach((e, idx)=>
        {
            switch(li[idx].type)
            {
                case "homework":
                    e.href = `/course/${li[idx].cid}/learning-activity#/${li[idx].aid}`
                    break
                case "exam":
                    e.href = `/course/${li[idx].cid}/learning-activity#/exam/${li[idx].aid}`
                    break
                case "questionnaire":
                    e.href = `/course/${li[idx].cid}/learning-activity/full-screen#/questionnaire/${li[idx].aid}`
                    break
            }
            e.addEventListener("click", (e) => e.stopImmediatePropagation(), true)
        })
    }

    document.querySelectorAll("[id^=learning-activity]").forEach((e) =>
    {
        const actID = e.id.match(/\d+/),
              collapse = e.querySelector(".expand-collapse-attachments")
        if(collapse) collapse.addEventListener("click", (e) => e.preventDefault())
        e.querySelectorAll(".attachment-row").forEach((e)=>e.addEventListener("click", (e) => e.preventDefault()))
        e.querySelector("[ng-click]").addEventListener("click", (e) => {
            if(!(e.target == collapse || e.target.closest(".attachment-row"))) e.stopImmediatePropagation();
        }, true);
        const type = e.querySelector("[ng-switch-when]").getAttribute("ng-switch-when"),
              a = e.catch()
        switch(type)
        {
            case "exam":
                a.href = `/course/${courseID}/learning-activity#/exam/${actID}`
                break
            case "web_link":
                requests({method:"get",url:`/api/activities/${actID}`,type:"json"}).then((r)=>{a.href = r.response.data.link})
                break
            case "homework":
                a.href = `/course/${courseID}/learning-activity#/${actID}`
                break
            case "questionnaire":
                a.href = `/course/${courseID}/learning-activity/full-screen#/questionnaire/${actID}`
                break
            case "material":
            case "online_video":
            case "forum":
            default:
                a.href = `/course/${courseID}/learning-activity/full-screen#/${actID}`
                break
            /* TODO:
            'slide': '微課程',
            'lesson': '錄影教材',
            'lesson_replay': '教室录播',
            'chatroom': 'iSlide 直播',
            'classroom': '隨堂測驗',
            'page': '頁面',
            'scorm': '第三方教材',
            'interaction': '互動教材',
            'feedback': '教學回饋',
            'virtual_classroom': 'Connect 直播',
            'zoom': 'Zoom直播',
            'microsoft_teams_meeting': 'Teams 直播',
            'google_meeting': 'Google Live',
            'webex_meeting': 'Webex 直播',
            'welink': 'Welink',
            'classin': 'ClassIn 直播',
            'live_record': '直播',
            'select_student': '選人',
            'race_answer': '搶答',
            'number_rollcall': '数字点名',
            'qr_rollcall': '二维码点名',
            'virtual_experiment': '模擬實驗',
            'wecom_meeting': 'WeCom會議',
            'vocabulary': '詞彙表',
            */
        }

    })
}

const videoSpeedrun = async (element) =>
{
    const [userID, orgID] = st ? [st.userId, st.orgId] : (await requests({method:"get",url:"/api/profile",type:"json"}).then((e)=>[e.response.id, e.response.org.id])),
          courseID = st?.tags.course_id || location.href.match(/(?<=course\/)\d+/),
          actID = st?.tags.activity_id || location.href.split('/').pop(),
          endTime = element.innerText.split(":").map((e)=> Number(e)).reduce((acc, curr, index) => acc + curr * [3600,60,1][index], 0),
          need = Number(document.querySelector(".completion-criterion > .attribute-value").innerText.match(/\d+(?=%)/)),
          now = (await requests({method:"POST", url:`/api/course/activities-read/${actID}`, type:"json"})).response.data?.completeness || 0

    await requests({ // increase student stat times for watch video
        method:"post",
        url:"/statistics/api/online-videos",
        data:`{"user_id":${userID},"org_id":${orgID},"course_id":${courseID},"activity_id":${actID},"action_type":"view","ts":${Date.now()}}`
    })

    if(now >= need)
    {
        console.log("速刷已完成");
        return;
    }

    const title = document.querySelector("span.title"),
          origText = title.innerText
    let res;
    for(let nowTime=Math.floor(endTime*0.01*Math.max(now-10,0)); nowTime!=endTime;)
    {
        const dur = endTime-nowTime<120 ? endTime-nowTime : Math.floor(120-Math.random()*66),
              newTime = nowTime + dur
        res = await requests({ // watch video api
            method:  "POST",
            url:     `/api/course/activities-read/${actID}`,
            data:    `{"start":${nowTime},"end":${newTime}}`,
            headers: {"Content-Type": "application/json"},
            type:    "json"
        }).catch(()=>alert("速刷失敗,請聯絡作者"))

        await requests({ // increase student stat video watching time
            method:"post",
            url:"/statistics/api/online-videos",
            data:`{"user_id":${userID},"org_id":${orgID},"course_id":${courseID},"activity_id":${actID},"action_type":"play","ts":${Date.now()},"start_at":${nowTime},"end_at":${newTime},"duration":${dur}}`
        })
        await requests({ // increase student stat times for access course
            method:"post",
            url:"/statistics/api/user-visits",
            data:`{"user_id":"${userID}","org_id":${orgID},"course_id":"${courseID}","visit_duration":${dur}}`
        })

        console.log(`${res.response.data.completeness}% ${nowTime}-${newTime} (${endTime})`)
        title.innerHTML = `<b>[速刷中] (${res.response.data.completeness}%) ${newTime}/${endTime}s</b> ${origText}`
        nowTime = newTime
    }
    await delay(100)
    if(res.response.data.completeness < need) alert("速刷失敗,請聯絡作者")
    location.reload()
}

const myStat = async () =>
{
    const courseID = location.href.match(/(?<=course\/)\d+/)

}

let lock = false;
waitElementLoad("#ngProgress",1,0,50).then((e)=>{
    new MutationObserver(()=>{
        if(lock==false && e.style.width=="100%")
        {
            lock = true
            embedLink()
            waitElementLoad("span[ng-bind='ui.duration|formatTime']").then(videoSpeedrun).catch(()=>{})
            if(location.href.match(/course\/\d+/)) myStat()
        }
        else if (e.style.width=="0%") lock = false;
    }).observe(e, {attributes:true})
})
waitElementLoad("[data-category=tronclass-footer]",1,10,200).then((e)=>e.remove())