USTC Helper

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

נכון ליום 26-11-2023. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         USTC Helper
// @name:zh-CN   USTC 助手
// @license      gpl-3.0
// @namespace    http://tampermonkey.net/
// @version      0.12.1
// @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://greasyfork.org/scripts/462234-message/code/Message.js?version=1192786
// @require      https://greasyfork.org/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 verification code or \"Login\" button"),
            "passport/service": boolDesc("Service", "Hint service domain and its credibility"),
            "passport/auto_login": boolDesc("Auto login", "Automatically clicks \"Login\" button (Official services only)"),
        },
        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);
            let is_official = false;
            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;
            }
            form.removeAttribute("style");
            let options = {
                childList: true,
                attributes: false,
                subtree: true
            }
            function login(click = false) {
                let code = document.getElementById('validate');
                if (code) {
                    code.focus();
                    return;
                }
                let btn = document.getElementById('login');
                if (!btn) {
                    console.error("[USTC Helper] Login button not found!");
                    return;
                }
                if (click) {
                    window.setTimeout(() => {
                        btn.click();
                    }, 5000);
                } else btn.focus();
            }
            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";
                        is_official = true;
                    }
                } 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>`;
                }
                document.getElementById("footer").before(notice);
            }
            function main() {
                if (config["passport/focus"]) login();
                if (config["passport/service"]) hint();
                if (config["passport/auto_login"] && is_official) login(true);
                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', 'cyan'];
                let hint_text = ['查询中', '已提交', '未提交', '查询错误', '已忽略', '已评分'];
                // let hint_text = ['Checking', 'Submitted', 'Not submitted', 'Error', 'Ignored', 'Graded'];
                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, null];
                    } else {
                        const parser = new DOMParser();
                        const html = await r.text();
                        const doc = parser.parseFromString(html, 'text/html');
                        const title = doc.getElementById('pageTitleText').textContent.trim();
                        if (title.startsWith('上载作业')) {
                            return [2, null];
                        } else if (title.startsWith('复查提交历史记录')) {
                            const grade = doc.getElementById("aggregateGrade");
                            const suffix = doc.getElementById("aggregateGrade_pointsPossible");
                            if (grade.value !== '-') {
                                return [5, `${parseFloat(grade.value)}${suffix.textContent.trim()}`];
                            } else {
                                return [1, null];
                            }
                        } else {
                            return [3, null];
                        }
                    }
                }
                async function process(hw) {
                    let link_ = hw.querySelector("h3 > a");
                    if (link_) {
                        let status = [0, null]; // 0: Checking  1: Uploaded  2: Not uploaded  3: Error
                        let hint = document.createElement('span');
                        hint.style.color = color_config[status[0]];
                        hint.textContent = `(${hint_text[status[0]]})`;
                        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);
                        // Check if this homework is ignored
                        if (ignored) {
                            ignored = JSON.parse(ignored).includes(content_id);
                            if (ignored) {
                                status[0] = 4;
                                link_.parentNode.parentNode.parentNode.style.opacity = 0.4;
                                console.log(`[USTC Helper] "${course_id}/${content_id}" present in ignore list, so this homework is ignored.`);
                            }
                        }
                        // Not in cache
                        if (!status[0]) {
                            status = await query_status(link);
                            if (status[0] == 1) {
                                console.log(`[USTC Helper] Online query indicated that "${course_id}/${content_id}" is uploaded.`);
                            } else if (status[0] == 2) {
                                console.log(`[USTC Helper] Online query indicated that "${course_id}/${content_id}" is not uploaded.`);
                            } else if (status[0] == 5) {
                                console.log(`[USTC Helper] Online query indicated that "${course_id}/${content_id}" is graded.`);
                            } else {
                                console.warn(`[USTC Helper] Online query "${course_id}/${content_id}" failed!`);
                            }
                        }
                        hint.style.color = color_config[status[0]];
                        hint.textContent = `(${hint_text[status[0]]}${status[1] ? " " + status[1] : ""})`;
                        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 });
                    }
                }
                for (let hw of hw_list.children) {
                    process(hw);
                }
            }
            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;
    }
})();