LINUX DO ReadBoost

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

질문, 리뷰하거나, 이 스크립트를 신고하세요.
  1. // ==UserScript==
  2. // @name LINUX DO ReadBoost
  3. // @author hmjz100
  4. // @namespace github.com/hmjz100
  5. // @version 1.0.1
  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 style = $(`<style id="readBoostStyle">
  40. #readBoost {
  41. position: fixed;
  42. top: 50%;
  43. left: 50%;
  44. transform: translate(-50%, -50%);
  45. padding: 1.3em;
  46. border-radius: 16px;
  47. z-index: 1000;
  48. background: var(--tertiary-medium);
  49. color: var(--primary);
  50. box-shadow: 0 8px 32px #0000001a;
  51. }
  52. div.readboost {
  53. padding-top: 10px;
  54. font-size: 16px;
  55. }
  56. label.readboost {
  57. display: flex;
  58. align-items: center;
  59. justify-content: space-between;
  60. padding-top: 10px;
  61. color: var(--primary);
  62. font-weight: normal;
  63. }
  64. label.readboost input {
  65. margin: 0;
  66. padding: 3px 5px;
  67. }
  68. .readboost.buttonCollection {
  69. display: flex;
  70. align-items: center;
  71. justify-content: space-evenly;
  72. }
  73.  
  74. div.topic-owner .topic-body .contents>.cooked::after {
  75. color: var(--tertiary-medium);
  76. content: "题主";
  77. }
  78. </style>`)
  79.  
  80. 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>`)
  81. let statusLabel = $('<span id="statusLabel" style="margin: 0 10px 0">ReadBoost 待命中</span>')
  82. settingsButton.on('click', showSettingsUI)
  83.  
  84. waitForKeyElements('.header-buttons', (element) => {
  85. element.append(statusLabel)
  86. element.append(settingsButton)
  87. }, true)
  88.  
  89. waitForKeyElements('body', (element) => {
  90. element.after(style)
  91. }, true)
  92.  
  93. let defaultConfig = {
  94. baseDelay: 2500,
  95. randomDelayRange: 800,
  96. minReqSize: 8,
  97. maxReqSize: 20,
  98. minReadTime: 800,
  99. maxReadTime: 3000,
  100. autoStart: false
  101. }
  102.  
  103. let config = { ...defaultConfig, ...getStoredConfig() }
  104. let csrfToken = $('meta[name=csrf-token]').attr('content')
  105.  
  106. function boost(url = (new URL(location.href)), auto = false) {
  107. console.log(`【LINUX DO ReadBoostInit\n收到新链接`, `\n链接:${url.href}`)
  108.  
  109. // 初始化
  110. let topicId = url?.pathname?.split("/")?.[3]
  111. let repliesInfo = $('div[class=timeline-replies]').text().trim()
  112. if (!topicId || !csrfToken || !repliesInfo) {
  113. console.log(`【LINUX DO ReadBoostInit\n缺失关键标识,跳过`)
  114. return;
  115. };
  116. let [currentPosition, totalReplies] = repliesInfo?.split("/")?.map(part => parseInt(part?.trim(), 10))
  117.  
  118. // 自启动处理
  119. if (config.autoStart || auto) {
  120. startReading(topicId, totalReplies)
  121. }
  122. }
  123. boost()
  124.  
  125. /**
  126. * 开始刷取已读话题
  127. * @param {string} topicId 主题ID
  128. * @param {number} totalReplies 总帖子数
  129. */
  130. async function startReading(topicId, totalReplies) {
  131. if (!reading.includes(topicId)) {
  132. reading.push(topicId);
  133. } else {
  134. console.log(`【LINUX DO ReadBoostRead\n正在处理此话题,跳过`)
  135. return;
  136. }
  137. if (readed.includes(topicId)) {
  138. console.log(`【LINUX DO ReadBoostRead\n已读过此话题,跳过`)
  139. let index = reading.indexOf(topicId);
  140. if (index !== -1) {
  141. reading.splice(index, 1);
  142. }
  143. return;
  144. }
  145. console.log(`【LINUX DO ReadBoostRead\n开始阅读……`, `\n话题标识:${topicId}`, `\n帖子数量:${totalReplies}`)
  146.  
  147. let baseRequestDelay = config.baseDelay
  148. let randomDelayRange = config.randomDelayRange
  149. let minBatchReplyCount = config.minReqSize
  150. let maxBatchReplyCount = config.maxReqSize
  151. let minReadTime = config.minReadTime
  152. let maxReadTime = config.maxReadTime
  153.  
  154. // 随机数生成
  155. function getRandomInt(min, max) {
  156. return Math.floor(Math.random() * (max - min + 1)) + min
  157. }
  158.  
  159. // 发起读帖请求
  160. async function sendBatch(startId, endId, retryCount = 3) {
  161. let params = createBatchParams(startId, endId)
  162. try {
  163. let response = await fetch("https://linux.do/topics/timings", {
  164. headers: {
  165. "accept": "*/*",
  166. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  167. "discourse-background": "true",
  168. "discourse-logged-in": "true",
  169. "discourse-present": "true",
  170. "priority": "u=1, i",
  171. "sec-fetch-dest": "empty",
  172. "sec-fetch-mode": "cors",
  173. "sec-fetch-site": "same-origin",
  174. "x-csrf-token": csrfToken,
  175. "x-requested-with": "XMLHttpRequest",
  176. "x-silence-logger": "true"
  177. },
  178. referrer: `https://linux.do/`,
  179. body: params.toString(),
  180. method: "POST",
  181. mode: "cors",
  182. credentials: "include"
  183. })
  184. if (!response.ok) {
  185. throw new Error(`请求失败,状态:${response.status}`)
  186. }
  187. console.log(`【LINUX DO ReadBoostRead\n处理成功`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`)
  188. updateStatus(`话题 ${topicId} 的帖子 ${startId}~${endId} 处理成功`, "green")
  189. } catch (error) {
  190. console.error(`【LINUX DO ReadBoostRead\n处理失败`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`, `\n错误详情:`, error)
  191.  
  192. if (retryCount > 0) {
  193. console.error(`【LINUX DO ReadBoostRead\n重新处理`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`)
  194. updateStatus(`重新处理话题 ${topicId} 的帖子 ${startId}~${endId}(${retryCount})`, "orange")
  195.  
  196. let retryDelay = 2000
  197. await new Promise(r => setTimeout(r, retryDelay))
  198. await sendBatch(startId, endId, retryCount - 1)
  199. } else {
  200. console.error(`【LINUX DO ReadBoostRead\n处理失败`, `\n话题标识:${topicId}`, `\n帖子标识:${startId}~${endId}`, `\n错误详情:`, error)
  201. updateStatus(`话题 ${topicId} 的帖子 ${startId}~${endId} 处理失败`, "red")
  202. }
  203. }
  204. let delay = baseRequestDelay + getRandomInt(0, randomDelayRange)
  205. await new Promise(r => setTimeout(r, delay))
  206. }
  207.  
  208. function createBatchParams(startId, endId) {
  209. let params = new URLSearchParams()
  210.  
  211. for (let i = startId; i <= endId; i++) {
  212. params.append(`timings[${i}]`, getRandomInt(minReadTime, maxReadTime).toString())
  213. }
  214. let topicTime = getRandomInt(minReadTime * (endId - startId + 1), maxReadTime * (endId - startId + 1)).toString()
  215. params.append('topic_time', topicTime)
  216. params.append('topic_id', topicId)
  217. return params
  218. }
  219.  
  220. // 批量阅读处理
  221. for (let i = 1; i <= totalReplies;) {
  222. let batchSize = getRandomInt(minBatchReplyCount, maxBatchReplyCount)
  223. let startId = i
  224. let endId = Math.min(i + batchSize - 1, totalReplies)
  225.  
  226. await sendBatch(startId, endId)
  227. i = endId + 1
  228. }
  229.  
  230. console.log(`【LINUX DO ReadBoostRead\n处理完成`, `\n话题标识:${topicId}`)
  231. updateStatus(`话题 ${topicId} 处理完成`, "green")
  232.  
  233. if (!readed.includes(topicId)) {
  234. readed.push(topicId);
  235. }
  236. let index = reading.indexOf(topicId);
  237. if (index !== -1) {
  238. reading.splice(index, 1);
  239. }
  240.  
  241. setTimeout(() => {
  242. updateStatus("ReadBoost 待命中", "")
  243. }, 3000)
  244. }
  245.  
  246. /**
  247. * 更新状态标签内容
  248. */
  249. function updateStatus(text, color) {
  250. statusLabel.text(text)
  251. if (color !== "") {
  252. statusLabel.css({ 'background-color': color, 'color': '#fff' })
  253. } else {
  254. statusLabel.css({ 'background-color': '', 'color': '' })
  255. }
  256. }
  257.  
  258. /**
  259. * 显示设置UI界面
  260. */
  261. function showSettingsUI() {
  262. if ($('#readBoost').length) return;
  263. let settingsDiv = $(`<div id="readBoost">
  264. <h3>ReadBoost 设置</h3>
  265. <div class="readboost">
  266. <label class="readboost"><span>基础延迟(ms)</span><input id="baseDelay" type="number" value="${config.baseDelay}"></label>
  267. <label class="readboost"><span>随机延迟范围(ms)</span><input id="randomDelayRange" type="number" value="${config.randomDelayRange}"></label>
  268. <label class="readboost"><span>最小请求量</span><input id="minReqSize" type="number" value="${config.minReqSize}"></label>
  269. <label class="readboost"><span>最大请求量</span><input id="maxReqSize" type="number" value="${config.maxReqSize}"></label>
  270. <label class="readboost"><span>最小时间(ms)</span><input id="minReadTime" type="number" value="${config.minReadTime}"></label>
  271. <label class="readboost"><span>最大时间(ms)</span><input id="maxReadTime" type="number" value="${config.maxReadTime}"></label>
  272. <label class="readboost"><span>解锁参数</span><input type="checkbox" id="advancedMode"></label>
  273. <label class="readboost"><span>自动运行</span><input type="checkbox" id="autoStart" ${config.autoStart ? "checked" : ""}></label>
  274. </div>
  275. <div class="readboost buttonCollection">
  276. <button class="btn btn-small" id="saveSettings">
  277. <span class="d-button-label">保存</span>
  278. </button>
  279. <button class="btn btn-small" id="resetDefaults">
  280. <span class="d-button-label">重置</span>
  281. </button>
  282. <button class="btn btn-small" id="startManually">
  283. <span class="d-button-label">运行</span>
  284. </button>
  285. <button class="btn btn-small" id="closeSettings">
  286. <span class="d-button-label">关闭</span>
  287. </button>
  288. </div>
  289. </div>`)
  290.  
  291. settingsDiv.find("#saveSettings").on("click", () => {
  292. config.baseDelay = parseInt(settingsDiv.find("#baseDelay").val(), 10);
  293. config.randomDelayRange = parseInt(settingsDiv.find("#randomDelayRange").val(), 10);
  294. config.minReqSize = parseInt(settingsDiv.find("#minReqSize").val(), 10);
  295. config.maxReqSize = parseInt(settingsDiv.find("#maxReqSize").val(), 10);
  296. config.minReadTime = parseInt(settingsDiv.find("#minReadTime").val(), 10);
  297. config.maxReadTime = parseInt(settingsDiv.find("#maxReadTime").val(), 10);
  298. config.autoStart = settingsDiv.find("#autoStart").prop("checked");
  299.  
  300. // 持久化保存设置
  301. GM_setValue("baseDelay", config.baseDelay);
  302. GM_setValue("randomDelayRange", config.randomDelayRange);
  303. GM_setValue("minReqSize", config.minReqSize);
  304. GM_setValue("maxReqSize", config.maxReqSize);
  305. GM_setValue("minReadTime", config.minReadTime);
  306. GM_setValue("maxReadTime", config.maxReadTime);
  307. GM_setValue("autoStart", config.autoStart);
  308.  
  309. settingsDiv.remove();
  310. location.reload();
  311. });
  312.  
  313. settingsDiv.find("#resetDefaults").on("click", () => {
  314. let result = confirm("你确定要重置吗?所有自定义数据都将丢失!");
  315. if (result) {
  316. config = { ...defaultConfig };
  317.  
  318. GM_setValue("baseDelay", defaultConfig.baseDelay);
  319. GM_setValue("randomDelayRange", defaultConfig.randomDelayRange);
  320. GM_setValue("minReqSize", defaultConfig.minReqSize);
  321. GM_setValue("maxReqSize", defaultConfig.maxReqSize);
  322. GM_setValue("minReadTime", defaultConfig.minReadTime);
  323. GM_setValue("maxReadTime", defaultConfig.maxReadTime);
  324. GM_setValue("autoStart", defaultConfig.autoStart);
  325.  
  326. settingsDiv.remove();
  327. location.reload();
  328. }
  329. });
  330.  
  331. settingsDiv.find("#startManually").on("click", () => {
  332. boost(location, true)
  333. settingsDiv.remove();
  334. });
  335.  
  336. function toggleSettingsInputs(enabled) {
  337. let inputs = [
  338. "baseDelay", "randomDelayRange", "minReqSize",
  339. "maxReqSize", "minReadTime", "maxReadTime"
  340. ];
  341. inputs.forEach(inputId => {
  342. let inputElement = settingsDiv.find(`#${inputId}`);
  343. if (inputElement.length) {
  344. inputElement.prop("disabled", !enabled);
  345. }
  346. });
  347. }
  348. toggleSettingsInputs(false);
  349.  
  350. settingsDiv.find("#advancedMode").on("change", (event) => {
  351. if ($(event.target).prop("checked")) {
  352. toggleSettingsInputs(true);
  353. } else {
  354. toggleSettingsInputs(false);
  355. }
  356. });
  357.  
  358. settingsDiv.find("#closeSettings").on("click", () => {
  359. settingsDiv.remove();
  360. });
  361.  
  362. $("body").append(settingsDiv);
  363. }
  364.  
  365. function getStoredConfig() {
  366. return {
  367. baseDelay: GM_getValue("baseDelay", defaultConfig.baseDelay),
  368. randomDelayRange: GM_getValue("randomDelayRange", defaultConfig.randomDelayRange),
  369. minReqSize: GM_getValue("minReqSize", defaultConfig.minReqSize),
  370. maxReqSize: GM_getValue("maxReqSize", defaultConfig.maxReqSize),
  371. minReadTime: GM_getValue("minReadTime", defaultConfig.minReadTime),
  372. maxReadTime: GM_getValue("maxReadTime", defaultConfig.maxReadTime),
  373. autoStart: GM_getValue("autoStart", defaultConfig.autoStart)
  374. }
  375. }
  376.  
  377. function waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector) {
  378. function findInShadowRoots(root, selector) {
  379. let elements = $(root).find(selector).toArray();
  380. $(root).find('*').each(function () {
  381. let shadowRoot = this.shadowRoot;
  382. if (shadowRoot) {
  383. elements = elements.concat(findInShadowRoots(shadowRoot, selector));
  384. }
  385. });
  386. return elements;
  387. }
  388. var targetElements;
  389. if (iframeSelector) {
  390. targetElements = $(iframeSelector).contents();
  391. } else {
  392. targetElements = $(document);
  393. }
  394. let allElements = findInShadowRoots(targetElements, selectorTxt);
  395. if (allElements.length > 0) {
  396. allElements.forEach(function (element) {
  397. var jThis = $(element);
  398. var uniqueIdentifier = 'alreadyFound';
  399. var alreadyFound = jThis.data(uniqueIdentifier) || false;
  400. if (!alreadyFound) {
  401. var cancelFound = actionFunction(jThis);
  402. if (cancelFound) {
  403. return false;
  404. } else {
  405. jThis.data(uniqueIdentifier, true);
  406. }
  407. }
  408. });
  409. }
  410. var controlObj = waitForKeyElements.controlObj || {};
  411. var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  412. var timeControl = controlObj[controlKey];
  413. if (allElements.length > 0 && bWaitOnce && timeControl) {
  414. clearInterval(timeControl);
  415. delete controlObj[controlKey];
  416. } else {
  417. if (!timeControl) {
  418. timeControl = setInterval(function () {
  419. waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector);
  420. }, 1000);
  421. controlObj[controlKey] = timeControl;
  422. }
  423. }
  424. waitForKeyElements.controlObj = controlObj;
  425. }
  426. })();