您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlights those who have signed the RMS Support letter
// ==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 }); }