- // ==UserScript==
- // @name LINUXDO ReadBoost
- // @namespace linux.do_ReadBoost
- // @match https://linux.do/*
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_registerMenuCommand
- // @version 2.2
- // @author Do
- // @description LINUXDO ReadBoost是一个LINUXDO刷取已读帖量脚本,理论上支持所有Discourse论坛
- // @description:zh-TW LINUXDO ReadBoost是一個LINUXDO刷取已讀帖量腳本,理論上支持所有Discourse論壇
- // @description:en LINUXDO ReadBoost is a script for LINUXDO to boost the number of read posts. It theoretically supports all Discourse forums.
- // ==/UserScript==
-
- (function () {
- 'use strict'
- const hasAgreed = GM_getValue("hasAgreed", false)
- if (!hasAgreed) {
- const userInput = prompt("[ LINUXDO ReadBoost ]\n检测到这是你第一次使用LINUXDO ReadBoost,使用前你必须知晓:使用该第三方脚本可能会导致包括并不限于账号被限制、被封禁的潜在风险,脚本不对出现的任何风险负责,这是一个开源脚本,你可以自由审核其中的内容,如果你同意以上内容,请输入'明白'")
- if (userInput !== "明白") {
- alert("您未同意风险提示,脚本已停止运行。")
- return
- }
- GM_setValue("hasAgreed", true)
- }
-
- // 默认参数
- const DEFAULT_CONFIG = {
- baseDelay: 2500,
- randomDelayRange: 800,
- minReqSize: 8,
- maxReqSize: 20,
- minReadTime: 800,
- maxReadTime: 3000,
- autoStart: false,
- startFromCurrent: false
- }
-
- let config = { ...DEFAULT_CONFIG, ...getStoredConfig() }
- let isRunning = false
- let shouldStop = false
- let statusLabel = null
- let initTimeout = null
- function isTopicPage() {
- return /^https:\/\/linux\.do\/t\/[^/]+\/\d+/.test(window.location.href)
- }
-
- function getPageInfo() {
- if (!isTopicPage()) {
- throw new Error("不在帖子页面")
- }
- const topicID = window.location.pathname.split("/")[3]
- const repliesElement = document.querySelector("div[class=timeline-replies]")
- const csrfElement = document.querySelector("meta[name=csrf-token]")
-
- if (!repliesElement || !csrfElement) {
- throw new Error("无法获取页面信息,请确认在正确的帖子页面")
- }
-
- const repliesInfo = repliesElement.textContent.trim()
- const [currentPosition, totalReplies] = repliesInfo.split("/").map(part => parseInt(part.trim(), 10))
- const csrfToken = csrfElement.getAttribute("content")
-
- return { topicID, currentPosition, totalReplies, csrfToken }
- }
-
-
- function getStoredConfig() {
- return {
- baseDelay: GM_getValue("baseDelay", DEFAULT_CONFIG.baseDelay),
- randomDelayRange: GM_getValue("randomDelayRange", DEFAULT_CONFIG.randomDelayRange),
- minReqSize: GM_getValue("minReqSize", DEFAULT_CONFIG.minReqSize),
- maxReqSize: GM_getValue("maxReqSize", DEFAULT_CONFIG.maxReqSize),
- minReadTime: GM_getValue("minReadTime", DEFAULT_CONFIG.minReadTime),
- maxReadTime: GM_getValue("maxReadTime", DEFAULT_CONFIG.maxReadTime),
- autoStart: GM_getValue("autoStart", DEFAULT_CONFIG.autoStart),
- startFromCurrent: GM_getValue("startFromCurrent", DEFAULT_CONFIG.startFromCurrent)
- }
- }
-
- function saveConfig(newConfig) {
- Object.keys(newConfig).forEach(key => {
- GM_setValue(key, newConfig[key])
- config[key] = newConfig[key]
- })
- location.reload()
- }
-
- function createStatusLabel() {
- // 移除已存在的状态标签
- const existingLabel = document.getElementById("readBoostStatusLabel")
- if (existingLabel) {
- existingLabel.remove()
- }
-
- const headerButtons = document.querySelector(".header-buttons")
- if (!headerButtons) return null
-
- const labelSpan = document.createElement("span")
- labelSpan.id = "readBoostStatusLabel"
- labelSpan.style.cssText = `
- margin-left: 10px;
- margin-right: 10px;
- padding: 5px 10px;
- border-radius: 4px;
- background: var(--primary-low);
- color: var(--primary);
- font-size: 12px;
- font-weight: bold;
- cursor: pointer;
- `
- labelSpan.textContent = "ReadBoost"+" ⚙️"
- labelSpan.addEventListener("click", showSettingsUI)
-
- headerButtons.appendChild(labelSpan)
- return labelSpan
- }
-
- // 更新状态
- function updateStatus(text, type = "info") {
- if (!statusLabel) return
-
- const colors = {
- info: "var(--primary)",
- success: "#2e8b57",
- warning: "#ff8c00",
- error: "#dc3545",
- running: "#007bff"
- }
-
- statusLabel.textContent = text + " ⚙️"
- statusLabel.style.color = colors[type] || colors.info
- }
-
- function showSettingsUI() {
- const settingsDiv = document.createElement("div")
- settingsDiv.style.cssText = `
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: 25px;
- border-radius: 12px;
- z-index: 10000;
- background: var(--secondary);
- color: var(--primary);
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- border: 1px solid var(--primary-low);
- min-width: 400px;
- max-width: 500px;
- `
-
- const autoStartChecked = config.autoStart ? "checked" : ""
- const startFromCurrentChecked = config.startFromCurrent ? "checked" : ""
- settingsDiv.innerHTML = `
- <h3 style="margin-top: 0; color: var(--primary); text-align: center;">ReadBoost 设置</h3>
- <div style="display: grid; gap: 15px;">
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
- <label style="display: flex; flex-direction: column; gap: 5px;">
- <span>基础延迟(ms):</span>
- <input id="baseDelay" type="number" value="${config.baseDelay}"
- style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
- </label>
- <label style="display: flex; flex-direction: column; gap: 5px;">
- <span>随机延迟范围(ms):</span>
- <input id="randomDelayRange" type="number" value="${config.randomDelayRange}"
- style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
- </label>
- </div>
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
- <label style="display: flex; flex-direction: column; gap: 5px;">
- <span>最小每次请求量:</span>
- <input id="minReqSize" type="number" value="${config.minReqSize}"
- style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
- </label>
- <label style="display: flex; flex-direction: column; gap: 5px;">
- <span>最大每次请求量:</span>
- <input id="maxReqSize" type="number" value="${config.maxReqSize}"
- style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
- </label>
- </div>
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
- <label style="display: flex; flex-direction: column; gap: 5px;">
- <span>最小阅读时间(ms):</span>
- <input id="minReadTime" type="number" value="${config.minReadTime}"
- style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
- </label>
- <label style="display: flex; flex-direction: column; gap: 5px;">
- <span>最大阅读时间(ms):</span>
- <input id="maxReadTime" type="number" value="${config.maxReadTime}"
- style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
- </label>
- </div>
- <div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
- <label style="display: flex; align-items: center; gap: 8px;">
- <input type="checkbox" id="advancedMode" style="transform: scale(1.2);">
- <span>高级设置模式</span>
- </label>
- <label style="display: flex; align-items: center; gap: 8px;">
- <input type="checkbox" id="autoStart" ${autoStartChecked} style="transform: scale(1.2);">
- <span>自动运行</span>
- </label>
- <label style="display: flex; align-items: center; gap: 8px;">
- <input type="checkbox" id="startFromCurrent" ${startFromCurrentChecked} style="transform: scale(1.2);">
- <span>从当前浏览位置开始</span>
- </label>
- </div>
- <div style="display: flex; gap: 10px; justify-content: center; margin-top: 10px;">
- <button id="saveSettings" style="padding: 10px 20px; border: none; border-radius: 6px; background: #007bff; color: white; cursor: pointer;">保存设置</button>
- <button id="resetDefaults" style="padding: 10px 20px; border: none; border-radius: 6px; background: #6c757d; color: white; cursor: pointer;">重置默认</button>
- <button id="closeSettings" style="padding: 10px 20px; border: none; border-radius: 6px; background: #dc3545; color: white; cursor: pointer;">关闭</button>
- </div>
- </div>
- `
-
- document.body.appendChild(settingsDiv)
-
- toggleAdvancedInputs(false)
-
- document.getElementById("advancedMode").addEventListener("change", (e) => {
- if (e.target.checked) {
- const confirmed = confirm("高级设置可能增加账号风险,确定要启用吗?")
- if (!confirmed) {
- e.target.checked = false
- return
- }
- }
- toggleAdvancedInputs(e.target.checked)
- })
-
- document.getElementById("saveSettings").addEventListener("click", () => {
- const newConfig = {
- baseDelay: parseInt(document.getElementById("baseDelay").value, 10),
- randomDelayRange: parseInt(document.getElementById("randomDelayRange").value, 10),
- minReqSize: parseInt(document.getElementById("minReqSize").value, 10),
- maxReqSize: parseInt(document.getElementById("maxReqSize").value, 10),
- minReadTime: parseInt(document.getElementById("minReadTime").value, 10),
- maxReadTime: parseInt(document.getElementById("maxReadTime").value, 10),
- autoStart: document.getElementById("autoStart").checked,
- startFromCurrent: document.getElementById("startFromCurrent").checked
- }
-
- saveConfig(newConfig)
- settingsDiv.remove()
- updateStatus("设置已保存", "success")
-
- })
-
- document.getElementById("resetDefaults").addEventListener("click", () => {
- if (confirm("确定要重置为默认设置吗?")) {
- saveConfig(DEFAULT_CONFIG)
- settingsDiv.remove()
- updateStatus("已重置为默认设置", "info")
- }
- })
-
- document.getElementById("closeSettings").addEventListener("click", () => {
- settingsDiv.remove()
- })
-
- function toggleAdvancedInputs(enabled) {
- const inputs = ["baseDelay", "randomDelayRange", "minReqSize", "maxReqSize", "minReadTime", "maxReadTime"]
- inputs.forEach(id => {
- const input = document.getElementById(id)
- if (input) {
- input.disabled = !enabled
- input.style.opacity = enabled ? "1" : "0.6"
- }
- })
- }
- }
-
- async function startReading() {
- if (isRunning) {
- updateStatus("脚本正在运行中...", "warning")
- return
- }
-
- try {
- const pageInfo = getPageInfo()
- isRunning = true
- shouldStop = false
-
- updateStatus("正在启动...", "running")
-
- await processReading(pageInfo)
-
- updateStatus("处理完成", "success")
- } catch (error) {
- console.error("执行错误:", error)
- if (error.message === "用户停止执行") {
- updateStatus("ReadBoost", "info")
- } else {
- updateStatus("执行失败: " + error.message, "error")
- }
- } finally {
- isRunning = false
- }
- }
-
-
- function stopReading() {
- shouldStop = true
- updateStatus("正在停止...", "warning")
- }
-
- // 处理阅读逻辑
- async function processReading(pageInfo) {
- const { topicID, currentPosition, totalReplies, csrfToken } = pageInfo
- const startPosition = config.startFromCurrent ? currentPosition : 1
-
- console.log(`开始处理,起始位置: ${startPosition}, 总回复: ${totalReplies}`)
-
- function getRandomInt(min, max) {
- return Math.floor(Math.random() * (max - min + 1)) + min
- }
-
- async function sendBatch(startId, endId, retryCount = 3) {
- // 停止检查
- if (shouldStop) throw new Error("用户停止执行")
-
- const params = new URLSearchParams()
-
- for (let i = startId; i <= endId; i++) {
- params.append(`timings[${i}]`, getRandomInt(config.minReadTime, config.maxReadTime).toString())
- }
-
- const topicTime = getRandomInt(
- config.minReadTime * (endId - startId + 1),
- config.maxReadTime * (endId - startId + 1)
- ).toString()
-
- params.append('topic_time', topicTime)
- params.append('topic_id', topicID)
-
- try {
- const response = await fetch("https://linux.do/topics/timings", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
- "X-CSRF-Token": csrfToken,
- "X-Requested-With": "XMLHttpRequest"
- },
- body: params,
- credentials: "include"
- })
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`)
- }
-
- // 再次检查是否应该停止
- if (shouldStop) throw new Error("用户停止执行")
-
- updateStatus(`处理回复 ${startId}-${endId} (${Math.round((endId / totalReplies) * 100)}%)`, "running")
-
- } catch (error) {
- if (shouldStop) throw error // 如果是停止信号,直接抛出
-
- if (retryCount > 0) {
- updateStatus(`重试 ${startId}-${endId} (剩余${retryCount}次)`, "warning")
- await new Promise(r => setTimeout(r, 2000))
- return await sendBatch(startId, endId, retryCount - 1)
- }
- throw error
- }
-
- // 延迟期间也检查停止信号
- const delay = config.baseDelay + getRandomInt(0, config.randomDelayRange)
- for (let i = 0; i < delay; i += 100) {
- if (shouldStop) throw new Error("用户停止执行")
- await new Promise(r => setTimeout(r, Math.min(100, delay - i)))
- }
- }
-
- // 批量处理
- for (let i = startPosition; i <= totalReplies;) {
- if (shouldStop) break
-
- const batchSize = getRandomInt(config.minReqSize, config.maxReqSize)
- const startId = i
- const endId = Math.min(i + batchSize - 1, totalReplies)
-
- await sendBatch(startId, endId)
- i = endId + 1
- }
- }
-
- // 注册(不可用)菜单命令
- GM_registerMenuCommand("🚀 开始执行", startReading)
- GM_registerMenuCommand("⏹️ 停止执行", stopReading)
- GM_registerMenuCommand("⚙️ 设置", showSettingsUI)
-
- function init() {
- statusLabel = createStatusLabel()
- // 强制停止之前的任务
- shouldStop = true
-
- // 等待当前任务停止后再继续
- if (isRunning) {
- setTimeout(init, 1000)
- return
- }
-
- // 重置状态
- isRunning = false
- shouldStop = false
-
- // 清除之前的超时
- if (initTimeout) {
- clearTimeout(initTimeout)
- }
- if (!isTopicPage()) return
-
- try {
- const pageInfo = getPageInfo()
- console.log("LINUXDO ReadBoost 已加载")
- console.log(`帖子ID: ${pageInfo.topicID}, 总回复: ${pageInfo.totalReplies}`)
-
- statusLabel = createStatusLabel()
-
-
- if (config.autoStart) {
- initTimeout = setTimeout(startReading, 1000)
- }
-
- } catch (error) {
- console.error("初始化失败:", error)
- initTimeout = setTimeout(init, 1000)
- }
- }
- // 监听 URL 变化
- function setupRouteListener() {
- let lastUrl = location.href
- const originalPushState = history.pushState
- history.pushState = function () {
- originalPushState.apply(history, arguments)
- if (location.href !== lastUrl) {
- lastUrl = location.href
- setTimeout(init, 500)
- }
- }
- }
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => {
- init()
- setupRouteListener()
- })
- } else {
- init()
- setupRouteListener()
- }
- })()