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