linux.do.level

Linux.Do 查看用户信任级别以及升级条件,数据来源于 https://connect.linux.do

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

  1. // ==UserScript==
  2. // @name linux.do.level
  3. // @namespace https://linux.do/u/io.oi/s/level
  4. // @version 1.4.7
  5. // @author LINUX.DO
  6. // @description Linux.Do 查看用户信任级别以及升级条件,数据来源于 https://connect.linux.do
  7. // @license MIT
  8. // @icon https://linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png
  9. // @match https://linux.do/*
  10. // @connect connect.linux.do
  11. // @grant GM.xmlHttpRequest
  12. // @grant GM_addStyle
  13. // ==/UserScript==
  14.  
  15. (e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const o=document.createElement("style");o.textContent=e,document.head.append(o)})(" .level-window{position:fixed;bottom:0;background:var(--secondary);z-index:999;padding:.5em;color:var(--primary);box-shadow:0 0 4px #00000020;border:1px solid var(--primary-low)}.level-window .title .close{width:24px;height:24px;color:#fff;background:red;display:inline-block;text-align:center;line-height:24px;float:right;cursor:pointer;border-radius:4px;font-size:var(--base-font-size-largest)}.level-window .bg-white{background-color:var(--primary-low);border-radius:.5em;padding:.5em;margin-top:.5em}.level-window h1{color:var(--primary);font-size:1.3rem}.level-window h2{font-size:1.25rem}.mb-4 table tr:nth-child(2n){background-color:var(--tertiary-400)}.level-window .text-red-500{color:#ef4444}.level-window .text-green-500{color:#10b981}.level-window .mb-4 table tr td{padding:4px 8px}.language-text{background:var(--primary-very-low);font-family:var(--d-font-family--monospace);font-size:var(--base-font-size-smallest);flex-grow:1}.code-box{display:flex;flex-direction:row;justify-content:space-between}.code-box .language-text{padding:.6em 1em}.code-box .copy{padding:.6em 1em;cursor:pointer;-webkit-user-select:none;user-select:none;font-size:var(--base-font-size-smallest);background:var(--secondary)}.connect-button{width:100%;padding:.5em;margin-top:.5em!important}.emoji-picker-category-buttons,.emoji-picker-emoji-area{justify-content:center;padding-left:initial}.emoji-picker-category-buttons::-webkit-scrollbar,.emoji-picker-emoji-area::-webkit-scrollbar{width:5px;height:auto;background:var(--primary)}.emoji-picker-category-buttons::-webkit-scrollbar-thumb,.emoji-picker-emoji-area::-webkit-scrollbar-thumb{box-shadow:inset 0 0 5px #0003;background:var(--secondary)}.emoji-picker-category-buttons::-webkit-scrollbar-track,.emoji-picker-emoji-area::-webkit-scrollbar-track{box-shadow:inset 0 0 5px #0003;background:var(--primary-low)}.floor-text{color:var(--tertiary)}.modal-container .published-page-content-body td{padding:.5em}.modal-container .d-modal__body::-webkit-scrollbar{width:1em;background:transparent;scrollbar-color:rgba(0,0,0,0) transparent;transition:scrollbar-color .25s ease-in-out;transition-delay:.5s}.modal-container .d-modal__body::-webkit-scrollbar-thumb{background:var(--primary-low)} ");
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. var __defProp = Object.defineProperty;
  21. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  22. var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  23. var _GM = /* @__PURE__ */ (() => typeof GM != "undefined" ? GM : void 0)();
  24. async function getLevelFromConnect() {
  25. return await new Promise((resolve, reject) => {
  26. _GM.xmlHttpRequest({
  27. method: "GET",
  28. url: "https://connect.linux.do",
  29. onload: (response) => {
  30. let regx = /<body[^>]*>([\s\S]+?)<\/body>/i;
  31. let contents = regx.exec(response.responseText);
  32. if (contents) {
  33. resolve({
  34. status: true,
  35. content: contents[1],
  36. error: ""
  37. });
  38. }
  39. },
  40. onerror: (e) => {
  41. reject({ status: false, error: e.error, content: "" });
  42. }
  43. });
  44. });
  45. }
  46. function observeDom(selector, onChanged, option) {
  47. let dom = typeof selector === "string" ? document.querySelector(selector) : selector;
  48. if (dom) {
  49. const observer = new MutationObserver(() => {
  50. onChanged(dom);
  51. });
  52. observer.observe(dom, { childList: true });
  53. return observer;
  54. } else {
  55. console.error(`query dom error: [${selector}]`);
  56. return null;
  57. }
  58. }
  59. function isOnTopicPage() {
  60. return window.location.href.includes("https://linux.do/t/topic");
  61. }
  62. function loadDomFromString(content) {
  63. let parser = new DOMParser();
  64. return parser.parseFromString(content, "text/html").body;
  65. }
  66. function getHtmlBody(html) {
  67. let regx = /<body[^>]*>([\s\S]+?)<\/body>/i;
  68. let contents = regx.exec(html);
  69. if (contents) {
  70. return loadDomFromString(contents[1]);
  71. }
  72. return null;
  73. }
  74. const _DomEventBus = class _DomEventBus {
  75. constructor() {
  76. __publicField(this, "listenerMap");
  77. __publicField(this, "observeMap");
  78. this.listenerMap = {};
  79. this.observeMap = {};
  80. }
  81. static getInstance() {
  82. if (!this.instance) {
  83. this.instance = new _DomEventBus();
  84. }
  85. return this.instance;
  86. }
  87. /**
  88. * 监听事件
  89. * @param name 事件名称
  90. * @param listener 事件监听器
  91. * @param dom 如果为 null,使用事件名称查找 dom, 不为空直接使用给定的 dom
  92. */
  93. add(name, listener, dom = null) {
  94. if (!this.listenerMap[name]) {
  95. this.listenerMap[name] = [];
  96. }
  97. if (this.listenerMap[name].length === 0) {
  98. let observe = dom === null ? observeDom(name, () => {
  99. this.domEmit(name);
  100. }) : observeDom(dom, () => {
  101. this.domEmit(name);
  102. });
  103. if (observe) {
  104. this.observeMap[name] = observe;
  105. }
  106. }
  107. this.listenerMap[name].push(listener);
  108. }
  109. domEmit(event) {
  110. const listeners = this.listenerMap[event];
  111. if (listeners) {
  112. for (const listener of listeners) {
  113. listener();
  114. }
  115. }
  116. }
  117. emit(name) {
  118. this.domEmit(name);
  119. }
  120. clear(name) {
  121. if (!this.listenerMap[name]) {
  122. return;
  123. }
  124. this.listenerMap[name] = [];
  125. }
  126. };
  127. __publicField(_DomEventBus, "instance");
  128. let DomEventBus = _DomEventBus;
  129. function createCodeElement(key) {
  130. var _a;
  131. let realKey = key;
  132. let copied = false;
  133. let root = document.createElement("div");
  134. root.className = "bg-white p-6 rounded-lg mb-4 shadow";
  135. root.innerHTML = `
  136. <h2>DeepLX Api Key</h2>
  137. <div class="code-box">
  138. <span class="hljs language-text">${key.replace(key.substring(12, 21), "**加密**")}</span>
  139. </div>
  140. `;
  141. let copyButton = document.createElement("span");
  142. copyButton.className = "copy";
  143. copyButton.innerHTML = "复制";
  144. copyButton.addEventListener("click", async () => {
  145. if (!copied) {
  146. await navigator.clipboard.writeText(realKey);
  147. copied = true;
  148. copyButton.innerHTML = "已复制";
  149. let timer = setTimeout(() => {
  150. copied = false;
  151. copyButton.innerHTML = "复制";
  152. clearInterval(timer);
  153. }, 2e3);
  154. }
  155. });
  156. (_a = root.querySelector("div.code-box")) == null ? void 0 : _a.appendChild(copyButton);
  157. let connectButton = document.createElement("a");
  158. connectButton.className = "btn btn-primary connect-button";
  159. connectButton.href = "https://connect.linux.do";
  160. connectButton.target = "_blank";
  161. connectButton.innerHTML = "前往 Connect 站";
  162. root.appendChild(connectButton);
  163. return root;
  164. }
  165. function createWindow(title, key, levelTable, onClose) {
  166. let root = document.createElement("div");
  167. root.setAttribute("id", "level-window");
  168. root.className = "level-window";
  169. root.style.right = document.querySelector("div.chat-drawer.is-expanded") ? "430px" : "15px";
  170. root.innerHTML = `
  171. <div class="title">
  172. <span class="close" id="close-button">
  173. <svg class="fa d-icon d-icon-times svg-icon svg-string" xmlns="http://www.w3.org/2000/svg">
  174. <use href="#xmark"></use>
  175. </svg>
  176. </span>
  177. <div id="content" class="content"></div>
  178. </div>`;
  179. let window2 = root.querySelector("div#content");
  180. if (window2) {
  181. window2.appendChild(title);
  182. window2.appendChild(createCodeElement(key));
  183. window2.appendChild(levelTable);
  184. }
  185. let close = root.querySelector("span#close-button");
  186. close == null ? void 0 : close.addEventListener("click", onClose);
  187. DomEventBus.getInstance().add("div.chat-drawer-outlet-container", () => {
  188. let chat = document.querySelector("div.chat-drawer.is-expanded");
  189. root.style.right = chat ? "430px" : "15px";
  190. });
  191. let chatContainer = document.querySelector("div.chat-drawer-outlet-container");
  192. if (chatContainer) {
  193. let observer = new MutationObserver((_) => {
  194. let chat = document.querySelector("div.chat-drawer.is-expanded");
  195. root.style.right = chat ? "430px" : "15px";
  196. });
  197. observer.observe(chatContainer, { childList: true });
  198. }
  199. return root;
  200. }
  201. class Modal {
  202. constructor(title, content, maxWidth = null) {
  203. __publicField(this, "element");
  204. __publicField(this, "backdrop");
  205. __publicField(this, "title");
  206. __publicField(this, "container");
  207. __publicField(this, "isShow", false);
  208. this.maxWidth = maxWidth;
  209. this.title = title;
  210. this.element = this.initElement();
  211. this.backdrop = this.createBackdrop();
  212. this.container = this.queryContainer();
  213. this.setContent(content);
  214. }
  215. initElement() {
  216. let root = document.createElement("div");
  217. root.id = "custom-modal";
  218. root.className = "ember-view modal d-modal discard-draft-modal";
  219. root.setAttribute("data-keyboard", "false");
  220. root.setAttribute("aria-modal", "true");
  221. root.setAttribute("role", "dialog");
  222. root.innerHTML = `
  223. <div class="d-modal__container" ${this.maxWidth === null ? "" : `style="max-width:${this.maxWidth}px;"`}>
  224. <div class="d-modal__header">
  225. <div class="d-modal__title">
  226. <h1 id="discourse-modal-title" class="d-modal__title-text">${this.title}</h1>
  227. </div>
  228. <button id="m-close-btn" class="btn no-text btn-icon btn-transparent modal-close" title="关闭" type="button">
  229. <svg class="fa d-icon d-icon-times svg-icon svg-string" xmlns="http://www.w3.org/2000/svg">
  230. <use href="#times"></use>
  231. </svg>
  232. </button>
  233. </div>
  234. <div class="d-modal__body" tabindex="-1">
  235. <div class="instructions">
  236. </div>
  237. </div>
  238. <div class="d-modal__footer">
  239. </div>
  240. </div>`;
  241. const close = root.querySelector("button#m-close-btn");
  242. close == null ? void 0 : close.addEventListener("click", () => this.close());
  243. return root;
  244. }
  245. queryContainer() {
  246. return document.querySelector("div.modal-container");
  247. }
  248. setContent(content) {
  249. const root = this.element.querySelector("div.instructions");
  250. const contentElement = typeof content === "string" ? loadDomFromString(content) : content;
  251. root == null ? void 0 : root.appendChild(contentElement);
  252. }
  253. setFooter(footer) {
  254. var _a;
  255. (_a = this.element.querySelector("div.d-modal__footer")) == null ? void 0 : _a.appendChild(footer);
  256. }
  257. show() {
  258. if (this.container) {
  259. this.container.appendChild(this.element);
  260. this.container.appendChild(this.backdrop);
  261. this.isShow = true;
  262. }
  263. }
  264. close() {
  265. if (this.isShow && this.container) {
  266. this.container.removeChild(this.element);
  267. this.container.removeChild(this.backdrop);
  268. this.isShow = false;
  269. }
  270. }
  271. createBackdrop() {
  272. const back = document.createElement("div");
  273. back.className = "d-modal__backdrop";
  274. return back;
  275. }
  276. }
  277. class MessageBox {
  278. constructor(title, content, buttons = [
  279. {
  280. text: "确认",
  281. type: "btn-primary",
  282. onclick: void 0
  283. }
  284. ]) {
  285. __publicField(this, "title", "MessageBox");
  286. __publicField(this, "content");
  287. __publicField(this, "buttons");
  288. this.title = title;
  289. this.content = content;
  290. this.buttons = buttons;
  291. }
  292. show() {
  293. const modal = new Modal(this.title, this.content);
  294. function createButton({ onclick, text, type }) {
  295. let button = document.createElement("button");
  296. button.className = "btn btn-text " + type;
  297. button.setAttribute("type", "button");
  298. button.innerHTML = `
  299. <span class="d-button-label">
  300. ${text}
  301. </span>`;
  302. button.addEventListener("click", () => {
  303. if (onclick) {
  304. onclick();
  305. }
  306. modal.close();
  307. });
  308. return button;
  309. }
  310. for (const btn of this.buttons) {
  311. modal.setFooter(createButton(btn));
  312. }
  313. modal.show();
  314. }
  315. }
  316. class Icons {
  317. static getLoading(size = 60) {
  318. return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}px" height="${size}px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-ring">
  319. <circle cx="50" cy="50" r="30" stroke="#B3B5B411" stroke-width="10" fill="none"/>
  320. <circle cx="50" cy="50" r="30" stroke="#808281" stroke-width="10" fill="none" transform="rotate(144 50 50)">
  321. <animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"/>
  322. <animate attributeName="stroke-dasharray" calcMode="linear" values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882" keyTimes="0;0.5;1" dur="1" begin="0s" repeatCount="indefinite"/>
  323. </circle>
  324. </svg>`;
  325. }
  326. }
  327. class Level {
  328. constructor() {
  329. __publicField(this, "levelWindow");
  330. __publicField(this, "loading", false);
  331. this.replaceConnectAnchor();
  332. }
  333. showErrorAndGotoConnect(error) {
  334. const message = new MessageBox("错误", error, [
  335. {
  336. text: "确认",
  337. type: "btn-primary"
  338. },
  339. {
  340. text: "前往 Connect 查看",
  341. type: "",
  342. onclick: () => {
  343. window.open("https://connect.linux.do/", "_blank");
  344. }
  345. }
  346. ]);
  347. message.show();
  348. }
  349. getContentsFromDom(dom) {
  350. var _a, _b, _c;
  351. let title = dom.querySelector("h1.text-2xl");
  352. (_a = dom.querySelector("h1.text-2xl a.text-blue-500")) == null ? void 0 : _a.remove();
  353. let levelTable = (_b = dom.querySelector("div.bg-white.p-6.rounded-lg.mb-4.shadow table")) == null ? void 0 : _b.parentElement;
  354. let key = (_c = dom.querySelector("div.bg-white.p-6.rounded-lg.mb-4.shadow p strong")) == null ? void 0 : _c.innerHTML;
  355. let status = key !== void 0 && levelTable !== null;
  356. return {
  357. status,
  358. key,
  359. title,
  360. content: levelTable,
  361. error: status ? null : "解析 Connect 数据错误。"
  362. };
  363. }
  364. replaceConnectAnchor() {
  365. let connectAnchor = document.querySelector('a[href="https://connect.linux.do"]');
  366. if (connectAnchor) {
  367. connectAnchor.href = "javascript:void(0);";
  368. connectAnchor.addEventListener("click", async () => {
  369. if (!this.loading && this.levelWindow === void 0) {
  370. this.loading = true;
  371. let icon = connectAnchor.querySelector("span.sidebar-section-link-prefix.icon");
  372. if (icon) {
  373. let defaultIcon = icon.innerHTML;
  374. icon.innerHTML = Icons.getLoading();
  375. let result = await getLevelFromConnect();
  376. this.loading = false;
  377. icon.innerHTML = defaultIcon;
  378. if (result.status) {
  379. let dom = loadDomFromString(result.content);
  380. let body = this.getContentsFromDom(dom);
  381. if (body.status) {
  382. this.levelWindow = createWindow(body.title, body.key, body.content, () => {
  383. this.close();
  384. });
  385. document.body.appendChild(this.levelWindow);
  386. } else {
  387. this.showErrorAndGotoConnect(body.error);
  388. }
  389. } else {
  390. this.showErrorAndGotoConnect(result.error);
  391. }
  392. }
  393. } else {
  394. this.close();
  395. }
  396. });
  397. return;
  398. }
  399. console.error("replace connect anchor error.");
  400. }
  401. close() {
  402. this.levelWindow.remove();
  403. this.levelWindow = void 0;
  404. }
  405. }
  406. function createFloor(num) {
  407. let button = document.createElement("button");
  408. button.className = "widget-button btn-flat reply create fade-out btn-icon-text";
  409. button.setAttribute("title", `${num}楼`);
  410. button.setAttribute("id", "floor-button");
  411. button.innerHTML = `<span class='d-button-label floor-text'>#${num}</span>`;
  412. return button;
  413. }
  414. class Floor {
  415. constructor() {
  416. __publicField(this, "eventBus");
  417. this.eventBus = DomEventBus.getInstance();
  418. this.observeUrl();
  419. }
  420. observeUrl() {
  421. const changed = () => {
  422. const timer = setInterval(() => {
  423. if (isOnTopicPage()) {
  424. this.eventBus.add("div.post-stream", () => {
  425. if (isOnTopicPage()) {
  426. this.fixFloorDom();
  427. }
  428. });
  429. this.fixFloorDom();
  430. } else {
  431. this.eventBus.clear("div.post-stream");
  432. }
  433. clearInterval(timer);
  434. });
  435. };
  436. this.eventBus.add("div#main-outlet", changed);
  437. if (isOnTopicPage()) {
  438. this.eventBus.emit("div#main-outlet");
  439. }
  440. }
  441. fixFloorDom() {
  442. let timer = setInterval(() => {
  443. var _a, _b;
  444. let floors = Array.from(document.querySelectorAll("div.container.posts section.topic-area div.ember-view div.topic-post"));
  445. for (const floor of floors) {
  446. if (floor.querySelector("button#floor-button")) {
  447. continue;
  448. }
  449. let article = floor.querySelector("article");
  450. if (article) {
  451. let id = (_a = article.getAttribute("id")) == null ? void 0 : _a.replace("post_", "");
  452. let actions = Array.from(floor.querySelectorAll("article section nav div.actions"));
  453. const button = createFloor(id ?? "??");
  454. (_b = actions.at(-1)) == null ? void 0 : _b.appendChild(button);
  455. }
  456. }
  457. clearInterval(timer);
  458. });
  459. }
  460. }
  461. class Emoji {
  462. constructor() {
  463. __publicField(this, "customs", ["飞书", "小红书", "b站", "贴吧"]);
  464. __publicField(this, "observe", new MutationObserver(() => {
  465. let emojiPicker = document.querySelector("div.emoji-picker.opened");
  466. if (!emojiPicker) {
  467. return;
  468. }
  469. let timer = setInterval(() => {
  470. let emojiButtons = emojiPicker.querySelector("div.emoji-picker-category-buttons");
  471. let emojiContainer = emojiPicker.querySelector("div.emojis-container");
  472. if (emojiButtons && emojiContainer) {
  473. for (const custom of this.customs) {
  474. this.moveElementToFirstBySelector(`button[data-section="custom-${custom}"]`, emojiButtons);
  475. this.moveElementToFirstBySelector(`div[data-section="custom-${custom}"]`, emojiContainer);
  476. }
  477. }
  478. clearInterval(timer);
  479. });
  480. }));
  481. observeDom("div#reply-control", (replay) => {
  482. this.onReplayOpen(replay);
  483. });
  484. }
  485. moveElementToFirstBySelector(selector, root) {
  486. let node = root.querySelector(selector);
  487. if (node) {
  488. root.insertBefore(node, root.children[0].nextSibling);
  489. }
  490. }
  491. onReplayOpen(replay) {
  492. if (replay.className.includes("open")) {
  493. let editor = replay.querySelector("div.d-editor");
  494. if (editor) {
  495. this.observe.observe(editor, { childList: true });
  496. } else {
  497. console.error("querySelector:div.d-editor");
  498. }
  499. } else {
  500. this.observe.disconnect();
  501. }
  502. }
  503. }
  504. class FriendLinks {
  505. constructor() {
  506. __publicField(this, "loading", false);
  507. __publicField(this, "defaultIcon", "");
  508. __publicField(this, "loadingIcon", Icons.getLoading());
  509. __publicField(this, "icon");
  510. __publicField(this, "modal");
  511. this.icon = this.replaceFriendAnchor();
  512. }
  513. setLoading(loading) {
  514. this.loading = loading;
  515. this.icon.innerHTML = this.loading ? this.loadingIcon : this.defaultIcon;
  516. }
  517. replaceFriendAnchor() {
  518. const anchor = document.querySelector('a[href="/pub/friend-links"]');
  519. if (!anchor) {
  520. throw new Error("query friend link error.");
  521. }
  522. anchor.href = "javascript:void(0);";
  523. const icon = anchor.querySelector("span.sidebar-section-link-prefix.icon");
  524. if (!icon) throw new Error("query friend link icon error");
  525. this.defaultIcon = icon.innerHTML;
  526. anchor.addEventListener("click", () => this.handleClick());
  527. return icon;
  528. }
  529. async handleClick() {
  530. var _a, _b;
  531. if (this.loading) return;
  532. this.setLoading(true);
  533. if ((_a = this.modal) == null ? void 0 : _a.isShow) {
  534. this.modal.close();
  535. this.modal = void 0;
  536. this.setLoading(false);
  537. return;
  538. }
  539. try {
  540. const response = await fetch("/pub/friend-links");
  541. if (!response.ok) throw new Error(`fetch friend links error: ${response.statusText}`);
  542. const text = await response.text();
  543. const body = getHtmlBody(text);
  544. if (!body) throw new Error("get html body error");
  545. (_b = body.querySelector("div.published-page-header")) == null ? void 0 : _b.remove();
  546. this.modal = new Modal("友链", body, 900);
  547. this.modal.show();
  548. } catch (error) {
  549. console.error(error);
  550. } finally {
  551. this.setLoading(false);
  552. }
  553. }
  554. }
  555. function init() {
  556. window.addEventListener("load", () => {
  557. new Level();
  558. new FriendLinks();
  559. new Floor();
  560. new Emoji();
  561. });
  562. }
  563. init();
  564.  
  565. })();