LINUX DO ReadBoost

基于【LINUXDO ReadBoost】改编,支持了响应式更新内容的论坛;LINUX DO ReadBoost 是一个刷取 LINUX DO 论坛已读帖量脚本,理论上支持所有的 Discourse 论坛

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

  1. // ==UserScript==
  2. // @name LINUX DO ReadBoost
  3. // @author hmjz100
  4. // @namespace github.com/hmjz100
  5. // @version 1.0.0
  6. // @description 基于【LINUXDO ReadBoost】改编,支持了响应式更新内容的论坛;LINUX DO ReadBoost 是一个刷取 LINUX DO 论坛已读帖量脚本,理论上支持所有的 Discourse 论坛
  7. // @icon data:image/webp;base64,UklGRtQCAABXRUJQVlA4TMcCAAAvH8AHEB8EtbZt18rc9+fosKCwdEX/Jqez23Aiybar9LnkqMCCwrL/VeFyeNeJJNuu0veRo8NmNPtfCe4XFpkzvAu3tm1V1XyCRWTuLh0QQQ1UzKASIjKH73ICEPjFPwJBKbf/EMiQI0GCSrumbWbUczuh287jjJr9vkm34Y/IyG6038M1wD8Ch3//03m0vhZ3DWoTW6yCtHeElhrib+prO+wcwAZVlNDC7O/33b/fH18YYo0MGQm/bZDLX1XzK2ZUWVy93YrNoVHVfy7KN2nyKpI+q/FdRyTSRx385eR9skeqREUqSCTSqBAlGf5KNijKd4G0/A3xuhyUVxWryt9x2c+SiCQSQkISdrOOJCQhCauKJCTh+bJ13089nuZWFQ/HmafzQgLhuOzB93U7KSasKl6uu51DoiTJpm3NtW3btu+zbevatvHpR2vH+4E1vTAR/Z8ACpuWAaC7t78gz7ODEGDTEG06IavPXxwfrK06ObAXvi8LSI8iA+jJPdmKT1ha34BeErflbQ8hI0ImsgdOlxNWdvd2nHOOLYws64Ec8mEykJ9ylLS6t7UZpdtbhUnRlAEgnXLcYeLu9maMAs3OUDqlAQNHet8q9NoIwBnoOUvyW0o9zjVZZN2LS15cUOqtkSxg8MHlzdW1Um3lAROmClNTlIvRuB2bvBug59shHuEAPWOYR4MQtsIBC//3EpRYliCELdgH4RmOJ6YDdHxL40XVVcqtM4Ta+08fPlJsn2ghNN/5++e3YmPuNKODGp7Nv/uilOdtAzoqCU2v5t59VchzRgOVaEN555u595++RSfasO8sRxuAVpSZ1/Pf332OBqyN7SyjVoS2o7zp+c+ZHx/ef/xijPHvHWbsc11O7QjfCtSXvvg8++/Xd621t7OQUkNwiNwGtDQWP375NuQti280QBuireggoKuupqTY6xp2ADqEwgIA
  8. // @license MIT
  9. // @match *://linux.do/*
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant unsafeWindow
  13. // @run-at document-body
  14. // @require https://unpkg.com/jquery@3.6.3/dist/jquery.min.js
  15. // ==/UserScript==
  16.  
  17. (function ReadBoost() {
  18. 'use strict';
  19.  
  20. let reading = [];
  21. let readed = [];
  22.  
  23. let originPushState = history.pushState;
  24. unsafeWindow.history.pushState = function (state, title, src) {
  25. setTimeout(() => {
  26. boost(new URL(src, location.href));
  27. }, 1500)
  28. return originPushState.call(unsafeWindow.history, state, title, src);
  29. };
  30.  
  31. let originReplaceState = history.replaceState;
  32. unsafeWindow.history.replaceState = function (state, title, src) {
  33. setTimeout(() => {
  34. boost(new URL(src, location.href));
  35. }, 1500)
  36. return originReplaceState.call(unsafeWindow.history, state, title, src);
  37. };
  38.  
  39. let settingsButton = $('<span class="auth-buttons"><button id="settingsButton" class="btn btn-small btn-icon-text"><svg class="fa svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#gear"></use></svg></button></span>')
  40. let statusLabel = $('<span id="statusLabel" style="margin: 0 10px 0">ReadBoost 待命中</span>')
  41. settingsButton.on('click', showSettingsUI)
  42.  
  43. waitForKeyElements('.header-buttons', (element) => {
  44. element.append(statusLabel)
  45. element.append(settingsButton)
  46. }, true)
  47.  
  48. let defaultConfig = {
  49. baseDelay: 2500,
  50. randomDelayRange: 800,
  51. minReqSize: 8,
  52. maxReqSize: 20,
  53. minReadTime: 800,
  54. maxReadTime: 3000,
  55. autoStart: false
  56. }
  57.  
  58. let config = { ...defaultConfig, ...getStoredConfig() }
  59. let csrfToken = $('meta[name=csrf-token]').attr('content')
  60.  
  61. function boost(url = (new URL(location.href))) {
  62. console.log(`【LINUX DO ReadBoostInit\n收到新链接`, `\n链接:${url.href}`)
  63.  
  64. // 初始化
  65. let topicId = url?.pathname?.split("/")?.[3]
  66. let repliesInfo = $('div[class=timeline-replies]').text().trim()
  67. if (!topicId || !csrfToken || !repliesInfo) {
  68. console.log(`【LINUX DO ReadBoostInit\n缺失关键标识,跳过`)
  69. return;
  70. };
  71. let [currentPosition, totalReplies] = repliesInfo?.split("/")?.map(part => parseInt(part?.trim(), 10))
  72.  
  73. // 自启动处理
  74. if (config.autoStart) {
  75. startReading(topicId, totalReplies)
  76. }
  77. }
  78. boost()
  79.  
  80. /**
  81. * 开始刷取已读话题
  82. * @param {string} topicId 主题ID
  83. * @param {number} totalReplies 总帖子数
  84. */
  85. async function startReading(topicId, totalReplies) {
  86. if (!reading.includes(topicId)) {
  87. reading.push(topicId);
  88. } else {
  89. console.log(`【LINUX DO ReadBoostRead\n正在处理此话题,跳过`)
  90. return;
  91. }
  92. if (readed.includes(topicId)) {
  93. console.log(`【LINUX DO ReadBoostRead\n已读过此话题,跳过`)
  94. let index = reading.indexOf(topicId);
  95. if (index !== -1) {
  96. reading.splice(index, 1);
  97. }
  98. return;
  99. }
  100. console.log(`【LINUX DO ReadBoostRead\n开始阅读……`, `\n话题标识:${topicId}`, `\n帖子数量:${totalReplies}`)
  101.  
  102. let baseRequestDelay = config.baseDelay
  103. let randomDelayRange = config.randomDelayRange
  104. let minBatchReplyCount = config.minReqSize
  105. let maxBatchReplyCount = config.maxReqSize
  106. let minReadTime = config.minReadTime
  107. let maxReadTime = config.maxReadTime
  108.  
  109. // 随机数生成
  110. function getRandomInt(min, max) {
  111. return Math.floor(Math.random() * (max - min + 1)) + min
  112. }
  113.  
  114. // 发起读帖请求
  115. async function sendBatch(startId, endId, retryCount = 3) {
  116. let params = createBatchParams(startId, endId)
  117. try {
  118. let response = await fetch("https://linux.do/topics/timings", {
  119. headers: {
  120. "accept": "*/*",
  121. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  122. "discourse-background": "true",
  123. "discourse-logged-in": "true",
  124. "discourse-present": "true",
  125. "priority": "u=1, i",
  126. "sec-fetch-dest": "empty",
  127. "sec-fetch-mode": "cors",
  128. "sec-fetch-site": "same-origin",
  129. "x-csrf-token": csrfToken,
  130. "x-requested-with": "XMLHttpRequest",
  131. "x-silence-logger": "true"
  132. },
  133. referrer: `https://linux.do/`,
  134. body: params.toString(),
  135. method: "POST",
  136. mode: "cors",
  137. credentials: "include"
  138. })
  139. if (!response.ok) {
  140. throw new Error(`请求失败,状态:${response.status}`)
  141. }
  142. console.log(`【LINUX DO ReadBoostRead\n处理成功`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`)
  143. updateStatus(`话题 ${topicId} 的帖子 ${startId}~${endId} 处理成功`, "green")
  144. } catch (error) {
  145. console.error(`【LINUX DO ReadBoostRead\n处理失败`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`, `\n错误详情:`, error)
  146.  
  147. if (retryCount > 0) {
  148. console.error(`【LINUX DO ReadBoostRead\n重新处理`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`)
  149. updateStatus(`重新处理话题 ${topicId} 的帖子 ${startId}~${endId}(${retryCount})`, "orange")
  150.  
  151. let retryDelay = 2000
  152. await new Promise(r => setTimeout(r, retryDelay))
  153. await sendBatch(startId, endId, retryCount - 1)
  154. } else {
  155. console.error(`【LINUX DO ReadBoostRead\n处理失败`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`, `\n错误详情:`, error)
  156. updateStatus(`话题 ${topicId} 的帖子 ${startId}~${endId} 处理失败`, "red")
  157. }
  158. }
  159. let delay = baseRequestDelay + getRandomInt(0, randomDelayRange)
  160. await new Promise(r => setTimeout(r, delay))
  161. }
  162.  
  163. function createBatchParams(startId, endId) {
  164. let params = new URLSearchParams()
  165.  
  166. for (let i = startId; i <= endId; i++) {
  167. params.append(`timings[${i}]`, getRandomInt(minReadTime, maxReadTime).toString())
  168. }
  169. let topicTime = getRandomInt(minReadTime * (endId - startId + 1), maxReadTime * (endId - startId + 1)).toString()
  170. params.append('topic_time', topicTime)
  171. params.append('topic_id', topicId)
  172. return params
  173. }
  174.  
  175. // 批量阅读处理
  176. for (let i = 1; i <= totalReplies;) {
  177. let batchSize = getRandomInt(minBatchReplyCount, maxBatchReplyCount)
  178. let startId = i
  179. let endId = Math.min(i + batchSize - 1, totalReplies)
  180.  
  181. await sendBatch(startId, endId)
  182. i = endId + 1
  183. }
  184.  
  185. console.log(`【LINUX DO ReadBoostRead\n处理完成`, `\n话题标识:${topicId}`)
  186. updateStatus(`话题 ${topicId} 处理完成`, "green")
  187.  
  188. if (!readed.includes(topicId)) {
  189. readed.push(topicId);
  190. }
  191. let index = reading.indexOf(topicId);
  192. if (index !== -1) {
  193. reading.splice(index, 1);
  194. }
  195.  
  196. setTimeout(() => {
  197. updateStatus("ReadBoost 待命中", "")
  198. }, 3000)
  199. }
  200.  
  201. /**
  202. * 更新状态标签内容
  203. */
  204. function updateStatus(text, color = "#555") {
  205. statusLabel.text(text)
  206. statusLabel.css({ 'background-color': color })
  207. }
  208.  
  209. /**
  210. * 显示设置UI界面
  211. */
  212. function showSettingsUI() {
  213. let settingsDiv = $(`<div id="readBoost">
  214. <h3>ReadBoost 设置</h3>
  215. <div class="readboost">
  216. <label class="readboost"><span>基础延迟(ms)</span><input id="baseDelay" type="number" value="${config.baseDelay}"></label>
  217. <label class="readboost"><span>随机延迟范围(ms)</span><input id="randomDelayRange" type="number" value="${config.randomDelayRange}"></label>
  218. <label class="readboost"><span>最小请求量</span><input id="minReqSize" type="number" value="${config.minReqSize}"></label>
  219. <label class="readboost"><span>最大请求量</span><input id="maxReqSize" type="number" value="${config.maxReqSize}"></label>
  220. <label class="readboost"><span>最小时间(ms)</span><input id="minReadTime" type="number" value="${config.minReadTime}"></label>
  221. <label class="readboost"><span>最大时间(ms)</span><input id="maxReadTime" type="number" value="${config.maxReadTime}"></label>
  222. <label class="readboost"><span>解锁参数</span><input type="checkbox" id="advancedMode"></label>
  223. <label class="readboost"><span>自动运行</span><input type="checkbox" id="autoStart" ${config.autoStart ? "checked" : ""}></label>
  224. </div>
  225. <div class="readboost">
  226. <button class="btn btn-small" id="saveSettings">
  227. <span class="d-button-label">保存</span>
  228. </button>
  229. <button class="btn btn-small" id="closeSettings">
  230. <span class="d-button-label">关闭</span>
  231. </button>
  232. <button class="btn btn-small" id="resetDefaults">
  233. <span class="d-button-label">恢复默认值</span>
  234. </button>
  235. </div>
  236. </div>
  237. <style>
  238. #readBoost {
  239. position: fixed;
  240. top: 50%;
  241. left: 50%;
  242. transform: translate(-50%, -50%);
  243. padding: 1.3em;
  244. border-radius: 16px;
  245. z-index: 1000;
  246. background: var(--tertiary-medium);
  247. color: var(--primary);
  248. box-shadow: 0 8px 32px #0000001a;
  249. }
  250. div.readboost {
  251. padding-top: 10px;
  252. font-size: 16px;
  253. }
  254. label.readboost {
  255. display: flex;
  256. align-items: center;
  257. justify-content: space-between;
  258. padding-top: 10px;
  259. color: var(--primary);
  260. font-weight: normal;
  261. }
  262. </style>`)
  263.  
  264. settingsDiv.find("#saveSettings").on("click", () => {
  265. config.baseDelay = parseInt(settingsDiv.find("#baseDelay").val(), 10);
  266. config.randomDelayRange = parseInt(settingsDiv.find("#randomDelayRange").val(), 10);
  267. config.minReqSize = parseInt(settingsDiv.find("#minReqSize").val(), 10);
  268. config.maxReqSize = parseInt(settingsDiv.find("#maxReqSize").val(), 10);
  269. config.minReadTime = parseInt(settingsDiv.find("#minReadTime").val(), 10);
  270. config.maxReadTime = parseInt(settingsDiv.find("#maxReadTime").val(), 10);
  271. config.autoStart = settingsDiv.find("#autoStart").prop("checked");
  272.  
  273. // 持久化保存设置
  274. GM_setValue("baseDelay", config.baseDelay);
  275. GM_setValue("randomDelayRange", config.randomDelayRange);
  276. GM_setValue("minReqSize", config.minReqSize);
  277. GM_setValue("maxReqSize", config.maxReqSize);
  278. GM_setValue("minReadTime", config.minReadTime);
  279. GM_setValue("maxReadTime", config.maxReadTime);
  280. GM_setValue("autoStart", config.autoStart);
  281.  
  282. location.reload();
  283. });
  284.  
  285. $("#resetDefaults").on("click", () => {
  286. config = { ...defaultConfig };
  287.  
  288. GM_setValue("baseDelay", defaultConfig.baseDelay);
  289. GM_setValue("randomDelayRange", defaultConfig.randomDelayRange);
  290. GM_setValue("minReqSize", defaultConfig.minReqSize);
  291. GM_setValue("maxReqSize", defaultConfig.maxReqSize);
  292. GM_setValue("minReadTime", defaultConfig.minReadTime);
  293. GM_setValue("maxReadTime", defaultConfig.maxReadTime);
  294. GM_setValue("autoStart", defaultConfig.autoStart);
  295.  
  296. location.reload();
  297. });
  298.  
  299. function toggleSettingsInputs(enabled) {
  300. let inputs = [
  301. "baseDelay", "randomDelayRange", "minReqSize",
  302. "maxReqSize", "minReadTime", "maxReadTime"
  303. ];
  304. inputs.forEach(inputId => {
  305. let inputElement = settingsDiv.find(`#${inputId}`);
  306. if (inputElement.length) {
  307. inputElement.prop("disabled", !enabled);
  308. }
  309. });
  310. }
  311. toggleSettingsInputs(false);
  312.  
  313. settingsDiv.find("#advancedMode").on("change", (event) => {
  314. if ($(event.target).prop("checked")) {
  315. toggleSettingsInputs(true);
  316. } else {
  317. toggleSettingsInputs(false);
  318. }
  319. });
  320.  
  321. settingsDiv.find("#closeSettings").on("click", () => {
  322. settingsDiv.remove();
  323. });
  324.  
  325. $("body").append(settingsDiv);
  326. }
  327.  
  328. function getStoredConfig() {
  329. return {
  330. baseDelay: GM_getValue("baseDelay", defaultConfig.baseDelay),
  331. randomDelayRange: GM_getValue("randomDelayRange", defaultConfig.randomDelayRange),
  332. minReqSize: GM_getValue("minReqSize", defaultConfig.minReqSize),
  333. maxReqSize: GM_getValue("maxReqSize", defaultConfig.maxReqSize),
  334. minReadTime: GM_getValue("minReadTime", defaultConfig.minReadTime),
  335. maxReadTime: GM_getValue("maxReadTime", defaultConfig.maxReadTime),
  336. autoStart: GM_getValue("autoStart", defaultConfig.autoStart)
  337. }
  338. }
  339.  
  340. function waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector) {
  341. function findInShadowRoots(root, selector) {
  342. let elements = $(root).find(selector).toArray();
  343. $(root).find('*').each(function () {
  344. let shadowRoot = this.shadowRoot;
  345. if (shadowRoot) {
  346. elements = elements.concat(findInShadowRoots(shadowRoot, selector));
  347. }
  348. });
  349. return elements;
  350. }
  351. var targetElements;
  352. if (iframeSelector) {
  353. targetElements = $(iframeSelector).contents();
  354. } else {
  355. targetElements = $(document);
  356. }
  357. let allElements = findInShadowRoots(targetElements, selectorTxt);
  358. if (allElements.length > 0) {
  359. allElements.forEach(function (element) {
  360. var jThis = $(element);
  361. var uniqueIdentifier = 'alreadyFound';
  362. var alreadyFound = jThis.data(uniqueIdentifier) || false;
  363. if (!alreadyFound) {
  364. var cancelFound = actionFunction(jThis);
  365. if (cancelFound) {
  366. return false;
  367. } else {
  368. jThis.data(uniqueIdentifier, true);
  369. }
  370. }
  371. });
  372. }
  373. var controlObj = waitForKeyElements.controlObj || {};
  374. var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  375. var timeControl = controlObj[controlKey];
  376. if (allElements.length > 0 && bWaitOnce && timeControl) {
  377. clearInterval(timeControl);
  378. delete controlObj[controlKey];
  379. } else {
  380. if (!timeControl) {
  381. timeControl = setInterval(function () {
  382. waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector);
  383. }, 1000);
  384. controlObj[controlKey] = timeControl;
  385. }
  386. }
  387. waitForKeyElements.controlObj = controlObj;
  388. }
  389. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址