LINUXDO ReadBoost

LINUXDO ReadBoost是一个LINUXDO刷取已读帖量脚本,理论上支持所有Discourse论坛

2024-12-05 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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('所有回复处理完成')
}