Highlight RMS supporters

Highlights those who have signed the RMS Support letter

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name            Highlight RMS supporters
// @description     Highlights those who have signed the RMS Support letter
// @include         https://github.com/*
// @author          stick
// @version         2
// @homepageURL     https://github.com/sticks-stuff/highlight-RMS-supporters
// @namespace       https://greasyfork.org/users/710818
// @grant           GM.xmlHttpRequest
// @connect         api.github.com
// @connect         codeload.github.com
// @require         https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js
// @require         https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.0.0/js-yaml.min.js
// ==/UserScript==
const RMS_LETTER_DOWNLOAD = "https://api.github.com/repos/rms-support-letter/rms-support-letter.github.io/zipball/";
const UPDATE_INTERVAL = 3600;

async function iterateDirectory(dir, cb) {
    let filename, relativePath, file;
    let promises = [];
    for (filename in dir.files) {
        if (!dir.files.hasOwnProperty(filename)) {
            continue;
        }

        file = dir.files[filename];
        relativePath = filename.slice(dir.root.length, filename.length);


        if (relativePath && filename.slice(0, dir.root.length) === dir.root) {
            promises.push(cb(file));
        }
    }
    await Promise.all(promises);
}

function linkToGithubUsername(rawLink) {
    // if the link is a link to a GitHub user, return the username in
    // URI-escaped form. otherwise, return `null`.
    if (!(typeof rawLink === 'string' || rawLink instanceof String)) {
        // the link is not a string
        return null;
    }

    // truncate to avoid maliciously excessively long URLs
    try {
        var link = new URL(rawLink.slice(0, 100));
    } catch (e) {
        return null;
    }

    if (!(link.protocol == 'http:' || link.protocol == 'https:')) {
        // link is not HTTP(S)
        return null;
    }
    if (!(link.host == 'github.com' || link.host == 'www.github.com')) {
        // link is not github.com
        return null;
    }

    // we `.slice(1)` to remove the leading backslash
    let pathParts = link.pathname.slice(1).split('/');
    if (pathParts.length != 1) {
        // link has the wrong number of path parts
        return null;
    }

    // url.parse automatically performs URI escaping
    return pathParts[0];
}

function getUpdates(db) {
    // We need to use GM_xmlhttpRequest here because our request is cross-origin.
    GM.xmlHttpRequest({
        url: RMS_LETTER_DOWNLOAD,
        method: "GET",
        nocache: true, // This request should never be cached
        responseType: "arraybuffer",
        onload: async (data) => {
            let zip = await JSZip.loadAsync(data.response);
            // The name of the directory that the signature folder is in will vary depending on the signature of the github repo (which changes every time it updates)
            // We can ensure we always get the right directory by using a regex.
            // This makes the assumption that the signatures are under _data/signed, so if they changed this location in the future, we will need to change it here too.
            let signature_folder = zip.folder(zip.folder(/^rms-support-letter.*_data\/signed\/$/g)[0].name);
            await iterateDirectory(signature_folder, async (file) => {
                let content = await file.async("string");
                let transaction = db.transaction(["signatures"], "readwrite");
                let signatures = transaction.objectStore("signatures");
                let username = linkToGithubUsername(jsyaml.load(content).link);
                if (username === null) return;
                signatures.put(0, username.toLowerCase());
            })
            window.localStorage.setItem("last_updated", new Date().getTime() / 1000);
            highlightNames(db);
        },
        onerror: (e) => {
            // TODO: We should notify the user of the error
            console.error(e);
        }
    });
}

function highlightNames(db) {
    let links = document.getElementsByTagName("a");
    for (const element of links) {
        let text = element.innerHTML.toLowerCase();
        let signatures = db.transaction(["signatures"], "readwrite").objectStore("signatures");
        // onsuccess will only fire when the key exists
        signatures.getKey(text).onsuccess = (event) => {
            if (event.target.result !== text) return;
            element.style.backgroundColor = "crimson";
        }
    };
}

var request = window.indexedDB.open("RMSSignatures", 3);
request.onerror = function(event) {
    console.error("Database error: " + event.target.errorCode);
};
request.onupgradeneeded = function(event) {
    let db = event.target.result;
    db.createObjectStore("signatures");
};
request.onsuccess = function(event) {
    let db = event.target.result;
    let transaction = db.transaction(["signatures"], "readwrite");
    let signatures = transaction.objectStore("signatures");

    if (window.localStorage.getItem("last_updated") === null) {
        window.localStorage.setItem("last_updated", 0);
    }
    if (new Date().getTime() / 1000 >= parseFloat(window.localStorage.getItem("last_updated")) + UPDATE_INTERVAL) {
        signatures.clear();
        try {
        	return getUpdates(db);
        }
        catch (e) {
           console.error(e);
        }
    }

    highlightNames(db);
    let observer = new MutationObserver(() => highlightNames(db));
    observer.observe(document.body, {
        childList: true
    });
}