基于【LINUXDO ReadBoost】改编,支持了响应式更新内容的论坛;LINUX DO ReadBoost 是一个刷取 LINUX DO 论坛已读帖量脚本,理论上支持所有的 Discourse 论坛
// ==UserScript== // @name LINUX DO ReadBoost // @author hmjz100 // @namespace github.com/hmjz100 // @version 1.0.1 // @description 基于【LINUXDO ReadBoost】改编,支持了响应式更新内容的论坛;LINUX DO ReadBoost 是一个刷取 LINUX DO 论坛已读帖量脚本,理论上支持所有的 Discourse 论坛 // @icon data:image/webp;base64,UklGRtQCAABXRUJQVlA4TMcCAAAvH8AHEB8EtbZt18rc9+fosKCwdEX/Jqez23Aiybar9LnkqMCCwrL/VeFyeNeJJNuu0veRo8NmNPtfCe4XFpkzvAu3tm1V1XyCRWTuLh0QQQ1UzKASIjKH73ICEPjFPwJBKbf/EMiQI0GCSrumbWbUczuh287jjJr9vkm34Y/IyG6038M1wD8Ch3//03m0vhZ3DWoTW6yCtHeElhrib+prO+wcwAZVlNDC7O/33b/fH18YYo0MGQm/bZDLX1XzK2ZUWVy93YrNoVHVfy7KN2nyKpI+q/FdRyTSRx385eR9skeqREUqSCTSqBAlGf5KNijKd4G0/A3xuhyUVxWryt9x2c+SiCQSQkISdrOOJCQhCauKJCTh+bJ13089nuZWFQ/HmafzQgLhuOzB93U7KSasKl6uu51DoiTJpm3NtW3btu+zbevatvHpR2vH+4E1vTAR/Z8ACpuWAaC7t78gz7ODEGDTEG06IavPXxwfrK06ObAXvi8LSI8iA+jJPdmKT1ha34BeErflbQ8hI0ImsgdOlxNWdvd2nHOOLYws64Ec8mEykJ9ylLS6t7UZpdtbhUnRlAEgnXLcYeLu9maMAs3OUDqlAQNHet8q9NoIwBnoOUvyW0o9zjVZZN2LS15cUOqtkSxg8MHlzdW1Um3lAROmClNTlIvRuB2bvBug59shHuEAPWOYR4MQtsIBC//3EpRYliCELdgH4RmOJ6YDdHxL40XVVcqtM4Ta+08fPlJsn2ghNN/5++e3YmPuNKODGp7Nv/uilOdtAzoqCU2v5t59VchzRgOVaEN555u595++RSfasO8sRxuAVpSZ1/Pf332OBqyN7SyjVoS2o7zp+c+ZHx/ef/xijPHvHWbsc11O7QjfCtSXvvg8++/Xd621t7OQUkNwiNwGtDQWP375NuQti280QBuireggoKuupqTY6xp2ADqEwgIA // @license MIT // @match *://linux.do/* // @grant GM_setValue // @grant GM_getValue // @grant unsafeWindow // @run-at document-body // @require https://unpkg.com/[email protected]/dist/jquery.min.js // ==/UserScript== (function ReadBoost() { 'use strict'; let reading = []; let readed = []; let originPushState = history.pushState; unsafeWindow.history.pushState = function (state, title, src) { setTimeout(() => { boost(new URL(src, location.href)); }, 1500) return originPushState.call(unsafeWindow.history, state, title, src); }; let originReplaceState = history.replaceState; unsafeWindow.history.replaceState = function (state, title, src) { setTimeout(() => { boost(new URL(src, location.href)); }, 1500) return originReplaceState.call(unsafeWindow.history, state, title, src); }; let style = $(`<style id="readBoostStyle"> #readBoost { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 1.3em; border-radius: 16px; z-index: 1000; background: var(--tertiary-medium); color: var(--primary); box-shadow: 0 8px 32px #0000001a; } div.readboost { padding-top: 10px; font-size: 16px; } label.readboost { display: flex; align-items: center; justify-content: space-between; padding-top: 10px; color: var(--primary); font-weight: normal; } label.readboost input { margin: 0; padding: 3px 5px; } .readboost.buttonCollection { display: flex; align-items: center; justify-content: space-evenly; } div.topic-owner .topic-body .contents>.cooked::after { color: var(--tertiary-medium); content: "题主"; } </style>`) let settingsButton = $(`<span class="auth-buttons"><button id="settingsButton" class="btn btn-small btn-icon-text"><svg class="fa svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#gear"></use></svg></button></span>`) let statusLabel = $('<span id="statusLabel" style="margin: 0 10px 0">ReadBoost 待命中</span>') settingsButton.on('click', showSettingsUI) waitForKeyElements('.header-buttons', (element) => { element.append(statusLabel) element.append(settingsButton) }, true) waitForKeyElements('body', (element) => { element.after(style) }, true) let defaultConfig = { baseDelay: 2500, randomDelayRange: 800, minReqSize: 8, maxReqSize: 20, minReadTime: 800, maxReadTime: 3000, autoStart: false } let config = { ...defaultConfig, ...getStoredConfig() } let csrfToken = $('meta[name=csrf-token]').attr('content') function boost(url = (new URL(location.href)), auto = false) { console.log(`【LINUX DO ReadBoost】Init\n收到新链接`, `\n链接:${url.href}`) // 初始化 let topicId = url?.pathname?.split("/")?.[3] let repliesInfo = $('div[class=timeline-replies]').text().trim() if (!topicId || !csrfToken || !repliesInfo) { console.log(`【LINUX DO ReadBoost】Init\n缺失关键标识,跳过`) return; }; let [currentPosition, totalReplies] = repliesInfo?.split("/")?.map(part => parseInt(part?.trim(), 10)) // 自启动处理 if (config.autoStart || auto) { startReading(topicId, totalReplies) } } boost() /** * 开始刷取已读话题 * @param {string} topicId 主题ID * @param {number} totalReplies 总帖子数 */ async function startReading(topicId, totalReplies) { if (!reading.includes(topicId)) { reading.push(topicId); } else { console.log(`【LINUX DO ReadBoost】Read\n正在处理此话题,跳过`) return; } if (readed.includes(topicId)) { console.log(`【LINUX DO ReadBoost】Read\n已读过此话题,跳过`) let index = reading.indexOf(topicId); if (index !== -1) { reading.splice(index, 1); } return; } console.log(`【LINUX DO ReadBoost】Read\n开始阅读……`, `\n话题标识:${topicId}`, `\n帖子数量:${totalReplies}`) let baseRequestDelay = config.baseDelay let randomDelayRange = config.randomDelayRange let minBatchReplyCount = config.minReqSize let maxBatchReplyCount = config.maxReqSize let minReadTime = config.minReadTime let maxReadTime = config.maxReadTime // 随机数生成 function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min } // 发起读帖请求 async function sendBatch(startId, endId, retryCount = 3) { let params = createBatchParams(startId, endId) try { let response = await fetch("https://linux.do/topics/timings", { headers: { "accept": "*/*", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "discourse-background": "true", "discourse-logged-in": "true", "discourse-present": "true", "priority": "u=1, i", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "x-csrf-token": csrfToken, "x-requested-with": "XMLHttpRequest", "x-silence-logger": "true" }, referrer: `https://linux.do/`, body: params.toString(), method: "POST", mode: "cors", credentials: "include" }) if (!response.ok) { throw new Error(`请求失败,状态:${response.status}`) } console.log(`【LINUX DO ReadBoost】Read\n处理成功`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`) updateStatus(`话题 ${topicId} 的帖子 ${startId}~${endId} 处理成功`, "green") } catch (error) { console.error(`【LINUX DO ReadBoost】Read\n处理失败`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`, `\n错误详情:`, error) if (retryCount > 0) { console.error(`【LINUX DO ReadBoost】Read\n重新处理`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`) updateStatus(`重新处理话题 ${topicId} 的帖子 ${startId}~${endId}(${retryCount})`, "orange") let retryDelay = 2000 await new Promise(r => setTimeout(r, retryDelay)) await sendBatch(startId, endId, retryCount - 1) } else { console.error(`【LINUX DO ReadBoost】Read\n处理失败`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`, `\n错误详情:`, error) updateStatus(`话题 ${topicId} 的帖子 ${startId}~${endId} 处理失败`, "red") } } let delay = baseRequestDelay + getRandomInt(0, randomDelayRange) await new Promise(r => setTimeout(r, delay)) } function createBatchParams(startId, endId) { let params = new URLSearchParams() for (let i = startId; i <= endId; i++) { params.append(`timings[${i}]`, getRandomInt(minReadTime, maxReadTime).toString()) } let topicTime = getRandomInt(minReadTime * (endId - startId + 1), maxReadTime * (endId - startId + 1)).toString() params.append('topic_time', topicTime) params.append('topic_id', topicId) return params } // 批量阅读处理 for (let i = 1; i <= totalReplies;) { let batchSize = getRandomInt(minBatchReplyCount, maxBatchReplyCount) let startId = i let endId = Math.min(i + batchSize - 1, totalReplies) await sendBatch(startId, endId) i = endId + 1 } console.log(`【LINUX DO ReadBoost】Read\n处理完成`, `\n话题标识:${topicId}`) updateStatus(`话题 ${topicId} 处理完成`, "green") if (!readed.includes(topicId)) { readed.push(topicId); } let index = reading.indexOf(topicId); if (index !== -1) { reading.splice(index, 1); } setTimeout(() => { updateStatus("ReadBoost 待命中", "") }, 3000) } /** * 更新状态标签内容 */ function updateStatus(text, color) { statusLabel.text(text) if (color !== "") { statusLabel.css({ 'background-color': color, 'color': '#fff' }) } else { statusLabel.css({ 'background-color': '', 'color': '' }) } } /** * 显示设置UI界面 */ function showSettingsUI() { if ($('#readBoost').length) return; let settingsDiv = $(`<div id="readBoost"> <h3>ReadBoost 设置</h3> <div class="readboost"> <label class="readboost"><span>基础延迟(ms)</span><input id="baseDelay" type="number" value="${config.baseDelay}"></label> <label class="readboost"><span>随机延迟范围(ms)</span><input id="randomDelayRange" type="number" value="${config.randomDelayRange}"></label> <label class="readboost"><span>最小请求量</span><input id="minReqSize" type="number" value="${config.minReqSize}"></label> <label class="readboost"><span>最大请求量</span><input id="maxReqSize" type="number" value="${config.maxReqSize}"></label> <label class="readboost"><span>最小时间(ms)</span><input id="minReadTime" type="number" value="${config.minReadTime}"></label> <label class="readboost"><span>最大时间(ms)</span><input id="maxReadTime" type="number" value="${config.maxReadTime}"></label> <label class="readboost"><span>解锁参数</span><input type="checkbox" id="advancedMode"></label> <label class="readboost"><span>自动运行</span><input type="checkbox" id="autoStart" ${config.autoStart ? "checked" : ""}></label> </div> <div class="readboost buttonCollection"> <button class="btn btn-small" id="saveSettings"> <span class="d-button-label">保存</span> </button> <button class="btn btn-small" id="resetDefaults"> <span class="d-button-label">重置</span> </button> <button class="btn btn-small" id="startManually"> <span class="d-button-label">运行</span> </button> <button class="btn btn-small" id="closeSettings"> <span class="d-button-label">关闭</span> </button> </div> </div>`) settingsDiv.find("#saveSettings").on("click", () => { config.baseDelay = parseInt(settingsDiv.find("#baseDelay").val(), 10); config.randomDelayRange = parseInt(settingsDiv.find("#randomDelayRange").val(), 10); config.minReqSize = parseInt(settingsDiv.find("#minReqSize").val(), 10); config.maxReqSize = parseInt(settingsDiv.find("#maxReqSize").val(), 10); config.minReadTime = parseInt(settingsDiv.find("#minReadTime").val(), 10); config.maxReadTime = parseInt(settingsDiv.find("#maxReadTime").val(), 10); config.autoStart = settingsDiv.find("#autoStart").prop("checked"); // 持久化保存设置 GM_setValue("baseDelay", config.baseDelay); GM_setValue("randomDelayRange", config.randomDelayRange); GM_setValue("minReqSize", config.minReqSize); GM_setValue("maxReqSize", config.maxReqSize); GM_setValue("minReadTime", config.minReadTime); GM_setValue("maxReadTime", config.maxReadTime); GM_setValue("autoStart", config.autoStart); settingsDiv.remove(); location.reload(); }); settingsDiv.find("#resetDefaults").on("click", () => { let r###lt = confirm("你确定要重置吗?所有自定义数据都将丢失!"); if (r###lt) { config = { ...defaultConfig }; GM_setValue("baseDelay", defaultConfig.baseDelay); GM_setValue("randomDelayRange", defaultConfig.randomDelayRange); GM_setValue("minReqSize", defaultConfig.minReqSize); GM_setValue("maxReqSize", defaultConfig.maxReqSize); GM_setValue("minReadTime", defaultConfig.minReadTime); GM_setValue("maxReadTime", defaultConfig.maxReadTime); GM_setValue("autoStart", defaultConfig.autoStart); settingsDiv.remove(); location.reload(); } }); settingsDiv.find("#startManually").on("click", () => { boost(location, true) settingsDiv.remove(); }); function toggleSettingsInputs(enabled) { let inputs = [ "baseDelay", "randomDelayRange", "minReqSize", "maxReqSize", "minReadTime", "maxReadTime" ]; inputs.forEach(inputId => { let inputElement = settingsDiv.find(`#${inputId}`); if (inputElement.length) { inputElement.prop("disabled", !enabled); } }); } toggleSettingsInputs(false); settingsDiv.find("#advancedMode").on("change", (event) => { if ($(event.target).prop("checked")) { toggleSettingsInputs(true); } else { toggleSettingsInputs(false); } }); settingsDiv.find("#closeSettings").on("click", () => { settingsDiv.remove(); }); $("body").append(settingsDiv); } function getStoredConfig() { return { baseDelay: GM_getValue("baseDelay", defaultConfig.baseDelay), randomDelayRange: GM_getValue("randomDelayRange", defaultConfig.randomDelayRange), minReqSize: GM_getValue("minReqSize", defaultConfig.minReqSize), maxReqSize: GM_getValue("maxReqSize", defaultConfig.maxReqSize), minReadTime: GM_getValue("minReadTime", defaultConfig.minReadTime), maxReadTime: GM_getValue("maxReadTime", defaultConfig.maxReadTime), autoStart: GM_getValue("autoStart", defaultConfig.autoStart) } } function waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector) { function findInShadowRoots(root, selector) { let elements = $(root).find(selector).toArray(); $(root).find('*').each(function () { let shadowRoot = this.shadowRoot; if (shadowRoot) { elements = elements.concat(findInShadowRoots(shadowRoot, selector)); } }); return elements; } var targetElements; if (iframeSelector) { targetElements = $(iframeSelector).contents(); } else { targetElements = $(document); } let allElements = findInShadowRoots(targetElements, selectorTxt); if (allElements.length > 0) { allElements.forEach(function (element) { var jThis = $(element); var uniqueIdentifier = 'alreadyFound'; var alreadyFound = jThis.data(uniqueIdentifier) || false; if (!alreadyFound) { var cancelFound = actionFunction(jThis); if (cancelFound) { return false; } else { jThis.data(uniqueIdentifier, true); } } }); } var controlObj = waitForKeyElements.controlObj || {}; var controlKey = selectorTxt.replace(/[^\w]/g, "_"); var timeControl = controlObj[controlKey]; if (allElements.length > 0 && bWaitOnce && timeControl) { clearInterval(timeControl); delete controlObj[controlKey]; } else { if (!timeControl) { timeControl = setInterval(function () { waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector); }, 1000); controlObj[controlKey] = timeControl; } } waitForKeyElements.controlObj = controlObj; } })();