LINUXDO ReadBoost

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

当前为 2024-12-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name LINUXDO ReadBoost
  3. // @namespace linux.do_ReadBoost
  4. // @match https://linux.do/t/topic/*
  5. // @grant GM_setValue
  6. // @grant GM_getValue
  7. // @version 1.0
  8. // @author Do
  9. // @description LINUXDO_ReadBoost是一个LINUXDO刷取已读帖量脚本,理论上支持所有Discourse论坛
  10. // ==/UserScript==
  11.  
  12. const hasAgreed = GM_getValue("hasAgreed", false)
  13. if (!hasAgreed) {
  14. const userInput = prompt("[ LINUXDO ReadBoost ]\n检测到这是你第一次使用LINUXDO ReadBoost,使用前你必须知晓:使用该第三方脚本可能会导致包括并不限于账号被限制、被封禁的潜在风险,脚本不对出现的任何风险负责,这是一个开源脚本,你可以自由审核其中的内容,如果你同意以上内容,请输入“明白”")
  15. if (userInput !== "明白") {
  16. alert("您未同意风险提示,脚本已停止运行。")
  17. throw new Error("未同意风险提示")
  18. }
  19.  
  20. GM_setValue("hasAgreed", true)
  21. }
  22.  
  23. // 初始化
  24.  
  25. const headerButtons = document.querySelector(".header-buttons")
  26. const topicID = window.location.pathname.split("/")[3]
  27. const repliesInfo = document.querySelector("div[class=timeline-replies]").textContent.trim()
  28. const [currentPosition, totalReplies] = repliesInfo.split("/").map(part => parseInt(part.trim(), 10))
  29. const csrfToken = document.querySelector("meta[name=csrf-token]").getAttribute("content")
  30.  
  31. console.log("LINUXDO ReadBoost 已加载")
  32. console.log(`帖子ID${topicID}`)
  33. console.log(`当前位置:${currentPosition}`)
  34. console.log(`总回复:${totalReplies}`)
  35.  
  36. // 默认参数
  37. const DEFAULT_CONFIG = {
  38. baseDelay: 1500,
  39. randomDelayRange: 300,
  40. minReqSize: 8,
  41. maxReqSize: 20,
  42. minReadTime: 800,
  43. maxReadTime: 3000,
  44. autoStart: false
  45. }
  46. let config = { ...DEFAULT_CONFIG, ...getStoredConfig() }
  47.  
  48. // 设置按钮和状态UI
  49. const settingsButton = createButton("设置", "settingsButton", "btn-icon-text")
  50. const statusLabel = createStatusLabel("LINUXDO ReadBoost待命中")
  51.  
  52. headerButtons.appendChild(statusLabel)
  53. headerButtons.appendChild(settingsButton)
  54. // 绑定设置按钮事件
  55. settingsButton.addEventListener("click", showSettingsUI)
  56.  
  57. // 自启动处理
  58. if (config.autoStart) {
  59. startReading(topicID, totalReplies)
  60. }
  61.  
  62.  
  63. function getStoredConfig() {
  64. return {
  65. baseDelay: GM_getValue("baseDelay", DEFAULT_CONFIG.baseDelay),
  66. randomDelayRange: GM_getValue("randomDelayRange", DEFAULT_CONFIG.randomDelayRange),
  67. minReqSize: GM_getValue("minReqSize", DEFAULT_CONFIG.minReqSize),
  68. maxReqSize: GM_getValue("maxReqSize", DEFAULT_CONFIG.maxReqSize),
  69. minReadTime: GM_getValue("minReadTime", DEFAULT_CONFIG.minReadTime),
  70. maxReadTime: GM_getValue("maxReadTime", DEFAULT_CONFIG.maxReadTime),
  71. autoStart: GM_getValue("autoStart", DEFAULT_CONFIG.autoStart)
  72. }
  73. }
  74.  
  75. /**
  76. * 按钮封装
  77. */
  78. function createButton(label, id, extraClass = "") {
  79. const outerSpan = document.createElement("span")
  80. outerSpan.className = "auth-buttons"
  81.  
  82. const button = document.createElement("button")
  83. button.className = `btn btn-small ${extraClass}`
  84. button.id = id
  85.  
  86. const span = document.createElement("span")
  87. span.className = "d-button-label"
  88. span.textContent = label
  89.  
  90. button.appendChild(span)
  91. outerSpan.appendChild(button)
  92.  
  93. return outerSpan
  94. }
  95.  
  96.  
  97. /**
  98. * 状态标签封装
  99. */
  100. function createStatusLabel(initialText) {
  101. const labelSpan = document.createElement("span")
  102. labelSpan.id = "statusLabel"
  103. labelSpan.style.marginLeft = "10px"
  104. labelSpan.style.marginRight = "10px"
  105.  
  106.  
  107. labelSpan.textContent = initialText
  108. return labelSpan
  109. }
  110.  
  111.  
  112. /**
  113. * 更新状态标签内容
  114. */
  115. function updateStatus(text, color = "#555") {
  116. const statusLabel = document.getElementById("statusLabel")
  117. if (statusLabel) {
  118. statusLabel.textContent = text
  119. statusLabel.style.color = color
  120. }
  121. }
  122.  
  123.  
  124. /**
  125. * 显示设置UI界面
  126. */
  127. function showSettingsUI() {
  128. const settingsDiv = document.createElement("div")
  129. settingsDiv.style.position = "fixed"
  130. settingsDiv.style.top = "50%"
  131. settingsDiv.style.left = "50%"
  132. settingsDiv.style.transform = "translate(-50%, -50%)"
  133. settingsDiv.style.padding = "20px"
  134. settingsDiv.style.border = "1px solid #ccc"
  135. settingsDiv.style.borderRadius = "10px"
  136. settingsDiv.style.backgroundColor = "#fff"
  137. settingsDiv.style.zIndex = "1000"
  138.  
  139. const autoStartChecked = config.autoStart ? "checked" : ""
  140. const settingsHtml = `
  141. <h3>设置参数</h3>
  142. <label>基础延迟(ms): <input id="baseDelay" type="number" value="${config.baseDelay}"></label><br>
  143. <label>随机延迟范围(ms): <input id="randomDelayRange" type="number" value="${config.randomDelayRange}"></label><br>
  144. <label>最小每次请求阅读量: <input id="minReqSize" type="number" value="${config.minReqSize}"></label><br>
  145. <label>最大每次请求阅读量: <input id="maxReqSize" type="number" value="${config.maxReqSize}"></label><br>
  146. <label>最小阅读时间(ms): <input id="minReadTime" type="number" value="${config.minReadTime}"></label><br>
  147. <label>最大阅读时间(ms): <input id="maxReadTime" type="number" value="${config.maxReadTime}"></label><br>
  148. <label><input type="checkbox" id="advancedMode"> 高级设置(解锁参数选项)</label><br>
  149. <label><input type="checkbox" id="autoStart" ${autoStartChecked}> 自动运行</label><br><br>
  150. <button class="btn btn-small" id="startManually" >
  151. <span class="d-button-label">手动开始</span>
  152. </button>
  153. <button class="btn btn-small" id="saveSettings" >
  154. <span class="d-button-label">保存</span>
  155. </button>
  156. <button class="btn btn-small" id="closeSettings">
  157. <span class="d-button-label">关闭</span>
  158. </button>
  159. <button class="btn btn-small" id="resetDefaults">
  160. <span class="d-button-label">恢复默认值</span>
  161. </button>
  162. `
  163.  
  164. settingsDiv.innerHTML = settingsHtml
  165.  
  166. document.body.appendChild(settingsDiv)
  167.  
  168. // 手动开始按钮
  169. document.getElementById("startManually").addEventListener("click", () => {
  170. settingsDiv.remove()
  171. startReading(topicID, totalReplies)
  172. })
  173.  
  174. // 保存设置
  175. document.getElementById("saveSettings").addEventListener("click", () => {
  176. config.baseDelay = parseInt(document.getElementById("baseDelay").value, 10)
  177. config.randomDelayRange = parseInt(document.getElementById("randomDelayRange").value, 10)
  178. config.minReqSize = parseInt(document.getElementById("minReqSize").value, 10)
  179. config.maxReqSize = parseInt(document.getElementById("maxReqSize").value, 10)
  180. config.minReadTime = parseInt(document.getElementById("minReadTime").value, 10)
  181. config.maxReadTime = parseInt(document.getElementById("maxReadTime").value, 10)
  182. config.autoStart = document.getElementById("autoStart").checked
  183.  
  184. // 持久化保存设置
  185. GM_setValue("baseDelay", config.baseDelay)
  186. GM_setValue("randomDelayRange", config.randomDelayRange)
  187. GM_setValue("minReqSize", config.minReqSize)
  188. GM_setValue("maxReqSize", config.maxReqSize)
  189. GM_setValue("minReadTime", config.minReadTime)
  190. GM_setValue("maxReadTime", config.maxReadTime)
  191. GM_setValue("autoStart", config.autoStart)
  192.  
  193. alert("设置已保存!")
  194. location.reload()
  195. })
  196. document.getElementById("resetDefaults").addEventListener("click", () => {
  197. // 重置为默认配置
  198. config = { ...DEFAULT_CONFIG }
  199.  
  200. // 保存默认配置到存储
  201. GM_setValue("baseDelay", DEFAULT_CONFIG.baseDelay)
  202. GM_setValue("randomDelayRange", DEFAULT_CONFIG.randomDelayRange)
  203. GM_setValue("minReqSize", DEFAULT_CONFIG.minReqSize)
  204. GM_setValue("maxReqSize", DEFAULT_CONFIG.maxReqSize)
  205. GM_setValue("minReadTime", DEFAULT_CONFIG.minReadTime)
  206. GM_setValue("maxReadTime", DEFAULT_CONFIG.maxReadTime)
  207. GM_setValue("autoStart", DEFAULT_CONFIG.autoStart)
  208.  
  209. alert("已恢复默认设置!")
  210. location.reload()
  211. })
  212.  
  213.  
  214. /**
  215. * 切换输入框状态,在默认状态下禁用
  216. */
  217. function toggleSettingsInputs(enabled) {
  218. const inputs = [
  219. "baseDelay", "randomDelayRange", "minReqSize",
  220. "maxReqSize", "minReadTime", "maxReadTime"
  221. ]
  222.  
  223. inputs.forEach(inputId => {
  224. const inputElement = document.getElementById(inputId)
  225. if (inputElement) {
  226. inputElement.disabled = !enabled
  227. }
  228. })
  229. }
  230.  
  231. toggleSettingsInputs(false)
  232.  
  233. // 启用高级设置告警弹窗
  234. document.getElementById("advancedMode").addEventListener("change", (event) => {
  235. if (event.target.checked) {
  236. const userInput = prompt("[ LINUXDO ReadBoost ]\n如果你不知道你在修改什么,那么不建议开启高级设置,随意修改可能会提高脚本崩溃、账号被禁等风险的可能!请输入 '明白' 确认继续开启高级设置:")
  237.  
  238. if (userInput !== "明白") {
  239. alert("您未确认风险,高级设置未启用。")
  240. event.target.checked = false
  241. return
  242. }
  243.  
  244. // 启用所有输入框
  245. toggleSettingsInputs(true)
  246. } else {
  247. // 禁用所有输入框
  248. toggleSettingsInputs(false)
  249. }
  250. })
  251.  
  252.  
  253.  
  254. // 关闭设置UI
  255. document.getElementById("closeSettings").addEventListener("click", () => {
  256. settingsDiv.remove()
  257. })
  258. }
  259.  
  260. /**
  261. * 开始刷取已读帖子
  262. * @param {string} topicId 主题ID
  263. * @param {number} totalReplies 总回复数
  264. */
  265. async function startReading(topicId, totalReplies) {
  266. console.log("启动阅读处理...")
  267.  
  268.  
  269. const baseRequestDelay = config.baseDelay
  270. const randomDelayRange = config.randomDelayRange
  271. const minBatchReplyCount = config.minReqSize
  272. const maxBatchReplyCount = config.maxReqSize
  273. const minReadTime = config.minReadTime
  274. const maxReadTime = config.maxReadTime
  275.  
  276. // 随机数生成
  277. function getRandomInt(min, max) {
  278. return Math.floor(Math.random() * (max - min + 1)) + min
  279. }
  280.  
  281. // 发起读帖请求
  282. async function sendBatch(startId, endId, retryCount = 3) {
  283. const params = createBatchParams(startId, endId)
  284. try {
  285. const response = await fetch("https://linux.do/topics/timings", {
  286. headers: {
  287. "accept": "*/*",
  288. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  289. "discourse-background": "true",
  290. "discourse-logged-in": "true",
  291. "discourse-present": "true",
  292. "priority": "u=1, i",
  293. "sec-fetch-dest": "empty",
  294. "sec-fetch-mode": "cors",
  295. "sec-fetch-site": "same-origin",
  296. "x-csrf-token": csrfToken,
  297. "x-requested-with": "XMLHttpRequest",
  298. "x-silence-logger": "true"
  299. },
  300. referrer: `https://linux.do/`,
  301. body: params.toString(),
  302. method: "POST",
  303. mode: "cors",
  304. credentials: "include"
  305. })
  306. if (!response.ok) {
  307. throw new Error(`HTTP请求失败,状态码:${response.status}`)
  308. }
  309. console.log(`成功处理回复 ${startId} - ${endId}`)
  310. updateStatus(`成功处理回复 ${startId} - ${endId}`, "green")
  311. } catch (e) {
  312. console.error(`处理回复 ${startId} - ${endId} 失败: `, e)
  313.  
  314. if (retryCount > 0) {
  315. console.log(`重试处理回复 ${startId} - ${endId},剩余重试次数:${retryCount}`)
  316. updateStatus(`重试处理回复 ${startId} - ${endId},剩余重试次数:${retryCount}`, "orange")
  317.  
  318. // 等待一段时间再重试
  319. const retryDelay = 2000 // 重试间隔时间(毫秒)
  320. await new Promise(r => setTimeout(r, retryDelay))
  321. await sendBatch(startId, endId, retryCount - 1)
  322. } else {
  323. console.error(`处理回复 ${startId} - ${endId} 失败,自动跳过`)
  324. updateStatus(`处理回复 ${startId} - ${endId} ,自动跳过`, "red")
  325. }
  326. }
  327. const delay = baseRequestDelay + getRandomInt(0, randomDelayRange)
  328. await new Promise(r => setTimeout(r, delay))
  329. }
  330.  
  331. // 生成请求body参数
  332. function createBatchParams(startId, endId) {
  333. const params = new URLSearchParams()
  334.  
  335. for (let i = startId; i <= endId; i++) {
  336. params.append(`timings[${i}]`, getRandomInt(minReadTime, maxReadTime).toString())
  337. }
  338. const topicTime = getRandomInt(minReadTime * (endId - startId + 1), maxReadTime * (endId - startId + 1)).toString()
  339. params.append('topic_time', topicTime)
  340. params.append('topic_id', topicId)
  341. return params
  342. }
  343.  
  344. // 批量阅读处理
  345. for (let i = 1; i <= totalReplies;) {
  346. const batchSize = getRandomInt(minBatchReplyCount, maxBatchReplyCount)
  347. const startId = i
  348. const endId = Math.min(i + batchSize - 1, totalReplies)
  349.  
  350. await sendBatch(startId, endId)
  351. i = endId + 1
  352. }
  353. updateStatus(`所有回复处理完成`, "green")
  354. console.log('所有回复处理完成')
  355. }