USTC Helper

Various useful functions for USTC students: verification code bypass, auto login, rec performance improvement and more.

目前為 2023-10-05 提交的版本,檢視 最新版本

// ==UserScript==
// @name         USTC Helper
// @name:zh-CN   USTC 助手
// @license      gpl-3.0
// @namespace    http://tampermonkey.net/
// @version      0.11.9
// @description  Various useful functions for USTC students: verification code bypass, auto login, rec performance improvement and more.
// @description:zh-CN  为 USTC 学生定制的各类实用功能:绕过验证码,自动登录(不可用),睿客网性能优化以及更多。
// @author       PRO
// @match        https://mail.ustc.edu.cn/
// @match        https://mail.ustc.edu.cn/coremail/index.jsp*
// @match        https://passport.ustc.edu.cn/*
// @match        https://rec.ustc.edu.cn/*
// @match        https://recapi.ustc.edu.cn/identity/other_login?*
// @match        https://www.bb.ustc.edu.cn/*
// @match        https://jw.ustc.edu.cn/*
// @match        https://young.ustc.edu.cn/login/*
// @match        https://young.ustc.edu.cn/nginx_auth/*
// @match        https://wvpn.ustc.edu.cn/*
// @icon         https://passport.ustc.edu.cn/images/favicon.ico
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @require      https://gf.qytechs.cn/scripts/462234-message/code/Message.js?version=1192786
// @require      https://gf.qytechs.cn/scripts/470224-tampermonkey-config/code/Tampermonkey%20Config.js?version=1244657
// ==/UserScript==

(function () {
    'use strict';
    let window = unsafeWindow;

    function boolDesc(name, title=null, defaultVal=true) {
        return {
            name: name,
            value: defaultVal,
            input: "current",
            processor: "not",
            formatter: "boolean",
            autoClose: false,
            title: title
        };
    }

    function values(list) {
        return function (val) {
            if (!list.includes(val)) {
                throw new Error(`Invalid value: ${val}, expected one of ${list}!`);
            }
            return val;
        }
    }

    let config_descs = {
        passport: {
            "passport/enabled": boolDesc("Enabled", "Whether to enable USTC Helper for this site"),
            "passport/bypass_code": boolDesc("Bypass verification code", "Enable bypassing verification code"),
            "passport/focus": boolDesc("Focus", "Automatically focuses on \"Login\" button"),
            "passport/service": boolDesc("Service", "Hint service domain and its credibility"),
        },
        mail: {
            "mail/enabled": boolDesc("Enabled", "Whether to enable USTC Helper for this site"),
            "mail/focus": boolDesc("Focus", "Automatically focuses on \"Login\" button"),
            "mail/domain": { // Expected values: 'mail.ustc.edu.cn', 'ustc.edu.cn', 'ah.edu.cn', '' (Do nothing)
                name: "Domain",
                value: "mail.ustc.edu.cn",
                processor: values(['mail.ustc.edu.cn', 'ustc.edu.cn', 'ah.edu.cn', '']),
                autoClose: false,
                title: "Automatically switch to given mail domain"
            }
        },
        rec: {
            "rec/enabled": boolDesc("Enabled", "Whether to enable USTC Helper for this site"),
            "rec/autologin": boolDesc("Auto login", "Automatically clicks \"Login\" button"),
            "rec/opencurrent": boolDesc("Open in current tab", "Set some links to be opened in current tab (Significantly improves performance)"),
        },
        bb: {
            "bb/enabled": boolDesc("Enabled", "Whether to enable USTC Helper for this site"),
            "bb/autoauth": boolDesc("Auto authenticate", "Automatically authenticate when accessing outside school net"),
            "bb/autologin": boolDesc("Auto login", "Automatically clicks \"Login\" button"),
            "bb/showhwstatus": boolDesc("Show homework status", "Query all homework status (may consume some network traffic)"),
        },
        jw: {
            "jw/enabled": boolDesc("Enabled", "Whether to enable USTC Helper for this site"),
            "jw/login": {
                name: "Login",
                value: "focus",
                processor: values(['none', 'focus', 'click']),
                autoClose: false,
                title: "What to do to the login button: 'none', 'focus', 'click'"
            },
            "jw/shortcut": boolDesc("Shortcut", "Enable shortcut support"),
            "jw/score_mask": boolDesc("Score mask", "Allows you to hide/reveal your scores with dblclick"),
            "jw/detailed_time": boolDesc("Detailed time", "Show start/end time of each class"),
            "jw/css": boolDesc("CSS improve", "Minor CSS improvements"),
        },
        young: {
            "young/enabled": boolDesc("Enabled", "Whether to enable USTC Helper for this site"),
            "young/auto_auth": boolDesc("Auto authenticate", "Automatically authenticate when accessing outside school net"),
            "young/default_tab": {
                name: "Default tab",
                value: "/myproject/SignUp",
                autoClose: false,
                title: "The tab to be opened on entering"
            },
            "young/auto_tab": boolDesc("Auto tab", "Auto navigate to frequently-used submenu"),
            "young/no_datascreen": boolDesc("No data screen", "Remove annoying data screen image"),
            "young/shortcut": boolDesc("Shortcut", "Enable shortcut support")
        },
        wvpn: {
            "wvpn/enabled": boolDesc("Enabled", "Whether to enable USTC Helper for this site"),
            "wvpn/custom_collection": boolDesc("Custom collection", "Allows you to fully customize your collection"),
        }
    };
    let name = "USTC Helper";
    window.QMSG_GLOBALS = {
        DEFAULTS: {
            showClose:true,
            timeout: 2000
        }
    }
    switch (window.location.host) {
        case 'mail.ustc.edu.cn': {
            let config_desc = config_descs.mail;
            let config = GM_config(config_desc);
            if (!config["mail/enabled"]) {
                console.info("[USTC Helper] 'mail' feature disabled.");
                break;
            }
            if (config["mail/domain"]) {
                changeDomain(config["mail/domain"]);
                console.info(`[USTC Helper] Domain changed to ${config["mail/domain"]}.`);
            }
            if (config["mail/focus"]) {
                document.getElementById("login_button").focus();
                console.info("[USTC Helper] Login button focused.");
            }
            break;
        }
        case 'passport.ustc.edu.cn': {
            let config_desc = config_descs.passport;
            let config = GM_config(config_desc);
            if (!config["passport/enabled"]) {
                console.info("[USTC Helper] 'passport' feature disabled.");
                break;
            }
            let form = document.getElementsByClassName('loginForm')[0];
            if (!form) {
                console.log("[USTC Helper] Form not found!");
                break;
            }
            let options = {
                childList: true,
                attributes: false,
                subtree: true
            }
            function bypass() {
                let showCode = document.getElementsByName('showCode')[0];
                showCode.value = "";
                let code = document.querySelector('#valiCode');
                if (code) {
                    code.remove();
                    console.info("[USTC Helper] Verification code bypassed.");
                } else {
                    console.info("[USTC Helper] Verification code not found.");
                }
            }
            function focus() {
                document.getElementById('login').focus();
                console.info("[USTC Helper] Login button focused.");
            }
            function hint() {
                let notice = document.createElement('p');
                let params = new URL(window.location.href).searchParams;
                let service_url = params.get('service');
                if (!service_url) return;
                service_url = decodeURIComponent(service_url);
                let domain = service_url.split('/')[2];
                let color;
                let status; // Official Student/Staff Third-party
                let suffix;
                if (/.+\.ustc\.edu\.cn/.test(domain)) {
                    if (domain == 'home.ustc.edu.cn') {
                        status = "Student";
                        color = "#d0d01b";
                        suffix = "@mail.ustc.edu.cn";
                    } else if (domain == 'staff.ustc.edu.cn') {
                        status = "Staff";
                        color = "#d0d01b";
                        suffix = "@ustc.edu.cn";
                    } else {
                        status = "Official";
                        color = "green";
                    }
                } else {
                    status = "Third-party";
                    color = "red";
                }
                console.info(`[USTC Helper] ${status} service: ${service_url}`);
                if (color == "#d0d01b") {
                    let regex = new RegExp(/https?:\/\/(home|staff)\.ustc\.edu\.cn\/~([^\/]+)/i);
                    let match = service_url.match(regex);
                    if (match) {
                        let name = match[2];
                        let email = name + suffix;
                        console.log("[USTC Helper] Contact email: " + email);
                        notice.innerHTML = `<a style="color: #d0d01b;" title="Contact" href="mailto:${email}">${status}</a> service: <span style="color: grey;" title="${service_url}">${domain}</span>`;
                    } else {
                        console.log("[USTC Helper] Unable to determine contact email!");
                        notice.innerHTML = `<a style="color: #d0d01b;" title="Unrecognized">${status}</a> service: <span style="color: grey;" title="${service_url}">${domain}</span>`;
                    }
                } else {
                    notice.innerHTML = `<span style="color: ${color};">${status}</span> service: <span style="color: grey;" title="${service_url}">${domain}</span>`;
                }
                let main_card = document.getElementsByClassName('card')[0];
                main_card.insertAdjacentElement('afterbegin', notice);
            }
            function main() {
                if (config["passport/bypass_code"]) bypass();
                if (config["passport/focus"]) focus();
                if (config["passport/service"]) hint();
                observer.disconnect();
            }
            let observer = new MutationObserver(main);
            observer.observe(form, options);
            break;
        }
        case 'rec.ustc.edu.cn': {
            let config_desc = config_descs.rec;
            let config = GM_config(config_desc);
            if (!config["rec/enabled"]) {
                console.info("[USTC Helper] 'rec' feature disabled.");
                break;
            }
            if (config["rec/opencurrent"]) {
                window.webpackJsonp.push_ = window.webpackJsonp.push;
                window.webpackJsonp.push = (val) => {
                    if (val[0][0] !== "chunk-5ae262a1")
                        return window.webpackJsonp.push_(val);
                    else { // Following script is adapted from https://rec.ustc.edu.cn/js/chunk-5ae262a1.b84e1461.js
                        val[1]["2c03"] = function (t, e, s) {
                            "use strict";
                            (function (t) {
                                s("55dd");
                                var r = s("a67e");
                                e["a"] = {
                                    name: "GroupLister",
                                    components: {
                                        GroupCreate: function () {
                                            return Promise.all([s.e("chunk-390136ce"), s.e("chunk-662e27b9")]).then(s.bind(null, "18fa"))
                                        },
                                        GroupAdd: function () {
                                            return s.e("chunk-5b916374").then(s.bind(null, "c1c7"))
                                        },
                                        GroupEdit: function () {
                                            return Promise.all([s.e("chunk-390136ce"), s.e("chunk-0daeb591")]).then(s.bind(null, "1fa6"))
                                        }
                                    },
                                    data: function () {
                                        return {
                                            status: {
                                                GroupCreateStatus: !1,
                                                GroupAddStatus: !1,
                                                GroupEditStatus: !1
                                            },
                                            loading: !1,
                                            nothing: !1,
                                            group: {},
                                            sortBy: {},
                                            headers: [{
                                                id: 1,
                                                title: "群名称",
                                                class: "groupname",
                                                sort: "asc",
                                                showSort: !0,
                                                field: "group_name"
                                            }, {
                                                id: 2,
                                                title: "群号",
                                                class: "groupid",
                                                sort: "des",
                                                showSort: !1,
                                                field: "group_number"
                                            }, {
                                                id: 3,
                                                title: "成员",
                                                class: "groupuser",
                                                sort: "des",
                                                showSort: !1,
                                                field: "group_memeber_count"
                                            }, {
                                                id: 5,
                                                title: "分享",
                                                class: "groupshare",
                                                sort: "des",
                                                showSort: !1,
                                                field: "group_share_file_count"
                                            }, {
                                                id: 6,
                                                title: "操作",
                                                class: "groupmenu",
                                                sort: "",
                                                showSort: !1
                                            }]
                                        }
                                    },
                                    created: function () {
                                        this.sortBy = this.headers[0],
                                            this.getGroups()
                                    },
                                    computed: {
                                        userInfo: function () {
                                            return this.$store.state.user.userInfo
                                        }
                                    },
                                    watch: {
                                        $route: function () {
                                            this.getGroups()
                                        }
                                    },
                                    filters: {
                                        identityNameFilter: function (t) {
                                            var e;
                                            switch (t) {
                                                case "owner":
                                                    e = "群主";
                                                    break;
                                                case "admin":
                                                    e = "管理员";
                                                    break;
                                                case "user":
                                                    e = "成员";
                                                    break;
                                                default:
                                                    break
                                            }
                                            return e
                                        }
                                    },
                                    methods: {
                                        createGroup: function () {
                                            t("#newgroup").modal("show")
                                        },
                                        addGroup: function () {
                                            t("#addgroup").modal("show")
                                        },
                                        invite: function (t) {
                                            var e = this.$router.resolve({
                                                name: "group",
                                                params: {
                                                    groupNumber: t.group_number
                                                }
                                            });
                                            this.$confirm({
                                                showYesBtn: !1,
                                                showCopyBtn: !0,
                                                copyBtnText: "复制文字",
                                                title: "邀请入群",
                                                type: "confirm",
                                                content: "打开链接进入群组主页即可申请加入群组:".concat(t.group_name, ",群组主页链接:").concat(window.location.origin).concat(e.href)
                                            }).then((function () { }
                                            )).catch((function () { }
                                            ))
                                        },
                                        goToGroupCloud: function (t, e) {
                                            if (["owner", "admin", "user"].indexOf(t.group_member_identity) < 0)
                                                return this.$message({
                                                    type: "warning",
                                                    message: "您不是组群成员,无法进入群盘"
                                                }),
                                                    !1;
                                            this.$store.commit("setSetting", {
                                                from: !0,
                                                drive: "groupdisk",
                                                tab: e,
                                                group: t
                                            }),
                                                this.$router.push({
                                                    name: "groupDisk",
                                                    params: {
                                                        groupNumber: t.group_number
                                                    }
                                                })
                                        },
                                        isShowMenu: function (t) {
                                            return ["owner", "admin", "user"].indexOf(t.group_member_identity) > -1
                                        },
                                        isEditGroup: function (t) {
                                            return ["owner", "admin"].indexOf(t.group_member_identity) > -1
                                        },
                                        goToGroup: function (t) {
                                            var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "group";
                                            if ("wait" === t.group_is_review)
                                                return this.$message({
                                                    type: "warning",
                                                    message: "群组待审核,不允许操作!"
                                                }),
                                                    !1;
                                            if ("refuse" === t.group_is_review)
                                                return this.$message({
                                                    type: "warning",
                                                    message: "群组审核未通过,不允许操作!"
                                                }),
                                                    !1;
                                            // Instead of opening in new tab, we prefer to use vue's solution
                                            // Modifiy start
                                            this.$router.replace({
                                                name: e,
                                                params: {
                                                    groupNumber: t.group_number
                                                }
                                            });
                                            // Modify end
                                        },
                                        goToGroupHome: function (t) {
                                            this.$store.commit("SET_GROUP_SHOWDESC", !1),
                                                this.$router.push({
                                                    name: "group",
                                                    params: {
                                                        groupNumber: t
                                                    }
                                                })
                                        },
                                        handleEditGroup: function (e) {
                                            var s = this;
                                            Object(r["g"])(e.group_number).then((function (t) {
                                                s.group = t.entity
                                            }
                                            )).catch((function (t) {
                                                s.$message({
                                                    type: "error",
                                                    message: t
                                                })
                                            }
                                            )),
                                                t("#editgroup").modal("show")
                                        },
                                        groupRefresh: function () {
                                            this.getGroups()
                                        },
                                        sortGroup: function (t) {
                                            if (6 === t)
                                                return !1;
                                            var e = this;
                                            this.headers.map((function (s) {
                                                return s.id === t ? (s.showSort = !0,
                                                    s.sort = "des" === s.sort ? "asc" : "des",
                                                    e.sortBy = s,
                                                    s) : (s.showSort = !1,
                                                        s.sort = "des",
                                                        s)
                                            }
                                            )),
                                                this.sortGroupBy()
                                        },
                                        getGroups: function () {
                                            var t = this;
                                            this.groups = [],
                                                this.loading = !0,
                                                this.nothing = !1,
                                                Object(r["r"])({}).then((function (e) {
                                                    if (200 === e.status_code)
                                                        if (t.loading = !1,
                                                            t.groups = e.entity.datas,
                                                            e.entity.total > 0) {
                                                            var s = 0;
                                                            e.entity.datas.map((function (t) {
                                                                "user" != t.group_member_identity && t.group_pending_member_count > 0 && (s += t.group_pending_member_count)
                                                            }
                                                            )),
                                                                t.$store.commit("setRequestNums", s),
                                                                t.sortGroupBy(!0)
                                                        } else
                                                            t.nothing = !0;
                                                    else
                                                        t.$message({
                                                            type: "error",
                                                            message: e.message
                                                        })
                                                }
                                                )).catch((function (e) {
                                                    t.$message({
                                                        type: "error",
                                                        message: e
                                                    })
                                                }
                                                ))
                                        },
                                        sortGroupBy: function () {
                                            var t = this
                                                , e = arguments.length > 0 && void 0 !== arguments[0] && arguments[0];
                                            this.groups.sort((function (s, r) {
                                                var o;
                                                return o = e ? r.group_is_review.localeCompare(s.group_is_review) : "group_name" === t.sortBy.field ? s[t.sortBy.field].localeCompare(r[t.sortBy.field]) : s[t.sortBy.field] - r[t.sortBy.field],
                                                    o = "asc" === t.sortBy.sort ? o : -o,
                                                    o
                                            }
                                            ))
                                        },
                                        groupCancel: function (t) {
                                            var e = this
                                                , s = "adopt" === t.group_is_review ? "解散" : "删除";
                                            this.$confirm({
                                                type: "confirm",
                                                content: "".concat(s, "群后,所有关于本群组的信息都将被删除且无法恢复,确定").concat(s, "【").concat(t.group_name, "】吗?"),
                                                showCancleBtn: !0,
                                                showYesBtn: !0,
                                                custom: []
                                            }).then((function () {
                                                Object(r["u"])({
                                                    groups_list: [t.group_number]
                                                }).then((function (t) {
                                                    200 === t.status_code ? (e.$message({
                                                        type: "success",
                                                        message: t.message
                                                    }),
                                                        e.getGroups()) : e.$message({
                                                            type: "error",
                                                            message: t.message
                                                        })
                                                }
                                                )).catch((function (t) {
                                                    e.$message({
                                                        type: "error",
                                                        message: t
                                                    })
                                                }
                                                ))
                                            }
                                            )).catch((function () { }
                                            ))
                                        },
                                        groupQuit: function (t) {
                                            var e = this;
                                            this.$confirm({
                                                type: "confirm",
                                                content: "确定退出该群组吗?",
                                                showCancleBtn: !0,
                                                showYesBtn: !0,
                                                custom: []
                                            }).then((function () {
                                                Object(r["v"])({
                                                    group_number: t,
                                                    action: "quit",
                                                    members_list: [e.userInfo.user_number]
                                                }).then((function (t) {
                                                    200 === t.status_code ? (e.$message({
                                                        type: "success",
                                                        message: t.message
                                                    }),
                                                        e.getGroups()) : e.$message({
                                                            type: "error",
                                                            message: t.message
                                                        })
                                                }
                                                )).catch((function (t) {
                                                    e.$message({
                                                        type: "error",
                                                        message: t
                                                    })
                                                }
                                                ))
                                            }
                                            )).catch((function () { }
                                            ))
                                        }
                                    },
                                    mounted: function () {
                                        var t = this;
                                        setTimeout((function () {
                                            for (var e in t.status)
                                                t.status[e] = !0
                                        }
                                        ), 500)
                                    }
                                }
                            }
                            ).call(this, s("1157"))
                        };
                        // console.log(val);
                        return window.webpackJsonp.push_(val);
                    }
                };
            }
            if (config["rec/autologin"] && document.location.pathname == '/') {
                let app = document.getElementById("app");
                let options = {
                    childList: true,
                    attributes: false,
                    subtree: true
                }
                let observer = new MutationObserver(() => {
                    let btn = document.getElementsByClassName('navbar-login-btn')[0];
                    if (btn) {
                        btn.click();
                        observer.disconnect();
                    }
                });
                observer.observe(app, options);
            } else if (config["rec/opencurrent"]) {
                let app = document.getElementById("app");
                let options = {
                    childList: true,
                    attributes: false,
                    subtree: true
                }
                let observer = new MutationObserver(() => {
                    let l = document.getElementsByClassName("app-list").length;
                    if (l) {
                        let links = app.getElementsByTagName("a");
                        for (let link of links) {
                            if (link.target == '_blank') link.removeAttribute("target");
                        }
                    }
                });
                observer.observe(app, options);
            }
            break;
        }
        case 'recapi.ustc.edu.cn': {
            let config_desc = config_descs.rec;
            let config = GM_config(config_desc);
            if (!config["rec/enabled"]) {
                console.info("[USTC Helper] 'rec' feature disabled.");
                break;
            }
            if (config["rec/autologin"]) {
                let btn = document.querySelector("#ltwo > div > button");
                if (!btn) {
                    console.error("[USTC Helper] Login button not found!");
                } else {
                    btn.click();
                }
            }
            break;
        }
        case 'www.bb.ustc.edu.cn': {
            let config_desc = config_descs.bb;
            let config = GM_config(config_desc);
            if (!config["bb/enabled"]) {
                console.info("[USTC Helper] 'bb' feature disabled.");
                break;
            }
            if (window.location.pathname == '/nginx_auth/' && config["bb/autoauth"]) {
                document.getElementsByTagName('a')[0].click();
            } else if ((window.location.pathname == '/' || window.location.pathname == '/webapps/login/') && config["bb/autologin"]) {
                document.querySelector('#login > table > tbody > tr > td:nth-child(2) > span > a').click();
            } else if (config["bb/showhwstatus"] && window.location.pathname == '/webapps/blackboard/content/listContent.jsp' && document.getElementById('pageTitleText').children[0].textContent == '作业区') {
                let hw_list = document.getElementById('content_listContainer');
                let color_config = ['grey', 'green', 'red', 'yellow', 'grey'];
                let hint_text = ['查询中', '已提交', '未提交', '查询错误', '已忽略'];
                // let hint_text = ['Checking', 'Submitted', 'Not submitted', 'Error', 'Ignored'];
                function ignore_hw(course_id, content_id, ignore) {
                    let ignored = localStorage.getItem(course_id) || '[]';
                    ignored = JSON.parse(ignored);
                    if (ignore && !ignored.includes(content_id)) {
                        ignored.push(content_id);
                        console.log(`[USTC Helper] Ignoring "${course_id}/${content_id}"...`);
                    } else if (!ignore && ignored.includes(content_id)) {
                        ignored = ignored.filter((v) => v != content_id);
                        console.log(`[USTC Helper] Un-ignoring "${course_id}/${content_id}"...`);
                    }
                    if (ignored.length) localStorage.setItem(course_id, JSON.stringify(ignored));
                    else localStorage.removeItem(course_id);
                }
                async function query_status(link) {
                    const r = await fetch(link);
                    if (!r.ok) {
                        console.log(`[USTC Helper] Failed to fetch "${r.url}": ${r.status} ${r.statusText}`);
                        return 3;
                    } else {
                        let html = await r.text();
                        if (html.match(/<span id="pageTitleText">\n  复查提交历史记录: .+<\/span>/)) return 1;
                        else if (html.match(/<span id="pageTitleText">\n  上载作业:.+<\/span>/)) return 2;
                        else return 3;
                    }
                }
                async function process(hw) {
                    let link_ = hw.querySelector("h3 > a");
                    if (link_) {
                        let status = 0; // 0: Checking  1: Uploaded  2: Not uploaded  3: Error
                        let hint = document.createElement('span');
                        let ret = '';
                        hint.style.color = color_config[status];
                        hint.textContent = `(${hint_text[status]})`;
                        link_.appendChild(hint);
                        let link = link_.href;
                        // https://www.bb.ustc.edu.cn/webapps/assignment/uploadAssignment?content_id=_106763_1&course_id=_12559_1&group_id=&mode=view
                        let params = new URL(link).searchParams;
                        let course_id = params.get("course_id");
                        let content_id = params.get("content_id");
                        let ignored = localStorage.getItem(course_id);
                        let uploaded = sessionStorage.getItem(course_id);
                        // Check if this homework is ignored
                        if (ignored) {
                            ignored = JSON.parse(ignored).includes(content_id);
                            if (ignored) {
                                status = 4;
                                console.log(`[USTC Helper] "${course_id}/${content_id}" present in ignore list, so this homework is ignored.`);
                            }
                        } else if (uploaded) { // Query from cache first
                            uploaded = JSON.parse(uploaded).includes(content_id);
                            if (uploaded) {
                                status = 1;
                                console.log(`[USTC Helper] "${course_id}/${content_id}" present in cache, so this homework is uploaded.`);
                            }
                        }
                        // Not in cache
                        if (!status) {
                            status = await query_status(link);
                            if (status == 1) {
                                ret = content_id;
                                console.log(`[USTC Helper] Online query indicated that "${course_id}/${content_id}" is uploaded.`);
                            } else if (status == 2) {
                                console.log(`[USTC Helper] Online query indicated that "${course_id}/${content_id}" is not uploaded.`);
                            } else {
                                console.warn(`[USTC Helper] Online query "${course_id}/${content_id}" failed!`);
                            }
                        }
                        hint.style.color = color_config[status];
                        hint.textContent = `(${hint_text[status]})`;
                        hint.title = ignored ? "点击取消忽略此作业" : "点击忽略此作业";
                        hint.addEventListener('click', e => {
                            e.preventDefault();
                            ignore_hw(course_id, content_id, !ignored);
                            hint.title = "刷新页面以生效";
                            hint.style.color = color_config[4];
                            hint.textContent = "(请刷新)";
                        }, { once: true });
                        return ret;
                    }
                }
                let promises = [];
                for (let hw of hw_list.children) {
                    promises.push(process(hw));
                }
                Promise.all(promises).then(
                    (values) => {
                        let params = new URL(window.location.href).searchParams;
                        let course_id = params.get('course_id');
                        let uploaded = sessionStorage.getItem(course_id);
                        if (uploaded) {
                            uploaded = JSON.parse(uploaded);
                        } else {
                            uploaded = [];
                        }
                        for (let content_id of values) {
                            if (content_id.length) {
                                uploaded.push(content_id);
                                console.log(`[USTC Helper] Saving "${course_id}/${content_id}" to cache...`);
                            }
                        }
                        sessionStorage.setItem(course_id, JSON.stringify(uploaded));
                    }
                );
            }
            break;
        }
        case 'jw.ustc.edu.cn': {
            let config_desc = config_descs.jw;
            let config = GM_config(config_desc, false);
            if (!config["jw/enabled"]) {
                console.info("[USTC Helper] 'jw' feature disabled.");
                break;
            }
            if (config["jw/login"] && window.location.pathname == "/login") {
                let btn = document.getElementById('login-unified-wrapper');
                if (config["jw/login"] == 'focus') {
                    btn.focus();
                } else if (config["jw/login"] == 'click') {
                    btn.click();
                } else {
                    console.error(`[USTC Helper] Unknown option for jw.login: ${config["jw/login"]}`);
                }
            }
            if (config["jw/shortcut"] && window.top.location.pathname == "/home") {
                // let shortcuts = ["ArrowLeft", "ArrowRight", "x", '1', '2', '3', '4', '5', '6', '7', '8', '9'];
                let shortcuts = ["x"];
                document.addEventListener("keydown", (e) => {
                    if (document.activeElement.nodeName != "INPUT" &&
                        shortcuts.includes(e.key)) {
                        let menu = window.top.document.getElementById("e-home-tab-list");
                        let tabs = Array.from(menu.children);
                        let home = window.top.document.querySelector("#e-top-home-page > li > a");
                        tabs.push(home);
                        let count = tabs.length;
                        let current = 0;
                        for (let tab of tabs) {
                            if (tab.classList.contains('active')) {
                                break;
                            }
                            current++;
                        }
                        if (current == count) current--;
                        switch (e.key) {
                            // case "ArrowLeft":
                            //     tabs[(current - 1 + count) % count].click();
                            //     break;
                            // case "ArrowRight":
                            //     tabs[(current + 1) % count].click();
                            //     break;
                            case "x":
                                let close = tabs[current].querySelector("a > i.fa-times");
                                if (close) close.click();
                                break;
                            default:
                                // if (e.key.length == 1) {
                                //     let idx = (Number(e.key) - 2 + count) % count;
                                //     if (0 <= idx && idx < count) {
                                //         tabs[idx].click();
                                //     }
                                // }
                                break;
                        }
                    }
                });
            }
            if (config["jw/score_mask"] && window.location.pathname == "/for-std/grade/sheet") {
                function get_status(entry) {
                    // Status:
                    // false: Normal display
                    // true: Masked
                    if (entry.classList.contains("masked")) return true;
                    else return false;
                }
                function set_status_internal(entry, state) {
                    let gpa = entry.children[entry.children.length - 2];
                    let score = entry.lastChild;
                    if (state) {
                        entry.classList.add("masked");
                        entry.setAttribute("data-gpa", gpa.textContent);
                        entry.setAttribute("data-score", score.textContent);
                        gpa.textContent = "";
                        score.textContent = "";
                    } else {
                        entry.classList.remove("masked");
                        let gpa_val = entry.getAttribute("data-gpa");
                        let score_val = entry.getAttribute("data-score");
                        if (gpa_val) gpa.textContent = gpa_val;
                        if (score_val) score.textContent = score_val;
                    }
                }
                function toggle() {
                    set_status_internal(this, !get_status(this));
                }
                function set_status(entry, state) {
                    if (get_status(entry) == state) return;
                    set_status_internal(entry, state);
                }
                function toggle_view() {
                    if (this.hasAttribute("data-value")) {
                        this.lastChild.textContent = this.getAttribute("data-value");
                        this.removeAttribute("data-value");
                    } else {
                        this.setAttribute("data-value", this.lastChild.textContent);
                        this.lastChild.textContent = "尚未评教";
                    }
                }
                function toggle_rank() {
                    if (this.hasAttribute("data-value")) {
                        this.textContent = this.getAttribute("data-value");
                        this.removeAttribute("data-value");
                    } else {
                        this.setAttribute("data-value", this.textContent);
                        this.textContent = "尚未评教";
                    }
                }
                function setup() {
                    let tables = document.querySelectorAll("div.semesters > section > div.semester > table");
                    tables.forEach((table) => {
                        let head = table.querySelector("thead");
                        let entries = table.querySelectorAll("tbody > tr");
                        head.addEventListener("dblclick", (e) => {
                            let status = head.getAttribute("data-masked") === "";
                            entries.forEach((entry) => {
                                set_status(entry, !status);
                            });
                            if (status) head.removeAttribute("data-masked");
                            else head.setAttribute("data-masked", "");
                        });
                        entries.forEach((entry) => {
                            entry.addEventListener("dblclick", toggle);
                        });
                    });
                    let history_table = document.querySelector("table.history-table");
                    history_table.tHead.addEventListener("dblclick", (e) => {
                        history_table.querySelectorAll("tbody:not(.hidden)").forEach((tbody) => {
                            let status = tbody.getAttribute("data-masked") === "";
                            tbody.querySelectorAll("tr").forEach((entry) => {
                                set_status(entry, !status);
                            });
                            if (status) tbody.removeAttribute("data-masked");
                            else tbody.setAttribute("data-masked", "");
                        });
                    });
                    history_table.querySelectorAll("tbody > tr").forEach((entry) => {
                        entry.addEventListener("dblclick", toggle);
                    });
                    let view = document.querySelector("div.overview > ul");
                    view.childNodes.forEach((node) => {
                        node.addEventListener("dblclick", toggle_view);
                    });
                    let rank = document.querySelector("div.rankinfo > div");
                    rank.querySelectorAll("b").forEach((node) => {
                        node.addEventListener("dblclick", toggle_rank);
                    });
                }
                let timer = window.setInterval(() => {
                    let test = document.querySelector("div.overview > ul > li > span:nth-child(2)");
                    if (test.textContent != "NaN") {
                        window.clearInterval(timer);
                        setup();
                    }
                }, 1000);
            }
            const jw_css = {
                "detailed_time" : `table.timetable tbody th.span::before, table.timetable tbody th.span::after { font-size: smaller; position: absolute; left: 0.1em; opacity: 0.3; }
                    table.timetable tbody th.span::before { content: attr(data-start); top: 0; } table.timetable tbody th.span::after { content: attr(data-end); bottom: 0; }`,
                "css": `div#dropdown-menu-filter { display: none; } div#dropdown-menu-bg { backdrop-filter: blur(3px); } div.second-menu-wrap div.menu-area { width: 100%; }
                    li.home div.dropdown-menu { width: 25vw !important; min-width: 400px !important; } .primary .item li.primaryLi.hover { transition: transform 0.25s ease; }
                    .primaryLi .subMenus { cursor: initial; opacity: 0.8; } div#shortcut { width: 27em; } .shortcut-panel .shortcut-item { width: 25%; }
                    .primary-container .primaryLi .subMenus { width: 400px; border-radius: inherit; overflow: auto; } #e-content-area #e-op-area div.e-toolbarTab { padding: 0 !important; }`,
            };
            function injectCSS(name) {
                let css = document.createElement("style");
                css.id = `ustc-helper-jw-${name}`;
                css.textContent = jw_css[name];
                document.head.appendChild(css);
            }
            if (window.location.pathname.startsWith("/for-std/course-table")) {
                if (config["jw/detailed_time"]) {
                    injectCSS("detailed_time");
                }
                window.top.addEventListener(GM_config_event, e => {
                    if (e.detail.type == "set" && e.detail.prop == "jw/detailed_time") {
                        let css = document.getElementById("ustc-helper-jw-detailed_time");
                        if (css) {
                            css.disabled = !e.detail.after;
                        } else if (e.detail.after) {
                            injectCSS("detailed_time");
                        }
                    }
                });
            }
            if (config["jw/css"]) {
                injectCSS("css");
            }
            window.top.addEventListener(GM_config_event, e => {
                if (e.detail.type == "set" && e.detail.prop == "jw/css") {
                    let css = document.getElementById("ustc-helper-jw-css");
                    if (css) {
                        css.disabled = !e.detail.after;
                    } else if (e.detail.after) {
                        injectCSS(name);
                    }
                }
            });
            break;
        }
        case 'young.ustc.edu.cn': {
            let config_desc = config_descs.young;
            let config = GM_config(config_desc, false);
            if (!config["young/enabled"]) {
                console.info("[USTC Helper] 'young' feature disabled.");
                break;
            }
            if (window.location.pathname == '/nginx_auth/' && config["young/auto_auth"]) {
                document.getElementsByTagName('a')[0].click();
                return;
            }
            let app = document.getElementById("app");
            let router = app.__vue__.$router;
            function main(mutations, observer) {
                let menu = app.querySelector(".ant-menu-root");
                if (!menu) return;
                let default_tab = config["young/default_tab"];
                if (default_tab.length)
                    router.push(default_tab);
                let submenus = menu.querySelectorAll("li.ant-menu-submenu-horizontal:not(.ant-menu-overflowed-submenu) > div");
                if (!submenus.length) return;
                observer.disconnect();
                if (config["young/auto_tab"]) {
                    submenus[0].onclick = (e) => {
                        router.push('/dataAnalysis/studentAnalysis');
                        e.stopImmediatePropagation();
                    }
                    submenus[1].onclick = (e) => {
                        router.push('/personalInformation/personalReport');
                    }
                    submenus[2].onclick = (e) => {
                        router.push('/myproject/SignUp');
                    }
                    submenus[5].onclick = (e) => {
                        router.push('/isystem/departUserList');
                    }
                    app.querySelector(".user-dropdown-menu").onclick = (e) => {
                        document.querySelector("ul.user-dropdown-menu-wrapper > li:nth-child(7) > a").click();
                    }
                }
                if (config["young/no_datascreen"]) {
                    app.querySelector("div.header-index-wide > a").remove();
                }
                if (config["young/shortcut"]) {
                    document.addEventListener("keydown", (e) => {
                        let tabs = document.querySelector(".ant-tabs-nav-animated > div").children;
                        let count = tabs.length;
                        let current = 0;
                        for (let tab of tabs) {
                            if (tab.attributes["aria-selected"].value == "true") {
                                break;
                            }
                            current++;
                        }
                        if (document.activeElement.nodeName != "INPUT") {
                            switch (e.key) {
                                case "ArrowLeft":
                                    tabs[(current - 1 + count) % count].click();
                                    break;
                                case "ArrowRight":
                                    tabs[(current + 1) % count].click();
                                    break;
                                case "x":
                                    tabs[current].querySelector("div > i").click();
                                    break;
                                default:
                                    if (e.key.length == 1) {
                                        let idx = Number(e.key);
                                        if (idx && 0 < idx && idx <= count) {
                                            tabs[idx - 1].click();
                                        }
                                    }
                                    break;
                            }
                        }
                    })
                }
            }
            let options = {
                childList: true,
                attributes: false,
                subtree: true
            }
            let observer = new MutationObserver(main);
            observer.observe(app, options);
            break;
        }
        case 'wvpn.ustc.edu.cn': {
            let config_desc = config_descs.wvpn;
            let config = GM_config(config_desc);
            if (!config["wvpn/enabled"]) {
                console.info("[USTC Helper] 'wvpn' feature disabled.");
                break;
            }
            if (config["wvpn/custom_collection"]) {
                // let element = document.querySelector("div.portal-search-input-wrap");
                let options = {
                    childList: true,
                    attributes: false,
                    subtree: true
                }
                let callback = (mutations, observer) => {
                    let input = document.querySelector("input.portal-search__input");
                    let ele = document.querySelector("div#__layout > div.wrd-webvpn");
                    if (!input || !input.placeholder || !ele) return;
                    let v = ele.__vue__;
                    observer.disconnect();
                    let loading = Qmsg.loading("📦 正在加载依赖库...");
                    let node = document.createElement("script");
                    node.src = "https://cdn.bootcdn.net/ajax/libs/aes-js/3.1.2/index.js";
                    function fail(s, hint) {
                        console.error("[USTC Helper]", s);
                        Qmsg.error(hint);
                    }
                    function success(s, hint) {
                        console.info("[USTC Helper]", s);
                        Qmsg.success(hint);
                    }
                    function cancel() {
                        console.info("[USTC Helper] User calcelled the operation.");
                        Qmsg.info("你终止了收藏操作!😢");
                    }
                    function invalid() {
                        console.warn("[USTC Helper] Invalid input!");
                        Qmsg.warning("你输入了一个不合法的值!🤔");
                    }
                    node.onload = () => {
                        loading.close();
                        success("Aes-js loaded.", "成功加载依赖库!🥳");
                        input.placeholder = "点击五角星或 Ctrl+D 以收藏 🍻";
                        // Encryption, adapted from https://blog.csdn.net/lijiext/article/details/110931285
                        var utf8 = aesjs.utils.utf8;
                        var hex = aesjs.utils.hex;
                        var AesCfb = aesjs.ModeOfOperation.cfb;
                        var wrdvpnKey = 'wrdvpnisthebest!';
                        var wrdvpnIV = 'wrdvpnisthebest!';
                        function textRightAppend(text, mode) {
                            var segmentByteSize = mode === 'utf8' ? 16 : 32;

                            if (text.length % segmentByteSize === 0) {
                                return text;
                            }

                            var appendLength = segmentByteSize - text.length % segmentByteSize;
                            var i = 0;
                            while (i++ < appendLength) {
                                text += '0';
                            }
                            return text;
                        }
                        function encrypt(text, key, iv) {
                            var textLength = text.length;
                            text = textRightAppend(text, 'utf8');
                            var keyBytes = utf8.toBytes(key);
                            var ivBytes = utf8.toBytes(iv);
                            var textBytes = utf8.toBytes(text);
                            var aesCfb = new AesCfb(keyBytes, ivBytes, 16);
                            var encryptBytes = aesCfb.encrypt(textBytes);
                            return hex.fromBytes(ivBytes) + hex.fromBytes(encryptBytes).slice(0, textLength * 2);
                        }
                        function encryptUrl(url) {
                            var port = "";
                            var segments = "";
                            var protocol = "";

                            if (url.startsWith("http://")) {
                                url = url.substr(7);
                                protocol = "http";
                            } else if (url.startsWith("https://")) {
                                url = url.substr(8);
                                protocol = "https";
                            } else {
                                return "";
                            }
                            var v6 = "";
                            var match = /\[[0-9a-fA-F:]+?\]/.exec(url);
                            if (match) {
                                v6 = match[0];
                                url = url.slice(match[0].length);
                            }
                            segments = url.split("?")[0].split(":");
                            if (segments.length > 1) {
                                port = segments[1].split("/")[0]
                                url = url.substr(0, segments[0].length) + url.substr(segments[0].length + port.length + 1);
                            }
                            var i = url.indexOf('/');
                            if (i == -1) {
                                if (v6 != "") {
                                    url = v6;
                                }
                                url = encrypt(url, wrdvpnKey, wrdvpnIV)
                            } else {
                                var host = url.slice(0, i);
                                var path = url.slice(i);
                                if (v6 != "") {
                                    host = v6;
                                }
                                url = encrypt(host, wrdvpnKey, wrdvpnIV) + path;
                            }
                            if (port != "") {
                                url = "/" + protocol + "-" + port + "/" + url;
                            } else {
                                url = "/" + protocol + "/" + url;
                            }
                            return url;
                        }
                        // Main functions
                        function random_color() {
                            let r = Math.floor(Math.random() * 256);
                            let g = Math.floor(Math.random() * 256);
                            let b = Math.floor(Math.random() * 256);
                            return `rgb(${r}, ${g}, ${b})`;
                        }
                        function add_collect() {
                            // Get url
                            let url = input.value;
                            if (url.length == 0) {
                                url = prompt("请输入要收藏的网址:");
                            } else {
                                input.value = '';
                            }
                            if (url == undefined || url == null) {
                                cancel();
                                return;
                            } else if (url.length == 0) {
                                invalid();
                                return;
                            }
                            if (!url.startsWith("http://") && !url.startsWith("https://")) {
                                url = "https://" + url;
                            }
                            let url_;
                            try {
                                url_ = new URL(url);
                            } catch (error) {
                                invalid();
                                return;
                            }
                            // Get name
                            let name = ""; let desc = "";
                            name = prompt("请输入收藏项目的名称:", url_.hostname);
                            if (name == null) {
                                cancel();
                                return;
                            }
                            desc = prompt("请输入收藏项目的备注:", url_.hostname);
                            if (desc == null) {
                                cancel();
                                return;
                            }
                            let id = document.querySelector("div[data-id=collection].block-group > div.block-group__content")?.childElementCount ?? 0;
                            let post_data = {
                                "resource_type": "vpn",
                                "name": name,
                                "detail": desc,
                                "url": url,
                                "redirect": encryptUrl(url),
                                "id": id,
                                "group_id": 2,
                                "logo": "",
                                "_isCollect": false,
                                "_displayName": name,
                                "_desc": desc,
                                "_icon": {
                                    "color": random_color(),
                                    "content": name[0]
                                }
                            }
                            v.addCollect(post_data);
                        }
                        // Simple UI
                        let a = document.createElement("a");
                        a.text = "⭐";
                        a.style = "position: absolute;left: 150px;top: 20px;";
                        a.onclick = add_collect;
                        input.parentElement.appendChild(a);
                        // Shortcut
                        input.addEventListener("keydown", (e) => {
                            if (e.key === 'd' && e.ctrlKey) {
                                e.preventDefault();
                                add_collect();
                            }
                        });
                    }
                    node.onerror = (e) => {
                        fail("Failed to load Aes-js. You won't be able to use \"custom_collection\" feature.", "依赖库加载失败,您将无法使用自定义收藏功能!⚠️");

                    }
                    document.head.appendChild(node);
                }
                let observer = new MutationObserver(callback);
                observer.observe(document.body, options);
            }
            break;
        }
        default:
            console.error("[USTC Helper] Unexpected host: " + window.location.host);
            break;
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址