雨课堂小助手

适配FJNU

// ==UserScript==
// @name         雨课堂小助手
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  适配FJNU
// @author       原作者:曦月
// @license      MIT
// @match        https://*.yuketang.cn/pro/lms/*/homework/*
// @match        https://*.yuketang.cn/pro/lms/*/studycontent
// @match        https://*.yuketang.cn/pro/lms/*/video/*
// @require      https://lib.baomitu.com/axios/0.27.2/axios.min.js
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // ajax监听列表
    const ajaxList = [];
    const listenList = [];

    const div = document.createElement("div")
    div.innerHTML = html
    document.body.appendChild(div)
    const showEl = document.querySelector(".title"),
        showEl2 = document.querySelector(".press")

    const url = location.pathname;
    console.log("脚本运行")
    // 同时播放视频数量
    const videoNum = 5;
    // 习题目录
    const exerciseList = new RegExp(/get_exercise_list\/.+term/)
    // 习题页面
    const homework = new RegExp(/lms\/.+\/homework\//)
    // 学习内容
    const studycontent = new RegExp(/lms\/.+\/studycontent/)
    // 学习列表
    const studyList = new RegExp(/\/lms\/learn\/course\/chapter.+/)
    // 完成进度
    const progress = new RegExp(/\lms\/learn\/course\/schedule/)
    // 视频播放
    const videoPlay = new RegExp(/\lms\/.+video\/.+/)
    // 获取视频链接
    const getVideoUrl = new RegExp(/audiovideo\/playurl/)
    // 视频进度提交
    const videoPress = new RegExp(/video-log\/heartbeat/)

    // 房间id
    const classroom_id = +location.pathname.split("/")[4];

    if (homework.test(url)) {
        console.log("习题页面")
        answerQuestions();
    } else if (studycontent.test(url)) {
        console.log("学习内容")
        studycontentList();
    } else if (videoPlay.test(url)) {
        console.log("视频播放")
        playVideo();
    }

    // 答题页面逻辑
    function answerQuestions() {
        const API_HEADER = localStorage.getItem("API_HEADER")
        let universityId = null
        if (API_HEADER) {
            universityId = JSON.parse(API_HEADER).school_id;
        } else {
            alert("缺少运行必要参数");
            return;
        }
        const ansUrl = `https://uestcedu.yuketang.cn/mooc-api/v1/lms/exercise/problem_apply/?term=latest&uv_id=${universityId}`

        // 题目处理
        async function ans(data) {
            let answ = data.res.data.problems
            console.log("所有题目", answ)
            for (let key = 0; key < answ.length; key++) {
                const el = answ[key];
                // 过滤已经答对的题目
                if (!el.user.is_show_answer) {
                    showEl.innerText = `正在答题 [${key + 1}]`
                    if (el.content.Type === "MultipleChoice") {
                        // 多选
                        console.log(`[${key + 1}]多选题:${el.content.Body}`)
                        showEl2.innerText = ""
                        await goAnas(el.content, true)
                    } else {
                        // 单选
                        console.log(`[${key + 1}]单选题:${el.content.Body}`)
                        showEl2.innerText = ""
                        await goAnas(el.content, false)
                    }
                } else {
                    console.log("题目已答对")
                }
            }
            console.log("所有题目已完成")
            showEl.innerText = "所有题目已完成"
            showEl2.innerText = ""
        }

        // 答题
        async function goAnas(data, isMultip) {
            const ansList = ansArr(data.Options, isMultip)
            for (let key = 0; key < ansList.length; key++) {
                await delay(3000);
                const res = await axios.post(ansUrl, {
                    answer: ansList[key],
                    classroom_id,
                    problem_id: data.ProblemID
                }, { headers: { 'xtbz': 'cloud' } })
                if (res.data.data.is_right || res.data.data.is_correct) {
                    console.log(`尝试[${key + 1}/${ansList.length}] ${ansList[key]} 成功`)
                    showEl2.innerText = `尝试[${key + 1}/${ansList.length}] ${ansList[key]} 成功`
                    break;
                } else {
                    console.log(`尝试[${key + 1}/${ansList.length}] ${ansList[key]} 失败`)
                    showEl2.innerText = `尝试[${key + 1}/${ansList.length}] ${ansList[key]} 失败`
                }
            }
        }

        // 请求间隔
        function delay(time) {
            return new Promise((res, rej) => {
                setTimeout(() => {
                    res()
                }, time)
            })
        }

        // 生成所有答案组合
        function ansArr(arr, isMultip) {
            let N = null, answerArr = arr.map(el => el.key);
            if (isMultip) {
                N = answerArr.length;
            } else {
                N = 1;
            }
            let newAns = [];
            if (N === 1) {
                newAns = newAns.concat(combine(answerArr, 1))
            } else {
                for (let i = 2; i <= N; i++) {
                    newAns = newAns.concat(combine(answerArr, i))
                }
            }
            return newAns;

            function combine(arr, N) {
                //存放索引号
                let res = []
                //存放最后的结果
                var stack = []
                arrayN(arr, 0, N, N, res, stack)
                // return pedding(stack); // 如果下面返回的答案全都不对再启用这个,上面的函数也要改
                return stack;
            }

            // 中间方法
            function pedding(arr) {
                let newArr = [];
                for (let key = 0; key < arr.length; key++) {
                    newArr.push(permute(arr[key]))
                }
                return newArr
            }

            // 所有组合方式
            function permute(input) {
                var permArr = [],
                    usedChars = [];
                function main(input) {
                    var i, ch;
                    for (i = 0; i < input.length; i++) {
                        ch = input.splice(i, 1)[0];
                        usedChars.push(ch);
                        if (input.length == 0) {
                            permArr.push(usedChars.slice());
                        }
                        main(input);
                        input.splice(i, 0, ch);
                        usedChars.pop();
                    }
                    return permArr
                }
                return main(input);
            };

            function arrayN(arr, start, count, Num, res, stack) {
                //用递归实现,把N个循环用同一个循环实现
                for (let i = start; i < arr.length - count + 1; i++) {
                    //记录索引号
                    res[count - 1] = i;
                    if (count - 1 == 0) {
                        let oneResult = []
                        for (let j = Num - 1; j >= 0; j--) {
                            oneResult.push(arr[res[j]])
                        }
                        stack.push(oneResult)
                    } else {
                        arrayN(arr, i + 1, count - 1, Num, res, stack)
                    }
                }
            }
        }

        listenAjax(exerciseList, ans);
    }

    // 学习内容页面
    function studycontentList() {
        showEl.innerText = "加载学习列表中";

        const learArr = [];
        let isLearArr = [];
        function reqs(data) {
            data.res.data.course_chapter.forEach(el => {
                if (!el.is_locked) {
                    el.section_leaf_list.forEach(el_ => {
                        if (el_.leaf_list) {
                            learArr.push(...el_.leaf_list)
                        } else {
                            learArr.push(el_)
                        }
                    })
                }
            });
            showEl.innerText = "获取到列表,查询进度";
        }
        function prss(data) {
            const prssArr = data.res.data.leaf_schedules
            // 未完成列表
            const dontLear = learArr.filter(el => {
                const keys = Object.keys(prssArr)
                const findData = keys.find(i => +i === el.id);
                if (findData !== undefined) {
                    const learData = prssArr[findData]
                    if (learData !== undefined) {
                        if (typeof learData === "number") {
                            if (+learData !== 1) {
                                return true;
                            }
                        } else if (learData.total !== learData.done) {
                            return true;
                        }
                    } else {
                        return true;
                    }
                } else {
                    return true;
                }
            })
            console.log(dontLear)
            // 作业
            const work = dontLear.filter(el => el.leaf_type == 6);
            // 视频
            const video = dontLear.filter(el => el.leaf_type != 6);
            showEl.innerText = `挂机执行中,剩余(${dontLear.length})...`;
            if (work.length) {
                startLears([work[0], ...video.splice(0, videoNum)])
            } else {
                startLears([...video.splice(0, videoNum)])
            }
        }
        function startLears(arr) {
            if (!arr.length) {
                showEl.innerText = "恭喜,所有课程均已完成!";
            }
            console.log("开始学习这些课程", arr)
            const app = document.querySelector('[data-v-3d8fef40]').__vue__;
            const learEl = document.querySelectorAll(".learing-iframe-box .boxs")
            arr.forEach(el => {
                if (isLearArr.find(i => i.id === el.id) === undefined) {
                    const div = document.createElement("div");
                    div.classList.add("boxs");
                    div.setAttribute("lear-id", el.id)
                    div.setAttribute("pr-name", el.name)
                    const tempIFrame = document.createElement("iframe");
                    div.appendChild(tempIFrame)
                    if (el.leaf_type === 6) {
                        tempIFrame.src = `https://uestcedu.yuketang.cn/pro/lms/${app.$data.sign}/${app.$data.classroom_id}/homework/${el.id}`
                    } else {
                        tempIFrame.src = `https://uestcedu.yuketang.cn/pro/lms/${app.$data.sign}/${app.$data.classroom_id}/video/${el.id}`
                    }
                    tempIFrame.classList.add("learing-iframe");
                    document.querySelector(".learing-iframe-box").appendChild(div);
                    isLearArr.push(el.id);
                } else { }
            })
            isLearArr = arr;
            learEl.forEach(el => {
                const find = isLearArr.find(i => i.id === +el.getAttribute("lear-id"))
                if (find === undefined) {
                    console.log(el.getAttribute("pr-name"), "此课已学习完成")
                    el.remove();
                }
            })
        }

        listenAjax(studyList, reqs);

        listenAjax(progress, prss);

        // 循环
        function loop() {
            setTimeout(() => {
                console.log("更新列表")
                document.querySelector('[data-v-3d8fef40]').__vue__.getLearnSchedule();
                const learEl = document.querySelectorAll(".learing-iframe-box .boxs")
                if (learEl.length) {
                    loop()
                }
            }, 10000);
        }
        loop()
    }

    // 视频播放
    function playVideo() {
        showEl.innerText = `视频播放挂机页面 [等待捕获视频url]`;

        const tims = setTimeout(() => {
            showEl.innerText = `页面加载错误,正在重载页面`;
            setTimeout(() => {
                location.reload()
            }, 3000)
        }, 60000)

        function startListen(data) {
            clearTimeout(tims)
            console.log("获取到视频链接", data.res)
            let lastTime = null,
                reloadCount = 30,
                thisCount = 0
            showEl.innerText = `视频播放挂机页面 [等待创建video节点]`;

            function loop() {
                const video = document.querySelector("video")
                if (video) {
                    video.muted = true;
                    video.playbackRate = 2;
                    video.play()
                    const dur = parseInt(video.duration),
                        curr = parseInt(video.currentTime)
                    showEl.innerText = `视频播放中 [${curr}/${dur}]`;
                    if (lastTime === curr) {
                        thisCount++;
                    } else {
                        thisCount = 0;
                    }
                    lastTime = curr;
                    if (reloadCount === thisCount) {
                        showEl.innerText = `视频长时间未播放,正在重载页面`;
                        setTimeout(() => {
                            location.reload()
                        }, 3000)
                        return;
                    }
                }
                setTimeout(loop, 2000)
            }
            loop();
        }
        let ones = true;
        listenAjax(videoPress, (data) => {
            console.log("捕获到更新时长请求", data);
            let urls = data.url
            let defaultHearder = data.header
            let heart_data = data.send.heart_data.reverse()[0]
            const newList = []
            console.log(heart_data)
            let leng = parseInt(heart_data.d / 10)
            for (let i = 0; i < leng; i++) {
                let newObj = JSON.parse(JSON.stringify(heart_data))
                if (i + 1 < leng) {
                    newObj.cp = heart_data.tp + parseInt((heart_data.d - heart_data.tp) / leng * i)
                } else {
                    newObj.cp = heart_data.d
                }
                newObj.et = 'play'
                newList.push(newObj)
            }
            console.log("新构建结构", newList)
            showEl.innerText = `构建虚拟学习进度`;
            let postData = {
                heart_data: newList
            }
            if (ones) {
                ones = false
                setTimeout(() => {
                    axios.post(urls, postData, defaultHearder).then(res => {
                        showEl.innerText = `请求结束,等待服务器更新`;
                        console.log("模拟请求返回", res)
                        setTimeout(() => {
                            document.querySelector(".log-detail").click()
                            showEl.innerText = `重载页面`;
                            setTimeout(() => {
                                location.reload()
                            }, 30000)
                        }, 3000)
                    })
                }, 3000)
            } else {
                console.log("已经提交过了")
            }

        })

        listenAjax(getVideoUrl, startListen);
    }

    // ajax监听
    // function listenAjax(rule, callback) {

    //     // 监听所有请求
    //     const originOpen = XMLHttpRequest.prototype.open;
    //     const originSend = XMLHttpRequest.prototype.send;

    //     // 重写open
    //     XMLHttpRequest.prototype.open = function () {
    //         this.addEventListener('load', function (obj) {
    //             const url = obj.target.responseURL; // obj.target -> this
    //             if (rule.test(url)) {
    //                 callback(JSON.parse(this.response))
    //             }
    //         });
    //         originOpen.apply(this, arguments);
    //     };

    //     // 重写send
    //     XMLHttpRequest.prototype.send = function () {
    //         originSend.apply(this, arguments);
    //     };
    // }


    // 监听所有请求
    const originOpen = XMLHttpRequest.prototype.open;
    const originSend = XMLHttpRequest.prototype.send;
    const originHeader = XMLHttpRequest.prototype.setRequestHeader

    // 重写open
    XMLHttpRequest.prototype.open = function () {
        this.addEventListener('load', function (obj) {
            const url = obj.target.responseURL; // obj.target -> this
            listenList.forEach(el => {
                if (el.rule.test(url)) {
                    const find = ajaxList.find(el => el.xml === this)
                    if (find) {
                        find.url = url
                        find.res = JSON.parse(this.response)
                        el.callback(find)
                    } else {
                        el.callback(false)
                    }
                }
            })
        });
        originOpen.apply(this, arguments);
    };

    // 重写send
    XMLHttpRequest.prototype.send = function () {
        const xml = ajaxList.find(el => el.xml === this)
        if (xml) {
            xml.send = JSON.parse(arguments[0])
        }
        originSend.apply(this, arguments);
    };

    // 重写setRequestHeader
    XMLHttpRequest.prototype.setRequestHeader = function () {
        const xml = ajaxList.find(el => el.xml === this)
        if (xml) {
            xml.header[arguments[0]] = arguments[1]
        } else {
            ajaxList.push({
                xml: this,
                url: "",
                header: {
                    [arguments[0]]: arguments[1]
                }
            })
        }
        originHeader.apply(this, arguments)
    }

    function listenAjax(rule, callback) {
        listenList.push({
            rule,
            callback
        })
    }
})();