- // ==UserScript==
- // @name LINUXDO ReadBoost
- // @namespace linux.do_ReadBoost
- // @match https://linux.do/t/topic/*
- // @grant GM_setValue
- // @grant GM_getValue
- // @version 1.3
- // @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==
-
- const hasAgreed = GM_getValue("hasAgreed", false)
- if (!hasAgreed) {
- const userInput = prompt("[ LINUXDO ReadBoost ]\n检测到这是你第一次使用LINUXDO ReadBoost,使用前你必须知晓:使用该第三方脚本可能会导致包括并不限于账号被限制、被封禁的潜在风险,脚本不对出现的任何风险负责,这是一个开源脚本,你可以自由审核其中的内容,如果你同意以上内容,请输入“明白”")
- if (userInput !== "明白") {
- alert("您未同意风险提示,脚本已停止运行。")
- throw new Error("未同意风险提示")
- }
-
- GM_setValue("hasAgreed", true)
- }
-
- // 初始化
-
- const headerButtons = document.querySelector(".header-buttons")
- const topicID = window.location.pathname.split("/")[3]
- const repliesInfo = document.querySelector("div[class=timeline-replies]").textContent.trim()
- const [currentPosition, totalReplies] = repliesInfo.split("/").map(part => parseInt(part.trim(), 10))
- const csrfToken = document.querySelector("meta[name=csrf-token]").getAttribute("content")
-
- console.log("LINUXDO ReadBoost 已加载")
- console.log(`帖子ID:${topicID}`)
- console.log(`当前位置:${currentPosition}`)
- console.log(`总回复:${totalReplies}`)
-
- // 默认参数
- const DEFAULT_CONFIG = {
- baseDelay: 2500,
- randomDelayRange: 800,
- minReqSize: 8,
- maxReqSize: 20,
- minReadTime: 800,
- maxReadTime: 3000,
- autoStart: false
- }
- let config = { ...DEFAULT_CONFIG, ...getStoredConfig() }
-
- // 设置按钮和状态UI
- const settingsButton = createButton("设置", "settingsButton", "btn-icon-text")
- const statusLabel = createStatusLabel("LINUXDO ReadBoost待命中")
-
- headerButtons.appendChild(statusLabel)
- headerButtons.appendChild(settingsButton)
- // 绑定设置按钮事件
- settingsButton.addEventListener("click", showSettingsUI)
-
- // 自启动处理
- if (config.autoStart) {
- startReading(topicID, totalReplies)
- }
-
-
- 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)
- }
- }
-
- /**
- * 按钮封装
- */
- function createButton(label, id, extraClass = "") {
- const outerSpan = document.createElement("span")
- outerSpan.className = "auth-buttons"
-
- const button = document.createElement("button")
- button.className = `btn btn-small ${extraClass}`
- button.id = id
-
- const span = document.createElement("span")
- span.className = "d-button-label"
- span.textContent = label
-
- button.appendChild(span)
- outerSpan.appendChild(button)
-
- return outerSpan
- }
-
-
- /**
- * 状态标签封装
- */
- function createStatusLabel(initialText) {
- const labelSpan = document.createElement("span")
- labelSpan.id = "statusLabel"
- labelSpan.style.marginLeft = "10px"
- labelSpan.style.marginRight = "10px"
-
-
- labelSpan.textContent = initialText
- return labelSpan
- }
-
-
- /**
- * 更新状态标签内容
- */
- function updateStatus(text, color = "#555") {
- const statusLabel = document.getElementById("statusLabel")
- if (statusLabel) {
- statusLabel.textContent = text
- statusLabel.style.color = color
- }
- }
-
-
- /**
- * 显示设置UI界面
- */
- function showSettingsUI() {
- const settingsDiv = document.createElement("div")
- settingsDiv.style.position = "fixed"
- settingsDiv.style.top = "50%"
- settingsDiv.style.left = "50%"
- settingsDiv.style.transform = "translate(-50%, -50%)"
- settingsDiv.style.padding = "20px"
- settingsDiv.style.border = "1px solid #ccc"
- settingsDiv.style.borderRadius = "10px"
- settingsDiv.style.backgroundColor = "#fff"
- settingsDiv.style.zIndex = "1000"
-
- const autoStartChecked = config.autoStart ? "checked" : ""
- const settingsHtml = `
- <h3>设置参数</h3>
- <label>基础延迟(ms): <input id="baseDelay" type="number" value="${config.baseDelay}"></label><br>
- <label>随机延迟范围(ms): <input id="randomDelayRange" type="number" value="${config.randomDelayRange}"></label><br>
- <label>最小每次请求阅读量: <input id="minReqSize" type="number" value="${config.minReqSize}"></label><br>
- <label>最大每次请求阅读量: <input id="maxReqSize" type="number" value="${config.maxReqSize}"></label><br>
- <label>最小阅读时间(ms): <input id="minReadTime" type="number" value="${config.minReadTime}"></label><br>
- <label>最大阅读时间(ms): <input id="maxReadTime" type="number" value="${config.maxReadTime}"></label><br>
- <label><input type="checkbox" id="advancedMode"> 高级设置(解锁参数选项)</label><br>
- <label><input type="checkbox" id="autoStart" ${autoStartChecked}> 自动运行</label><br><br>
- <button class="btn btn-small" id="startManually" >
- <span class="d-button-label">手动开始</span>
- </button>
- <button class="btn btn-small" id="saveSettings" >
- <span class="d-button-label">保存</span>
- </button>
- <button class="btn btn-small" id="closeSettings">
- <span class="d-button-label">关闭</span>
- </button>
- <button class="btn btn-small" id="resetDefaults">
- <span class="d-button-label">恢复默认值</span>
- </button>
- `
-
- settingsDiv.innerHTML = settingsHtml
-
- document.body.appendChild(settingsDiv)
-
- // 手动开始按钮
- document.getElementById("startManually").addEventListener("click", () => {
- settingsDiv.remove()
- startReading(topicID, totalReplies)
- })
-
- // 保存设置
- document.getElementById("saveSettings").addEventListener("click", () => {
- config.baseDelay = parseInt(document.getElementById("baseDelay").value, 10)
- config.randomDelayRange = parseInt(document.getElementById("randomDelayRange").value, 10)
- config.minReqSize = parseInt(document.getElementById("minReqSize").value, 10)
- config.maxReqSize = parseInt(document.getElementById("maxReqSize").value, 10)
- config.minReadTime = parseInt(document.getElementById("minReadTime").value, 10)
- config.maxReadTime = parseInt(document.getElementById("maxReadTime").value, 10)
- config.autoStart = document.getElementById("autoStart").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)
-
- alert("设置已保存!")
- location.reload()
- })
- document.getElementById("resetDefaults").addEventListener("click", () => {
- // 重置为默认配置
- config = { ...DEFAULT_CONFIG }
-
- // 保存默认配置到存储
- GM_setValue("baseDelay", DEFAULT_CONFIG.baseDelay)
- GM_setValue("randomDelayRange", DEFAULT_CONFIG.randomDelayRange)
- GM_setValue("minReqSize", DEFAULT_CONFIG.minReqSize)
- GM_setValue("maxReqSize", DEFAULT_CONFIG.maxReqSize)
- GM_setValue("minReadTime", DEFAULT_CONFIG.minReadTime)
- GM_setValue("maxReadTime", DEFAULT_CONFIG.maxReadTime)
- GM_setValue("autoStart", DEFAULT_CONFIG.autoStart)
-
- alert("已恢复默认设置!")
- location.reload()
- })
-
-
- /**
- * 切换输入框状态,在默认状态下禁用
- */
- function toggleSettingsInputs(enabled) {
- const inputs = [
- "baseDelay", "randomDelayRange", "minReqSize",
- "maxReqSize", "minReadTime", "maxReadTime"
- ]
-
- inputs.forEach(inputId => {
- const inputElement = document.getElementById(inputId)
- if (inputElement) {
- inputElement.disabled = !enabled
- }
- })
- }
-
- toggleSettingsInputs(false)
-
- // 启用高级设置告警弹窗
- document.getElementById("advancedMode").addEventListener("change", (event) => {
- if (event.target.checked) {
- const userInput = prompt("[ LINUXDO ReadBoost ]\n如果你不知道你在修改什么,那么不建议开启高级设置,随意修改可能会提高脚本崩溃、账号被禁等风险的可能!请输入 '明白' 确认继续开启高级设置:")
-
- if (userInput !== "明白") {
- alert("您未确认风险,高级设置未启用。")
- event.target.checked = false
- return
- }
-
- // 启用所有输入框
- toggleSettingsInputs(true)
- } else {
- // 禁用所有输入框
- toggleSettingsInputs(false)
- }
- })
-
-
-
- // 关闭设置UI
- document.getElementById("closeSettings").addEventListener("click", () => {
- settingsDiv.remove()
- })
- }
-
- /**
- * 开始刷取已读帖子
- * @param {string} topicId 主题ID
- * @param {number} totalReplies 总回复数
- */
- async function startReading(topicId, totalReplies) {
- console.log("启动阅读处理...")
-
-
- const baseRequestDelay = config.baseDelay
- const randomDelayRange = config.randomDelayRange
- const minBatchReplyCount = config.minReqSize
- const maxBatchReplyCount = config.maxReqSize
- const minReadTime = config.minReadTime
- const maxReadTime = config.maxReadTime
-
- // 随机数生成
- function getRandomInt(min, max) {
- return Math.floor(Math.random() * (max - min + 1)) + min
- }
-
- // 发起读帖请求
- async function sendBatch(startId, endId, retryCount = 3) {
- const params = createBatchParams(startId, endId)
- try {
- const 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(`HTTP请求失败,状态码:${response.status}`)
- }
- console.log(`成功处理回复 ${startId} - ${endId}`)
- updateStatus(`成功处理回复 ${startId} - ${endId}`, "green")
- } catch (e) {
- console.error(`处理回复 ${startId} - ${endId} 失败: `, e)
-
- if (retryCount > 0) {
- console.log(`重试处理回复 ${startId} - ${endId},剩余重试次数:${retryCount}`)
- updateStatus(`重试处理回复 ${startId} - ${endId},剩余重试次数:${retryCount}`, "orange")
-
- // 等待一段时间再重试
- const retryDelay = 2000 // 重试间隔时间(毫秒)
- await new Promise(r => setTimeout(r, retryDelay))
- await sendBatch(startId, endId, retryCount - 1)
- } else {
- console.error(`处理回复 ${startId} - ${endId} 失败,自动跳过`)
- updateStatus(`处理回复 ${startId} - ${endId} ,自动跳过`, "red")
- }
- }
- const delay = baseRequestDelay + getRandomInt(0, randomDelayRange)
- await new Promise(r => setTimeout(r, delay))
- }
-
- // 生成请求body参数
- function createBatchParams(startId, endId) {
- const params = new URLSearchParams()
-
- for (let i = startId; i <= endId; i++) {
- params.append(`timings[${i}]`, getRandomInt(minReadTime, maxReadTime).toString())
- }
- const 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;) {
- const batchSize = getRandomInt(minBatchReplyCount, maxBatchReplyCount)
- const startId = i
- const endId = Math.min(i + batchSize - 1, totalReplies)
-
- await sendBatch(startId, endId)
- i = endId + 1
- }
- updateStatus(`所有回复处理完成`, "green")
- console.log('所有回复处理完成')
- }